/* * SPDX-FileCopyrightText: 2025 Kozmotronik Tech * * SPDX-License-Identifier: MIT */ #include "test_relay_chn_notify_common.h" // This is a private header, but we need it for direct notification calls and queue length. // It's included conditionally in the build via CMakeLists.txt when NVS is enabled. #include "relay_chn_notify.h" // ### Listener Functionality Tests TEST_CASE("Listener is called on state change for each channel", "[relay_chn][notify]") { // 1. Register the listener and reset info reset_listener_info(&listener1_info); TEST_ESP_OK(relay_chn_register_listener(test_listener_1)); vTaskDelay(pdMS_TO_TICKS(TEST_DELAY_MARGIN_MS)); // Allow registration to be processed // Loop through each channel for (uint8_t ch = 0; ch < CONFIG_RELAY_CHN_COUNT; ch++) { // 2. Trigger a state change on the current channel. // tearDown() ensures each channel starts as IDLE. relay_chn_run_forward(ch); vTaskDelay(pdMS_TO_TICKS(TEST_DELAY_MARGIN_MS)); // Allow event to be processed // 3. Verify the listener was called with correct parameters for this channel. // The listener_info struct is overwritten each time, but we check it before the next iteration. TEST_ASSERT_EQUAL(ch, listener1_info.chn_id); TEST_ASSERT_EQUAL(RELAY_CHN_STATE_IDLE, listener1_info.old_state); TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FORWARD, listener1_info.new_state); } // 4. Verify the total call count after the loop TEST_ASSERT_EQUAL(CONFIG_RELAY_CHN_COUNT, listener1_info.call_count); // 5. Unregister to clean up relay_chn_unregister_listener(test_listener_1); } TEST_CASE("Unregistered listener is not called for any channel", "[relay_chn][notify]") { reset_listener_info(&listener1_info); // 1. Register and then immediately unregister the listener TEST_ESP_OK(relay_chn_register_listener(test_listener_1)); relay_chn_unregister_listener(test_listener_1); vTaskDelay(pdMS_TO_TICKS(TEST_DELAY_MARGIN_MS)); // Allow commands to process // 2. Trigger a state change on all channels relay_chn_run_forward_all(); vTaskDelay(pdMS_TO_TICKS(TEST_DELAY_MARGIN_MS * CONFIG_RELAY_CHN_COUNT)); // Allow all events to be processed // 3. Verify the listener was NOT called TEST_ASSERT_EQUAL(0, listener1_info.call_count); } TEST_CASE("Multiple listeners are called on state change for each channel", "[relay_chn][notify]") { // 1. Register listeners and reset info reset_listener_info(&listener1_info); reset_listener_info(&listener2_info); TEST_ESP_OK(relay_chn_register_listener(test_listener_1)); TEST_ESP_OK(relay_chn_register_listener(test_listener_2)); vTaskDelay(pdMS_TO_TICKS(TEST_DELAY_MARGIN_MS)); // Allow registration commands to be processed // Loop through each channel for (uint8_t ch = 0; ch < CONFIG_RELAY_CHN_COUNT; ch++) { // 2. Trigger a state change on the current channel relay_chn_run_forward(ch); vTaskDelay(pdMS_TO_TICKS(TEST_DELAY_MARGIN_MS)); // 3. Verify listener 1 was called correctly for this channel TEST_ASSERT_EQUAL(ch, listener1_info.chn_id); TEST_ASSERT_EQUAL(RELAY_CHN_STATE_IDLE, listener1_info.old_state); TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FORWARD, listener1_info.new_state); // 4. Verify listener 2 was also called correctly for this channel TEST_ASSERT_EQUAL(ch, listener2_info.chn_id); TEST_ASSERT_EQUAL(RELAY_CHN_STATE_IDLE, listener2_info.old_state); TEST_ASSERT_EQUAL(RELAY_CHN_STATE_FORWARD, listener2_info.new_state); } // 5. Verify total call counts TEST_ASSERT_EQUAL(CONFIG_RELAY_CHN_COUNT, listener1_info.call_count); TEST_ASSERT_EQUAL(CONFIG_RELAY_CHN_COUNT, listener2_info.call_count); // 6. Clean up relay_chn_unregister_listener(test_listener_1); relay_chn_unregister_listener(test_listener_2); } TEST_CASE("Listener registration handles invalid arguments and duplicates", "[relay_chn][notify]") { reset_listener_info(&listener1_info); // 1. Registering a NULL listener should fail TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, relay_chn_register_listener(NULL)); // 2. Unregistering a NULL listener should not crash relay_chn_unregister_listener(NULL); vTaskDelay(pdMS_TO_TICKS(TEST_DELAY_MARGIN_MS)); // Allow commands to process // 3. Registering the same listener twice should be handled gracefully TEST_ESP_OK(relay_chn_register_listener(test_listener_1)); TEST_ESP_OK(relay_chn_register_listener(test_listener_1)); // Second call should be a no-op vTaskDelay(pdMS_TO_TICKS(TEST_DELAY_MARGIN_MS)); // Allow registration commands to be processed // 4. Trigger a state change on all channels and verify the listener is only called ONCE per channel relay_chn_run_forward_all(); vTaskDelay(pdMS_TO_TICKS(TEST_DELAY_MARGIN_MS * CONFIG_RELAY_CHN_COUNT)); // Allow all events to be processed TEST_ASSERT_EQUAL(CONFIG_RELAY_CHN_COUNT, listener1_info.call_count); // 5. Clean up relay_chn_unregister_listener(test_listener_1); } TEST_CASE("Notify queue full scenario is handled gracefully", "[relay_chn][notify]") { // 1. Setup blocking_listener_sem = xSemaphoreCreateBinary(); log_check_sem = xSemaphoreCreateBinary(); blocking_listener_call_count = 0; // Intercept logs to check for the "queue full" warning original_vprintf = esp_log_set_vprintf(log_check_vprintf); // 2. Register a listener that will block, allowing the queue to fill up TEST_ESP_OK(relay_chn_register_listener(blocking_listener)); vTaskDelay(pdMS_TO_TICKS(TEST_DELAY_MARGIN_MS)); // Allow task to start // 3. Fill the queue. The first event will be consumed immediately by the dispatcher, // which will then call the blocking_listener and block. The remaining (LEN - 1) // events will sit in the queue, leaving one empty slot. // Use different channel IDs to make the test more robust. for (int i = 0; i < TEST_RELAY_CHN_NOTIFY_QUEUE_LEN; i++) { uint8_t ch = i % CONFIG_RELAY_CHN_COUNT; TEST_ESP_OK(relay_chn_notify_state_change(ch, RELAY_CHN_STATE_IDLE, RELAY_CHN_STATE_FORWARD)); } // 4. Send one more event to fill the last slot in the queue. This should succeed. TEST_ESP_OK(relay_chn_notify_state_change(0, RELAY_CHN_STATE_IDLE, RELAY_CHN_STATE_FORWARD)); // Use any valid channel // 5. Now the queue is full. Trigger one more event to cause an overflow. // This call should fail and log the warning. TEST_ASSERT_EQUAL(ESP_FAIL, relay_chn_notify_state_change(1 % CONFIG_RELAY_CHN_COUNT, RELAY_CHN_STATE_IDLE, RELAY_CHN_STATE_FORWARD)); // 6. Wait for the "queue full" log message to be captured by our vprintf hook TEST_ASSERT_TRUE_MESSAGE(xSemaphoreTake(log_check_sem, pdMS_TO_TICKS(1000)) == pdTRUE, "Did not receive 'queue full' log message"); // 7. Unblock the listener so it can process all queued items. // There was 1 initial event + QUEUE_LEN events that were successfully queued. for (int i = 0; i < TEST_RELAY_CHN_NOTIFY_QUEUE_LEN + 1; i++) { xSemaphoreGive(blocking_listener_sem); // Give the dispatcher task a moment to process one item from the queue vTaskDelay(pdMS_TO_TICKS(10)); } // 8. Verify the listener was called exactly QUEUE_LEN + 1 times TEST_ASSERT_EQUAL_INT(TEST_RELAY_CHN_NOTIFY_QUEUE_LEN + 1, blocking_listener_call_count); // 9. Cleanup esp_log_set_vprintf(original_vprintf); relay_chn_unregister_listener(blocking_listener); vSemaphoreDelete(blocking_listener_sem); vSemaphoreDelete(log_check_sem); blocking_listener_sem = NULL; log_check_sem = NULL; original_vprintf = NULL; }