403 lines
15 KiB
C
403 lines
15 KiB
C
/**
|
|
* Copyright (c) 2026 Dominic Masters
|
|
*
|
|
* This software is released under the MIT License.
|
|
* https://opensource.org/licenses/MIT
|
|
*/
|
|
|
|
#include "dusktest.h"
|
|
#include "asset/asset.h"
|
|
#include "asset/loader/assetloader.h"
|
|
#include "asset/loader/assetentry.h"
|
|
#include "util/memory.h"
|
|
#include "util/string.h"
|
|
|
|
// ============================================================
|
|
// Stub loader callbacks
|
|
// ============================================================
|
|
|
|
static errorret_t stub_load_success(assetloading_t *loading) {
|
|
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
|
|
errorOk();
|
|
}
|
|
|
|
static errorret_t stub_load_fail(assetloading_t *loading) {
|
|
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
|
|
errorThrow("Stub loader failed");
|
|
}
|
|
|
|
static errorret_t stub_dispose(assetentry_t *entry) {
|
|
errorOk();
|
|
}
|
|
|
|
// ============================================================
|
|
// Per-test setup / teardown
|
|
// ============================================================
|
|
|
|
static assetloadercallbacks_t saved_callbacks[ASSET_LOADER_TYPE_COUNT];
|
|
|
|
static int asset_setup(void **state) {
|
|
// Save real callbacks so we can restore them in teardown.
|
|
memoryCopy(saved_callbacks, ASSET_LOADER_CALLBACKS, sizeof(saved_callbacks));
|
|
|
|
// Manually init ASSET — no thread, no ZIP.
|
|
memoryZero(&ASSET, sizeof(ASSET));
|
|
for(size_t i = 0; i < ASSET_LOADING_COUNT_MAX; i++) {
|
|
threadMutexInit(&ASSET.loading[i].mutex);
|
|
}
|
|
|
|
// Replace all loader callbacks with stubs.
|
|
for(int i = 0; i < ASSET_LOADER_TYPE_COUNT; i++) {
|
|
ASSET_LOADER_CALLBACKS[i].loadSync = stub_load_success;
|
|
ASSET_LOADER_CALLBACKS[i].dispose = stub_dispose;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int asset_teardown(void **state) {
|
|
// Dispose any entries that tests left behind.
|
|
for(int i = 0; i < ASSET_ENTRY_COUNT_MAX; i++) {
|
|
if(ASSET.entries[i].type != ASSET_LOADER_TYPE_NULL) {
|
|
errorret_t ret = assetEntryDispose(&ASSET.entries[i]);
|
|
if(errorIsNotOk(ret)) errorCatch(ret);
|
|
}
|
|
}
|
|
|
|
for(size_t i = 0; i < ASSET_LOADING_COUNT_MAX; i++) {
|
|
threadMutexDispose(&ASSET.loading[i].mutex);
|
|
}
|
|
|
|
// Restore real callbacks before zeroing state.
|
|
memoryCopy(ASSET_LOADER_CALLBACKS, saved_callbacks, sizeof(saved_callbacks));
|
|
memoryZero(&ASSET, sizeof(ASSET));
|
|
return 0;
|
|
}
|
|
|
|
// ============================================================
|
|
// Helper: find which loading slot owns a given entry
|
|
// ============================================================
|
|
|
|
static bool_t loading_slot_has_entry(const assetentry_t *entry) {
|
|
for(size_t i = 0; i < ASSET_LOADING_COUNT_MAX; i++) {
|
|
if(ASSET.loading[i].entry == entry) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// ============================================================
|
|
// assetGetEntry tests
|
|
// ============================================================
|
|
|
|
static void test_getEntry_creates_new(void **state) {
|
|
assetentry_t *entry = assetGetEntry("test.locale", ASSET_LOADER_TYPE_LOCALE, NULL);
|
|
|
|
assert_non_null(entry);
|
|
assert_int_equal(entry->state, ASSET_ENTRY_STATE_NOT_STARTED);
|
|
assert_int_equal(entry->type, ASSET_LOADER_TYPE_LOCALE);
|
|
assert_true(stringEquals(entry->name, "test.locale"));
|
|
|
|
assert_int_equal(memoryGetAllocatedCount(), 0);
|
|
}
|
|
|
|
static void test_getEntry_dedup(void **state) {
|
|
assetentry_t *a = assetGetEntry("test.locale", ASSET_LOADER_TYPE_LOCALE, NULL);
|
|
assetentry_t *b = assetGetEntry("test.locale", ASSET_LOADER_TYPE_LOCALE, NULL);
|
|
|
|
assert_ptr_equal(a, b);
|
|
assert_int_equal(memoryGetAllocatedCount(), 0);
|
|
}
|
|
|
|
static void test_getEntry_distinct_names(void **state) {
|
|
assetentry_t *a = assetGetEntry("a.locale", ASSET_LOADER_TYPE_LOCALE, NULL);
|
|
assetentry_t *b = assetGetEntry("b.locale", ASSET_LOADER_TYPE_LOCALE, NULL);
|
|
|
|
assert_ptr_not_equal(a, b);
|
|
assert_int_equal(memoryGetAllocatedCount(), 0);
|
|
}
|
|
|
|
// ============================================================
|
|
// assetUpdate — state machine tests
|
|
// ============================================================
|
|
|
|
static void test_update_entry_reaches_loaded(void **state) {
|
|
assetentry_t *entry = assetGetEntry("test.locale", ASSET_LOADER_TYPE_LOCALE, NULL);
|
|
assert_int_equal(entry->state, ASSET_ENTRY_STATE_NOT_STARTED);
|
|
|
|
errorret_t ret = assetUpdate();
|
|
assert_true(errorIsOk(ret));
|
|
assert_int_equal(entry->state, ASSET_ENTRY_STATE_LOADED);
|
|
|
|
assert_int_equal(memoryGetAllocatedCount(), 0);
|
|
}
|
|
|
|
static void test_update_slot_occupied_after_first_update(void **state) {
|
|
assetentry_t *entry = assetGetEntry("test.locale", ASSET_LOADER_TYPE_LOCALE, NULL);
|
|
|
|
assetUpdate();
|
|
assert_int_equal(entry->state, ASSET_ENTRY_STATE_LOADED);
|
|
|
|
// Slot not cleared until the second update processes the LOADED case.
|
|
assert_true(loading_slot_has_entry(entry));
|
|
|
|
assert_int_equal(memoryGetAllocatedCount(), 0);
|
|
}
|
|
|
|
static void test_update_slot_cleared_after_second_update(void **state) {
|
|
assetentry_t *entry = assetGetEntry("test.locale", ASSET_LOADER_TYPE_LOCALE, NULL);
|
|
|
|
assetUpdate();
|
|
assetUpdate();
|
|
|
|
assert_false(loading_slot_has_entry(entry));
|
|
assert_int_equal(entry->state, ASSET_ENTRY_STATE_LOADED);
|
|
|
|
assert_int_equal(memoryGetAllocatedCount(), 0);
|
|
}
|
|
|
|
static void test_update_four_slots_fill_independently(void **state) {
|
|
// ASSET_LOADING_COUNT_MAX concurrent entries should all load in one pass.
|
|
assetentry_t *entries[ASSET_LOADING_COUNT_MAX];
|
|
for(int i = 0; i < ASSET_LOADING_COUNT_MAX; 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);
|
|
}
|
|
|
|
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(memoryGetAllocatedCount(), 0);
|
|
}
|
|
|
|
static void test_update_error_state(void **state) {
|
|
ASSET_LOADER_CALLBACKS[ASSET_LOADER_TYPE_LOCALE].loadSync = stub_load_fail;
|
|
|
|
assetentry_t *entry = assetGetEntry("fail.locale", ASSET_LOADER_TYPE_LOCALE, NULL);
|
|
|
|
// First update: dispatches and calls the failing stub.
|
|
// assetUpdate itself returns OK here; the error from loadSync is caught internally.
|
|
errorret_t ret = assetUpdate();
|
|
assert_true(errorIsOk(ret));
|
|
assert_int_equal(entry->state, ASSET_ENTRY_STATE_ERROR);
|
|
|
|
// Second update: sees ERROR state and propagates it.
|
|
ret = assetUpdate();
|
|
assert_true(errorIsNotOk(ret));
|
|
errorCatch(ret);
|
|
|
|
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
|
|
// ============================================================
|
|
|
|
static void test_requireLoaded_already_loaded(void **state) {
|
|
assetentry_t *entry = assetGetEntry("test.locale", ASSET_LOADER_TYPE_LOCALE, NULL);
|
|
assetUpdate();
|
|
assert_int_equal(entry->state, ASSET_ENTRY_STATE_LOADED);
|
|
|
|
// Should return immediately without calling assetUpdate again.
|
|
errorret_t ret = assetRequireLoaded(entry);
|
|
assert_true(errorIsOk(ret));
|
|
|
|
assert_int_equal(memoryGetAllocatedCount(), 0);
|
|
}
|
|
|
|
static void test_requireLoaded_spins_to_loaded(void **state) {
|
|
assetentry_t *entry = assetGetEntry("test.locale", ASSET_LOADER_TYPE_LOCALE, NULL);
|
|
assert_int_equal(entry->state, ASSET_ENTRY_STATE_NOT_STARTED);
|
|
|
|
// requireLoaded calls assetUpdate internally until LOADED.
|
|
errorret_t ret = assetRequireLoaded(entry);
|
|
assert_true(errorIsOk(ret));
|
|
assert_int_equal(entry->state, ASSET_ENTRY_STATE_LOADED);
|
|
|
|
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);
|
|
}
|