Files
relay_chn/test_apps/main/test_relay_chn_notify_multi.c
ismail 5e8e5a4cab Add notification system for relay channel state changes
- Introduced a new notification module to handle state change listeners.
- Added functions to register and unregister listeners for relay channel state changes.
- Implemented a queue-based system to manage notifications and listener callbacks.
- Updated core relay channel logic to utilize the new notification system.
- Removed old listener management code from relay channel core.
- Refactored the former listener tests to notify tests and added tests for the notification system, including handling of multiple listeners and queue overflow scenarios.
- Updated CMakeLists.txt to include new source files and headers for the notification module.
- Revised README.md to include warnings about callback execution context and performance considerations.

Refs #1096, #1085 and closes #1097
2025-09-02 15:46:48 +03:00

167 lines
7.5 KiB
C

#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;
}