/** * Copyright (c) 2026 Dominic Masters * * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ #include "dusktest.h" #include "script/script.h" #include "console/console.h" #include "asset/asset.h" #include "asset/assetdsk.h" #include "util/memory.h" static void test_scriptRunsAndPrintsToConsole(void **state) { scripterror_t error; scriptresult_t result; consoleInit(); assert_int_equal(errorIsOk(scriptInit()), true); result = scriptRuntimeRunFile(SCRIPT.runtime, DUSKSCRIPT_TEST_FIXTURE, true, &error); assert_int_equal(result, SCRIPT_OK); // print() reaches the console through scriptPrintHandler. assert_string_equal(CONSOLE.line[CONSOLE_HISTORY_MAX - 2], "print hello"); // consolePrint(text), the native, writes directly. assert_string_equal(CONSOLE.line[CONSOLE_HISTORY_MAX - 1], "consolePrint hello"); assert_int_equal(errorIsOk(scriptDispose()), true); consoleDispose(); } static void test_scriptNumbersAreAllFloats(void **state) { scripterror_t error; scriptresult_t result; // In the order fixture_numbers.ds prints them. const char_t *expected[] = { "5", "3.75", "2.5", "false", "10", "20", "1", "16", "3", "3.5" }; const int32_t count = (int32_t)(sizeof(expected) / sizeof(expected[0])); int32_t i; consoleInit(); assert_int_equal(errorIsOk(scriptInit()), true); result = scriptRuntimeRunFile(SCRIPT.runtime, DUSKSCRIPT_TEST_NUMBERS, true, &error); assert_int_equal(result, SCRIPT_OK); for(i = 0; i < count; i++) { assert_string_equal(CONSOLE.line[CONSOLE_HISTORY_MAX - count + i], expected[i]); } assert_int_equal(errorIsOk(scriptDispose()), true); consoleDispose(); } // Runs a packed fixture and describes where it stopped, for a failure message static scriptresult_t duskscript_test_run(const char_t *path, scripterror_t *error) { scriptresult_t result; consoleInit(); assert_int_equal(errorIsOk(scriptInit()), true); result = scriptRuntimeRunFile(SCRIPT.runtime, path, true, error); return result; } static void duskscript_test_finish(void) { assert_int_equal(errorIsOk(scriptDispose()), true); consoleDispose(); } // A pass fixture is a script of assert() calls, it has to run to the end static void test_scriptPasses(void **state) { const char_t *path = (const char_t *)*state; scripterror_t error; scriptresult_t result; uint8_t i; memoryZero(&error, sizeof(error)); result = duskscript_test_run(path, &error); duskscript_test_finish(); if(result != SCRIPT_OK) { print_error("%s stopped with code %d: %s\n", path, (int)result, error.message); for(i = 0; i < error.frameCount; i++) { print_error(" at %s %s:%d:%d\n", error.frames[i].name, error.frames[i].file, (int)error.frames[i].line, (int)error.frames[i].column); } } assert_int_equal(result, SCRIPT_OK); } // A fail fixture does one thing the engine has to refuse, as a runtime error // with a message, and not a crash, a hang, or going on as if nothing happened static void test_scriptFailsWithRuntimeError(void **state) { const char_t *path = (const char_t *)*state; scripterror_t error; scriptresult_t result; memoryZero(&error, sizeof(error)); result = duskscript_test_run(path, &error); duskscript_test_finish(); if(result != SCRIPT_ERROR_RUNTIME) print_error("%s was expected to fail but gave code %d\n", path, (int)result); assert_int_equal(result, SCRIPT_ERROR_RUNTIME); assert_true(error.message[0] != '\0'); } // The engine is still usable after a script failed: a run after an error static void test_scriptRunsAfterAnError(void **state) { scripterror_t error; memoryZero(&error, sizeof(error)); consoleInit(); assert_int_equal(errorIsOk(scriptInit()), true); assert_int_equal(scriptRuntimeRunFile(SCRIPT.runtime, "fail_assert_false.dsb", true, &error), SCRIPT_ERROR_RUNTIME); assert_int_equal(scriptRuntimeRunFile(SCRIPT.runtime, "fail_div_zero.dsb", true, &error), SCRIPT_ERROR_RUNTIME); assert_int_equal(scriptRuntimeRunFile(SCRIPT.runtime, "pass_arrays.dsb", true, &error), SCRIPT_OK); assert_int_equal(scriptRuntimeRunFile(SCRIPT.runtime, "pass_structs.dsb", true, &error), SCRIPT_OK); duskscript_test_finish(); } // The same script can run again and again, nothing of it is left behind static void test_scriptRunsRepeatedly(void **state) { scripterror_t error; int32_t i; memoryZero(&error, sizeof(error)); consoleInit(); assert_int_equal(errorIsOk(scriptInit()), true); for(i = 0; i < 20; i++) { assert_int_equal(scriptRuntimeRunFile(SCRIPT.runtime, "pass_mixed.dsb", true, &error), SCRIPT_OK); assert_int_equal(scriptRuntimeRunFile(SCRIPT.runtime, "pass_strings.dsb", true, &error), SCRIPT_OK); } duskscript_test_finish(); } // A file that does not exist is an error, not a crash static void test_scriptMissingFile(void **state) { scripterror_t error; memoryZero(&error, sizeof(error)); consoleInit(); assert_int_equal(errorIsOk(scriptInit()), true); assert_int_not_equal(scriptRuntimeRunFile(SCRIPT.runtime, "no_such_script.dsb", true, &error), SCRIPT_OK); duskscript_test_finish(); } #define SCRIPT_PASS(name) { "pass_" #name, test_scriptPasses, NULL, NULL, (void *)("pass_" #name ".dsb") } #define SCRIPT_FAIL(name) { "fail_" #name, test_scriptFailsWithRuntimeError, NULL, NULL, (void *)("fail_" #name ".dsb") } // Fixtures load through the asset system now, so ASSET needs a real zip // behind it - open the packed test.dsk directly rather than the full // assetInit()/assetInitPlatform(), which expects a real launched engine. static int duskscript_test_setup(void **state) { memoryZero(&ASSET, sizeof(ASSET)); threadMutexInit(&ASSET.zipLock); assert_int_equal(errorIsOk(assetDskOpenFromPath(DUSKSCRIPT_TEST_DSK, &ASSET.zip)), true); return 0; } static int duskscript_test_teardown(void **state) { zip_close(ASSET.zip); threadMutexDispose(&ASSET.zipLock); memoryZero(&ASSET, sizeof(ASSET)); return 0; } int main(void) { const struct CMUnitTest tests[] = { cmocka_unit_test(test_scriptRunsAndPrintsToConsole), cmocka_unit_test(test_scriptNumbersAreAllFloats), cmocka_unit_test(test_scriptRunsAfterAnError), cmocka_unit_test(test_scriptRunsRepeatedly), cmocka_unit_test(test_scriptMissingFile), SCRIPT_PASS(arrays), SCRIPT_PASS(async), SCRIPT_PASS(constants), SCRIPT_PASS(control), SCRIPT_PASS(conversion), SCRIPT_PASS(functions), SCRIPT_PASS(logic), SCRIPT_PASS(math), SCRIPT_PASS(mixed), SCRIPT_PASS(numbers), SCRIPT_PASS(strings), SCRIPT_PASS(structs), SCRIPT_PASS(types), SCRIPT_FAIL(abs_string), SCRIPT_FAIL(add_array), SCRIPT_FAIL(add_bool), SCRIPT_FAIL(add_null), SCRIPT_FAIL(array_too_long), SCRIPT_FAIL(array_too_long_merge), SCRIPT_FAIL(assert_empty_string), SCRIPT_FAIL(assert_false), SCRIPT_FAIL(assert_no_arguments), SCRIPT_FAIL(assert_null), SCRIPT_FAIL(assert_zero), SCRIPT_FAIL(async_error), SCRIPT_FAIL(async_error_nested), SCRIPT_FAIL(bitwise_string), SCRIPT_FAIL(call_missing_member), SCRIPT_FAIL(call_null), SCRIPT_FAIL(call_number), SCRIPT_FAIL(callback_raises), SCRIPT_FAIL(chr_fraction), SCRIPT_FAIL(chr_too_big), SCRIPT_FAIL(chr_zero), SCRIPT_FAIL(compare_mixed), SCRIPT_FAIL(compare_null), SCRIPT_FAIL(compare_strings), SCRIPT_FAIL(const_array_alias_assign), SCRIPT_FAIL(const_array_pop), SCRIPT_FAIL(const_array_push), SCRIPT_FAIL(const_array_sort), SCRIPT_FAIL(const_struct_alias_assign), SCRIPT_FAIL(const_struct_alias_new_key), SCRIPT_FAIL(contains_number), SCRIPT_FAIL(deepclone_cycle), SCRIPT_FAIL(deepclone_struct_cycle), SCRIPT_FAIL(div_zero), SCRIPT_FAIL(error_after_output), SCRIPT_FAIL(error_in_loop), SCRIPT_FAIL(error_inside_function), SCRIPT_FAIL(error_inside_nested_calls), SCRIPT_FAIL(fill_negative), SCRIPT_FAIL(fill_too_many), SCRIPT_FAIL(filter_non_function), SCRIPT_FAIL(flatten_non_array_items), SCRIPT_FAIL(foreach_null), SCRIPT_FAIL(foreach_number), SCRIPT_FAIL(foreach_struct), SCRIPT_FAIL(include_missing), SCRIPT_FAIL(index_assign_negative), SCRIPT_FAIL(index_assign_oob), SCRIPT_FAIL(index_fraction), SCRIPT_FAIL(index_negative), SCRIPT_FAIL(index_number), SCRIPT_FAIL(index_read_oob), SCRIPT_FAIL(index_string_key), SCRIPT_FAIL(indexof_non_array), SCRIPT_FAIL(indexof_string_long_needle), SCRIPT_FAIL(insert_negative), SCRIPT_FAIL(insert_out_of_range), SCRIPT_FAIL(insertarr_out_of_range), SCRIPT_FAIL(invert_string), SCRIPT_FAIL(join_non_array), SCRIPT_FAIL(join_non_string_separator), SCRIPT_FAIL(len_null), SCRIPT_FAIL(len_number), SCRIPT_FAIL(map_non_array), SCRIPT_FAIL(map_non_function), SCRIPT_FAIL(member_of_null), SCRIPT_FAIL(member_of_number), SCRIPT_FAIL(min_string), SCRIPT_FAIL(missing_parameter), SCRIPT_FAIL(missing_second_parameter), SCRIPT_FAIL(mod_zero), SCRIPT_FAIL(mul_string), SCRIPT_FAIL(mutual_recursion_too_deep), SCRIPT_FAIL(negate_string), SCRIPT_FAIL(pop_string), SCRIPT_FAIL(pow_fraction), SCRIPT_FAIL(push_string), SCRIPT_FAIL(rand_backwards), SCRIPT_FAIL(rand_empty_range), SCRIPT_FAIL(rand_fraction), SCRIPT_FAIL(randf_empty_range), SCRIPT_FAIL(recursion_too_deep), SCRIPT_FAIL(reduce_non_function), SCRIPT_FAIL(reverse_string), SCRIPT_FAIL(sort_bad_comparer_result), SCRIPT_FAIL(sort_mixed), SCRIPT_FAIL(sort_non_array), SCRIPT_FAIL(sort_structs), SCRIPT_FAIL(splice_negative_count), SCRIPT_FAIL(splice_out_of_range), SCRIPT_FAIL(split_non_string), SCRIPT_FAIL(sqrt_negative), SCRIPT_FAIL(string_assign), SCRIPT_FAIL(string_index_oob), SCRIPT_FAIL(string_too_long), SCRIPT_FAIL(struct_number_key), SCRIPT_FAIL(struct_number_key_assign), SCRIPT_FAIL(struct_too_many_keys), SCRIPT_FAIL(sub_string), SCRIPT_FAIL(subarr_backwards), SCRIPT_FAIL(subarr_negative), SCRIPT_FAIL(subarr_past_end), SCRIPT_FAIL(subarr_string_past_end), SCRIPT_FAIL(tobase_bad_base), SCRIPT_FAIL(tobase_big_base), SCRIPT_FAIL(tobase_fraction), SCRIPT_FAIL(tobase_string), SCRIPT_FAIL(tobool_invalid), SCRIPT_FAIL(tobool_null), SCRIPT_FAIL(tonum_bad_digit), SCRIPT_FAIL(tonum_bool), SCRIPT_FAIL(tonum_empty), SCRIPT_FAIL(tonum_invalid), SCRIPT_FAIL(tonum_too_big), SCRIPT_FAIL(tonum_trailing), SCRIPT_FAIL(too_few_arguments), SCRIPT_FAIL(too_many_arguments), SCRIPT_FAIL(tostr_decimals_on_string), SCRIPT_FAIL(tostr_negative_decimals), SCRIPT_FAIL(tostr_too_many_decimals), SCRIPT_FAIL(trim_null), SCRIPT_FAIL(undefined_function_call), SCRIPT_FAIL(undefined_variable_read), SCRIPT_FAIL(undefined_variable_write), SCRIPT_FAIL(upcase_number), }; return cmocka_run_group_tests(tests, duskscript_test_setup, duskscript_test_teardown); }