From 6ddf5cddc92710b0b1b831839850d3013d6c2eca Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 7 Jun 2026 10:09:54 +0000 Subject: [PATCH] tests: cover native VM module alias order Co-authored-by: Tim Fox --- CMakeLists.txt | 8 ++- src/qcommon/vm.c | 56 +++------------------ src/qcommon/vm_native_module.c | 57 +++++++++++++++++++++ src/qcommon/vm_native_module.h | 11 ++++ tests/README.md | 2 +- tests/unit/test_vm_native_module.c | 81 ++++++++++++++++++++++++++++++ 6 files changed, 164 insertions(+), 51 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 38f9da8b00..9dc2d5e02f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2648,8 +2648,14 @@ if(BUILD_UNIT_TESTS) add_executable(unit_vm_native_module tests/unit/test_vm_native_module.c - src/qcommon/vm_native_module.c) + tests/stub_qcommon_min.c + src/qcommon/vm_native_module.c + src/qcommon/q_shared.c + src/qcommon/q_math.c) target_include_directories(unit_vm_native_module PRIVATE ${CMAKE_SOURCE_DIR}/src) + if(NOT MSVC) + target_link_libraries(unit_vm_native_module PRIVATE m) + endif() add_test(NAME unit_vm_native_module COMMAND unit_vm_native_module) set_tests_properties(unit_vm_native_module PROPERTIES LABELS "unit;validation") endif() diff --git a/src/qcommon/vm.c b/src/qcommon/vm.c index 64daf86413..2e122d388a 100644 --- a/src/qcommon/vm.c +++ b/src/qcommon/vm.c @@ -1741,64 +1741,22 @@ static void *VM_TryLoadNativeModule( const char *moduleName, char *filename, int static void * QDECL loadNative( const char *name, vmMainFunc_t *entryPoint, dllSyscall_t systemcalls ) { char filename[ MAX_QPATH ]; + char moduleNames[ VM_MAX_NATIVE_MODULE_LOAD_NAMES ][ MAX_QPATH ]; void *libHandle; dllEntry_t dllEntry; void *sym; void *vmMainAddr; qboolean isGenericModule = qfalse; + int i; + int moduleNameCount; // For ui, game, cgame, qagame and custom aliases, try generic names first. - if ( Q_stricmp( name, "ui" ) == 0 || Q_stricmp( name, "game" ) == 0 || Q_stricmp( name, "cgame" ) == 0 || - Q_stricmp( name, "qagame" ) == 0 || Q_stricmp( name, "frontend" ) == 0 || - Q_stricmp( name, "client" ) == 0 || Q_stricmp( name, "server" ) == 0 ) { + moduleNameCount = VM_BuildNativeModuleLoadOrder( name, moduleNames, (int)ARRAY_LEN( moduleNames ) ); + if ( moduleNameCount > 0 ) { isGenericModule = qtrue; - libHandle = VM_TryLoadNativeModule( name, filename, sizeof( filename ) ); - if ( libHandle ) { - goto loadSuccess; - } - - // qagame has historically been loaded from game.* as an alias. - if ( Q_stricmp( name, "qagame" ) == 0 ) { - libHandle = VM_TryLoadNativeModule( "game", filename, sizeof( filename ) ); - if ( libHandle ) { - goto loadSuccess; - } - } - - // Support renamed native modules for this project. - if ( Q_stricmp( name, "qagame" ) == 0 || Q_stricmp( name, "game" ) == 0 ) { - libHandle = VM_TryLoadNativeModule( "server", filename, sizeof( filename ) ); - if ( libHandle ) { - goto loadSuccess; - } - } - if ( Q_stricmp( name, "cgame" ) == 0 ) { - libHandle = VM_TryLoadNativeModule( "client", filename, sizeof( filename ) ); - if ( libHandle ) { - goto loadSuccess; - } - } - if ( Q_stricmp( name, "ui" ) == 0 ) { - libHandle = VM_TryLoadNativeModule( "frontend", filename, sizeof( filename ) ); - if ( libHandle ) { - goto loadSuccess; - } - } - if ( Q_stricmp( name, "server" ) == 0 ) { - libHandle = VM_TryLoadNativeModule( "game", filename, sizeof( filename ) ); - if ( libHandle ) { - goto loadSuccess; - } - } - if ( Q_stricmp( name, "client" ) == 0 ) { - libHandle = VM_TryLoadNativeModule( "cgame", filename, sizeof( filename ) ); - if ( libHandle ) { - goto loadSuccess; - } - } - if ( Q_stricmp( name, "frontend" ) == 0 ) { - libHandle = VM_TryLoadNativeModule( "ui", filename, sizeof( filename ) ); + for ( i = 0; i < moduleNameCount; i++ ) { + libHandle = VM_TryLoadNativeModule( moduleNames[i], filename, sizeof( filename ) ); if ( libHandle ) { goto loadSuccess; } diff --git a/src/qcommon/vm_native_module.c b/src/qcommon/vm_native_module.c index be2f9d7b78..d02e974286 100644 --- a/src/qcommon/vm_native_module.c +++ b/src/qcommon/vm_native_module.c @@ -1,6 +1,63 @@ #include "vm_native_module.h" #include +static void VM_CopyNativeModuleName( char *dst, const char *src ) { + (void)snprintf( dst, MAX_QPATH, "%s", src ); +} + +static qboolean VM_IsGenericNativeModuleName( const char *moduleName ) { + return Q_stricmp( moduleName, "ui" ) == 0 || + Q_stricmp( moduleName, "game" ) == 0 || + Q_stricmp( moduleName, "cgame" ) == 0 || + Q_stricmp( moduleName, "qagame" ) == 0 || + Q_stricmp( moduleName, "frontend" ) == 0 || + Q_stricmp( moduleName, "client" ) == 0 || + Q_stricmp( moduleName, "server" ) == 0; +} + +static int VM_AppendNativeModuleName( char out[][MAX_QPATH], int maxModules, int count, const char *moduleName ) { + if ( count >= maxModules ) { + return count; + } + + VM_CopyNativeModuleName( out[count], moduleName ); + return count + 1; +} + +int VM_BuildNativeModuleLoadOrder( const char *moduleName, char out[][MAX_QPATH], int maxModules ) { + int count = 0; + + if ( !moduleName || !moduleName[0] || !out || maxModules <= 0 ) { + return 0; + } + + if ( !VM_IsGenericNativeModuleName( moduleName ) ) { + return 0; + } + + count = VM_AppendNativeModuleName( out, maxModules, count, moduleName ); + + /* qagame historically probes game.*, and this fork also supports server.*. */ + if ( Q_stricmp( moduleName, "qagame" ) == 0 ) { + count = VM_AppendNativeModuleName( out, maxModules, count, "game" ); + count = VM_AppendNativeModuleName( out, maxModules, count, "server" ); + } else if ( Q_stricmp( moduleName, "game" ) == 0 ) { + count = VM_AppendNativeModuleName( out, maxModules, count, "server" ); + } else if ( Q_stricmp( moduleName, "cgame" ) == 0 ) { + count = VM_AppendNativeModuleName( out, maxModules, count, "client" ); + } else if ( Q_stricmp( moduleName, "ui" ) == 0 ) { + count = VM_AppendNativeModuleName( out, maxModules, count, "frontend" ); + } else if ( Q_stricmp( moduleName, "server" ) == 0 ) { + count = VM_AppendNativeModuleName( out, maxModules, count, "game" ); + } else if ( Q_stricmp( moduleName, "client" ) == 0 ) { + count = VM_AppendNativeModuleName( out, maxModules, count, "cgame" ); + } else if ( Q_stricmp( moduleName, "frontend" ) == 0 ) { + count = VM_AppendNativeModuleName( out, maxModules, count, "ui" ); + } + + return count; +} + int VM_BuildNativeModuleCandidates( const char *moduleName, char out[][MAX_QPATH], int maxCandidates ) { int count = 0; diff --git a/src/qcommon/vm_native_module.h b/src/qcommon/vm_native_module.h index e8bd630f99..3e4003d210 100644 --- a/src/qcommon/vm_native_module.h +++ b/src/qcommon/vm_native_module.h @@ -11,4 +11,15 @@ */ int VM_BuildNativeModuleCandidates( const char *moduleName, char out[][MAX_QPATH], int maxCandidates ); +enum { + VM_MAX_NATIVE_MODULE_LOAD_NAMES = 3 +}; + +/* + * Builds logical native VM module names in probe order for the built-in game, + * cgame, and UI aliases. Non-generic module names return zero so loadNative() + * preserves the legacy platform-specific fallback path for custom modules. + */ +int VM_BuildNativeModuleLoadOrder( const char *moduleName, char out[][MAX_QPATH], int maxModules ); + #endif diff --git a/tests/README.md b/tests/README.md index f155d19aa4..4a09fc9100 100644 --- a/tests/README.md +++ b/tests/README.md @@ -1,5 +1,5 @@ # Tests -- **Unit** (`BUILD_UNIT_TESTS=ON`): `unit_macros`, `unit_qmath`, `unit_surfaceflags`, `unit_qhelpers`, `unit_crc`, `unit_pathutil`, `unit_msg`, `unit_info`, `unit_cm_bounds`, `unit_parse`, `unit_endian` (CRC, COM path, `Info_*`, `COM_Parse*`, and endian tests use the same minimal `stub_qcommon_min.c` + `q_shared.c` + `q_math.c` link as `unit_qhelpers`; `unit_msg` links `msg.c` + `huffman_static.c` with `stub_qcommon_min.c`, `stub_msg_cvar.c`, and `-DDEDICATED`; `unit_cm_bounds` links `cm_bounds.c` + `q_math.c` only) — run `ctest -R unit_` or `./unit_*` from the build directory. +- **Unit** (`BUILD_UNIT_TESTS=ON`): `unit_macros`, `unit_qmath`, `unit_surfaceflags`, `unit_qhelpers`, `unit_crc`, `unit_pathutil`, `unit_msg`, `unit_info`, `unit_cm_bounds`, `unit_parse`, `unit_endian`, `unit_vm_native_module` (CRC, COM path, `Info_*`, `COM_Parse*`, endian, and native-module tests use the same minimal `stub_qcommon_min.c` + `q_shared.c` + `q_math.c` link as `unit_qhelpers`; `unit_msg` links `msg.c` + `huffman_static.c` with `stub_qcommon_min.c`, `stub_msg_cvar.c`, and `-DDEDICATED`; `unit_cm_bounds` links `cm_bounds.c` + `q_math.c` only) — run `ctest -R unit_` or `./unit_*` from the build directory. - **Script regression tests**: `test_botlib_bounded_strings` (botlib string invariants). Run with `ctest -R test_botlib_bounded_strings` from the build directory. - **Validation**: `smoke_test`, `renderer_regression_check`, `check_artifacts`, `test_run_vulkan_script`, `test_compile_engine_lto`, `test_demo_game_pk3`, `test_vk_vegetation_dispatch_order`, `test_vulkan_mesh_shader_opt_in`, `test_vulkan_runtime_regressions`, `test_botlib_chat_message_bounds`, `test_vulkan_renderer_guards`, `test_vulkan_regression_source_guards`, `test_gltf_opengl_regressions`, `test_botlib_bounded_strings` — see `scripts/`, `tests/scripts/`, and `docs/RENDERER_CONFIDENCE.md`. diff --git a/tests/unit/test_vm_native_module.c b/tests/unit/test_vm_native_module.c index 51ee8d9035..d9188b405e 100644 --- a/tests/unit/test_vm_native_module.c +++ b/tests/unit/test_vm_native_module.c @@ -64,6 +64,78 @@ static int test_candidate_limit(void) { return 0; } +static int test_load_order_empty_inputs(void) { + char out[VM_MAX_NATIVE_MODULE_LOAD_NAMES][MAX_QPATH]; + + ASSERT(VM_BuildNativeModuleLoadOrder(NULL, out, VM_MAX_NATIVE_MODULE_LOAD_NAMES) == 0, "NULL load-order moduleName"); + ASSERT(VM_BuildNativeModuleLoadOrder("", out, VM_MAX_NATIVE_MODULE_LOAD_NAMES) == 0, "empty load-order moduleName"); + ASSERT(VM_BuildNativeModuleLoadOrder("qagame", NULL, VM_MAX_NATIVE_MODULE_LOAD_NAMES) == 0, "NULL load-order output buffer"); + ASSERT(VM_BuildNativeModuleLoadOrder("qagame", out, 0) == 0, "zero load-order maxModules"); + + return 0; +} + +static int assert_load_order(const char *moduleName, const char *expected0, const char *expected1, const char *expected2) { + char out[VM_MAX_NATIVE_MODULE_LOAD_NAMES][MAX_QPATH]; + int expectedCount = 0; + int count; + + if ( expected0 ) { + expectedCount++; + } + if ( expected1 ) { + expectedCount++; + } + if ( expected2 ) { + expectedCount++; + } + + count = VM_BuildNativeModuleLoadOrder(moduleName, out, VM_MAX_NATIVE_MODULE_LOAD_NAMES); + ASSERT(count == expectedCount, "load-order count"); + if ( expected0 ) { + ASSERT(strcmp(out[0], expected0) == 0, "load-order primary"); + } + if ( expected1 ) { + ASSERT(strcmp(out[1], expected1) == 0, "load-order first alias"); + } + if ( expected2 ) { + ASSERT(strcmp(out[2], expected2) == 0, "load-order second alias"); + } + + return 0; +} + +static int test_load_order_aliases(void) { + ASSERT(assert_load_order("qagame", "qagame", "game", "server") == 0, "qagame load order"); + ASSERT(assert_load_order("game", "game", "server", NULL) == 0, "game load order"); + ASSERT(assert_load_order("server", "server", "game", NULL) == 0, "server load order"); + ASSERT(assert_load_order("cgame", "cgame", "client", NULL) == 0, "cgame load order"); + ASSERT(assert_load_order("client", "client", "cgame", NULL) == 0, "client load order"); + ASSERT(assert_load_order("ui", "ui", "frontend", NULL) == 0, "ui load order"); + ASSERT(assert_load_order("frontend", "frontend", "ui", NULL) == 0, "frontend load order"); + + return 0; +} + +static int test_load_order_case_and_limits(void) { + char out[VM_MAX_NATIVE_MODULE_LOAD_NAMES][MAX_QPATH]; + int count; + + count = VM_BuildNativeModuleLoadOrder("QAGAME", out, 2); + ASSERT(count == 2, "limited qagame load-order count"); + ASSERT(strcmp(out[0], "QAGAME") == 0, "primary load-order name preserves input case"); + ASSERT(strcmp(out[1], "game") == 0, "limited qagame first alias"); + + count = VM_BuildNativeModuleLoadOrder("FrontEnd", out, VM_MAX_NATIVE_MODULE_LOAD_NAMES); + ASSERT(count == 2, "mixed-case frontend load-order count"); + ASSERT(strcmp(out[0], "FrontEnd") == 0, "frontend primary preserves input case"); + ASSERT(strcmp(out[1], "ui") == 0, "frontend reverse alias"); + + ASSERT(VM_BuildNativeModuleLoadOrder("renderer", out, VM_MAX_NATIVE_MODULE_LOAD_NAMES) == 0, "custom modules keep legacy fallback path"); + + return 0; +} + int main(void) { if (test_empty_inputs() != 0) { return 1; @@ -74,6 +146,15 @@ int main(void) { if (test_candidate_limit() != 0) { return 1; } + if (test_load_order_empty_inputs() != 0) { + return 1; + } + if (test_load_order_aliases() != 0) { + return 1; + } + if (test_load_order_case_and_limits() != 0) { + return 1; + } printf("PASS: unit_vm_native_module\n"); return 0;