Files
dusk/test/asset/test_assetfile.c
T
YourWishes 93ab7690ba 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>
2026-08-11 14:34:51 -05:00

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);
}