Two vehicles with automatic couplers coupled the moment they touched, in CouplerForce(), before CollisionDetect() ran; its derail check only covers uncoupled vehicles, so they never collided nor derailed, at any speed. With crash damage on, the coupling now waits for a closing speed of at most 15 km/h, the limit of the derail check. A faster hit locks the automatic coupling until the vehicles part, so the collision runs its course instead of the couplers catching once it has slowed them down. Two collision bugs on the way: - CollisionVelocity() took the length of the difference of the heading quaternions times the speeds; a quaternion turns by half its angle, so for vehicles facing each other the speeds came out at right angles: a head-on hit at 10 + 10 km/h read as 14.1 km/h, under the limit. The closing speed is now taken from the touching ends, as CouplerForce() relates the vehicles: the sum of the speeds head-on, the difference rear-end, negative when the ends part, - CollisionDetect() gave ComputeCollision() the other vehicle's speed along that vehicle's own axis, which points the other way for vehicles facing each other: a wagon hit head-on counted as running into the hitter, and the hitter bounced back as from a wall (20 km/h into a standing wagon: it ran off at 6 km/h instead of 17). The speed is turned into this vehicle's frame and back. Head-on hits now count with both speeds, so with crash damage on they derail sooner than before, at the 15 km/h the check was meant for. Tests: test::pair runs two test vehicles on one track the way vehicle_table::update() runs a scene (neighbours, forces, movement), with coupling, buffing, collisions and derailing coming from the physics. tests/unit/test_couplers.cpp: catching up to 15 km/h and not above; a slow hit couples, a fast one doesn't, nor once slowed down, the lock resets once the vehicles part; a hit a little over 15 km/h bounces apart on the rails, harder ones derail; head-on 7 + 7 couples, 8 + 8 bounces, 15 + 15 derails; no catching a coupler of another type, couplers of different control systems couple without the control connection.
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 printsUnit 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 indata/orrun_tests.cmakechanged (Ninja and Makefile generators).-DWITH_TESTS=OFFbuilds the game without them. - From the IDE: run or debug the
eu07_unit_teststarget. 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, e.g.eu07_unit_tests -tc="brakes*".EU07_UNIT_TESTS_LOG=1shows the engine log, quiet otherwise.
Writing a test
Add a test_<topic>.cpp here, it's picked up on its own.
#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>")readsdata/<name>.json, with the sections and keys of the game's vehicle definitions;set()andremove()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 invehicle.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 needBrake.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.
- Logic which lives in classes too big to set up in a test (the AI driver, the cab) goes into free
functions on the vehicle state the class calls, and the test calls them the same way: see
vehicle/handover.h, whichTController::TakeControl()uses, andtest_handover.cpp. vehiclecreates the vehicle the wayTDynamicObject::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 (
REQUIREthat 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.