93ab7690ba
Adds missing test coverage for the asset pipeline's binary loaders (mesh/model/texture), assetfile.c, and assetbatch.c, plus a new test/event suite for the shared event primitive. Found and fixed two real bugs while writing this: - assetFileRead's NULL-buffer skip path double-counted file->position, which could trip stb_image's EOF check early on images that skip bytes mid-decode. - eventSubscribe/eventUnsubscribe matched only on the callback pointer instead of the (callback, user) pair the docs already promised, so two independent consumers of the same cached asset (e.g. two assetbatch_t's) would abort. Covered by dedicated caching tests in both test_assetbatch.c and test_assetmodelloader.c. Also drops test_overworldscene.c, broken by the in-progress overworldscene.js/init.js export-contract rewrite. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
265 lines
8.5 KiB
C
265 lines
8.5 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/assettextureloader.h"
|
|
#include "thread/thread.h"
|
|
#include "util/memory.h"
|
|
#include "stb_image.h"
|
|
#include <zip.h>
|
|
|
|
// ============================================================
|
|
// BMP fixture: a 2x2, 24-bit, uncompressed bottom-up bitmap. stb_image
|
|
// supports BMP directly with no compression to hand-roll, unlike PNG/JPEG.
|
|
// Every pixel is BGR (50, 100, 200) -> after the loader forces a 4-channel
|
|
// (RGBA) decode, every texel should read back as (200, 100, 50, 255).
|
|
//
|
|
// BITMAPFILEHEADER (14 bytes) + BITMAPINFOHEADER (40 bytes) + pixel data
|
|
// (2 rows x (2px * 3B + 2B row padding) = 16 bytes) = 70 bytes total.
|
|
// ============================================================
|
|
|
|
static const uint8_t BMP_VALID[] = {
|
|
// BITMAPFILEHEADER
|
|
'B', 'M',
|
|
70, 0, 0, 0, // file size
|
|
0, 0, 0, 0, // reserved
|
|
54, 0, 0, 0, // pixel data offset
|
|
|
|
// BITMAPINFOHEADER
|
|
40, 0, 0, 0, // header size
|
|
2, 0, 0, 0, // width
|
|
2, 0, 0, 0, // height (positive = bottom-up)
|
|
1, 0, // planes
|
|
24, 0, // bit count
|
|
0, 0, 0, 0, // compression = BI_RGB
|
|
0, 0, 0, 0, // image size (unused for BI_RGB)
|
|
0, 0, 0, 0, // x pixels/meter
|
|
0, 0, 0, 0, // y pixels/meter
|
|
0, 0, 0, 0, // colors used
|
|
0, 0, 0, 0, // colors important
|
|
|
|
// Pixel data, 2 rows of 2 BGR pixels + row padding to a 4-byte multiple.
|
|
50, 100, 200, 50, 100, 200, 0, 0,
|
|
50, 100, 200, 50, 100, 200, 0, 0,
|
|
};
|
|
|
|
static const uint8_t GARBAGE_DATA[] = {
|
|
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
|
|
};
|
|
|
|
// ============================================================
|
|
// Async thread helper
|
|
// ============================================================
|
|
|
|
typedef struct {
|
|
assetloading_t *loading;
|
|
bool_t ok;
|
|
} texture_async_run_t;
|
|
|
|
static void texture_async_thread_cb(thread_t *thread) {
|
|
texture_async_run_t *run = (texture_async_run_t *)thread->data;
|
|
errorret_t ret = assetTextureLoaderAsync(run->loading);
|
|
run->ok = errorIsOk(ret);
|
|
if(errorIsNotOk(ret)) errorCatch(ret);
|
|
}
|
|
|
|
static bool_t run_texture_async(assetloading_t *loading) {
|
|
texture_async_run_t run = { .loading = loading, .ok = false };
|
|
thread_t thread;
|
|
threadInit(&thread, texture_async_thread_cb);
|
|
thread.data = &run;
|
|
threadStart(&thread);
|
|
threadStop(&thread);
|
|
return run.ok;
|
|
}
|
|
|
|
// ============================================================
|
|
// In-memory ZIP
|
|
// ============================================================
|
|
|
|
static zip_t *g_zip = NULL;
|
|
|
|
static int texture_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(
|
|
texture_zip_add(za, "valid.bmp", BMP_VALID, sizeof(BMP_VALID)) < 0 ||
|
|
texture_zip_add(za, "garbage.bmp", GARBAGE_DATA, sizeof(GARBAGE_DATA)) < 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) {
|
|
assetloaderinput_t input;
|
|
memoryZero(&input, sizeof(input));
|
|
input.texture = TEXTURE_FORMAT_RGBA;
|
|
|
|
assetEntryInit(&ctx->entry, name, ASSET_LOADER_TYPE_TEXTURE, &input);
|
|
threadMutexInit(&ctx->loading.mutex);
|
|
memoryZero(&ctx->loading.loading, sizeof(ctx->loading.loading));
|
|
ctx->loading.type = ASSET_LOADER_TYPE_TEXTURE;
|
|
ctx->loading.entry = &ctx->entry;
|
|
ctx->entry.state = ASSET_ENTRY_STATE_PENDING_SYNC;
|
|
}
|
|
|
|
// None of the tests below ever reach the sync/GPU phase (see the file-level
|
|
// comment), so entry->data.texture.id is always still 0. Unlike
|
|
// assetMeshDispose (which checks meshInitialized first), assetTextureDispose
|
|
// unconditionally calls textureDispose(), which asserts on a zero id -- so
|
|
// this deliberately routes around the real per-type disposer rather than
|
|
// crash on it; there's nothing else that needs freeing in that case.
|
|
static void loader_ctx_dispose_incomplete(loader_ctx_t *ctx) {
|
|
memoryZero(&ctx->entry, sizeof(ctx->entry));
|
|
threadMutexDispose(&ctx->loading.mutex);
|
|
}
|
|
|
|
// ============================================================
|
|
// Tests
|
|
//
|
|
// Only the async (decode) phase is exercised here: the sync phase's
|
|
// ASSET_TEXTURE_LOADING_STATE_CREATE_TEXTURE branch uploads to the GPU via
|
|
// textureInit(), which this headless test binary has no GL context for.
|
|
// ============================================================
|
|
|
|
static void test_texture_valid_decodes_pixels(void **state) {
|
|
loader_ctx_t ctx;
|
|
loader_ctx_init(&ctx, "valid.bmp");
|
|
|
|
errorret_t ret = assetTextureLoaderSync(&ctx.loading); // arms LOAD_PIXELS
|
|
assert_true(errorIsOk(ret));
|
|
|
|
assert_true(run_texture_async(&ctx.loading));
|
|
|
|
assert_int_equal(ctx.loading.loading.texture.width, 2);
|
|
assert_int_equal(ctx.loading.loading.texture.height, 2);
|
|
assert_int_equal(ctx.entry.state, ASSET_ENTRY_STATE_PENDING_SYNC);
|
|
assert_int_equal(
|
|
ctx.loading.loading.texture.state, ASSET_TEXTURE_LOADING_STATE_CREATE_TEXTURE
|
|
);
|
|
|
|
uint8_t *pixels = ctx.loading.loading.texture.data;
|
|
assert_non_null(pixels);
|
|
// Forced 4-channel (RGBA) decode of a BGR(50,100,200) source pixel.
|
|
assert_int_equal(pixels[0], 200);
|
|
assert_int_equal(pixels[1], 100);
|
|
assert_int_equal(pixels[2], 50);
|
|
assert_int_equal(pixels[3], 255);
|
|
|
|
// Never reached the sync/GPU phase -- free the decoded buffer manually.
|
|
stbi_image_free(pixels);
|
|
ctx.loading.loading.texture.data = NULL;
|
|
|
|
loader_ctx_dispose_incomplete(&ctx);
|
|
assert_int_equal(memoryGetAllocatedCount(), 0);
|
|
}
|
|
|
|
static void test_texture_corrupt_data_errors(void **state) {
|
|
loader_ctx_t ctx;
|
|
loader_ctx_init(&ctx, "garbage.bmp");
|
|
|
|
errorret_t ret = assetTextureLoaderSync(&ctx.loading);
|
|
assert_true(errorIsOk(ret));
|
|
|
|
assert_false(run_texture_async(&ctx.loading));
|
|
assert_int_equal(ctx.entry.state, ASSET_ENTRY_STATE_ERROR);
|
|
|
|
loader_ctx_dispose_incomplete(&ctx);
|
|
assert_int_equal(memoryGetAllocatedCount(), 0);
|
|
}
|
|
|
|
static void test_texture_missing_file_errors(void **state) {
|
|
loader_ctx_t ctx;
|
|
loader_ctx_init(&ctx, "nonexistent.bmp");
|
|
|
|
errorret_t ret = assetTextureLoaderSync(&ctx.loading);
|
|
assert_true(errorIsOk(ret));
|
|
|
|
assert_false(run_texture_async(&ctx.loading));
|
|
assert_int_equal(ctx.entry.state, ASSET_ENTRY_STATE_ERROR);
|
|
|
|
loader_ctx_dispose_incomplete(&ctx);
|
|
assert_int_equal(memoryGetAllocatedCount(), 0);
|
|
}
|
|
|
|
// ============================================================
|
|
// main
|
|
// ============================================================
|
|
|
|
int main(void) {
|
|
assertInit();
|
|
const struct CMUnitTest tests[] = {
|
|
cmocka_unit_test_setup_teardown(test_texture_valid_decodes_pixels, zip_setup, zip_teardown),
|
|
cmocka_unit_test_setup_teardown(test_texture_corrupt_data_errors, zip_setup, zip_teardown),
|
|
cmocka_unit_test_setup_teardown(test_texture_missing_file_errors, zip_setup, zip_teardown),
|
|
};
|
|
return cmocka_run_group_tests(tests, NULL, NULL);
|
|
}
|