/** * 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 "util/memory.h" #include // ============================================================ // 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.0–1.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.0–1.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 }; // ============================================================ // 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; ret = assetTilesetLoaderAsync(&ctx->loading); if(errorIsNotOk(ret)) return ret; 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)); ret = assetTilesetLoaderAsync(&ctx.loading); assert_true(errorIsOk(ret)); 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)); ret = assetTilesetLoaderAsync(&ctx.loading); assert_true(errorIsOk(ret)); 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)); ret = assetTilesetLoaderAsync(&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); } // ============================================================ // main // ============================================================ int main(void) { 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); }