/** * 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 // ============================================================ // 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); }