Skip to content

Commit 32a361b

Browse files
cursoragenttimfox
andcommitted
tests: cover native VM module fallbacks
Co-authored-by: Tim Fox <timfox@outlook.com>
1 parent 61d0e81 commit 32a361b

6 files changed

Lines changed: 211 additions & 63 deletions

File tree

CMakeLists.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2648,8 +2648,14 @@ if(BUILD_UNIT_TESTS)
26482648

26492649
add_executable(unit_vm_native_module
26502650
tests/unit/test_vm_native_module.c
2651+
tests/stub_qcommon_min.c
2652+
src/qcommon/q_shared.c
2653+
src/qcommon/q_math.c
26512654
src/qcommon/vm_native_module.c)
26522655
target_include_directories(unit_vm_native_module PRIVATE ${CMAKE_SOURCE_DIR}/src)
2656+
if(NOT MSVC)
2657+
target_link_libraries(unit_vm_native_module PRIVATE m)
2658+
endif()
26532659
add_test(NAME unit_vm_native_module COMMAND unit_vm_native_module)
26542660
set_tests_properties(unit_vm_native_module PROPERTIES LABELS "unit;validation")
26552661
endif()

src/qcommon/vm.c

Lines changed: 9 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1746,59 +1746,15 @@ static void * QDECL loadNative( const char *name, vmMainFunc_t *entryPoint, dllS
17461746
void *sym;
17471747
void *vmMainAddr;
17481748
qboolean isGenericModule = qfalse;
1749+
char moduleNames[3][MAX_QPATH];
1750+
int moduleCount;
1751+
int i;
17491752

1750-
// For ui, game, cgame, qagame and custom aliases, try generic names first.
1751-
if ( Q_stricmp( name, "ui" ) == 0 || Q_stricmp( name, "game" ) == 0 || Q_stricmp( name, "cgame" ) == 0 ||
1752-
Q_stricmp( name, "qagame" ) == 0 || Q_stricmp( name, "frontend" ) == 0 ||
1753-
Q_stricmp( name, "client" ) == 0 || Q_stricmp( name, "server" ) == 0 ) {
1753+
moduleCount = VM_BuildNativeModuleLoadOrder( name, moduleNames, (int)ARRAY_LEN( moduleNames ) );
1754+
if ( moduleCount > 0 ) {
17541755
isGenericModule = qtrue;
1755-
1756-
libHandle = VM_TryLoadNativeModule( name, filename, sizeof( filename ) );
1757-
if ( libHandle ) {
1758-
goto loadSuccess;
1759-
}
1760-
1761-
// qagame has historically been loaded from game.* as an alias.
1762-
if ( Q_stricmp( name, "qagame" ) == 0 ) {
1763-
libHandle = VM_TryLoadNativeModule( "game", filename, sizeof( filename ) );
1764-
if ( libHandle ) {
1765-
goto loadSuccess;
1766-
}
1767-
}
1768-
1769-
// Support renamed native modules for this project.
1770-
if ( Q_stricmp( name, "qagame" ) == 0 || Q_stricmp( name, "game" ) == 0 ) {
1771-
libHandle = VM_TryLoadNativeModule( "server", filename, sizeof( filename ) );
1772-
if ( libHandle ) {
1773-
goto loadSuccess;
1774-
}
1775-
}
1776-
if ( Q_stricmp( name, "cgame" ) == 0 ) {
1777-
libHandle = VM_TryLoadNativeModule( "client", filename, sizeof( filename ) );
1778-
if ( libHandle ) {
1779-
goto loadSuccess;
1780-
}
1781-
}
1782-
if ( Q_stricmp( name, "ui" ) == 0 ) {
1783-
libHandle = VM_TryLoadNativeModule( "frontend", filename, sizeof( filename ) );
1784-
if ( libHandle ) {
1785-
goto loadSuccess;
1786-
}
1787-
}
1788-
if ( Q_stricmp( name, "server" ) == 0 ) {
1789-
libHandle = VM_TryLoadNativeModule( "game", filename, sizeof( filename ) );
1790-
if ( libHandle ) {
1791-
goto loadSuccess;
1792-
}
1793-
}
1794-
if ( Q_stricmp( name, "client" ) == 0 ) {
1795-
libHandle = VM_TryLoadNativeModule( "cgame", filename, sizeof( filename ) );
1796-
if ( libHandle ) {
1797-
goto loadSuccess;
1798-
}
1799-
}
1800-
if ( Q_stricmp( name, "frontend" ) == 0 ) {
1801-
libHandle = VM_TryLoadNativeModule( "ui", filename, sizeof( filename ) );
1756+
for ( i = 0; i < moduleCount; i++ ) {
1757+
libHandle = VM_TryLoadNativeModule( moduleNames[i], filename, sizeof( filename ) );
18021758
if ( libHandle ) {
18031759
goto loadSuccess;
18041760
}
@@ -2051,7 +2007,6 @@ intptr_t QDECL VM_Call( vm_t *vm, int nargs, int callnum, ... )
20512007
{
20522008
//vm_t *oldVM;
20532009
intptr_t r;
2054-
int i;
20552010

20562011
if ( !vm ) {
20572012
Com_Error( ERR_FATAL, "VM_Call with NULL vm" );
@@ -2077,19 +2032,10 @@ intptr_t QDECL VM_Call( vm_t *vm, int nargs, int callnum, ... )
20772032
// if we have a dll loaded, call it directly
20782033
if ( vm->entryPoint )
20792034
{
2080-
/* Pass three arg slots: native vmMain only receives nargs from varargs; zero the rest
2081-
* so e.g. UI_GETAPIVERSION (nargs=0) does not read stack garbage. */
2082-
int32_t args[MAX_VMMAIN_CALL_ARGS-1];
2083-
Com_Memset( args, 0, sizeof( args ) );
20842035
va_list ap;
20852036
va_start( ap, callnum );
2086-
for ( i = 0; i < nargs; i++ ) {
2087-
args[i] = va_arg( ap, int32_t );
2088-
}
2037+
r = VM_CallNativeModuleEntryPoint( vm->entryPoint, nargs, callnum, ap );
20892038
va_end( ap );
2090-
2091-
// add more arguments if you're changed MAX_VMMAIN_CALL_ARGS:
2092-
r = vm->entryPoint( callnum, args[0], args[1], args[2] );
20932039
} else {
20942040
#if id386 && !defined __clang__ // calling convention doesn't need conversion in some cases
20952041
#ifndef NO_VM_COMPILED
@@ -2100,6 +2046,7 @@ intptr_t QDECL VM_Call( vm_t *vm, int nargs, int callnum, ... )
21002046
r = VM_CallInterpreted2( vm, nargs+1, (int32_t*)&callnum );
21012047
#else
21022048
int32_t args[MAX_VMMAIN_CALL_ARGS];
2049+
int i;
21032050
va_list ap;
21042051

21052052
args[0] = callnum;

src/qcommon/vm_native_module.c

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
#include "vm_native_module.h"
22
#include <stdio.h>
33

4+
static void VM_AddNativeModuleName( char out[][MAX_QPATH], int maxModules, int *count, const char *moduleName ) {
5+
if ( *count >= maxModules ) {
6+
return;
7+
}
8+
9+
(void)snprintf( out[*count], MAX_QPATH, "%s", moduleName );
10+
(*count)++;
11+
}
12+
413
int VM_BuildNativeModuleCandidates( const char *moduleName, char out[][MAX_QPATH], int maxCandidates ) {
514
int count = 0;
615

@@ -24,3 +33,56 @@ int VM_BuildNativeModuleCandidates( const char *moduleName, char out[][MAX_QPATH
2433

2534
return count;
2635
}
36+
37+
int VM_BuildNativeModuleLoadOrder( const char *moduleName, char out[][MAX_QPATH], int maxModules ) {
38+
int count = 0;
39+
40+
if ( !moduleName || !moduleName[0] || !out || maxModules <= 0 ) {
41+
return 0;
42+
}
43+
44+
if ( Q_stricmp( moduleName, "ui" ) != 0 && Q_stricmp( moduleName, "game" ) != 0 &&
45+
Q_stricmp( moduleName, "cgame" ) != 0 && Q_stricmp( moduleName, "qagame" ) != 0 &&
46+
Q_stricmp( moduleName, "frontend" ) != 0 && Q_stricmp( moduleName, "client" ) != 0 &&
47+
Q_stricmp( moduleName, "server" ) != 0 ) {
48+
return 0;
49+
}
50+
51+
VM_AddNativeModuleName( out, maxModules, &count, moduleName );
52+
53+
if ( Q_stricmp( moduleName, "qagame" ) == 0 ) {
54+
VM_AddNativeModuleName( out, maxModules, &count, "game" );
55+
}
56+
if ( Q_stricmp( moduleName, "qagame" ) == 0 || Q_stricmp( moduleName, "game" ) == 0 ) {
57+
VM_AddNativeModuleName( out, maxModules, &count, "server" );
58+
}
59+
if ( Q_stricmp( moduleName, "cgame" ) == 0 ) {
60+
VM_AddNativeModuleName( out, maxModules, &count, "client" );
61+
}
62+
if ( Q_stricmp( moduleName, "ui" ) == 0 ) {
63+
VM_AddNativeModuleName( out, maxModules, &count, "frontend" );
64+
}
65+
if ( Q_stricmp( moduleName, "server" ) == 0 ) {
66+
VM_AddNativeModuleName( out, maxModules, &count, "game" );
67+
}
68+
if ( Q_stricmp( moduleName, "client" ) == 0 ) {
69+
VM_AddNativeModuleName( out, maxModules, &count, "cgame" );
70+
}
71+
if ( Q_stricmp( moduleName, "frontend" ) == 0 ) {
72+
VM_AddNativeModuleName( out, maxModules, &count, "ui" );
73+
}
74+
75+
return count;
76+
}
77+
78+
intptr_t VM_CallNativeModuleEntryPoint( vmNativeModuleEntryPoint_t entryPoint, int nargs, int callnum, va_list ap ) {
79+
int32_t args[MAX_VMMAIN_CALL_ARGS - 1];
80+
int i;
81+
82+
Com_Memset( args, 0, sizeof( args ) );
83+
for ( i = 0; i < nargs; i++ ) {
84+
args[i] = va_arg( ap, int32_t );
85+
}
86+
87+
return entryPoint( callnum, args[0], args[1], args[2] );
88+
}

src/qcommon/vm_native_module.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
#include "q_shared.h"
55

6+
typedef intptr_t (QDECL *vmNativeModuleEntryPoint_t)( int command, int arg0, int arg1, int arg2 );
7+
68
/*
79
* Builds native VM module filename candidates in priority order:
810
* 1) module.so
@@ -11,4 +13,12 @@
1113
*/
1214
int VM_BuildNativeModuleCandidates( const char *moduleName, char out[][MAX_QPATH], int maxCandidates );
1315

16+
/*
17+
* Builds logical native VM module names in the order loadNative() should probe
18+
* them before falling back to the legacy platform-specific filename.
19+
*/
20+
int VM_BuildNativeModuleLoadOrder( const char *moduleName, char out[][MAX_QPATH], int maxModules );
21+
22+
intptr_t VM_CallNativeModuleEntryPoint( vmNativeModuleEntryPoint_t entryPoint, int nargs, int callnum, va_list ap );
23+
1424
#endif

tests/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Tests
22

3-
- **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.
3+
- **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 VM 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.
44
- **Script regression tests**: `test_botlib_bounded_strings` (botlib string invariants). Run with `ctest -R test_botlib_bounded_strings` from the build directory.
55
- **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`.

tests/unit/test_vm_native_module.c

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,120 @@ static int test_candidate_limit(void) {
6464
return 0;
6565
}
6666

67+
static int assert_load_order(const char *moduleName, int maxModules, int expectedCount, const char **expected) {
68+
char out[3][MAX_QPATH];
69+
int count;
70+
int i;
71+
72+
count = VM_BuildNativeModuleLoadOrder(moduleName, out, maxModules);
73+
ASSERT(count == expectedCount, "load-order count");
74+
for (i = 0; i < count; i++) {
75+
ASSERT(strcmp(out[i], expected[i]) == 0, "load-order entry");
76+
}
77+
78+
return 0;
79+
}
80+
81+
static int test_load_order_aliases(void) {
82+
const char *qagame[] = { "qagame", "game", "server" };
83+
const char *game[] = { "game", "server" };
84+
const char *cgame[] = { "cgame", "client" };
85+
const char *ui[] = { "ui", "frontend" };
86+
const char *server[] = { "server", "game" };
87+
const char *client[] = { "client", "cgame" };
88+
const char *frontend[] = { "frontend", "ui" };
89+
const char *mixedCase[] = { "QAGAME", "game", "server" };
90+
91+
if (assert_load_order("qagame", 3, 3, qagame) != 0) {
92+
return 1;
93+
}
94+
if (assert_load_order("game", 3, 2, game) != 0) {
95+
return 1;
96+
}
97+
if (assert_load_order("cgame", 3, 2, cgame) != 0) {
98+
return 1;
99+
}
100+
if (assert_load_order("ui", 3, 2, ui) != 0) {
101+
return 1;
102+
}
103+
if (assert_load_order("server", 3, 2, server) != 0) {
104+
return 1;
105+
}
106+
if (assert_load_order("client", 3, 2, client) != 0) {
107+
return 1;
108+
}
109+
if (assert_load_order("frontend", 3, 2, frontend) != 0) {
110+
return 1;
111+
}
112+
if (assert_load_order("QAGAME", 3, 3, mixedCase) != 0) {
113+
return 1;
114+
}
115+
116+
return 0;
117+
}
118+
119+
static int test_load_order_limits_and_custom_modules(void) {
120+
const char *limited[] = { "qagame", "game" };
121+
char out[3][MAX_QPATH];
122+
123+
if (assert_load_order("qagame", 2, 2, limited) != 0) {
124+
return 1;
125+
}
126+
127+
ASSERT(VM_BuildNativeModuleLoadOrder(NULL, out, 3) == 0, "NULL load-order input");
128+
ASSERT(VM_BuildNativeModuleLoadOrder("", out, 3) == 0, "empty load-order input");
129+
ASSERT(VM_BuildNativeModuleLoadOrder("custommod", out, 3) == 0, "custom module should use legacy platform-specific fallback only");
130+
ASSERT(VM_BuildNativeModuleLoadOrder("qagame", NULL, 3) == 0, "NULL load-order output");
131+
ASSERT(VM_BuildNativeModuleLoadOrder("qagame", out, 0) == 0, "zero load-order capacity");
132+
133+
return 0;
134+
}
135+
136+
static int capturedCommand;
137+
static int capturedArg0;
138+
static int capturedArg1;
139+
static int capturedArg2;
140+
141+
static intptr_t QDECL capture_entry_point(int command, int arg0, int arg1, int arg2) {
142+
capturedCommand = command;
143+
capturedArg0 = arg0;
144+
capturedArg1 = arg1;
145+
capturedArg2 = arg2;
146+
return 1234;
147+
}
148+
149+
static intptr_t call_native_entry_point(int nargs, int callnum, ...) {
150+
intptr_t result;
151+
va_list ap;
152+
153+
va_start(ap, callnum);
154+
result = VM_CallNativeModuleEntryPoint(capture_entry_point, nargs, callnum, ap);
155+
va_end(ap);
156+
return result;
157+
}
158+
159+
static int test_native_call_args_zero_fill(void) {
160+
ASSERT(call_native_entry_point(0, 77) == 1234, "native entry return value");
161+
ASSERT(capturedCommand == 77, "zero-arg callnum");
162+
ASSERT(capturedArg0 == 0, "zero-arg arg0");
163+
ASSERT(capturedArg1 == 0, "zero-arg arg1");
164+
ASSERT(capturedArg2 == 0, "zero-arg arg2");
165+
166+
ASSERT(call_native_entry_point(1, 88, 111) == 1234, "one-arg native entry return value");
167+
ASSERT(capturedCommand == 88, "one-arg callnum");
168+
ASSERT(capturedArg0 == 111, "one-arg arg0");
169+
ASSERT(capturedArg1 == 0, "one-arg arg1 zero-filled");
170+
ASSERT(capturedArg2 == 0, "one-arg arg2 zero-filled");
171+
172+
ASSERT(call_native_entry_point(3, 99, 11, 22, 33) == 1234, "three-arg native entry return value");
173+
ASSERT(capturedCommand == 99, "three-arg callnum");
174+
ASSERT(capturedArg0 == 11, "three-arg arg0");
175+
ASSERT(capturedArg1 == 22, "three-arg arg1");
176+
ASSERT(capturedArg2 == 33, "three-arg arg2");
177+
178+
return 0;
179+
}
180+
67181
int main(void) {
68182
if (test_empty_inputs() != 0) {
69183
return 1;
@@ -74,6 +188,15 @@ int main(void) {
74188
if (test_candidate_limit() != 0) {
75189
return 1;
76190
}
191+
if (test_load_order_aliases() != 0) {
192+
return 1;
193+
}
194+
if (test_load_order_limits_and_custom_modules() != 0) {
195+
return 1;
196+
}
197+
if (test_native_call_args_zero_fill() != 0) {
198+
return 1;
199+
}
77200

78201
printf("PASS: unit_vm_native_module\n");
79202
return 0;

0 commit comments

Comments
 (0)