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>
396 lines
13 KiB
C
396 lines
13 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/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);
|
|
}
|