|
VoltMod
C++23 framework for CS2 server plugins
|
Unit tests use doctest and stay SDK-free: no HL2SDK, no live Runtime, entity or database connection. Test logic that takes plain values and returns plain values - parsers, the target-selector grammar, angle math, decaying scores, throttles, detector heuristics. Keep that logic in free functions over structs and the rest follows.
Each test case is its own CTest entry, so a CI report names the failing case. The presets set noTestsAction: error, so a run that discovers nothing fails the job instead of passing.
Run the binary under build/<preset>/ directly for doctest's own filters:
conan create excludes tests/, so a package build compiles no tests. CI builds the source checkout separately:
--no-lockfile resolves without conan.lock, which CI needs because it builds against SDK packages it just created from the HEAD recipes.
Put cases in tests/<Module>/*.cpp. voltmod_add_tests() supplies main and picks up new files.
CHECK* records a failure and continues; REQUIRE* stops the case, so use it before dereferencing or indexing a value under test:
Compare directly. CHECK_EQ and friends print both operands; wrapping the comparison in a predicate loses that:
doctest::Approx's tolerance is relative: |a - b| < epsilon * (scale + max(|a|, |b|)), so Approx(180.0f).epsilon(0.01) accepts a 1.81 gap. Where the test means an absolute tolerance (degrees, score units), write a local Near(a, b, eps) helper; the angle and decaying-score suites do. For throws use CHECK_THROWS_AS, CHECK_THROWS_WITH or CHECK_NOTHROW.
Each SUBCASE re-runs the enclosing body from the top, so setup is written once and every branch gets a fresh copy with no fixture class:
TEST_CASE_TEMPLATE instantiates the body once per type, reporting each as its own case:
Discovery runs the freshly built binary with --list-test-cases and parses the output as a CMake list, where [...] groups and ; separates. An unmatched bracket folds every following case into one entry and fails configure with the unrelated-looking add_test called with incorrect number of arguments; a semicolon silently splits one case into two bogus entries. voltmod_add_tests() scans the sources and fails configure naming the offending file instead.
Spell interval bounds out - wraps to -180 exclusive through 180 inclusive, not wraps into (-180, 180]. Parentheses, commas, colons, <, > and :: are fine.
voltmod_add_tests(<name> [DATABASE] [SOURCES ...] [DEFINITIONS ...]) comes from cmake/VoltModTests.cmake, a build module of the Conan package. It globs tests/**/*.cpp (excluding tests/Api/), supplies doctest's main, links doctest::doctest and VoltMod::Portable (the framework code that builds without the game SDK), adds the plugin's src/ and tests/ to the include path, and registers the cases with CTest. It is a no-op when BUILD_TESTING is off.
| Argument | Means |
|---|---|
SOURCES | the plugin's SDK-free translation units to compile beside the test cases |
DATABASE | also link VoltMod::Database, so a test can open a SQLite database and run the plugin's migrations |
DEFINITIONS | compile definitions for the test target |
Test binaries never link the plugin module or VoltMod::Sdk, so nothing drags in the game SDK. The Conan side is one line in conanfile.py:
Each Api.hpp aggregate must compile as the only VoltMod include in a translation unit, and RootApiSurfaceTest.cpp checks that the main umbrella pulls in neither the JSON layer nor the menu-building surface. These are compile-only, and they need the full HL2SDK build, so they live in tests/Api/ and compile into voltmod-api-surface-check - an object library in the root CMakeLists.txt linked against VoltMod::Sdk and VoltMod::Database.
In the framework checkout lint checks the module dependencies and the framework's source conventions. In a consumer repo it checks the source conventions under plugins/. -C <dir> runs it on another repo.