Remove script

This commit is contained in:
2026-06-17 09:54:02 -05:00
parent 43d0593872
commit 0ea6dd9219
234 changed files with 101 additions and 16537 deletions
-1
View File
@@ -4,7 +4,6 @@
# https://opensource.org/licenses/MIT
add_subdirectory(assert)
add_subdirectory(asset)
add_subdirectory(error)
add_subdirectory(thread)
add_subdirectory(display)
-11
View File
@@ -1,11 +0,0 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
include(dusktest)
dusktest(test_assetlocale.c)
dusktest(test_asset.c)
dusktest(test_assetjsonloader.c)
dusktest(test_assettilesetloader.c)
-514
View File
@@ -1,514 +0,0 @@
/**
* 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);
assetEntryLock(entry);
errorret_t ret = assetUpdate();
assert_true(errorIsOk(ret));
assert_int_equal(entry->state, ASSET_ENTRY_STATE_LOADED);
assetEntryUnlock(entry);
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);
assetEntryLock(entry);
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));
assetEntryUnlock(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);
assetEntryLock(entry);
assetUpdate();
assetUpdate();
assert_false(loading_slot_has_entry(entry));
assert_int_equal(entry->state, ASSET_ENTRY_STATE_LOADED);
assetEntryUnlock(entry);
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);
assetEntryLock(entries[i]);
}
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);
assetEntryUnlock(entries[i]);
}
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);
assetEntryLock(entry);
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));
assetEntryUnlock(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);
}
// Lock first batch so the reaper won't collect them before we check.
for(int i = 0; i < ASSET_LOADING_COUNT_MAX; i++) {
assetEntryLock(entries[i]);
}
// 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);
// Unlock first batch — the reaper can collect them in update 2.
for(int i = 0; i < ASSET_LOADING_COUNT_MAX; i++) {
assetEntryUnlock(entries[i]);
}
// 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);
// Lock overflow entry so it stays LOADED after update 3.
assetEntryLock(entries[ASSET_LOADING_COUNT_MAX]);
// 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);
assetEntryUnlock(entries[ASSET_LOADING_COUNT_MAX]);
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);
}
// ============================================================
// assetEntryInit — input copy
// ============================================================
static void test_getEntry_null_input_stays_null(void **state) {
assetentry_t *entry = assetGetEntry("test.locale", ASSET_LOADER_TYPE_LOCALE, NULL);
assert_null(entry->input);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_getEntry_input_copied_into_entry(void **state) {
assetloaderinput_t input;
memoryZero(&input, sizeof(input));
input.texture = (textureformat_t)42;
assetentry_t *entry = assetGetEntry("test.texture", ASSET_LOADER_TYPE_TEXTURE, &input);
// input must have been copied — entry->input must point inside the entry.
assert_non_null(entry->input);
assert_ptr_equal(entry->input, &entry->inputData);
assert_int_equal((int)entry->input->texture, 42);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
// ============================================================
// assetUpdate — re-entrant sync loader
// ============================================================
static assetentry_t *reentrant_inner_entry = NULL;
static bool_t reentrant_inner_loaded = false;
static errorret_t reentrant_stub_load(assetloading_t *loading) {
if(reentrant_inner_entry == NULL) {
// Simulate a script loading another asset from within its own sync step.
reentrant_inner_entry = assetGetEntry(
"inner.json", ASSET_LOADER_TYPE_JSON, NULL
);
errorret_t inner_ret = assetRequireLoaded(reentrant_inner_entry);
reentrant_inner_loaded = errorIsOk(inner_ret);
if(errorIsNotOk(inner_ret)) errorChain(inner_ret);
}
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
errorOk();
}
static void test_update_reentrant_sync_loader(void **state) {
reentrant_inner_entry = NULL;
reentrant_inner_loaded = false;
// LOCALE uses the re-entrant loader; JSON keeps the default stub_load_success.
ASSET_LOADER_CALLBACKS[ASSET_LOADER_TYPE_LOCALE].loadSync = reentrant_stub_load;
assetentry_t *outer = assetGetEntry("outer.locale", ASSET_LOADER_TYPE_LOCALE, NULL);
errorret_t ret = assetRequireLoaded(outer);
assert_true(errorIsOk(ret));
assert_int_equal(outer->state, ASSET_ENTRY_STATE_LOADED);
// Verify the re-entrant load ran and completed successfully.
// (The inner entry may have been reaped by the time we check here,
// so we capture the result inside the callback rather than reading state.)
assert_non_null(reentrant_inner_entry);
assert_true(reentrant_inner_loaded);
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);
assetEntryLock(a);
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);
assetEntryUnlock(a);
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);
assetEntryLock(entry);
assetUpdate();
assetUpdate(); // ensure loading slot is freed before disposing
assert_int_equal(entry->state, ASSET_ENTRY_STATE_LOADED);
assetEntryUnlock(entry);
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);
assetEntryLock(a);
assetUpdate();
assetUpdate();
assetEntryUnlock(a);
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);
assetEntryLock(b);
errorret_t ret = assetUpdate();
assert_true(errorIsOk(ret));
assert_int_equal(b->state, ASSET_ENTRY_STATE_LOADED);
assetEntryUnlock(b);
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);
assetEntryLock(entry);
assetUpdate();
assert_int_equal(entry->state, ASSET_ENTRY_STATE_LOADED);
assetEntryUnlock(entry);
// 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) {
assertInit();
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),
cmocka_unit_test_setup_teardown(test_getEntry_null_input_stays_null, asset_setup, asset_teardown),
cmocka_unit_test_setup_teardown(test_getEntry_input_copied_into_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),
cmocka_unit_test_setup_teardown(test_update_reentrant_sync_loader, 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);
}
-249
View File
@@ -1,249 +0,0 @@
/**
* 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 "asset/loader/json/assetjsonloader.h"
#include "thread/thread.h"
#include "util/memory.h"
#include <zip.h>
// ============================================================
// Fixtures
// ============================================================
static const char_t *JSON_VALID = "{\"hello\":\"world\",\"count\":42}";
static const char_t *JSON_INVALID = "{ this is definitely not valid json !!!";
// ============================================================
// Async thread helper
// ============================================================
typedef struct {
assetloading_t *loading;
bool_t ok;
} json_async_run_t;
static void json_async_thread_cb(thread_t *thread) {
json_async_run_t *run = (json_async_run_t *)thread->data;
errorret_t ret = assetJsonLoaderAsync(run->loading);
run->ok = errorIsOk(ret);
if(errorIsNotOk(ret)) errorCatch(ret);
}
static bool_t run_json_async(assetloading_t *loading) {
json_async_run_t run = { .loading = loading, .ok = false };
thread_t thread;
threadInit(&thread, json_async_thread_cb);
thread.data = &run;
threadStart(&thread);
threadStop(&thread);
return run.ok;
}
// ============================================================
// In-memory ZIP
// ============================================================
static zip_t *g_zip = NULL;
static int zip_setup(void **state) {
zip_error_t err;
zip_error_init(&err);
zip_source_t *write_src = zip_source_buffer_create(NULL, 0, 1, &err);
if(!write_src) return -1;
zip_t *za = zip_open_from_source(write_src, ZIP_TRUNCATE, &err);
if(!za) { zip_source_free(write_src); return -1; }
// valid.json
zip_source_t *s;
s = zip_source_buffer(za, JSON_VALID, strlen(JSON_VALID), 0);
if(zip_file_add(za, "valid.json", s, ZIP_FL_OVERWRITE) < 0) {
zip_close(za); return -1;
}
// invalid.json
s = zip_source_buffer(za, JSON_INVALID, strlen(JSON_INVALID), 0);
if(zip_file_add(za, "invalid.json", s, ZIP_FL_OVERWRITE) < 0) {
zip_close(za); return -1;
}
zip_source_keep(write_src);
if(zip_close(za) != 0) { zip_source_free(write_src); return -1; }
zip_stat_t zs;
memset(&zs, 0, sizeof(zs));
if(zip_source_stat(write_src, &zs) != 0 || !(zs.valid & ZIP_STAT_SIZE)) {
zip_source_free(write_src); return -1;
}
void *zipbuf = malloc((size_t)zs.size);
if(!zipbuf) { zip_source_free(write_src); return -1; }
if(zip_source_open(write_src) != 0) {
free(zipbuf); zip_source_free(write_src); return -1;
}
zip_source_read(write_src, zipbuf, (zip_uint64_t)zs.size);
zip_source_close(write_src);
zip_source_free(write_src);
zip_error_init(&err);
zip_source_t *read_src = zip_source_buffer_create(
zipbuf, (zip_uint64_t)zs.size, 1, &err
);
if(!read_src) { free(zipbuf); return -1; }
g_zip = zip_open_from_source(read_src, 0, &err);
if(!g_zip) { zip_source_free(read_src); return -1; }
ASSET.zip = g_zip;
return 0;
}
static int zip_teardown(void **state) {
if(g_zip) { zip_close(g_zip); g_zip = NULL; }
ASSET.zip = NULL;
return 0;
}
// ============================================================
// Loader pipeline helper
// ============================================================
typedef struct {
assetentry_t entry;
assetloading_t loading;
} loader_ctx_t;
static void loader_ctx_init(loader_ctx_t *ctx, const char_t *name) {
assetEntryInit(&ctx->entry, name, ASSET_LOADER_TYPE_JSON, NULL);
threadMutexInit(&ctx->loading.mutex);
memoryZero(&ctx->loading.loading, sizeof(ctx->loading.loading));
ctx->loading.type = ASSET_LOADER_TYPE_JSON;
ctx->loading.entry = &ctx->entry;
ctx->entry.state = ASSET_ENTRY_STATE_PENDING_SYNC;
}
// Drives sync(INITIAL) -> async(READ) -> sync(PARSE).
static errorret_t loader_ctx_run(loader_ctx_t *ctx) {
errorret_t ret = assetJsonLoaderSync(&ctx->loading);
if(errorIsNotOk(ret)) return ret;
if(!run_json_async(&ctx->loading)) {
ctx->entry.state = ASSET_ENTRY_STATE_ERROR;
errorThrow("Async JSON load failed");
}
return assetJsonLoaderSync(&ctx->loading);
}
static void loader_ctx_dispose(loader_ctx_t *ctx) {
if(ctx->entry.type != ASSET_LOADER_TYPE_NULL) {
errorret_t ret = assetEntryDispose(&ctx->entry);
if(errorIsNotOk(ret)) errorCatch(ret);
}
threadMutexDispose(&ctx->loading.mutex);
}
// ============================================================
// Tests
// ============================================================
static void test_json_valid_loads(void **state) {
loader_ctx_t ctx;
loader_ctx_init(&ctx, "valid.json");
errorret_t ret = loader_ctx_run(&ctx);
assert_true(errorIsOk(ret));
assert_int_equal(ctx.entry.state, ASSET_ENTRY_STATE_LOADED);
assert_non_null(ctx.entry.data.json);
// Verify content is accessible
yyjson_val *root = yyjson_doc_get_root(ctx.entry.data.json);
assert_non_null(root);
assert_true(yyjson_is_obj(root));
yyjson_val *hello = yyjson_obj_get(root, "hello");
assert_non_null(hello);
assert_string_equal(yyjson_get_str(hello), "world");
yyjson_val *count = yyjson_obj_get(root, "count");
assert_non_null(count);
assert_int_equal((int)yyjson_get_int(count), 42);
loader_ctx_dispose(&ctx);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_json_parse_error(void **state) {
loader_ctx_t ctx;
loader_ctx_init(&ctx, "invalid.json");
// Async read succeeds; sync parse fails because yyjson rejects the content.
errorret_t ret = assetJsonLoaderSync(&ctx.loading);
assert_true(errorIsOk(ret));
assert_true(run_json_async(&ctx.loading));
ret = assetJsonLoaderSync(&ctx.loading);
assert_true(errorIsNotOk(ret));
errorCatch(ret);
assert_int_equal(ctx.entry.state, ASSET_ENTRY_STATE_ERROR);
assert_null(ctx.entry.data.json);
loader_ctx_dispose(&ctx);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_json_missing_file(void **state) {
loader_ctx_t ctx;
loader_ctx_init(&ctx, "nonexistent.json");
errorret_t ret = assetJsonLoaderSync(&ctx.loading);
assert_true(errorIsOk(ret));
// Async phase stat-fails because the file isn't in the ZIP.
assert_false(run_json_async(&ctx.loading));
assert_int_equal(ctx.entry.state, ASSET_ENTRY_STATE_ERROR);
loader_ctx_dispose(&ctx);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_json_buffer_cleared_after_load(void **state) {
loader_ctx_t ctx;
loader_ctx_init(&ctx, "valid.json");
loader_ctx_run(&ctx);
// The scratch buffer must be freed by the time sync returns LOADED.
assert_null(ctx.loading.loading.json.buffer);
loader_ctx_dispose(&ctx);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
// ============================================================
// main
// ============================================================
int main(void) {
assertInit();
const struct CMUnitTest tests[] = {
cmocka_unit_test_setup_teardown(test_json_valid_loads, zip_setup, zip_teardown),
cmocka_unit_test_setup_teardown(test_json_parse_error, zip_setup, zip_teardown),
cmocka_unit_test_setup_teardown(test_json_missing_file, zip_setup, zip_teardown),
cmocka_unit_test_setup_teardown(test_json_buffer_cleared_after_load, zip_setup, zip_teardown),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}
-423
View File
@@ -1,423 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "dusktest.h"
#include "asset/loader/locale/assetlocaleloader.h"
#include "asset/asset.h"
#include "util/memory.h"
#include <zip.h>
// ============================================================
// Test locale file (gettext PO format)
// ============================================================
static const char_t *LOCALE_EN =
"msgid \"\"\n"
"msgstr \"\"\n"
"\"Plural-Forms: nplurals=2; plural=(n != 1 ? 1 : 0);\\n\"\n"
"\n"
"msgid \"greeting\"\n"
"msgstr \"Hello, World!\"\n"
"\n"
"msgid \"item\"\n"
"msgid_plural \"items\"\n"
"msgstr[0] \"one item\"\n"
"msgstr[1] \"many items\"\n"
"\n"
"msgid \"score\"\n"
"msgstr \"Score: %d\"\n"
"\n"
"msgid \"player\"\n"
"msgstr \"Player: %s\"\n";
// ============================================================
// In-memory ZIP fixture (shared across all ZIP-based tests)
// ============================================================
static zip_t *g_zip = NULL;
static assetlocalefile_t g_locale;
static int locale_setup(void **state) {
zip_error_t err;
zip_error_init(&err);
// Phase 1: write the zip to a growable buffer source
zip_source_t *write_src = zip_source_buffer_create(NULL, 0, 1, &err);
if(!write_src) return -1;
zip_t *za = zip_open_from_source(write_src, ZIP_TRUNCATE, &err);
if(!za) { zip_source_free(write_src); return -1; }
size_t flen = strlen(LOCALE_EN);
zip_source_t *fs = zip_source_buffer(za, LOCALE_EN, flen, 0);
if(zip_file_add(za, "en.locale", fs, ZIP_FL_OVERWRITE) < 0) {
zip_close(za); return -1;
}
// Keep write_src alive after zip_close so we can read the bytes back out
zip_source_keep(write_src);
if(zip_close(za) != 0) { zip_source_free(write_src); return -1; }
// Phase 2: extract the raw zip bytes from the write buffer.
// zip_source_stat must be called before zip_source_open on a written source.
zip_stat_t zs;
memset(&zs, 0, sizeof(zs));
if(zip_source_stat(write_src, &zs) != 0 || !(zs.valid & ZIP_STAT_SIZE)) {
zip_source_free(write_src); return -1;
}
void *zipbuf = malloc((size_t)zs.size);
if(!zipbuf) { zip_source_free(write_src); return -1; }
if(zip_source_open(write_src) != 0) {
free(zipbuf); zip_source_free(write_src); return -1;
}
zip_source_read(write_src, zipbuf, (zip_uint64_t)zs.size);
zip_source_close(write_src);
zip_source_free(write_src);
// Phase 3: open a fresh read-only archive from the extracted bytes.
// The archive takes ownership of the source (and thus zipbuf via freep=1).
zip_error_init(&err);
zip_source_t *read_src = zip_source_buffer_create(
zipbuf, (zip_uint64_t)zs.size, 1, &err
);
if(!read_src) { free(zipbuf); return -1; }
g_zip = zip_open_from_source(read_src, 0, &err);
if(!g_zip) { zip_source_free(read_src); return -1; }
ASSET.zip = g_zip;
// Init locale file and parse the header
memoryZero(&g_locale, sizeof(g_locale));
errorret_t ret = assetFileInit(&g_locale.file, "en.locale", NULL, NULL);
if(errorIsNotOk(ret)) { errorCatch(ret); goto fail; }
ret = assetFileOpen(&g_locale.file);
if(errorIsNotOk(ret)) { errorCatch(ret); goto fail; }
char_t header[512];
ret = assetLocaleGetString(&g_locale, "", 0, header, sizeof(header));
if(errorIsNotOk(ret)) { errorCatch(ret); assetFileClose(&g_locale.file); goto fail; }
ret = assetLocaleParseHeader(&g_locale, header, sizeof(header));
if(errorIsNotOk(ret)) { errorCatch(ret); assetFileClose(&g_locale.file); goto fail; }
return 0;
fail:
zip_close(g_zip); g_zip = NULL;
ASSET.zip = NULL;
return -1;
}
static int locale_teardown(void **state) {
if(g_locale.file.zipFile != NULL) {
errorret_t ret = assetFileClose(&g_locale.file);
if(errorIsNotOk(ret)) errorCatch(ret);
}
if(g_zip != NULL) {
zip_close(g_zip); // also frees the read_src and zipbuf
g_zip = NULL;
}
ASSET.zip = NULL;
memoryZero(&g_locale, sizeof(g_locale));
return 0;
}
// ============================================================
// assetLocaleParseHeader — pure tests (no ZIP required)
// ============================================================
static void test_parseHeader_english(void **state) {
assetlocalefile_t locale;
memoryZero(&locale, sizeof(locale));
char_t hdr[] = "Plural-Forms: nplurals=2; plural=(n != 1 ? 1 : 0);\n";
errorret_t ret = assetLocaleParseHeader(&locale, hdr, sizeof(hdr));
assert_true(errorIsOk(ret));
assert_int_equal(locale.pluralStateCount, 2);
assert_int_equal(locale.pluralDefaultIndex, 0);
assert_int_equal(locale.pluralOps[0], ASSET_LOCALE_PLURAL_OP_NOT_EQUAL);
assert_int_equal(locale.pluralValues[0], 1);
assert_int_equal(locale.pluralIndices[0], 1);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_parseHeader_singular(void **state) {
assetlocalefile_t locale;
memoryZero(&locale, sizeof(locale));
char_t hdr[] = "Plural-Forms: nplurals=1; plural=(0);\n";
errorret_t ret = assetLocaleParseHeader(&locale, hdr, sizeof(hdr));
assert_true(errorIsOk(ret));
assert_int_equal(locale.pluralStateCount, 1);
assert_int_equal(locale.pluralDefaultIndex, 0);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_parseHeader_less_than(void **state) {
assetlocalefile_t locale;
memoryZero(&locale, sizeof(locale));
char_t hdr[] = "Plural-Forms: nplurals=2; plural=(n < 2 ? 0 : 1);\n";
errorret_t ret = assetLocaleParseHeader(&locale, hdr, sizeof(hdr));
assert_true(errorIsOk(ret));
assert_int_equal(locale.pluralStateCount, 2);
assert_int_equal(locale.pluralOps[0], ASSET_LOCALE_PLURAL_OP_LESS);
assert_int_equal(locale.pluralValues[0], 2);
assert_int_equal(locale.pluralIndices[0], 0);
assert_int_equal(locale.pluralDefaultIndex, 1);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_parseHeader_greater_equal(void **state) {
assetlocalefile_t locale;
memoryZero(&locale, sizeof(locale));
char_t hdr[] = "Plural-Forms: nplurals=2; plural=(n >= 2 ? 1 : 0);\n";
errorret_t ret = assetLocaleParseHeader(&locale, hdr, sizeof(hdr));
assert_true(errorIsOk(ret));
assert_int_equal(locale.pluralStateCount, 2);
assert_int_equal(locale.pluralOps[0], ASSET_LOCALE_PLURAL_OP_GREATER_EQUAL);
assert_int_equal(locale.pluralValues[0], 2);
assert_int_equal(locale.pluralIndices[0], 1);
assert_int_equal(locale.pluralDefaultIndex, 0);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_parseHeader_no_plural_forms(void **state) {
assetlocalefile_t locale;
memoryZero(&locale, sizeof(locale));
char_t hdr[] = "Content-Type: text/plain; charset=UTF-8\n";
errorret_t ret = assetLocaleParseHeader(&locale, hdr, sizeof(hdr));
assert_true(errorIsOk(ret));
assert_int_equal(locale.pluralStateCount, 0);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_parseHeader_error_nplurals_zero(void **state) {
assetlocalefile_t locale;
memoryZero(&locale, sizeof(locale));
char_t hdr[] = "Plural-Forms: nplurals=0; plural=(0);\n";
errorret_t ret = assetLocaleParseHeader(&locale, hdr, sizeof(hdr));
assert_true(errorIsNotOk(ret));
errorCatch(ret);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_parseHeader_error_nplurals_too_large(void **state) {
assetlocalefile_t locale;
memoryZero(&locale, sizeof(locale));
char_t hdr[] = "Plural-Forms: nplurals=7; plural=(0);\n";
errorret_t ret = assetLocaleParseHeader(&locale, hdr, sizeof(hdr));
assert_true(errorIsNotOk(ret));
errorCatch(ret);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_parseHeader_error_missing_nplurals(void **state) {
assetlocalefile_t locale;
memoryZero(&locale, sizeof(locale));
char_t hdr[] = "Plural-Forms: plural=(0);\n";
errorret_t ret = assetLocaleParseHeader(&locale, hdr, sizeof(hdr));
assert_true(errorIsNotOk(ret));
errorCatch(ret);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
// ============================================================
// assetLocaleEvaluatePlural — pure tests
// ============================================================
static void test_evaluatePlural_english_singular(void **state) {
assetlocalefile_t locale;
memoryZero(&locale, sizeof(locale));
char_t hdr[] = "Plural-Forms: nplurals=2; plural=(n != 1 ? 1 : 0);\n";
assetLocaleParseHeader(&locale, hdr, sizeof(hdr));
assert_int_equal(assetLocaleEvaluatePlural(&locale, 1), 0);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_evaluatePlural_english_plural(void **state) {
assetlocalefile_t locale;
memoryZero(&locale, sizeof(locale));
char_t hdr[] = "Plural-Forms: nplurals=2; plural=(n != 1 ? 1 : 0);\n";
assetLocaleParseHeader(&locale, hdr, sizeof(hdr));
assert_int_equal(assetLocaleEvaluatePlural(&locale, 0), 1);
assert_int_equal(assetLocaleEvaluatePlural(&locale, 2), 1);
assert_int_equal(assetLocaleEvaluatePlural(&locale, 100), 1);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_evaluatePlural_singular_only(void **state) {
assetlocalefile_t locale;
memoryZero(&locale, sizeof(locale));
char_t hdr[] = "Plural-Forms: nplurals=1; plural=(0);\n";
assetLocaleParseHeader(&locale, hdr, sizeof(hdr));
assert_int_equal(assetLocaleEvaluatePlural(&locale, 0), 0);
assert_int_equal(assetLocaleEvaluatePlural(&locale, 1), 0);
assert_int_equal(assetLocaleEvaluatePlural(&locale, 99), 0);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_evaluatePlural_less_than_boundary(void **state) {
assetlocalefile_t locale;
memoryZero(&locale, sizeof(locale));
char_t hdr[] = "Plural-Forms: nplurals=2; plural=(n < 2 ? 0 : 1);\n";
assetLocaleParseHeader(&locale, hdr, sizeof(hdr));
assert_int_equal(assetLocaleEvaluatePlural(&locale, 0), 0);
assert_int_equal(assetLocaleEvaluatePlural(&locale, 1), 0);
assert_int_equal(assetLocaleEvaluatePlural(&locale, 2), 1);
assert_int_equal(assetLocaleEvaluatePlural(&locale, 10), 1);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
// ============================================================
// assetLocaleGetString — ZIP-based tests
// ============================================================
static void test_getString_simple(void **state) {
char_t result[256];
errorret_t ret = assetLocaleGetString(&g_locale, "greeting", 0, result, sizeof(result));
assert_true(errorIsOk(ret));
assert_string_equal(result, "Hello, World!");
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_getString_plural_singular(void **state) {
char_t result[256];
errorret_t ret = assetLocaleGetString(&g_locale, "item", 1, result, sizeof(result));
assert_true(errorIsOk(ret));
assert_string_equal(result, "one item");
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_getString_plural_many(void **state) {
char_t result[256];
errorret_t ret = assetLocaleGetString(&g_locale, "item", 5, result, sizeof(result));
assert_true(errorIsOk(ret));
assert_string_equal(result, "many items");
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_getString_multiple_calls(void **state) {
char_t a[256], b[256];
errorret_t ret = assetLocaleGetString(&g_locale, "greeting", 0, a, sizeof(a));
assert_true(errorIsOk(ret));
// Second call rewinds the file and re-reads from scratch.
ret = assetLocaleGetString(&g_locale, "greeting", 0, b, sizeof(b));
assert_true(errorIsOk(ret));
assert_string_equal(a, b);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_getString_missing_id(void **state) {
char_t result[256];
errorret_t ret = assetLocaleGetString(&g_locale, "nonexistent", 0, result, sizeof(result));
assert_true(errorIsNotOk(ret));
errorCatch(ret);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
// ============================================================
// assetLocaleGetStringWithArgs — ZIP-based tests
// ============================================================
static void test_getStringWithArgs_int(void **state) {
assetlocalearg_t args[] = {
{ .type = ASSET_LOCALE_ARG_INT, .intValue = 42 }
};
char_t result[256];
errorret_t ret = assetLocaleGetStringWithArgs(
&g_locale, "score", 0, result, sizeof(result), args, 1
);
assert_true(errorIsOk(ret));
assert_string_equal(result, "Score: 42");
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_getStringWithArgs_string(void **state) {
assetlocalearg_t args[] = {
{ .type = ASSET_LOCALE_ARG_STRING, .stringValue = "Alice" }
};
char_t result[256];
errorret_t ret = assetLocaleGetStringWithArgs(
&g_locale, "player", 0, result, sizeof(result), args, 1
);
assert_true(errorIsOk(ret));
assert_string_equal(result, "Player: Alice");
assert_int_equal(memoryGetAllocatedCount(), 0);
}
// ============================================================
// main
// ============================================================
int main(void) {
const struct CMUnitTest tests[] = {
// parseHeader — pure
cmocka_unit_test(test_parseHeader_english),
cmocka_unit_test(test_parseHeader_singular),
cmocka_unit_test(test_parseHeader_less_than),
cmocka_unit_test(test_parseHeader_greater_equal),
cmocka_unit_test(test_parseHeader_no_plural_forms),
cmocka_unit_test(test_parseHeader_error_nplurals_zero),
cmocka_unit_test(test_parseHeader_error_nplurals_too_large),
cmocka_unit_test(test_parseHeader_error_missing_nplurals),
// evaluatePlural — pure
cmocka_unit_test(test_evaluatePlural_english_singular),
cmocka_unit_test(test_evaluatePlural_english_plural),
cmocka_unit_test(test_evaluatePlural_singular_only),
cmocka_unit_test(test_evaluatePlural_less_than_boundary),
// getString — in-memory ZIP
cmocka_unit_test_setup_teardown(test_getString_simple, locale_setup, locale_teardown),
cmocka_unit_test_setup_teardown(test_getString_plural_singular, locale_setup, locale_teardown),
cmocka_unit_test_setup_teardown(test_getString_plural_many, locale_setup, locale_teardown),
cmocka_unit_test_setup_teardown(test_getString_multiple_calls, locale_setup, locale_teardown),
cmocka_unit_test_setup_teardown(test_getString_missing_id, locale_setup, locale_teardown),
// getStringWithArgs — in-memory ZIP
cmocka_unit_test_setup_teardown(test_getStringWithArgs_int, locale_setup, locale_teardown),
cmocka_unit_test_setup_teardown(test_getStringWithArgs_string, locale_setup, locale_teardown),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}
-384
View File
@@ -1,384 +0,0 @@
/**
* 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 "asset/loader/display/assettilesetloader.h"
#include "thread/thread.h"
#include "util/memory.h"
#include <zip.h>
// ============================================================
// DTF binary fixtures
// DTF layout:
// [0-2] magic "DTF"
// [3] version 0x00
// [4-5] tileWidth (uint16 LE)
// [6-7] tileHeight (uint16 LE)
// [8-9] columns (uint16 LE)
// [10-11] rows (uint16 LE)
// [12-15] reserved
// [16-19] uv[0] (float LE)
// [20-23] uv[1] (float LE, must be 0.01.0)
// ============================================================
// uv[1] = 0.5f = 0x3F000000 LE = {0x00,0x00,0x00,0x3F}
// uv[1] = 2.0f = 0x40000000 LE = {0x00,0x00,0x00,0x40} (out-of-range)
static const uint8_t DTF_VALID[] = {
'D','T','F', 0x00,
16, 0,
16, 0,
4, 0,
4, 0,
0, 0, 0, 0,
0, 0, 0, 0,
0x00, 0x00, 0x00, 0x3F
};
static const uint8_t DTF_BAD_MAGIC[] = {
'X','Y','Z', 0x00,
16, 0, 16, 0, 4, 0, 4, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0x00, 0x00, 0x00, 0x3F
};
static const uint8_t DTF_BAD_VERSION[] = {
'D','T','F', 0xFF,
16, 0, 16, 0, 4, 0, 4, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0x00, 0x00, 0x00, 0x3F
};
static const uint8_t DTF_ZERO_WIDTH[] = {
'D','T','F', 0x00,
0, 0,
16, 0, 4, 0, 4, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0x00, 0x00, 0x00, 0x3F
};
static const uint8_t DTF_ZERO_HEIGHT[] = {
'D','T','F', 0x00,
16, 0,
0, 0,
4, 0, 4, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0x00, 0x00, 0x00, 0x3F
};
static const uint8_t DTF_ZERO_COLUMNS[] = {
'D','T','F', 0x00,
16, 0, 16, 0,
0, 0,
4, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0x00, 0x00, 0x00, 0x3F
};
static const uint8_t DTF_ZERO_ROWS[] = {
'D','T','F', 0x00,
16, 0, 16, 0, 4, 0,
0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0x00, 0x00, 0x00, 0x3F
};
// uv[1] = 2.0f, which fails the 0.01.0 range check
static const uint8_t DTF_INVALID_UV[] = {
'D','T','F', 0x00,
16, 0, 16, 0, 4, 0, 4, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0x00, 0x00, 0x00, 0x40
};
// ============================================================
// Async thread helper
// ============================================================
typedef struct {
assetloading_t *loading;
bool_t ok;
} tileset_async_run_t;
static void tileset_async_thread_cb(thread_t *thread) {
tileset_async_run_t *run = (tileset_async_run_t *)thread->data;
errorret_t ret = assetTilesetLoaderAsync(run->loading);
run->ok = errorIsOk(ret);
if(errorIsNotOk(ret)) errorCatch(ret);
}
static bool_t run_tileset_async(assetloading_t *loading) {
tileset_async_run_t run = { .loading = loading, .ok = false };
thread_t thread;
threadInit(&thread, tileset_async_thread_cb);
thread.data = &run;
threadStart(&thread);
threadStop(&thread);
return run.ok;
}
// ============================================================
// In-memory ZIP
// ============================================================
static zip_t *g_zip = NULL;
static int tileset_zip_add(zip_t *za, const char_t *name, const void *data, size_t len) {
zip_source_t *s = zip_source_buffer(za, data, len, 0);
return (int)zip_file_add(za, name, s, ZIP_FL_OVERWRITE);
}
static int zip_setup(void **state) {
zip_error_t err;
zip_error_init(&err);
zip_source_t *write_src = zip_source_buffer_create(NULL, 0, 1, &err);
if(!write_src) return -1;
zip_t *za = zip_open_from_source(write_src, ZIP_TRUNCATE, &err);
if(!za) { zip_source_free(write_src); return -1; }
if(tileset_zip_add(za, "valid.tileset", DTF_VALID, sizeof(DTF_VALID)) < 0 ||
tileset_zip_add(za, "badmagic.tileset", DTF_BAD_MAGIC, sizeof(DTF_BAD_MAGIC)) < 0 ||
tileset_zip_add(za, "badversion.tileset", DTF_BAD_VERSION, sizeof(DTF_BAD_VERSION)) < 0 ||
tileset_zip_add(za, "zerowidth.tileset", DTF_ZERO_WIDTH, sizeof(DTF_ZERO_WIDTH)) < 0 ||
tileset_zip_add(za, "zeroheight.tileset", DTF_ZERO_HEIGHT, sizeof(DTF_ZERO_HEIGHT)) < 0 ||
tileset_zip_add(za, "zerocolumns.tileset", DTF_ZERO_COLUMNS, sizeof(DTF_ZERO_COLUMNS)) < 0 ||
tileset_zip_add(za, "zerorows.tileset", DTF_ZERO_ROWS, sizeof(DTF_ZERO_ROWS)) < 0 ||
tileset_zip_add(za, "invaliduv.tileset", DTF_INVALID_UV, sizeof(DTF_INVALID_UV)) < 0) {
zip_close(za); return -1;
}
zip_source_keep(write_src);
if(zip_close(za) != 0) { zip_source_free(write_src); return -1; }
zip_stat_t zs;
memset(&zs, 0, sizeof(zs));
if(zip_source_stat(write_src, &zs) != 0 || !(zs.valid & ZIP_STAT_SIZE)) {
zip_source_free(write_src); return -1;
}
void *zipbuf = malloc((size_t)zs.size);
if(!zipbuf) { zip_source_free(write_src); return -1; }
if(zip_source_open(write_src) != 0) {
free(zipbuf); zip_source_free(write_src); return -1;
}
zip_source_read(write_src, zipbuf, (zip_uint64_t)zs.size);
zip_source_close(write_src);
zip_source_free(write_src);
zip_error_init(&err);
zip_source_t *read_src = zip_source_buffer_create(
zipbuf, (zip_uint64_t)zs.size, 1, &err
);
if(!read_src) { free(zipbuf); return -1; }
g_zip = zip_open_from_source(read_src, 0, &err);
if(!g_zip) { zip_source_free(read_src); return -1; }
ASSET.zip = g_zip;
return 0;
}
static int zip_teardown(void **state) {
if(g_zip) { zip_close(g_zip); g_zip = NULL; }
ASSET.zip = NULL;
return 0;
}
// ============================================================
// Loader pipeline helper
// ============================================================
typedef struct {
assetentry_t entry;
assetloading_t loading;
} loader_ctx_t;
static void loader_ctx_init(loader_ctx_t *ctx, const char_t *name) {
assetEntryInit(&ctx->entry, name, ASSET_LOADER_TYPE_TILESET, NULL);
threadMutexInit(&ctx->loading.mutex);
memoryZero(&ctx->loading.loading, sizeof(ctx->loading.loading));
ctx->loading.type = ASSET_LOADER_TYPE_TILESET;
ctx->loading.entry = &ctx->entry;
ctx->entry.state = ASSET_ENTRY_STATE_PENDING_SYNC;
}
static errorret_t loader_ctx_run(loader_ctx_t *ctx) {
errorret_t ret = assetTilesetLoaderSync(&ctx->loading);
if(errorIsNotOk(ret)) return ret;
if(!run_tileset_async(&ctx->loading)) {
ctx->entry.state = ASSET_ENTRY_STATE_ERROR;
errorThrow("Async tileset load failed");
}
return assetTilesetLoaderSync(&ctx->loading);
}
static void loader_ctx_dispose(loader_ctx_t *ctx) {
if(ctx->entry.type != ASSET_LOADER_TYPE_NULL) {
errorret_t ret = assetEntryDispose(&ctx->entry);
if(errorIsNotOk(ret)) errorCatch(ret);
}
threadMutexDispose(&ctx->loading.mutex);
}
// ============================================================
// Tests
// ============================================================
static void test_tileset_valid_loads(void **state) {
loader_ctx_t ctx;
loader_ctx_init(&ctx, "valid.tileset");
errorret_t ret = loader_ctx_run(&ctx);
assert_true(errorIsOk(ret));
assert_int_equal(ctx.entry.state, ASSET_ENTRY_STATE_LOADED);
tileset_t *ts = &ctx.entry.data.tileset;
assert_int_equal(ts->tileWidth, 16);
assert_int_equal(ts->tileHeight, 16);
assert_int_equal(ts->columns, 4);
assert_int_equal(ts->rows, 4);
assert_int_equal(ts->tileCount, 16); // columns * rows
loader_ctx_dispose(&ctx);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_tileset_data_cleared_after_load(void **state) {
loader_ctx_t ctx;
loader_ctx_init(&ctx, "valid.tileset");
loader_ctx_run(&ctx);
// The async scratch buffer must be freed by the time sync returns LOADED.
assert_null(ctx.loading.loading.tileset.data);
loader_ctx_dispose(&ctx);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_tileset_bad_magic(void **state) {
loader_ctx_t ctx;
loader_ctx_init(&ctx, "badmagic.tileset");
errorret_t ret = assetTilesetLoaderSync(&ctx.loading);
assert_true(errorIsOk(ret));
assert_true(run_tileset_async(&ctx.loading));
ret = assetTilesetLoaderSync(&ctx.loading);
assert_true(errorIsNotOk(ret));
errorCatch(ret);
assert_int_equal(ctx.entry.state, ASSET_ENTRY_STATE_ERROR);
loader_ctx_dispose(&ctx);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_tileset_bad_version(void **state) {
loader_ctx_t ctx;
loader_ctx_init(&ctx, "badversion.tileset");
errorret_t ret = assetTilesetLoaderSync(&ctx.loading);
assert_true(errorIsOk(ret));
assert_true(run_tileset_async(&ctx.loading));
ret = assetTilesetLoaderSync(&ctx.loading);
assert_true(errorIsNotOk(ret));
errorCatch(ret);
assert_int_equal(ctx.entry.state, ASSET_ENTRY_STATE_ERROR);
loader_ctx_dispose(&ctx);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_tileset_zero_tile_width(void **state) {
loader_ctx_t ctx;
loader_ctx_init(&ctx, "zerowidth.tileset");
errorret_t ret = loader_ctx_run(&ctx);
assert_true(errorIsNotOk(ret));
errorCatch(ret);
assert_int_equal(ctx.entry.state, ASSET_ENTRY_STATE_ERROR);
loader_ctx_dispose(&ctx);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_tileset_zero_tile_height(void **state) {
loader_ctx_t ctx;
loader_ctx_init(&ctx, "zeroheight.tileset");
errorret_t ret = loader_ctx_run(&ctx);
assert_true(errorIsNotOk(ret));
errorCatch(ret);
assert_int_equal(ctx.entry.state, ASSET_ENTRY_STATE_ERROR);
loader_ctx_dispose(&ctx);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_tileset_zero_columns(void **state) {
loader_ctx_t ctx;
loader_ctx_init(&ctx, "zerocolumns.tileset");
errorret_t ret = loader_ctx_run(&ctx);
assert_true(errorIsNotOk(ret));
errorCatch(ret);
assert_int_equal(ctx.entry.state, ASSET_ENTRY_STATE_ERROR);
loader_ctx_dispose(&ctx);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_tileset_zero_rows(void **state) {
loader_ctx_t ctx;
loader_ctx_init(&ctx, "zerorows.tileset");
errorret_t ret = loader_ctx_run(&ctx);
assert_true(errorIsNotOk(ret));
errorCatch(ret);
assert_int_equal(ctx.entry.state, ASSET_ENTRY_STATE_ERROR);
loader_ctx_dispose(&ctx);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_tileset_invalid_uv(void **state) {
loader_ctx_t ctx;
loader_ctx_init(&ctx, "invaliduv.tileset");
errorret_t ret = loader_ctx_run(&ctx);
assert_true(errorIsNotOk(ret));
errorCatch(ret);
assert_int_equal(ctx.entry.state, ASSET_ENTRY_STATE_ERROR);
loader_ctx_dispose(&ctx);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_tileset_missing_file(void **state) {
loader_ctx_t ctx;
loader_ctx_init(&ctx, "nonexistent.tileset");
errorret_t ret = assetTilesetLoaderSync(&ctx.loading);
assert_true(errorIsOk(ret));
assert_false(run_tileset_async(&ctx.loading));
assert_int_equal(ctx.entry.state, ASSET_ENTRY_STATE_ERROR);
loader_ctx_dispose(&ctx);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
// ============================================================
// main
// ============================================================
int main(void) {
assertInit();
const struct CMUnitTest tests[] = {
cmocka_unit_test_setup_teardown(test_tileset_valid_loads, zip_setup, zip_teardown),
cmocka_unit_test_setup_teardown(test_tileset_data_cleared_after_load, zip_setup, zip_teardown),
cmocka_unit_test_setup_teardown(test_tileset_bad_magic, zip_setup, zip_teardown),
cmocka_unit_test_setup_teardown(test_tileset_bad_version, zip_setup, zip_teardown),
cmocka_unit_test_setup_teardown(test_tileset_zero_tile_width, zip_setup, zip_teardown),
cmocka_unit_test_setup_teardown(test_tileset_zero_tile_height, zip_setup, zip_teardown),
cmocka_unit_test_setup_teardown(test_tileset_zero_columns, zip_setup, zip_teardown),
cmocka_unit_test_setup_teardown(test_tileset_zero_rows, zip_setup, zip_teardown),
cmocka_unit_test_setup_teardown(test_tileset_invalid_uv, zip_setup, zip_teardown),
cmocka_unit_test_setup_teardown(test_tileset_missing_file, zip_setup, zip_teardown),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}