444 lines
14 KiB
C
444 lines
14 KiB
C
/**
|
|
* 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);
|
|
}
|