Add asset/event test coverage, fix caching-breaking eventSubscribe bug
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>
This commit is contained in:
@@ -0,0 +1,395 @@
|
||||
/**
|
||||
* 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/assetfile.h"
|
||||
#include "util/memory.h"
|
||||
#include <zip.h>
|
||||
|
||||
// ============================================================
|
||||
// Fixtures
|
||||
// ============================================================
|
||||
|
||||
static const char_t *TXT_HELLO = "Hello, World!";
|
||||
static const char_t *TXT_LINES = "line one\nline two\r\nline three";
|
||||
static const char_t *TXT_LONGLINE = "abcdefghijklmnopqrstuvwxyz\n";
|
||||
|
||||
// ============================================================
|
||||
// In-memory ZIP
|
||||
// ============================================================
|
||||
|
||||
static zip_t *g_zip = NULL;
|
||||
|
||||
static int file_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(
|
||||
file_zip_add(za, "hello.txt", TXT_HELLO, strlen(TXT_HELLO)) < 0 ||
|
||||
file_zip_add(za, "empty.txt", "", 0) < 0 ||
|
||||
file_zip_add(za, "lines.txt", TXT_LINES, strlen(TXT_LINES)) < 0 ||
|
||||
file_zip_add(za, "longline.txt", TXT_LONGLINE, strlen(TXT_LONGLINE)) < 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;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// assetFileInit tests
|
||||
// ============================================================
|
||||
|
||||
static void test_assetFileInit_valid(void **state) {
|
||||
assetfile_t file;
|
||||
errorret_t ret = assetFileInit(&file, "hello.txt", NULL, NULL);
|
||||
assert_true(errorIsOk(ret));
|
||||
assert_int_equal((int)file.size, (int)strlen(TXT_HELLO));
|
||||
|
||||
errorret_t disposeRet = assetFileDispose(&file);
|
||||
assert_true(errorIsOk(disposeRet));
|
||||
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||
}
|
||||
|
||||
static void test_assetFileInit_missing_file(void **state) {
|
||||
assetfile_t file;
|
||||
errorret_t ret = assetFileInit(&file, "nonexistent.txt", NULL, NULL);
|
||||
assert_true(errorIsNotOk(ret));
|
||||
errorCatch(ret);
|
||||
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||
}
|
||||
|
||||
static void test_assetFileInit_zero_size_errors(void **state) {
|
||||
assetfile_t file;
|
||||
errorret_t ret = assetFileInit(&file, "empty.txt", NULL, NULL);
|
||||
assert_true(errorIsNotOk(ret));
|
||||
errorCatch(ret);
|
||||
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Open / read / close tests
|
||||
// ============================================================
|
||||
|
||||
static void test_assetFile_open_read_close(void **state) {
|
||||
assetfile_t file;
|
||||
assetFileInit(&file, "hello.txt", NULL, NULL);
|
||||
|
||||
errorret_t ret = assetFileOpen(&file);
|
||||
assert_true(errorIsOk(ret));
|
||||
|
||||
char_t buf[32];
|
||||
ret = assetFileRead(&file, buf, strlen(TXT_HELLO));
|
||||
assert_true(errorIsOk(ret));
|
||||
buf[strlen(TXT_HELLO)] = '\0';
|
||||
assert_string_equal(buf, TXT_HELLO);
|
||||
assert_int_equal((int)file.position, (int)strlen(TXT_HELLO));
|
||||
assert_int_equal((int)file.lastRead, (int)strlen(TXT_HELLO));
|
||||
|
||||
ret = assetFileClose(&file);
|
||||
assert_true(errorIsOk(ret));
|
||||
assert_null(file.zipFile);
|
||||
|
||||
errorret_t disposeRet = assetFileDispose(&file);
|
||||
assert_true(errorIsOk(disposeRet));
|
||||
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||
}
|
||||
|
||||
static void test_assetFile_read_skip_advances_position_by_exactly_n(
|
||||
void **state
|
||||
) {
|
||||
// Regression test: assetFileRead's NULL-buffer skip path used to advance
|
||||
// file->position by 2x the skipped amount (the recursive real-read call
|
||||
// already advances it once, and the skip loop wrongly advanced it again).
|
||||
assetfile_t file;
|
||||
assetFileInit(&file, "hello.txt", NULL, NULL);
|
||||
assetFileOpen(&file);
|
||||
|
||||
errorret_t ret = assetFileRead(&file, NULL, 7); // skip "Hello, "
|
||||
assert_true(errorIsOk(ret));
|
||||
assert_int_equal((int)file.position, 7);
|
||||
assert_int_equal((int)file.lastRead, 7);
|
||||
|
||||
char_t buf[16];
|
||||
ret = assetFileRead(&file, buf, 6); // "World!"
|
||||
assert_true(errorIsOk(ret));
|
||||
buf[6] = '\0';
|
||||
assert_string_equal(buf, "World!");
|
||||
assert_int_equal((int)file.position, 13);
|
||||
|
||||
assetFileClose(&file);
|
||||
errorret_t disposeRet = assetFileDispose(&file);
|
||||
assert_true(errorIsOk(disposeRet));
|
||||
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||
}
|
||||
|
||||
static void test_assetFile_skip_spanning_multiple_chunks(void **state) {
|
||||
// The skip loop reads through a 256-byte scratch buffer per chunk; verify
|
||||
// a skip larger than that scratch buffer still lands on the right byte.
|
||||
assetfile_t file;
|
||||
assetFileInit(&file, "longline.txt", NULL, NULL);
|
||||
assetFileOpen(&file);
|
||||
|
||||
// longline.txt is the 26-letter alphabet + '\n' = 27 bytes; skip the
|
||||
// first 25 letters (a..y), leaving "z\n".
|
||||
errorret_t ret = assetFileRead(&file, NULL, 25);
|
||||
assert_true(errorIsOk(ret));
|
||||
assert_int_equal((int)file.position, 25);
|
||||
|
||||
char_t buf[4];
|
||||
ret = assetFileRead(&file, buf, 2);
|
||||
assert_true(errorIsOk(ret));
|
||||
buf[2] = '\0';
|
||||
assert_string_equal(buf, "z\n");
|
||||
|
||||
assetFileClose(&file);
|
||||
errorret_t disposeRet = assetFileDispose(&file);
|
||||
assert_true(errorIsOk(disposeRet));
|
||||
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||
}
|
||||
|
||||
static void test_assetFile_rewind_reads_from_start_again(void **state) {
|
||||
assetfile_t file;
|
||||
assetFileInit(&file, "hello.txt", NULL, NULL);
|
||||
assetFileOpen(&file);
|
||||
|
||||
char_t buf[16];
|
||||
assetFileRead(&file, buf, 5);
|
||||
buf[5] = '\0';
|
||||
assert_string_equal(buf, "Hello");
|
||||
|
||||
errorret_t ret = assetFileRewind(&file);
|
||||
assert_true(errorIsOk(ret));
|
||||
assert_int_equal((int)file.position, 0);
|
||||
|
||||
assetFileRead(&file, buf, 5);
|
||||
buf[5] = '\0';
|
||||
assert_string_equal(buf, "Hello");
|
||||
|
||||
assetFileClose(&file);
|
||||
errorret_t disposeRet = assetFileDispose(&file);
|
||||
assert_true(errorIsOk(disposeRet));
|
||||
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||
}
|
||||
|
||||
static void test_assetFile_rewind_noop_at_start(void **state) {
|
||||
assetfile_t file;
|
||||
assetFileInit(&file, "hello.txt", NULL, NULL);
|
||||
assetFileOpen(&file);
|
||||
|
||||
// Never having read anything, position is already 0 -- rewind must be a
|
||||
// cheap no-op rather than closing/reopening the handle.
|
||||
errorret_t ret = assetFileRewind(&file);
|
||||
assert_true(errorIsOk(ret));
|
||||
assert_non_null(file.zipFile);
|
||||
|
||||
assetFileClose(&file);
|
||||
errorret_t disposeRet = assetFileDispose(&file);
|
||||
assert_true(errorIsOk(disposeRet));
|
||||
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||
}
|
||||
|
||||
static void test_assetFile_dispose_closes_open_handle(void **state) {
|
||||
assetfile_t file;
|
||||
assetFileInit(&file, "hello.txt", NULL, NULL);
|
||||
assetFileOpen(&file);
|
||||
|
||||
errorret_t ret = assetFileDispose(&file);
|
||||
assert_true(errorIsOk(ret));
|
||||
assert_null(file.zipFile);
|
||||
assert_int_equal((int)file.size, 0);
|
||||
|
||||
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// assetFileReadEntire tests
|
||||
// ============================================================
|
||||
|
||||
static void test_assetFileReadEntire_reads_full_contents(void **state) {
|
||||
assetfile_t file;
|
||||
assetFileInit(&file, "hello.txt", NULL, NULL);
|
||||
|
||||
uint8_t *buffer = NULL;
|
||||
size_t size = 0;
|
||||
errorret_t ret = assetFileReadEntire(&file, &buffer, &size);
|
||||
assert_true(errorIsOk(ret));
|
||||
assert_non_null(buffer);
|
||||
assert_int_equal((int)size, (int)strlen(TXT_HELLO));
|
||||
assert_memory_equal(buffer, TXT_HELLO, size);
|
||||
|
||||
// The file handle is closed again by the time this returns.
|
||||
assert_null(file.zipFile);
|
||||
|
||||
memoryFree(buffer);
|
||||
errorret_t disposeRet = assetFileDispose(&file);
|
||||
assert_true(errorIsOk(disposeRet));
|
||||
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Line reader tests
|
||||
// ============================================================
|
||||
|
||||
static void test_lineReader_reads_lines_stripping_crlf(void **state) {
|
||||
assetfile_t file;
|
||||
assetFileInit(&file, "lines.txt", NULL, NULL);
|
||||
assetFileOpen(&file);
|
||||
|
||||
uint8_t readBuf[64];
|
||||
uint8_t outBuf[64];
|
||||
assetfilelinereader_t reader;
|
||||
assetFileLineReaderInit(
|
||||
&reader, &file, readBuf, sizeof(readBuf), outBuf, sizeof(outBuf)
|
||||
);
|
||||
|
||||
errorret_t ret = assetFileLineReaderNext(&reader);
|
||||
assert_true(errorIsOk(ret));
|
||||
assert_string_equal((char_t *)outBuf, "line one");
|
||||
|
||||
ret = assetFileLineReaderNext(&reader);
|
||||
assert_true(errorIsOk(ret));
|
||||
assert_string_equal((char_t *)outBuf, "line two"); // \r stripped
|
||||
|
||||
ret = assetFileLineReaderNext(&reader);
|
||||
assert_true(errorIsOk(ret));
|
||||
assert_string_equal((char_t *)outBuf, "line three"); // no trailing \n
|
||||
|
||||
assetFileClose(&file);
|
||||
errorret_t disposeRet = assetFileDispose(&file);
|
||||
assert_true(errorIsOk(disposeRet));
|
||||
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||
}
|
||||
|
||||
static void test_lineReader_next_past_eof_throws(void **state) {
|
||||
assetfile_t file;
|
||||
assetFileInit(&file, "lines.txt", NULL, NULL);
|
||||
assetFileOpen(&file);
|
||||
|
||||
uint8_t readBuf[64];
|
||||
uint8_t outBuf[64];
|
||||
assetfilelinereader_t reader;
|
||||
assetFileLineReaderInit(
|
||||
&reader, &file, readBuf, sizeof(readBuf), outBuf, sizeof(outBuf)
|
||||
);
|
||||
|
||||
assetFileLineReaderNext(&reader);
|
||||
assetFileLineReaderNext(&reader);
|
||||
assetFileLineReaderNext(&reader); // consumes "line three", the last line
|
||||
|
||||
errorret_t ret = assetFileLineReaderNext(&reader);
|
||||
assert_true(errorIsNotOk(ret));
|
||||
errorCatch(ret);
|
||||
|
||||
assetFileClose(&file);
|
||||
errorret_t disposeRet = assetFileDispose(&file);
|
||||
assert_true(errorIsOk(disposeRet));
|
||||
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||
}
|
||||
|
||||
static void test_lineReader_small_read_buffer_spans_multiple_fills(
|
||||
void **state
|
||||
) {
|
||||
// readBuffer is far smaller than the line, forcing assetFileLineReaderFill
|
||||
// to be called repeatedly (and to slide unread bytes down) before the
|
||||
// newline is finally seen.
|
||||
assetfile_t file;
|
||||
assetFileInit(&file, "longline.txt", NULL, NULL);
|
||||
assetFileOpen(&file);
|
||||
|
||||
uint8_t readBuf[4];
|
||||
uint8_t outBuf[64];
|
||||
assetfilelinereader_t reader;
|
||||
assetFileLineReaderInit(
|
||||
&reader, &file, readBuf, sizeof(readBuf), outBuf, sizeof(outBuf)
|
||||
);
|
||||
|
||||
errorret_t ret = assetFileLineReaderNext(&reader);
|
||||
assert_true(errorIsOk(ret));
|
||||
assert_string_equal((char_t *)outBuf, "abcdefghijklmnopqrstuvwxyz");
|
||||
|
||||
assetFileClose(&file);
|
||||
errorret_t disposeRet = assetFileDispose(&file);
|
||||
assert_true(errorIsOk(disposeRet));
|
||||
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// main
|
||||
// ============================================================
|
||||
|
||||
int main(void) {
|
||||
assertInit();
|
||||
const struct CMUnitTest tests[] = {
|
||||
cmocka_unit_test_setup_teardown(test_assetFileInit_valid, zip_setup, zip_teardown),
|
||||
cmocka_unit_test_setup_teardown(test_assetFileInit_missing_file, zip_setup, zip_teardown),
|
||||
cmocka_unit_test_setup_teardown(test_assetFileInit_zero_size_errors, zip_setup, zip_teardown),
|
||||
|
||||
cmocka_unit_test_setup_teardown(test_assetFile_open_read_close, zip_setup, zip_teardown),
|
||||
cmocka_unit_test_setup_teardown(test_assetFile_read_skip_advances_position_by_exactly_n, zip_setup, zip_teardown),
|
||||
cmocka_unit_test_setup_teardown(test_assetFile_skip_spanning_multiple_chunks, zip_setup, zip_teardown),
|
||||
cmocka_unit_test_setup_teardown(test_assetFile_rewind_reads_from_start_again, zip_setup, zip_teardown),
|
||||
cmocka_unit_test_setup_teardown(test_assetFile_rewind_noop_at_start, zip_setup, zip_teardown),
|
||||
cmocka_unit_test_setup_teardown(test_assetFile_dispose_closes_open_handle, zip_setup, zip_teardown),
|
||||
|
||||
cmocka_unit_test_setup_teardown(test_assetFileReadEntire_reads_full_contents, zip_setup, zip_teardown),
|
||||
|
||||
cmocka_unit_test_setup_teardown(test_lineReader_reads_lines_stripping_crlf, zip_setup, zip_teardown),
|
||||
cmocka_unit_test_setup_teardown(test_lineReader_next_past_eof_throws, zip_setup, zip_teardown),
|
||||
cmocka_unit_test_setup_teardown(test_lineReader_small_read_buffer_spans_multiple_fills, zip_setup, zip_teardown),
|
||||
};
|
||||
return cmocka_run_group_tests(tests, NULL, NULL);
|
||||
}
|
||||
Reference in New Issue
Block a user