/** * 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 "script/scriptmanager.h" #include "script/module/modulebase.h" #include "util/memory.h" #include #include #include // ============================================================ // In-memory ZIP fixture + script manager lifecycle // ============================================================ static zip_t *g_zip = NULL; static int require_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; } #define ADD_FILE(name, content) do { \ zip_source_t *s = zip_source_buffer(za, content, strlen(content), 0); \ if(zip_file_add(za, name, s, ZIP_FL_OVERWRITE) < 0) { \ zip_close(za); return -1; \ } \ } while(0) ADD_FILE( "lib.js", "module.exports = { test: function() { return 'hello world'; } };" ); ADD_FILE( "counted.js", "GLOBAL_COUNTER = " "(typeof GLOBAL_COUNTER === 'undefined' ? 0 : GLOBAL_COUNTER) + 1;\n" "module.exports = { count: GLOBAL_COUNTER };" ); ADD_FILE( "dir/parent.js", "var child = require('./child.js');\n" "module.exports = { childValue: child.value };" ); ADD_FILE("dir/child.js", "module.exports = { value: 42 };"); ADD_FILE( "circular-a.js", "var b = require('./circular-b.js');\n" "module.exports = " "{ name: 'a', bName: b.name, bSawUndefinedA: b.aWasUndefined };" ); ADD_FILE( "circular-b.js", "var a = require('./circular-a.js');\n" "module.exports = " "{ name: 'b', aWasUndefined: (typeof a === 'undefined') };" ); #undef ADD_FILE 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; errorret_t ret = scriptManagerInit(); if(errorIsNotOk(ret)) { errorCatch(ret); return -1; } return 0; } static int require_teardown(void **state) { errorret_t ret = scriptManagerDispose(); if(errorIsNotOk(ret)) errorCatch(ret); if(g_zip) { zip_close(g_zip); g_zip = NULL; } ASSET.zip = NULL; return 0; } // ============================================================ // Tests // ============================================================ static void test_require_basic(void **state) { jerry_value_t result; errorret_t ret = scriptManagerExec( "var something = require('./lib.js'); something.test();", &result ); assert_true(errorIsOk(ret)); assert_true(jerry_value_is_string(result)); char_t buf[64]; moduleBaseToString(result, buf, sizeof(buf)); assert_string_equal(buf, "hello world"); jerry_value_free(result); assert_int_equal(memoryGetAllocatedCount(), 0); } static void test_require_caches_and_executes_once(void **state) { jerry_value_t result; errorret_t ret = scriptManagerExec( "require('./counted.js'); require('./counted.js');" "require('./counted.js').count;", &result ); assert_true(errorIsOk(ret)); assert_true(jerry_value_is_number(result)); assert_int_equal(moduleBaseValueInt(result), 1); jerry_value_free(result); assert_int_equal(memoryGetAllocatedCount(), 0); } static void test_require_relative_path_resolves_to_requiring_files_dir( void **state ) { jerry_value_t result; errorret_t ret = scriptManagerExec( "require('dir/parent.js').childValue;", &result ); assert_true(errorIsOk(ret)); assert_true(jerry_value_is_number(result)); assert_int_equal(moduleBaseValueInt(result), 42); jerry_value_free(result); assert_int_equal(memoryGetAllocatedCount(), 0); } static void test_require_circular_does_not_hang(void **state) { jerry_value_t result; errorret_t ret = scriptManagerExec("require('./circular-a.js');", &result); assert_true(errorIsOk(ret)); assert_true(jerry_value_is_object(result)); jerry_value_t nameVal = moduleBaseGetProp(result, "name"); char_t nameBuf[16]; moduleBaseToString(nameVal, nameBuf, sizeof(nameBuf)); assert_string_equal(nameBuf, "a"); jerry_value_free(nameVal); jerry_value_t bNameVal = moduleBaseGetProp(result, "bName"); char_t bNameBuf[16]; moduleBaseToString(bNameVal, bNameBuf, sizeof(bNameBuf)); assert_string_equal(bNameBuf, "b"); jerry_value_free(bNameVal); // circular-b.js's require('./circular-a.js') ran while circular-a.js was // still mid-execution (before it set module.exports), so it must have // seen the in-progress module's exports as still undefined. jerry_value_t sawVal = moduleBaseGetProp(result, "bSawUndefinedA"); assert_true(jerry_value_is_true(sawVal)); jerry_value_free(sawVal); jerry_value_free(result); assert_int_equal(memoryGetAllocatedCount(), 0); } static void test_require_missing_file_throws(void **state) { errorret_t ret = scriptManagerExec("require('./does-not-exist.js');", NULL); assert_true(errorIsNotOk(ret)); errorCatch(ret); assert_int_equal(memoryGetAllocatedCount(), 0); } // ============================================================ // main // ============================================================ int main(void) { assertInit(); const struct CMUnitTest tests[] = { cmocka_unit_test_setup_teardown( test_require_basic, require_setup, require_teardown ), cmocka_unit_test_setup_teardown( test_require_caches_and_executes_once, require_setup, require_teardown ), cmocka_unit_test_setup_teardown( test_require_relative_path_resolves_to_requiring_files_dir, require_setup, require_teardown ), cmocka_unit_test_setup_teardown( test_require_circular_does_not_hang, require_setup, require_teardown ), cmocka_unit_test_setup_teardown( test_require_missing_file_throws, require_setup, require_teardown ), }; return cmocka_run_group_tests(tests, NULL, NULL); }