223 lines
6.6 KiB
C
223 lines
6.6 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 "asset/loader/json/assetjsonloader.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 !!!";
|
|
|
|
// ============================================================
|
|
// 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);
|
|
}
|