/** * 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" #define TEST_EVENT_MAX 4 static void incrementCallback(void *params, void *user) { (*(int32_t *)user)++; } static void recordingCallback(void *params, void *user) { *(void **)user = params; } // ============================================================ // eventInit // ============================================================ static void test_eventInit_clears_state(void **state) { eventcallback_t callbacks[TEST_EVENT_MAX]; void *users[TEST_EVENT_MAX]; event_t event; eventInit(&event, callbacks, users, TEST_EVENT_MAX); assert_int_equal((int)event.count, 0); assert_int_equal((int)event.size, TEST_EVENT_MAX); assert_ptr_equal(event.callbacks, callbacks); assert_ptr_equal(event.users, users); } // ============================================================ // eventSubscribe / eventInvoke // ============================================================ static void test_subscribe_and_invoke_single(void **state) { eventcallback_t callbacks[TEST_EVENT_MAX]; void *users[TEST_EVENT_MAX]; event_t event; eventInit(&event, callbacks, users, TEST_EVENT_MAX); int32_t counter = 0; eventSubscribe(&event, incrementCallback, &counter); eventInvoke(&event, NULL); assert_int_equal(counter, 1); eventInvoke(&event, NULL); assert_int_equal(counter, 2); } static void test_invoke_passes_params_through(void **state) { eventcallback_t callbacks[TEST_EVENT_MAX]; void *users[TEST_EVENT_MAX]; event_t event; eventInit(&event, callbacks, users, TEST_EVENT_MAX); void *seenParams = NULL; eventSubscribe(&event, recordingCallback, &seenParams); int32_t sentinel = 0; eventInvoke(&event, &sentinel); assert_ptr_equal(seenParams, &sentinel); } static void test_subscribe_multiple_distinct_callbacks(void **state) { eventcallback_t callbacks[TEST_EVENT_MAX]; void *users[TEST_EVENT_MAX]; event_t event; eventInit(&event, callbacks, users, TEST_EVENT_MAX); int32_t counterA = 0, counterB = 0; eventSubscribe(&event, incrementCallback, &counterA); eventSubscribe(&event, incrementCallback, &counterB); eventInvoke(&event, NULL); assert_int_equal(counterA, 1); assert_int_equal(counterB, 1); } // This is the exact shape assetbatch.c relies on: two independent owners // (e.g. two assetbatch_t's) both waiting on the same cached/shared asset // entry subscribe the SAME callback function, distinguished only by a // different `user` pointer. Before the fix, eventSubscribe's dedup check // matched on callback alone and would assertUnreachable() here. static void test_subscribe_same_callback_different_users_both_fire( void **state ) { eventcallback_t callbacks[TEST_EVENT_MAX]; void *users[TEST_EVENT_MAX]; event_t event; eventInit(&event, callbacks, users, TEST_EVENT_MAX); int32_t counterA = 0, counterB = 0; eventSubscribe(&event, incrementCallback, &counterA); eventSubscribe(&event, incrementCallback, &counterB); assert_int_equal((int)event.count, 2); eventInvoke(&event, NULL); assert_int_equal(counterA, 1); assert_int_equal(counterB, 1); } static void test_subscribe_duplicate_pair_asserts(void **state) { eventcallback_t callbacks[TEST_EVENT_MAX]; void *users[TEST_EVENT_MAX]; event_t event; eventInit(&event, callbacks, users, TEST_EVENT_MAX); int32_t counter = 0; eventSubscribe(&event, incrementCallback, &counter); // Same (callback, user) pair a second time must still assert. expect_assert_failure(eventSubscribe(&event, incrementCallback, &counter)); } static void test_subscribe_capacity_exceeded_asserts(void **state) { eventcallback_t callbacks[1]; void *users[1]; event_t event; eventInit(&event, callbacks, users, 1); int32_t counterA = 0, counterB = 0; eventSubscribe(&event, incrementCallback, &counterA); expect_assert_failure(eventSubscribe(&event, incrementCallback, &counterB)); } // ============================================================ // eventUnsubscribe // ============================================================ static void test_unsubscribe_removes_only_matching_pair(void **state) { eventcallback_t callbacks[TEST_EVENT_MAX]; void *users[TEST_EVENT_MAX]; event_t event; eventInit(&event, callbacks, users, TEST_EVENT_MAX); int32_t counterA = 0, counterB = 0; eventSubscribe(&event, incrementCallback, &counterA); eventSubscribe(&event, incrementCallback, &counterB); // Unsubscribing A's registration must not disturb B's, even though they // share the same callback function pointer. eventUnsubscribe(&event, incrementCallback, &counterA); assert_int_equal((int)event.count, 1); eventInvoke(&event, NULL); assert_int_equal(counterA, 0); assert_int_equal(counterB, 1); } static void test_unsubscribe_nonexistent_pair_is_noop(void **state) { eventcallback_t callbacks[TEST_EVENT_MAX]; void *users[TEST_EVENT_MAX]; event_t event; eventInit(&event, callbacks, users, TEST_EVENT_MAX); int32_t counterA = 0, counterB = 0; eventSubscribe(&event, incrementCallback, &counterA); // Same callback, but a user pointer that was never subscribed. eventUnsubscribe(&event, incrementCallback, &counterB); assert_int_equal((int)event.count, 1); eventInvoke(&event, NULL); assert_int_equal(counterA, 1); } static void test_unsubscribe_from_middle_keeps_remaining_working( void **state ) { eventcallback_t callbacks[TEST_EVENT_MAX]; void *users[TEST_EVENT_MAX]; event_t event; eventInit(&event, callbacks, users, TEST_EVENT_MAX); int32_t counterA = 0, counterB = 0, counterC = 0; eventSubscribe(&event, incrementCallback, &counterA); eventSubscribe(&event, incrementCallback, &counterB); eventSubscribe(&event, incrementCallback, &counterC); // Removing the middle subscriber swaps the last slot into its place; // verify every remaining subscriber still fires exactly once. eventUnsubscribe(&event, incrementCallback, &counterB); assert_int_equal((int)event.count, 2); eventInvoke(&event, NULL); assert_int_equal(counterA, 1); assert_int_equal(counterB, 0); assert_int_equal(counterC, 1); } // ============================================================ // main // ============================================================ int main(void) { assertInit(); const struct CMUnitTest tests[] = { cmocka_unit_test(test_eventInit_clears_state), cmocka_unit_test(test_subscribe_and_invoke_single), cmocka_unit_test(test_invoke_passes_params_through), cmocka_unit_test(test_subscribe_multiple_distinct_callbacks), cmocka_unit_test(test_subscribe_same_callback_different_users_both_fire), cmocka_unit_test(test_subscribe_duplicate_pair_asserts), cmocka_unit_test(test_subscribe_capacity_exceeded_asserts), cmocka_unit_test(test_unsubscribe_removes_only_matching_pair), cmocka_unit_test(test_unsubscribe_nonexistent_pair_is_noop), cmocka_unit_test(test_unsubscribe_from_middle_keeps_remaining_working), }; return cmocka_run_group_tests(tests, NULL, NULL); }