/** * 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 "util/memory.h" #include // ============================================================ // Fixtures // ============================================================ static const char_t *JSON_VALID = "{\"hello\":\"world\",\"count\":42}"; static const char_t *JSON_INVALID = "{ this is definitely not valid json !!!"; // ============================================================ // 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; ret = assetJsonLoaderAsync(&ctx->loading); if(errorIsNotOk(ret)) return ret; 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)); ret = assetJsonLoaderAsync(&ctx.loading); assert_true(errorIsOk(ret)); 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. ret = assetJsonLoaderAsync(&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_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) { 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); }