Files
YourWishes 93ab7690ba Add asset/event test coverage, fix caching-breaking eventSubscribe bug
Adds missing test coverage for the asset pipeline's binary loaders
(mesh/model/texture), assetfile.c, and assetbatch.c, plus a new
test/event suite for the shared event primitive.

Found and fixed two real bugs while writing this:
- assetFileRead's NULL-buffer skip path double-counted file->position,
  which could trip stb_image's EOF check early on images that skip
  bytes mid-decode.
- eventSubscribe/eventUnsubscribe matched only on the callback pointer
  instead of the (callback, user) pair the docs already promised, so
  two independent consumers of the same cached asset (e.g. two
  assetbatch_t's) would abort. Covered by dedicated caching tests in
  both test_assetbatch.c and test_assetmodelloader.c.

Also drops test_overworldscene.c, broken by the in-progress
overworldscene.js/init.js export-contract rewrite.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-11 14:34:51 -05:00

222 lines
7.0 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"
#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);
}