359 lines
11 KiB
C
359 lines
11 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/display/assettilesetloader.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.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);
|
||
}
|