From eedb7769e6c2ccf7a57657b22a556f89465c6c2f Mon Sep 17 00:00:00 2001 From: Dominic Masters Date: Mon, 1 Jun 2026 13:48:29 -0500 Subject: [PATCH] Add some extra tests --- scripts/test-linux.sh | 2 + test/asset/test_asset.c | 160 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 162 insertions(+) diff --git a/scripts/test-linux.sh b/scripts/test-linux.sh index 977eb1a4..2c63bff4 100755 --- a/scripts/test-linux.sh +++ b/scripts/test-linux.sh @@ -1,4 +1,6 @@ #!/bin/bash +set -e +rm -rf build-tests cmake -S . -B build-tests -DDUSK_BUILD_TESTS=ON -DDUSK_TARGET_SYSTEM=linux cmake --build build-tests -- -j$(nproc) ctest --output-on-failure --test-dir build-tests \ No newline at end of file diff --git a/test/asset/test_asset.c b/test/asset/test_asset.c index 8eaad51a..7f6d4b59 100644 --- a/test/asset/test_asset.c +++ b/test/asset/test_asset.c @@ -193,6 +193,136 @@ static void test_update_error_state(void **state) { assert_int_equal(memoryGetAllocatedCount(), 0); } +static void test_update_noop_on_empty_table(void **state) { + errorret_t ret = assetUpdate(); + assert_true(errorIsOk(ret)); + assert_int_equal(memoryGetAllocatedCount(), 0); +} + +static void test_update_loaded_entry_not_redispatched(void **state) { + assetentry_t *entry = assetGetEntry("test.locale", ASSET_LOADER_TYPE_LOCALE, NULL); + assetUpdate(); + assetUpdate(); // slot freed + assert_int_equal(entry->state, ASSET_ENTRY_STATE_LOADED); + + // Further updates must not re-dispatch or modify a LOADED entry. + assetUpdate(); + assetUpdate(); + assetUpdate(); + + assert_int_equal(entry->state, ASSET_ENTRY_STATE_LOADED); + assert_false(loading_slot_has_entry(entry)); + assert_int_equal(memoryGetAllocatedCount(), 0); +} + +static void test_update_overflow_queues_entries(void **state) { + // Create one more entry than there are loading slots. + const int TOTAL = ASSET_LOADING_COUNT_MAX + 1; + assetentry_t *entries[ASSET_LOADING_COUNT_MAX + 1]; + + for(int i = 0; i < TOTAL; i++) { + char_t name[ASSET_FILE_NAME_MAX]; + snprintf(name, sizeof(name), "asset%d.locale", i); + entries[i] = assetGetEntry(name, ASSET_LOADER_TYPE_LOCALE, NULL); + } + + // Update 1: fills all slots, first ASSET_LOADING_COUNT_MAX entries reach LOADED. + // The overflow entry has no slot yet and stays NOT_STARTED. + errorret_t ret = assetUpdate(); + assert_true(errorIsOk(ret)); + for(int i = 0; i < ASSET_LOADING_COUNT_MAX; i++) { + assert_int_equal(entries[i]->state, ASSET_ENTRY_STATE_LOADED); + } + assert_int_equal(entries[ASSET_LOADING_COUNT_MAX]->state, ASSET_ENTRY_STATE_NOT_STARTED); + + // Update 2: LOADED slots are freed. Overflow entry still NOT_STARTED because + // the dispatch phase ran before the slots were cleared this turn. + ret = assetUpdate(); + assert_true(errorIsOk(ret)); + assert_int_equal(entries[ASSET_LOADING_COUNT_MAX]->state, ASSET_ENTRY_STATE_NOT_STARTED); + + // Update 3: now a slot is available; the overflow entry is dispatched and loaded. + ret = assetUpdate(); + assert_true(errorIsOk(ret)); + assert_int_equal(entries[ASSET_LOADING_COUNT_MAX]->state, ASSET_ENTRY_STATE_LOADED); + + assert_int_equal(memoryGetAllocatedCount(), 0); +} + +static void test_update_error_slot_stays_occupied(void **state) { + ASSET_LOADER_CALLBACKS[ASSET_LOADER_TYPE_LOCALE].loadSync = stub_load_fail; + + assetentry_t *entry = assetGetEntry("fail.locale", ASSET_LOADER_TYPE_LOCALE, NULL); + + assetUpdate(); + assert_int_equal(entry->state, ASSET_ENTRY_STATE_ERROR); + + // Unlike LOADED, the ERROR case does NOT clear the slot — it throws instead. + assert_true(loading_slot_has_entry(entry)); + + errorret_t ret = assetUpdate(); + assert_true(errorIsNotOk(ret)); + errorCatch(ret); + + assert_int_equal(memoryGetAllocatedCount(), 0); +} + +// ============================================================ +// assetGetEntry — dedup against non-NOT_STARTED entries +// ============================================================ + +static void test_getEntry_returns_loaded_entry(void **state) { + assetentry_t *a = assetGetEntry("test.locale", ASSET_LOADER_TYPE_LOCALE, NULL); + assetUpdate(); + assert_int_equal(a->state, ASSET_ENTRY_STATE_LOADED); + + // A second request for the same name must return the same entry even though + // it is already LOADED rather than NOT_STARTED. + assetentry_t *b = assetGetEntry("test.locale", ASSET_LOADER_TYPE_LOCALE, NULL); + assert_ptr_equal(a, b); + assert_int_equal(b->state, ASSET_ENTRY_STATE_LOADED); + + assert_int_equal(memoryGetAllocatedCount(), 0); +} + +// ============================================================ +// assetEntryDispose tests +// ============================================================ + +static void test_entry_dispose_clears_entry(void **state) { + assetentry_t *entry = assetGetEntry("test.locale", ASSET_LOADER_TYPE_LOCALE, NULL); + assetUpdate(); + assetUpdate(); // ensure loading slot is freed before disposing + assert_int_equal(entry->state, ASSET_ENTRY_STATE_LOADED); + + errorret_t ret = assetEntryDispose(entry); + assert_true(errorIsOk(ret)); + assert_int_equal(entry->type, ASSET_LOADER_TYPE_NULL); + + assert_int_equal(memoryGetAllocatedCount(), 0); +} + +static void test_entry_dispose_slot_reusable(void **state) { + assetentry_t *a = assetGetEntry("a.locale", ASSET_LOADER_TYPE_LOCALE, NULL); + assetUpdate(); + assetUpdate(); + assert_false(loading_slot_has_entry(a)); + + assetEntryDispose(a); + assert_int_equal(a->type, ASSET_LOADER_TYPE_NULL); + + // The freed slot should now accept a new entry. + assetentry_t *b = assetGetEntry("b.locale", ASSET_LOADER_TYPE_LOCALE, NULL); + assert_non_null(b); + assert_int_equal(b->state, ASSET_ENTRY_STATE_NOT_STARTED); + + errorret_t ret = assetUpdate(); + assert_true(errorIsOk(ret)); + assert_int_equal(b->state, ASSET_ENTRY_STATE_LOADED); + + assert_int_equal(memoryGetAllocatedCount(), 0); +} + // ============================================================ // assetRequireLoaded tests // ============================================================ @@ -221,22 +351,52 @@ static void test_requireLoaded_spins_to_loaded(void **state) { assert_int_equal(memoryGetAllocatedCount(), 0); } +static void test_requireLoaded_propagates_error(void **state) { + ASSET_LOADER_CALLBACKS[ASSET_LOADER_TYPE_LOCALE].loadSync = stub_load_fail; + + assetentry_t *entry = assetGetEntry("fail.locale", ASSET_LOADER_TYPE_LOCALE, NULL); + + // requireLoaded spins assetUpdate until LOADED — but the loader always fails, + // so the second assetUpdate sees ERROR and throws, which errorChain propagates. + errorret_t ret = assetRequireLoaded(entry); + assert_true(errorIsNotOk(ret)); + errorCatch(ret); + + assert_int_equal(entry->state, ASSET_ENTRY_STATE_ERROR); + assert_int_equal(memoryGetAllocatedCount(), 0); +} + // ============================================================ // main // ============================================================ int main(void) { const struct CMUnitTest tests[] = { + // getEntry cmocka_unit_test_setup_teardown(test_getEntry_creates_new, asset_setup, asset_teardown), cmocka_unit_test_setup_teardown(test_getEntry_dedup, asset_setup, asset_teardown), cmocka_unit_test_setup_teardown(test_getEntry_distinct_names, asset_setup, asset_teardown), + cmocka_unit_test_setup_teardown(test_getEntry_returns_loaded_entry, asset_setup, asset_teardown), + + // assetUpdate — state machine + cmocka_unit_test_setup_teardown(test_update_noop_on_empty_table, asset_setup, asset_teardown), cmocka_unit_test_setup_teardown(test_update_entry_reaches_loaded, asset_setup, asset_teardown), cmocka_unit_test_setup_teardown(test_update_slot_occupied_after_first_update, asset_setup, asset_teardown), cmocka_unit_test_setup_teardown(test_update_slot_cleared_after_second_update, asset_setup, asset_teardown), cmocka_unit_test_setup_teardown(test_update_four_slots_fill_independently, asset_setup, asset_teardown), + cmocka_unit_test_setup_teardown(test_update_loaded_entry_not_redispatched, asset_setup, asset_teardown), + cmocka_unit_test_setup_teardown(test_update_overflow_queues_entries, asset_setup, asset_teardown), cmocka_unit_test_setup_teardown(test_update_error_state, asset_setup, asset_teardown), + cmocka_unit_test_setup_teardown(test_update_error_slot_stays_occupied, asset_setup, asset_teardown), + + // assetEntryDispose + cmocka_unit_test_setup_teardown(test_entry_dispose_clears_entry, asset_setup, asset_teardown), + cmocka_unit_test_setup_teardown(test_entry_dispose_slot_reusable, asset_setup, asset_teardown), + + // assetRequireLoaded cmocka_unit_test_setup_teardown(test_requireLoaded_already_loaded, asset_setup, asset_teardown), cmocka_unit_test_setup_teardown(test_requireLoaded_spins_to_loaded, asset_setup, asset_teardown), + cmocka_unit_test_setup_teardown(test_requireLoaded_propagates_error, asset_setup, asset_teardown), }; return cmocka_run_group_tests(tests, NULL, NULL); }