/** * Copyright (c) 2026 Dominic Masters * * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ #include "dusktest.h" #include "event/event.h" typedef struct { int32_t callCount; void *lastParams; void *lastUser; } eventrecord_t; // Distinct function pointers are required per-subscriber - the same callback // pointer cannot be subscribed twice, even with a different user pointer. static void helper_recordA(void *params, void *user) { eventrecord_t *record = (eventrecord_t *)user; record->callCount++; record->lastParams = params; record->lastUser = user; } static void helper_recordB(void *params, void *user) { eventrecord_t *record = (eventrecord_t *)user; record->callCount++; record->lastParams = params; record->lastUser = user; } static void helper_recordC(void *params, void *user) { eventrecord_t *record = (eventrecord_t *)user; record->callCount++; record->lastParams = params; record->lastUser = user; } // Records via `params` instead of `user` - for cases exercising a NULL user // (e.g. no users array), where dereferencing `user` would crash. static void helper_recordViaParams(void *params, void *user) { eventrecord_t *record = (eventrecord_t *)params; record->callCount++; record->lastParams = params; record->lastUser = user; } #define EVENT_CAPACITY 4 // --- eventInit --- static void test_eventInitSetsUpBackingArrays(void **state) { eventcallback_t callbacks[EVENT_CAPACITY]; void *users[EVENT_CAPACITY]; memset(callbacks, 0xAA, sizeof(callbacks)); memset(users, 0xAA, sizeof(users)); event_t event; eventInit(&event, callbacks, users, EVENT_CAPACITY); assert_ptr_equal(event.callbacks, callbacks); assert_ptr_equal(event.users, users); assert_int_equal(event.size, EVENT_CAPACITY); assert_int_equal(event.count, 0); for(int32_t i = 0; i < EVENT_CAPACITY; i++) { assert_null(callbacks[i]); assert_null(users[i]); } } static void test_eventInitAllowsNullUsersArray(void **state) { eventcallback_t callbacks[EVENT_CAPACITY]; event_t event; eventInit(&event, callbacks, NULL, EVENT_CAPACITY); assert_null(event.users); } static void test_eventInitResetClearsSubscribersOnly(void **state) { eventcallback_t callbacks[EVENT_CAPACITY]; void *users[EVENT_CAPACITY]; eventrecord_t record = { 0 }; event_t event; eventInit(&event, callbacks, users, EVENT_CAPACITY); eventSubscribe(&event, helper_recordA, &record); assert_int_equal(event.count, 1); // Re-init (reset) with the same backing arrays. eventInit(&event, callbacks, users, EVENT_CAPACITY); assert_ptr_equal(event.callbacks, callbacks); assert_int_equal(event.count, 0); assert_null(callbacks[0]); assert_null(users[0]); } static void test_eventInitNullEventAsserts(void **state) { eventcallback_t callbacks[EVENT_CAPACITY]; expect_assert_failure(eventInit(NULL, callbacks, NULL, EVENT_CAPACITY)); } static void test_eventInitNullCallbacksAsserts(void **state) { event_t event; expect_assert_failure(eventInit(&event, NULL, NULL, EVENT_CAPACITY)); } static void test_eventInitZeroSizeAsserts(void **state) { eventcallback_t callbacks[EVENT_CAPACITY]; event_t event; expect_assert_failure(eventInit(&event, callbacks, NULL, 0)); } // --- eventSubscribe --- static void test_eventSubscribeAddsCallback(void **state) { eventcallback_t callbacks[EVENT_CAPACITY]; void *users[EVENT_CAPACITY]; eventrecord_t record = { 0 }; event_t event; eventInit(&event, callbacks, users, EVENT_CAPACITY); eventSubscribe(&event, helper_recordA, &record); assert_int_equal(event.count, 1); assert_ptr_equal(event.callbacks[0], helper_recordA); assert_ptr_equal(event.users[0], &record); } static void test_eventSubscribeMultiple(void **state) { eventcallback_t callbacks[EVENT_CAPACITY]; void *users[EVENT_CAPACITY]; eventrecord_t recordA = { 0 }, recordB = { 0 }, recordC = { 0 }; event_t event; eventInit(&event, callbacks, users, EVENT_CAPACITY); eventSubscribe(&event, helper_recordA, &recordA); eventSubscribe(&event, helper_recordB, &recordB); eventSubscribe(&event, helper_recordC, &recordC); assert_int_equal(event.count, 3); } static void test_eventSubscribeNullUserLeavesSlotNull(void **state) { eventcallback_t callbacks[EVENT_CAPACITY]; void *users[EVENT_CAPACITY]; event_t event; eventInit(&event, callbacks, users, EVENT_CAPACITY); eventSubscribe(&event, helper_recordA, NULL); assert_null(event.users[0]); } static void test_eventSubscribeNullUserAfterUnsubscribeStaysNull(void **state) { // The vacated slot must actually be cleared, not left holding a stale // user pointer from whatever previously occupied it. eventcallback_t callbacks[EVENT_CAPACITY]; void *users[EVENT_CAPACITY]; eventrecord_t recordA = { 0 }, recordB = { 0 }; event_t event; eventInit(&event, callbacks, users, EVENT_CAPACITY); eventSubscribe(&event, helper_recordA, &recordA); eventSubscribe(&event, helper_recordB, &recordB); eventUnsubscribe(&event, helper_recordA); eventSubscribe(&event, helper_recordC, NULL); for(int32_t i = 0; i < event.count; i++) { if(event.callbacks[i] == helper_recordC) { assert_null(event.users[i]); return; } } fail_msg("helper_recordC was not found after subscribing."); } static void test_eventSubscribeUserWithoutUsersArrayAsserts(void **state) { eventcallback_t callbacks[EVENT_CAPACITY]; eventrecord_t record = { 0 }; event_t event; eventInit(&event, callbacks, NULL, EVENT_CAPACITY); expect_assert_failure(eventSubscribe(&event, helper_recordA, &record)); } static void test_eventSubscribeDuplicateCallbackAsserts(void **state) { eventcallback_t callbacks[EVENT_CAPACITY]; void *users[EVENT_CAPACITY]; eventrecord_t record = { 0 }; event_t event; eventInit(&event, callbacks, users, EVENT_CAPACITY); eventSubscribe(&event, helper_recordA, &record); expect_assert_failure(eventSubscribe(&event, helper_recordA, &record)); } static void test_eventSubscribeSameCallbackDifferentUserStillAsserts(void **state) { // Documented as "the same (callback, user) pair may only be subscribed // once", implying a different user should be fine - but the actual // uniqueness check only looks at the callback pointer, so this asserts // too. Captures actual behavior; flag if the doc/implementation should // instead match on the (callback, user) pair. eventcallback_t callbacks[EVENT_CAPACITY]; void *users[EVENT_CAPACITY]; eventrecord_t recordA = { 0 }, recordB = { 0 }; event_t event; eventInit(&event, callbacks, users, EVENT_CAPACITY); eventSubscribe(&event, helper_recordA, &recordA); expect_assert_failure(eventSubscribe(&event, helper_recordA, &recordB)); } static void test_eventSubscribeCapacityExceededAsserts(void **state) { eventcallback_t callbacks[2]; void *users[2]; eventrecord_t recordA = { 0 }, recordB = { 0 }, recordC = { 0 }; event_t event; eventInit(&event, callbacks, users, 2); eventSubscribe(&event, helper_recordA, &recordA); eventSubscribe(&event, helper_recordB, &recordB); expect_assert_failure(eventSubscribe(&event, helper_recordC, &recordC)); } static void test_eventSubscribeNullEventAsserts(void **state) { expect_assert_failure(eventSubscribe(NULL, helper_recordA, NULL)); } static void test_eventSubscribeNullCallbackAsserts(void **state) { eventcallback_t callbacks[EVENT_CAPACITY]; event_t event; eventInit(&event, callbacks, NULL, EVENT_CAPACITY); expect_assert_failure(eventSubscribe(&event, NULL, NULL)); } // --- eventUnsubscribe --- static void test_eventUnsubscribeMiddleSwapsLastIntoPlace(void **state) { eventcallback_t callbacks[EVENT_CAPACITY]; void *users[EVENT_CAPACITY]; eventrecord_t recordA = { 0 }, recordB = { 0 }, recordC = { 0 }; event_t event; eventInit(&event, callbacks, users, EVENT_CAPACITY); eventSubscribe(&event, helper_recordA, &recordA); eventSubscribe(&event, helper_recordB, &recordB); eventSubscribe(&event, helper_recordC, &recordC); eventUnsubscribe(&event, helper_recordB); assert_int_equal(event.count, 2); assert_ptr_equal(event.callbacks[0], helper_recordA); // recordC (previously last) was swapped into the vacated middle slot. assert_ptr_equal(event.callbacks[1], helper_recordC); assert_ptr_equal(event.users[1], &recordC); // The old tail slot is fully cleared. assert_null(event.callbacks[2]); assert_null(event.users[2]); } static void test_eventUnsubscribeLastElement(void **state) { eventcallback_t callbacks[EVENT_CAPACITY]; void *users[EVENT_CAPACITY]; eventrecord_t recordA = { 0 }, recordB = { 0 }; event_t event; eventInit(&event, callbacks, users, EVENT_CAPACITY); eventSubscribe(&event, helper_recordA, &recordA); eventSubscribe(&event, helper_recordB, &recordB); eventUnsubscribe(&event, helper_recordB); assert_int_equal(event.count, 1); assert_ptr_equal(event.callbacks[0], helper_recordA); assert_null(event.callbacks[1]); } static void test_eventUnsubscribeNotSubscribedIsNoop(void **state) { eventcallback_t callbacks[EVENT_CAPACITY]; void *users[EVENT_CAPACITY]; eventrecord_t recordA = { 0 }; event_t event; eventInit(&event, callbacks, users, EVENT_CAPACITY); eventSubscribe(&event, helper_recordA, &recordA); eventUnsubscribe(&event, helper_recordB); assert_int_equal(event.count, 1); assert_ptr_equal(event.callbacks[0], helper_recordA); } static void test_eventUnsubscribeThenResubscribeSucceeds(void **state) { eventcallback_t callbacks[EVENT_CAPACITY]; void *users[EVENT_CAPACITY]; eventrecord_t recordA = { 0 }; event_t event; eventInit(&event, callbacks, users, EVENT_CAPACITY); eventSubscribe(&event, helper_recordA, &recordA); eventUnsubscribe(&event, helper_recordA); // Would have asserted (duplicate) had the removal not actually happened. eventSubscribe(&event, helper_recordA, &recordA); assert_int_equal(event.count, 1); } static void test_eventUnsubscribeWithNullUsersArrayIsSafe(void **state) { eventcallback_t callbacks[EVENT_CAPACITY]; event_t event; eventInit(&event, callbacks, NULL, EVENT_CAPACITY); eventSubscribe(&event, helper_recordA, NULL); eventUnsubscribe(&event, helper_recordA); assert_int_equal(event.count, 0); } static void test_eventUnsubscribeNullEventAsserts(void **state) { expect_assert_failure(eventUnsubscribe(NULL, helper_recordA)); } static void test_eventUnsubscribeNullCallbackAsserts(void **state) { eventcallback_t callbacks[EVENT_CAPACITY]; event_t event; eventInit(&event, callbacks, NULL, EVENT_CAPACITY); expect_assert_failure(eventUnsubscribe(&event, NULL)); } // --- eventInvoke --- static void test_eventInvokeCallsAllSubscribersWithParamsAndUser(void **state) { eventcallback_t callbacks[EVENT_CAPACITY]; void *users[EVENT_CAPACITY]; eventrecord_t recordA = { 0 }, recordB = { 0 }; int32_t params = 123; event_t event; eventInit(&event, callbacks, users, EVENT_CAPACITY); eventSubscribe(&event, helper_recordA, &recordA); eventSubscribe(&event, helper_recordB, &recordB); eventInvoke(&event, ¶ms); assert_int_equal(recordA.callCount, 1); assert_ptr_equal(recordA.lastParams, ¶ms); assert_ptr_equal(recordA.lastUser, &recordA); assert_int_equal(recordB.callCount, 1); assert_ptr_equal(recordB.lastParams, ¶ms); assert_ptr_equal(recordB.lastUser, &recordB); } static void test_eventInvokeWithNoSubscribersDoesNothing(void **state) { eventcallback_t callbacks[EVENT_CAPACITY]; void *users[EVENT_CAPACITY]; event_t event; eventInit(&event, callbacks, users, EVENT_CAPACITY); // No subscribers exist, so there is nothing to observe beyond "no crash". eventInvoke(&event, NULL); } static void test_eventInvokeWithNullUsersArrayPassesNull(void **state) { eventcallback_t callbacks[EVENT_CAPACITY]; eventrecord_t record = { 0 }; event_t event; eventInit(&event, callbacks, NULL, EVENT_CAPACITY); eventSubscribe(&event, helper_recordViaParams, NULL); eventInvoke(&event, &record); assert_int_equal(record.callCount, 1); assert_ptr_equal(record.lastParams, &record); assert_null(record.lastUser); } static void test_eventInvokeSkipsUnsubscribedCallback(void **state) { eventcallback_t callbacks[EVENT_CAPACITY]; void *users[EVENT_CAPACITY]; eventrecord_t recordA = { 0 }, recordB = { 0 }; event_t event; eventInit(&event, callbacks, users, EVENT_CAPACITY); eventSubscribe(&event, helper_recordA, &recordA); eventSubscribe(&event, helper_recordB, &recordB); eventUnsubscribe(&event, helper_recordA); eventInvoke(&event, NULL); assert_int_equal(recordA.callCount, 0); assert_int_equal(recordB.callCount, 1); } static void test_eventInvokeNullEventAsserts(void **state) { expect_assert_failure(eventInvoke(NULL, NULL)); } int main(void) { const struct CMUnitTest tests[] = { cmocka_unit_test(test_eventInitSetsUpBackingArrays), cmocka_unit_test(test_eventInitAllowsNullUsersArray), cmocka_unit_test(test_eventInitResetClearsSubscribersOnly), cmocka_unit_test(test_eventInitNullEventAsserts), cmocka_unit_test(test_eventInitNullCallbacksAsserts), cmocka_unit_test(test_eventInitZeroSizeAsserts), cmocka_unit_test(test_eventSubscribeAddsCallback), cmocka_unit_test(test_eventSubscribeMultiple), cmocka_unit_test(test_eventSubscribeNullUserLeavesSlotNull), cmocka_unit_test(test_eventSubscribeNullUserAfterUnsubscribeStaysNull), cmocka_unit_test(test_eventSubscribeUserWithoutUsersArrayAsserts), cmocka_unit_test(test_eventSubscribeDuplicateCallbackAsserts), cmocka_unit_test(test_eventSubscribeSameCallbackDifferentUserStillAsserts), cmocka_unit_test(test_eventSubscribeCapacityExceededAsserts), cmocka_unit_test(test_eventSubscribeNullEventAsserts), cmocka_unit_test(test_eventSubscribeNullCallbackAsserts), cmocka_unit_test(test_eventUnsubscribeMiddleSwapsLastIntoPlace), cmocka_unit_test(test_eventUnsubscribeLastElement), cmocka_unit_test(test_eventUnsubscribeNotSubscribedIsNoop), cmocka_unit_test(test_eventUnsubscribeThenResubscribeSucceeds), cmocka_unit_test(test_eventUnsubscribeWithNullUsersArrayIsSafe), cmocka_unit_test(test_eventUnsubscribeNullEventAsserts), cmocka_unit_test(test_eventUnsubscribeNullCallbackAsserts), cmocka_unit_test(test_eventInvokeCallsAllSubscribersWithParamsAndUser), cmocka_unit_test(test_eventInvokeWithNoSubscribersDoesNothing), cmocka_unit_test(test_eventInvokeWithNullUsersArrayPassesNull), cmocka_unit_test(test_eventInvokeSkipsUnsubscribedCallback), cmocka_unit_test(test_eventInvokeNullEventAsserts), }; return cmocka_run_group_tests(tests, NULL, NULL); }