17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-17 05:09:20 +02:00

Test/physics unit tests (#213)

* build: doctest 2.4.11 in ref/

Single header test framework (MIT), vendored like the other libraries in
ref/ so the tests need nothing extra installed on the CI runners.

* refactor(physics): read vehicle definitions from text as well as files

TMoverParameters::LoadFIZ(path) opens the file and hands the parser over
to LoadFIZ_Parse(), which holds what LoadFIZ() did with it before. The
new LoadFIZ_Text() reads the same format from a string, so a definition
can be made up in memory, e.g. by a unit test. No change in behaviour for
files.

* build: the simulation core as a library the game links

The vehicle physics (McZapkie), the vehicle definition parser and the
helpers they use build as eu07_core, a static library the game links,
so the unit tests to come can link the very same objects instead of
compiling their own copy with their own settings.

The game's build settings (include dirs, definitions, compile options,
libraries) move from the executable to eu07_settings, an interface
target the library links publicly, so the game, the library and later
the tests all build with the same ones. What only the executable needs
stays with it: /MAP, /LARGEADDRESSAWARE, output dirs, the precompiled
header, runtime DLL copying, the crash reporter and the experimental
renderer. No change in what the game builds or links.

* test: unit tests of the simulation core, run as part of the build

eu07_unit_tests: C++ tests (doctest) of the simulation, which set up the
conditions, run the code and check the outcome. No game data, no window.

- the code under test is the core library (eu07_core), the very objects
  the game links, built with the game's settings; engine_stubs.cpp stands
  in for the logs, settings and scenery layers the library reaches for.
  python gets linked with the settings but isn't needed to run the tests
- test vehicles are JSON definitions in tests/unit/data, with the sections
  and keys of the game's vehicle definitions (tables as "rows", _keys are
  comments), changed by the tests where needed and loaded from memory by
  the game's own loader; an unknown section is an error rather than
  silently skipped
- test::vehicle creates the mover the way TDynamicObject::Init() does and
  steps it the way the simulation loop does, on a straight track of given
  friction and gradient, optionally holding the brake pipe pressure the
  way a coupled locomotive would
- the random numbers of the simulation are reseeded before every test
  case, so a test gives the same result alone (IDE) or among the others
- building the game runs the tests at the end, once the game is linked,
  and prints a one line summary ("Unit tests: 11/11 passed (80 checks)").
  A failure prints the full report and fails the build, in CI and locally
  alike, and removes the game binary so the next build reruns them. A
  change of the tests alone relinks the game so they run (Ninja and
  Makefile generators). -DWITH_TESTS=OFF leaves them out
- first tests: definitions reach the vehicle, KE service application and
  release, deeper reduction brakes harder, every implemented distributor
  valve charges/applies/releases, service braking stops a moving wagon

* test: stub basic_node::radius_() for GCC and Clang

The Linux build failed to link the unit tests: undefined reference to
scene::basic_node::radius_() from basic_node's vtable in engine_stubs.o.
basic_node has no key function for the Itanium C++ ABI (tooltip() is
declared plain but defined inline in the header), so GCC and Clang emit
its vtable in every unit using scenenode.h, and the vtable needs
radius_(), defined in scenenode.cpp, which the tests don't link. MSVC
emits vtables differently, so the Windows build linked.

* test: test vehicle definitions load with GCC and Clang

All the tests reading a definition failed on Linux with "unknown vehicle
definition section 0": the parsed definition was brace-initialised
(auto const json{parse(...)}), which GCC and Clang take for the
initializer list constructor of nlohmann::json, making an array holding
the definition, while MSVC copies it. Initialise it with =, and require
the definition to be a JSON object.


* test: SonarCloud issues of the unit tests

- test::definition_error for unusable test vehicle definitions instead of
  std::runtime_error (S112)
- the rows of a table section written by a function of their own, less
  nesting in definition::text() (S134)
- std::less<> for the section table (S6045)
- comments in the empty methods of the reseeding listener and stub (S1186)
- NOSONAR where the engine's headers fix the stubs: the extern globals
  and the member function signatures (S5421, S5817, S1238)
- NOSONAR on LoadFIZ_Parse(): the cognitive complexity is the former body
  of LoadFIZ(), moved as it was (S3776)
- a comment Sonar took for code reworded (S125)
This commit is contained in:
jerrrrycho
2026-10-15 17:56:41 +02:00
committed by GitHub
parent 05ffb9058c
commit b400255dae
15 changed files with 7899 additions and 63 deletions

68
tests/unit/README.md Normal file
View File

@@ -0,0 +1,68 @@
# Unit tests
C++ tests of the simulation logic: set up the conditions, run the code, check the outcome. They run
as part of every build of the game, in CI and locally alike, and from the IDE. They need no game
data and no window: the test vehicles are JSON definitions in `data/`, loaded from memory by the
game's own vehicle loader.
The code under test is `eu07_core`, the library of the simulation parts the game links as well
(physics, vehicle definition loader, ...): built once, with the game's settings, and tested as the
game uses it. The python library gets linked along with the game's settings, but the tests don't
use it and run without it.
## Running
- **With the build**: building the game (`WITH_TESTS`, on by default) runs them at the end, once the
game is linked, and prints `Unit tests: 11/11 passed (80 checks)`. A failing test prints the full
report and fails the build; the game binary gets removed, so the next build links it again and
reruns them. They also run when only the tests, a definition in `data/` or `run_tests.cmake`
changed (Ninja and Makefile generators). `-DWITH_TESTS=OFF` builds the game without them.
- **From the IDE**: run or debug the `eu07_unit_tests` target. CLion recognises doctest and lists the
test cases, so a single one can be run or debugged from the gutter.
- **By hand**: `ctest --test-dir <build dir> --build-config Release`, or the binary itself, which takes
[doctest options](https://github.com/doctest/doctest/blob/master/doc/markdown/commandline.md), e.g.
`eu07_unit_tests -tc="brakes*"`. `EU07_UNIT_TESTS_LOG=1` shows the engine log, quiet otherwise.
## Writing a test
Add a `test_<topic>.cpp` here, it's picked up on its own.
```cpp
#include "stdafx.h"
#include "doctest.h"
#include "vehicle.h"
TEST_CASE("brakes: service braking stops a heavier wagon too") {
// conditions: the wagon from data/wagon.json, heavier, running at 40 km/h
test::vehicle wagon{test::definition::load("wagon").set("Param", "M", 60000), 40.0 /* km/h */};
// action: full service application, as a coupled locomotive would make it
wagon.hold_brake_pipe(3.5); // bar
wagon.run(40.0 /* s */, test::track{0.15 /* friction */, 0.0 /* gradient */});
// outcome
CHECK(std::abs(wagon.speed()) < 0.1);
}
```
- `definition::load("<name>")` reads `data/<name>.json`, with the sections and keys of the game's
vehicle definitions; `set()` and `remove()` change it for the test. Table sections give their
rows as `"rows": [[...], ...]`, keys starting with `_` are comments. A section the loader doesn't
know is an error (the table of them is in `vehicle.cpp`); a misspelt key isn't caught, so check
in the test that the value the test is about made it to the vehicle. Keep the definitions
physically sensible: some parts need keys others don't (the ESt brake valves need `Brake.Size`),
and a test on a half-defined vehicle tests the definition rather than the simulation.
- The random numbers of the simulation are reseeded before every test case, so a test gives the
same result alone or among the others.
- `vehicle` creates the vehicle the way `TDynamicObject::Init()` does and steps it the way the
simulation loop does, on a straight track of the given friction and gradient. Coupled vehicles
aren't supported yet.
- Check behaviour (speeds, pressures, states the player sees) rather than internal flags, and make
the test check its own preconditions (`REQUIRE` that the braking skidded, before checking what a
skid leads to).
## Code to test, and when the tests fail to link
A source joins the tested code by moving from `SOURCES` to `CORE_SOURCES` in the main
`CMakeLists.txt`; the game links it from the library from then on. The library reaches for a little
of the rest of the engine, which `engine_stubs.cpp` stands in for in the tests (logs, settings,
scenery layers). If a change makes it use something new, either move the real source into the
library too, when it stands on its own, or add a stub to `engine_stubs.cpp`.