Skip to content

Commit 53521fa

Browse files
LpsdFileEX
andauthored
Add automated client test infrastructure (#4789)
#### Summary Add a standalone Google Test project (`Tests_Client`) and GitHub Actions CI workflow that builds and runs 213 automated tests without requiring GTA:SA binaries or a running game client. **Test infrastructure:** - Vendored Google Test v1.17.0 (`vendor/googletest/`) — sources only, no prebuilt binaries - New premake5 project `Tests_Client` under `Tests/client/` producing `Tests_Client_d.exe` - Implementation shim (`Tests/client/SharedUtil_Impl.cpp`) that includes SharedUtil implementations without pulling in full client dependencies - Links vendor libraries `cryptopp`, `blowfish_bcrypt`, and `zlib` for hash/crypto coverage **CI pipeline** (`.github/workflows/tests.yml`): - Triggers on pushes to `master`/`tests` and all PRs targeting `master` - Generates premake projects, builds with MSBuild (Debug/Win32), runs tests, uploads JUnit XML results as artifacts **213 tests across 11 files:** | File | Tests | What's covered | |------|-------|----------------| | `CVector_Tests.cpp` | 22 | Constructors, length, normalize, dot/cross product, arithmetic, validity | | `CVector2D_Tests.cpp` | 20 | Constructors (from CVector/CVector4D), length, normalize, dot product, all operators | | `CVector4D_Tests.cpp` | 10 | Constructors, 4D length/normalize/dot, arithmetic | | `CMatrix_Tests.cpp` | 25 | Identity, rotation/position/scale get/set, inverse, ortho-normalize, transform, buffer layout | | `CMatrix4_Tests.cpp` | 11 | Identity, RotX/Y/Z, translate, multiply (vector & matrix), subscript | | `CRect_Tests.cpp` | 13 | Constructors, stretch-to-point, restrict, circle intersection, reset, fix-top-left | | `SharedUtil_Tests.cpp` | 20 | SString format/split/join/replace, path join, dynamic cast, wildcard matching, container erase patterns, color code removal | | `SharedUtilMath_Tests.cpp` | 28 | Square, degrees-to-radians, should-use-int, float comparison helpers, significant bits | | `CFastList_Tests.cpp` | 17 | Push/pop, contains, remove, iteration, suspended modification, revision tracking | | `CDuplicateLineFilter_Tests.cpp` | 6 | Unique/duplicate line detection, flush, multi-line patterns | | `SharedUtilHash_Tests.cpp` | 22 | Hex round-trip, TEA encode/decode, HMAC (MD5/SHA1/SHA224/SHA256/SHA384/SHA512), RSA keygen + encrypt/decrypt (1024/2048-bit), RSA known-ciphertext decryption (1024/2048/4096-bit), AES-128-CTR round-trip, file-based MD5/SHA1/SHA224/SHA256/SHA384/SHA512 | All test expectations were ported from the existing `SharedUtil.Tests.hpp` internal test framework and verified to produce identical results. **Implementation notes:** - `SString(const char*)` is ambiguous with the printf-style `SString(const char*, ...)` overload in MSVC. Tests use `SStringX` for plain strings and a `MakeRawString()` helper (constructs via `std::string`) for binary data with explicit lengths. - All tested code comes from `Shared/sdk/` headers - no Game SA symbols are referenced, so no GTA:SA installation is needed. #### Motivation MTA has no automated test suite that runs on CI. The existing tests in `SharedUtil.Tests.hpp` only run inside the game client process, meaning regressions can only be caught by manually launching the game. This makes it easy for changes to shared utility code (math, strings, crypto, collections) to silently break. This PR establishes the foundation for CI-gated testing. The immediate value is regression coverage for 213 behaviors across the shared utility layer. The longer-term goal is to expand coverage into client-specific logic by mocking the Game SDK interfaces. **Next phase - Game SDK mocking:** The current project is limited to `Shared/sdk/` code because anything touching `Client/game_sa/` requires a running GTA:SA process with hooked game memory. The next step is to introduce mock/stub implementations of the Game SDK (`Client/sdk/game/`) interfaces, which would unlock testing for: - **CClientEntity hierarchy** - entity creation, parent/child attachment, element tree traversal, spatial DB updates - **Lua argument parsing** - `CLuaArguments` round-trip for all types, argument validation for every exported function - **Networking / packet handlers** - bitstream serialize/deserialize round-trips, fuzz-testing packet parsing - **Resource system** -start/stop lifecycle, file checksum validation, download retry logic - **Collision shapes** -hit-test all shape types (sphere, cube, polygon, tube), edge-case `IsEntityInside` checks - **Camera system** - mode transitions, interpolation correctness - **Vehicle handling** - `CHandlingEntry` property round-trips, handling mod delta computation - **RTree spatial index** - already header-only (no mock needed), bulk insert/query/remove verification The approach: create a `Tests_ClientMocked` project with lightweight `CGameSA` stubs (Google Mock or hand-written), linking against mocks instead of the real `game_sa` DLL. #### Test plan 1. Build locally: run `win-create-projects.bat`, then build `Tests_Client.vcxproj` (Debug/Win32) 2. Run `Bin\tests\Tests_Client_d.exe --gtest_brief=1` - all 213 tests pass 3. CI workflow validates the same on `windows-latest` with every push/PR 4. Hash test expectations were cross-checked against the original `SharedUtil.Tests.hpp` values 5. For future changes to any tested code: run `Tests_Client_d.exe` and confirm no regressions. The CI workflow will also catch failures automatically on PRs. #### Checklist * [x] Your code should follow the [coding guidelines](https://wiki.multitheftauto.com/index.php?title=Coding_guidelines). * [x] Smaller pull requests are easier to review. If your pull request is beefy, your pull request should be reviewable commit-by-commit. --------- Co-authored-by: FileEX <kongali@interia.pl>
1 parent 911111e commit 53521fa

55 files changed

Lines changed: 29850 additions & 8 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/tests.yml

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
name: "Tests"
2+
3+
on:
4+
push:
5+
branches: ["master", "tests"]
6+
pull_request:
7+
branches: ["master"]
8+
workflow_dispatch:
9+
10+
jobs:
11+
client-tests:
12+
name: Client Tests (x86 Debug)
13+
runs-on: windows-latest
14+
steps:
15+
- name: Checkout
16+
uses: actions/checkout@v4
17+
18+
- name: Setup MSBuild
19+
uses: microsoft/setup-msbuild@v2
20+
21+
- name: Generate project files
22+
shell: cmd
23+
run: |
24+
utils\premake5.exe install_cef
25+
utils\premake5.exe install_unifont
26+
utils\premake5.exe install_discord
27+
utils\premake5.exe vs2026
28+
29+
- name: Build Tests_Client
30+
shell: cmd
31+
run: msbuild Build\Tests_Client.vcxproj /p:Configuration=Debug /p:Platform=Win32 /p:PlatformToolset=v143 /nologo /v:minimal
32+
33+
- name: Run tests
34+
run: Bin\tests\Tests_Client_d.exe --gtest_output=xml:Bin\tests\test_results.xml
35+
36+
- name: Upload test results
37+
if: always()
38+
uses: actions/upload-artifact@v4
39+
with:
40+
name: test-results
41+
path: Bin/tests/test_results.xml

Client/game_sa/CRenderWareSA.TextureReplacing.cpp

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -937,7 +937,7 @@ namespace
937937

938938
const auto* pSnapshotEntry = FindPendingSnapshotEntry(usModelId, pReplacement);
939939
const bool bMatchesSnapshotWaitCondition = pSnapshotEntry && pSnapshotEntry->uiExpectedParentModelId == uiExpectedParentModelId &&
940-
pSnapshotEntry->usExpectedParentTxdId == usExpectedParentTxdId;
940+
pSnapshotEntry->usExpectedParentTxdId == usExpectedParentTxdId;
941941

942942
if (bMatchesSnapshotWaitCondition)
943943
{
@@ -4678,8 +4678,8 @@ CModelTexturesInfo* CRenderWareSA::GetModelTexturesInfo(unsigned short usModelId
46784678
unsigned int uiTxdStreamId = usTxdId + pGame->GetBaseIDforTXD();
46794679
CStreamingInfo* pStreamInfoBusyCheck = IsStreamingInfoSlot(usTxdId) ? GetStreamingInfoSafe(uiTxdStreamId) : nullptr;
46804680
bool bBusy = IsStreamingInfoSlot(usTxdId) && pStreamInfoBusyCheck &&
4681-
(pStreamInfoBusyCheck->loadState == eModelLoadState::LOADSTATE_READING ||
4682-
pStreamInfoBusyCheck->loadState == eModelLoadState::LOADSTATE_FINISHING);
4681+
(pStreamInfoBusyCheck->loadState == eModelLoadState::LOADSTATE_READING ||
4682+
pStreamInfoBusyCheck->loadState == eModelLoadState::LOADSTATE_FINISHING);
46834683
if (bBusy && !pCurrentTxd)
46844684
return nullptr;
46854685

@@ -4790,8 +4790,8 @@ CModelTexturesInfo* CRenderWareSA::GetModelTexturesInfo(unsigned short usModelId
47904790
const unsigned int uiTxdDataStreamId = usTxdId + static_cast<unsigned int>(iBaseIDforTXD);
47914791
CStreamingInfo* pStreamInfo = GetStreamingInfoSafe(uiTxdDataStreamId);
47924792
const bool bBusyOrLoaded = pStreamInfo && (pStreamInfo->loadState == eModelLoadState::LOADSTATE_READING ||
4793-
pStreamInfo->loadState == eModelLoadState::LOADSTATE_FINISHING ||
4794-
pStreamInfo->loadState == eModelLoadState::LOADSTATE_LOADED);
4793+
pStreamInfo->loadState == eModelLoadState::LOADSTATE_FINISHING ||
4794+
pStreamInfo->loadState == eModelLoadState::LOADSTATE_LOADED);
47954795
if (!bBusyOrLoaded)
47964796
pGame->GetStreaming()->RequestModel(uiTxdDataStreamId, 0x16);
47974797
}
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
/*****************************************************************************
2+
*
3+
* PROJECT: Multi Theft Auto
4+
* LICENSE: See LICENSE in the top level directory
5+
* FILE: Tests/client/CDuplicateLineFilter_Tests.cpp
6+
* PURPOSE: Google Test suite for CDuplicateLineFilter
7+
*
8+
*****************************************************************************/
9+
10+
#include <gtest/gtest.h>
11+
#include <SharedUtil.h>
12+
#include <CDuplicateLineFilter.h>
13+
14+
///////////////////////////////////////////////////////////////
15+
//
16+
// Basic pass-through: unique lines appear immediately
17+
//
18+
///////////////////////////////////////////////////////////////
19+
20+
// Verify a single unique line passes through immediately
21+
TEST(CDuplicateLineFilter, UniqueLinePassesThrough)
22+
{
23+
CDuplicateLineFilter<SString> filter;
24+
filter.AddLine("hello");
25+
SString output;
26+
EXPECT_TRUE(filter.PopOutputLine(output));
27+
EXPECT_STREQ(output.c_str(), "hello");
28+
}
29+
30+
// Verify multiple distinct lines all pass through in order
31+
TEST(CDuplicateLineFilter, MultipleUniqueLinesPassThrough)
32+
{
33+
CDuplicateLineFilter<SString> filter;
34+
filter.AddLine("line1");
35+
filter.AddLine("line2");
36+
filter.AddLine("line3");
37+
38+
SString output;
39+
EXPECT_TRUE(filter.PopOutputLine(output));
40+
EXPECT_STREQ(output.c_str(), "line1");
41+
EXPECT_TRUE(filter.PopOutputLine(output));
42+
EXPECT_STREQ(output.c_str(), "line2");
43+
EXPECT_TRUE(filter.PopOutputLine(output));
44+
EXPECT_STREQ(output.c_str(), "line3");
45+
EXPECT_FALSE(filter.PopOutputLine(output));
46+
}
47+
48+
///////////////////////////////////////////////////////////////
49+
//
50+
// Single-line duplicate detection
51+
//
52+
///////////////////////////////////////////////////////////////
53+
54+
// Verify consecutive duplicates are absorbed until a different line breaks the sequence
55+
TEST(CDuplicateLineFilter, SingleLineDuplicateDetected)
56+
{
57+
CDuplicateLineFilter<SString> filter;
58+
// First occurrence goes to output
59+
filter.AddLine("repeated");
60+
SString output;
61+
EXPECT_TRUE(filter.PopOutputLine(output));
62+
EXPECT_STREQ(output.c_str(), "repeated");
63+
64+
// Second and third occurrences are absorbed as duplicates
65+
filter.AddLine("repeated");
66+
filter.AddLine("repeated");
67+
filter.AddLine("repeated");
68+
// The duplicates are held until a non-matching line or flush
69+
EXPECT_FALSE(filter.PopOutputLine(output));
70+
71+
// A different line breaks the match
72+
filter.AddLine("different");
73+
// Now the duplicate line(s) plus the new line should be available
74+
EXPECT_TRUE(filter.PopOutputLine(output));
75+
// The output should contain the repeated line with a DUP marker
76+
EXPECT_NE(output.find("repeated"), SString::npos);
77+
}
78+
79+
///////////////////////////////////////////////////////////////
80+
//
81+
// Flush releases held duplicates
82+
//
83+
///////////////////////////////////////////////////////////////
84+
85+
// Verify Flush() releases held duplicate lines without requiring a different line
86+
TEST(CDuplicateLineFilter, FlushReleasesHeldLines)
87+
{
88+
CDuplicateLineFilter<SString> filter;
89+
filter.AddLine("alpha");
90+
// Consume the first output
91+
SString output;
92+
EXPECT_TRUE(filter.PopOutputLine(output));
93+
94+
// Start duplicate matching
95+
filter.AddLine("alpha");
96+
filter.AddLine("alpha");
97+
// Without flush, nothing should be available
98+
EXPECT_FALSE(filter.PopOutputLine(output));
99+
100+
filter.Flush();
101+
// After flush, the held lines should be released
102+
EXPECT_TRUE(filter.PopOutputLine(output));
103+
EXPECT_NE(output.find("alpha"), SString::npos);
104+
}
105+
106+
///////////////////////////////////////////////////////////////
107+
//
108+
// Multi-line pattern detection
109+
//
110+
///////////////////////////////////////////////////////////////
111+
112+
// Verify multi-line repeating patterns are detected and collapsed
113+
TEST(CDuplicateLineFilter, MultiLinePatternDetection)
114+
{
115+
CDuplicateLineFilter<SString> filter(4);
116+
// Produce a 2-line pattern: "A", "B"
117+
filter.AddLine("A");
118+
filter.AddLine("B");
119+
120+
// Consume initial output
121+
SString output;
122+
while (filter.PopOutputLine(output))
123+
{
124+
}
125+
126+
// Repeat the pattern
127+
filter.AddLine("A");
128+
filter.AddLine("B");
129+
filter.AddLine("A");
130+
filter.AddLine("B");
131+
132+
// Duplicates should be held
133+
// Flush to release
134+
filter.Flush();
135+
136+
// Should have output with dup count
137+
bool foundOutput = false;
138+
while (filter.PopOutputLine(output))
139+
foundOutput = true;
140+
EXPECT_TRUE(foundOutput);
141+
}
142+
143+
///////////////////////////////////////////////////////////////
144+
//
145+
// Empty output when nothing added
146+
//
147+
///////////////////////////////////////////////////////////////
148+
149+
// Verify an empty filter produces no output even after Flush()
150+
TEST(CDuplicateLineFilter, EmptyOutputWhenNothingAdded)
151+
{
152+
CDuplicateLineFilter<SString> filter;
153+
SString output;
154+
// PopOutputLine with no delay should return false (we can't test time-based
155+
// behavior reliably, so just test the immediate case)
156+
filter.Flush();
157+
EXPECT_FALSE(filter.PopOutputLine(output));
158+
}

0 commit comments

Comments
 (0)