mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-10-29 04:37:30 +01:00
fix(ai): release horn when human driver takes over from AI
This commit is contained in:
31
tests/unit/CMakeLists.txt
Normal file
31
tests/unit/CMakeLists.txt
Normal file
@@ -0,0 +1,31 @@
|
||||
# Unit tests of the simulation, built and run without the game data: the test vehicles are JSON
|
||||
# definitions in data/ (see vehicle.h), loaded from memory by the game's own vehicle loader. The code
|
||||
# under test is the game's core library (eu07_core), the very objects the game links; the rest of the
|
||||
# engine it reaches for is replaced with stubs (engine_stubs.cpp). They run as part of the build,
|
||||
# from the IDE, or with ctest.
|
||||
|
||||
set(UNIT_TESTS_NAME eu07_unit_tests)
|
||||
|
||||
# a new test_*.cpp file or data/*.json definition is picked up on its own
|
||||
file(GLOB UNIT_TEST_CASES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/test_*.cpp")
|
||||
file(GLOB UNIT_TEST_DATA CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/data/*.json")
|
||||
|
||||
add_executable(${UNIT_TESTS_NAME}
|
||||
main.cpp
|
||||
vehicle.cpp
|
||||
engine_stubs.cpp
|
||||
${UNIT_TEST_CASES})
|
||||
|
||||
# the core library brings the game's build settings along
|
||||
target_link_libraries(${UNIT_TESTS_NAME} PRIVATE eu07_core)
|
||||
target_include_directories(${UNIT_TESTS_NAME} PRIVATE "${DEPS_DIR}/doctest")
|
||||
target_compile_definitions(${UNIT_TESTS_NAME} PRIVATE EU07_UNIT_TESTS_DATA="${CMAKE_CURRENT_SOURCE_DIR}/data")
|
||||
if (MSVC)
|
||||
target_compile_options(${UNIT_TESTS_NAME} PRIVATE /bigobj)
|
||||
endif()
|
||||
set_target_properties(${UNIT_TESTS_NAME} PROPERTIES FOLDER "tests")
|
||||
|
||||
add_test(NAME ${UNIT_TESTS_NAME} COMMAND ${UNIT_TESTS_NAME})
|
||||
|
||||
# for running the tests at the end of building the game, see the main CMakeLists.txt
|
||||
set(UNIT_TEST_DATA ${UNIT_TEST_DATA} PARENT_SCOPE)
|
||||
68
tests/unit/README.md
Normal file
68
tests/unit/README.md
Normal 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`.
|
||||
12
tests/unit/data/wagon.json
Normal file
12
tests/unit/data/wagon.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"_comment": "Passenger wagon with a KE distributor valve, made after a real one so the defaults make physical sense. Sections and keys are those of the game's vehicle definitions; tests change only the keys they're about.",
|
||||
"Param": { "Category": "train", "M": 42200, "Vmax": 120 },
|
||||
"Dimensions": { "L": 26.8, "H": 4.7, "W": 2.88, "Cx": 0.15 },
|
||||
"Wheels": { "D": 0.96, "Tw": 1.435, "Axle": "2'2'", "Ad": 2.5, "Bd": 19.5, "Rmin": 80, "BearingType": "Roll" },
|
||||
"Brake": {
|
||||
"BrakeValve": "KE", "NBpA": 6, "MBF": 15, "MaxBP": 3.8, "TareMaxBP": 2.3,
|
||||
"BCN": 12, "BCR": 0.127, "BCD": 0.04, "BCM": 1.5, "BCS": 1.2, "BSA": 0, "BM": "Disk1", "BVV": 125
|
||||
},
|
||||
"BuffCoupl": { "CType": "Screw", "kB": 9200, "DmaxB": 0.11, "FmaxB": 320, "kC": 4500, "DmaxC": 0.1, "FmaxC": 850, "beta": 0.58, "AllowedFlag": 115 },
|
||||
"Cntrl": { "BrakeSystem": "Pneumatic", "LocalBrake": "ManualBrake", "BrakeDelays": "P", "MaxBPMass": 63 }
|
||||
}
|
||||
81
tests/unit/engine_stubs.cpp
Normal file
81
tests/unit/engine_stubs.cpp
Normal file
@@ -0,0 +1,81 @@
|
||||
// stand-ins for the parts of the engine which the core library (eu07_core) links against, but which the
|
||||
// unit tests don't exercise. when a change makes the library use something new from the engine and the
|
||||
// test build fails to link, either move the real source into the library (CORE_SOURCES in the main
|
||||
// CMakeLists.txt, if it stands on its own) or add a stub here.
|
||||
|
||||
#include "stdafx.h"
|
||||
#include "utilities/Globals.h"
|
||||
#include "utilities/Logs.h"
|
||||
#include "scene/scenenodegroups.h"
|
||||
#include "scene/scenelayers.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
|
||||
// settings ---------------------------------------------------------------------------------------
|
||||
|
||||
global_settings &GetGlobalSettings() {
|
||||
// defaults, as before reading eu07.ini
|
||||
static global_settings settings;
|
||||
return settings;
|
||||
}
|
||||
|
||||
// logs: quiet, unless EU07_UNIT_TESTS_LOG is set --------------------------------------------------
|
||||
|
||||
namespace {
|
||||
bool log_enabled() {
|
||||
static bool const enabled{std::getenv("EU07_UNIT_TESTS_LOG") != nullptr};
|
||||
return enabled;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void WriteLog(const char *str, logtype const, bool const isError) {
|
||||
if (log_enabled() && str != nullptr) {
|
||||
(isError ? std::cerr : std::cout) << str << "\n";
|
||||
}
|
||||
}
|
||||
|
||||
void WriteLog(const std::string &str, logtype const Type) {
|
||||
WriteLog(str.c_str(), Type, false);
|
||||
}
|
||||
|
||||
void ErrorLog(const std::string &str, logtype const Type) {
|
||||
WriteLog(str.c_str(), Type, true);
|
||||
}
|
||||
|
||||
// scenery structure, which the parser reports include directives to --------------------------------
|
||||
// the declarations come from the engine's headers, the stubs can't change them (hence the NOSONARs)
|
||||
|
||||
namespace scene {
|
||||
|
||||
node_groups Groups; // NOSONAR the engine's extern global, scene/scenenodegroups.h
|
||||
node_layers Layers; // NOSONAR the engine's extern global, scene/scenelayers.h
|
||||
|
||||
group_handle node_groups::create(bool const) { // NOSONAR signature from scene/scenenodegroups.h
|
||||
return null_handle;
|
||||
}
|
||||
|
||||
group_handle node_groups::close() { // NOSONAR signature from scene/scenenodegroups.h
|
||||
return null_handle;
|
||||
}
|
||||
|
||||
layer_handle node_layers::open(std::string const &, include_site) { // NOSONAR signature from scene/scenelayers.h
|
||||
return null_handle;
|
||||
}
|
||||
|
||||
layer_handle node_layers::close() { // NOSONAR signature from scene/scenelayers.h
|
||||
return null_handle;
|
||||
}
|
||||
|
||||
void node_layers::instance_begin(std::string const &, source_span const &) { // NOSONAR signature from scene/scenelayers.h
|
||||
// no layers in the tests, so nothing to keep track of
|
||||
}
|
||||
|
||||
// basic_node has no key function for the Itanium C++ ABI (tooltip() is declared plain but defined inline), so
|
||||
// GCC and Clang emit its vtable wherever scenenode.h is used, here too, and the vtable wants radius_().
|
||||
// the same as the game's, in scene/scenenode.cpp
|
||||
float basic_node::radius_() {
|
||||
return 0.f;
|
||||
}
|
||||
|
||||
} // namespace scene
|
||||
40
tests/unit/main.cpp
Normal file
40
tests/unit/main.cpp
Normal file
@@ -0,0 +1,40 @@
|
||||
#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
|
||||
#include "stdafx.h"
|
||||
#include "doctest.h"
|
||||
|
||||
#include "utilities/Globals.h"
|
||||
|
||||
namespace {
|
||||
|
||||
// the simulation draws random numbers (brake delays, pressures, wheel damage) from engines shared by
|
||||
// everything. reseeding them before every run of a test case, subcases included, gives each test the
|
||||
// same numbers whether it runs alone from the IDE or among all the others
|
||||
class deterministic_random : public doctest::IReporter {
|
||||
public:
|
||||
explicit deterministic_random(doctest::ContextOptions const &) {}
|
||||
|
||||
void test_case_start(doctest::TestCaseData const &) override { reseed(); }
|
||||
void test_case_reenter(doctest::TestCaseData const &) override { reseed(); }
|
||||
|
||||
// the rest of doctest's reporter interface: a listener reports nothing, the console reporter does that
|
||||
void report_query(doctest::QueryData const &) override { /* nothing to report */ }
|
||||
void test_run_start() override { /* nothing to report */ }
|
||||
void test_run_end(doctest::TestRunStats const &) override { /* nothing to report */ }
|
||||
void test_case_end(doctest::CurrentTestCaseStats const &) override { /* nothing to report */ }
|
||||
void test_case_exception(doctest::TestCaseException const &) override { /* nothing to report */ }
|
||||
void subcase_start(doctest::SubcaseSignature const &) override { /* nothing to report */ }
|
||||
void subcase_end() override { /* nothing to report */ }
|
||||
void log_assert(doctest::AssertData const &) override { /* nothing to report */ }
|
||||
void log_message(doctest::MessageData const &) override { /* nothing to report */ }
|
||||
void test_case_skipped(doctest::TestCaseData const &) override { /* nothing to report */ }
|
||||
|
||||
private:
|
||||
static void reseed() {
|
||||
Global.random_engine.seed(1);
|
||||
Global.local_random_engine.seed(1);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
DOCTEST_REGISTER_LISTENER("deterministic_random", 1, deterministic_random);
|
||||
27
tests/unit/run_tests.cmake
Normal file
27
tests/unit/run_tests.cmake
Normal file
@@ -0,0 +1,27 @@
|
||||
# runs the unit tests at the end of building the game: cmake -DTESTS=<test binary> -DGAME=<game binary> -P run_tests.cmake
|
||||
# prints a one line summary when they pass, the full report when they don't. a failure fails the build and
|
||||
# removes the game binary, so the next build links it again and reruns the tests
|
||||
|
||||
execute_process(
|
||||
COMMAND "${TESTS}" --no-intro --no-version
|
||||
RESULT_VARIABLE result
|
||||
OUTPUT_VARIABLE report
|
||||
ERROR_VARIABLE report)
|
||||
|
||||
string(REGEX MATCH "test cases: *([0-9]+) *\\| *([0-9]+) passed *\\| *([0-9]+) failed" _ "${report}")
|
||||
set(cases "${CMAKE_MATCH_1}")
|
||||
set(passed "${CMAKE_MATCH_2}")
|
||||
set(failed "${CMAKE_MATCH_3}")
|
||||
string(REGEX MATCH "assertions: *([0-9]+)" _ "${report}")
|
||||
set(checks "${CMAKE_MATCH_1}")
|
||||
|
||||
if(result EQUAL 0)
|
||||
message("Unit tests: ${passed}/${cases} passed (${checks} checks)")
|
||||
else()
|
||||
message("${report}")
|
||||
file(REMOVE "${GAME}")
|
||||
if(cases STREQUAL "")
|
||||
message(FATAL_ERROR "Unit tests: didn't run (${result}), see above. -DWITH_TESTS=OFF builds the game without them")
|
||||
endif()
|
||||
message(FATAL_ERROR "Unit tests: ${failed} of ${cases} FAILED, see above. -DWITH_TESTS=OFF builds the game without them")
|
||||
endif()
|
||||
93
tests/unit/test_brakes.cpp
Normal file
93
tests/unit/test_brakes.cpp
Normal file
@@ -0,0 +1,93 @@
|
||||
#include "stdafx.h"
|
||||
#include "doctest.h"
|
||||
#include "vehicle.h"
|
||||
|
||||
using test::definition;
|
||||
using test::vehicle;
|
||||
|
||||
// the brake pipe of a lone wagon is held at a pressure the way a coupled locomotive would hold it.
|
||||
// pressures are in bar, the brake pipe runs at 5.0, a 1.5 bar reduction is a full service application.
|
||||
|
||||
TEST_CASE("brakes: KE wagon applies up to its tare limit on a service application and releases again") {
|
||||
vehicle wagon{definition::load("wagon")}; // empty wagon, TareMaxBP=2.3
|
||||
|
||||
wagon.hold_brake_pipe(5.0);
|
||||
wagon.run(60.0);
|
||||
CHECK(wagon->BrakePress < 0.1);
|
||||
CHECK(wagon->Hamulec->GetBRP() > 4.5); // auxiliary reservoir charged
|
||||
|
||||
wagon.hold_brake_pipe(3.5);
|
||||
wagon.run(10.0);
|
||||
CHECK(wagon->BrakePress == doctest::Approx(2.3).epsilon(0.05));
|
||||
|
||||
wagon.hold_brake_pipe(5.0);
|
||||
wagon.run(60.0);
|
||||
CHECK(wagon->BrakePress < 0.1);
|
||||
}
|
||||
|
||||
TEST_CASE("brakes: a deeper brake pipe reduction brakes harder") {
|
||||
auto applied = [](double Pipe) {
|
||||
vehicle wagon{definition::load("wagon").set("Brake", "TareMaxBP", 3.8)}; // no tare limit in the way
|
||||
wagon.hold_brake_pipe(5.0);
|
||||
wagon.run(60.0);
|
||||
wagon.hold_brake_pipe(Pipe);
|
||||
wagon.run(20.0);
|
||||
return wagon->BrakePress;
|
||||
};
|
||||
auto const light{applied(4.6)};
|
||||
auto const medium{applied(4.2)};
|
||||
auto const full{applied(3.5)};
|
||||
CHECK(light > 0.1);
|
||||
CHECK(light < medium);
|
||||
CHECK(medium < full);
|
||||
CHECK(full <= 3.8 + 0.1); // MaxBP
|
||||
}
|
||||
|
||||
// every distributor valve the engine implements goes through the same cycle; the ESt family needs the size
|
||||
// of its reservoirs, as in the real .fiz files using it
|
||||
TEST_CASE("brakes: every implemented distributor valve charges, applies and releases") {
|
||||
for (auto const *valve : {"W", "K", "KE", "ESt", "ESt3", "NESt3", "ESt4", "ESt3AL2", "LSt", "EStED", "EP1", "EP2", "CV1"}) {
|
||||
CAPTURE(valve);
|
||||
vehicle wagon{definition::load("wagon").set("Brake", "BrakeValve", valve).set("Brake", "Size", 16)};
|
||||
wagon.hold_brake_pipe(5.0);
|
||||
wagon.run(90.0);
|
||||
CHECK(wagon->BrakePress < 0.1);
|
||||
wagon.hold_brake_pipe(3.5);
|
||||
wagon.run(20.0);
|
||||
CHECK(wagon->BrakePress > 1.0);
|
||||
CHECK(wagon->BrakePress <= 3.8 + 0.1); // MaxBP
|
||||
wagon.hold_brake_pipe(5.0);
|
||||
wagon.run(90.0);
|
||||
CHECK(wagon->BrakePress < 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("brakes: service braking stops a wagon running at 60 km/h and keeps it stopped") {
|
||||
vehicle wagon{definition::load("wagon"), 60.0};
|
||||
wagon.hold_brake_pipe(5.0);
|
||||
wagon.run(10.0);
|
||||
REQUIRE(wagon.speed() > 50.0); // still running before the braking
|
||||
|
||||
wagon.hold_brake_pipe(3.5);
|
||||
wagon.run(30.0);
|
||||
CHECK(std::abs(wagon.speed()) < 0.1);
|
||||
wagon.run(30.0);
|
||||
CHECK(std::abs(wagon.speed()) < 0.1);
|
||||
}
|
||||
|
||||
TEST_CASE("brakes: a released wagon rolls on") {
|
||||
vehicle wagon{definition::load("wagon"), 60.0};
|
||||
wagon.hold_brake_pipe(5.0);
|
||||
wagon.run(30.0);
|
||||
CHECK(wagon.speed() > 50.0);
|
||||
CHECK(wagon.speed() < 60.0); // running resistance slows it down a little
|
||||
}
|
||||
|
||||
// the example in README.md
|
||||
TEST_CASE("brakes: service braking stops a heavier wagon too") {
|
||||
// the default wagon, with the keys the test is about changed
|
||||
test::vehicle wagon{test::definition::load("wagon").set("Param", "M", 60000), 40.0 /* km/h */};
|
||||
wagon.hold_brake_pipe(3.5); // bar, as a coupled locomotive would hold it
|
||||
wagon.run(40.0 /* s */, test::track{0.15 /* friction */, 0.0 /* gradient */});
|
||||
CHECK(std::abs(wagon.speed()) < 0.1);
|
||||
}
|
||||
45
tests/unit/test_definition.cpp
Normal file
45
tests/unit/test_definition.cpp
Normal file
@@ -0,0 +1,45 @@
|
||||
#include "stdafx.h"
|
||||
#include "doctest.h"
|
||||
#include "vehicle.h"
|
||||
|
||||
using test::definition;
|
||||
using test::vehicle;
|
||||
|
||||
TEST_CASE("definition: the wagon from data/wagon.json loads with its values") {
|
||||
vehicle wagon{definition::load("wagon")};
|
||||
|
||||
CHECK(wagon->Mass == doctest::Approx(42200.0));
|
||||
CHECK(wagon->Vmax == doctest::Approx(120.0));
|
||||
CHECK(wagon->Dim.L == doctest::Approx(26.8));
|
||||
CHECK(wagon->WheelDiameter == doctest::Approx(0.96));
|
||||
CHECK(wagon->NAxles == 4);
|
||||
CHECK(wagon->MaxBrakePress[3] == doctest::Approx(3.8));
|
||||
CHECK(wagon->BrakeSystem == TBrakeSystem::Pneumatic);
|
||||
}
|
||||
|
||||
TEST_CASE("definition: keys set by the test reach the vehicle") {
|
||||
vehicle heavy{definition::load("wagon").set("Param", "M", 60000).set("Wheels", "D", 1.0)};
|
||||
|
||||
CHECK(heavy->Mass == doctest::Approx(60000.0));
|
||||
CHECK(heavy->WheelDiameter == doctest::Approx(1.0));
|
||||
}
|
||||
|
||||
TEST_CASE("definition: an unknown test vehicle is an error, not a silently empty one") {
|
||||
CHECK_THROWS(definition::load("no such vehicle"));
|
||||
}
|
||||
|
||||
TEST_CASE("definition: a misspelt section is an error, not silently skipped by the loader") {
|
||||
CHECK_THROWS(definition::load("wagon").set("Brakes", "MBF", 30));
|
||||
}
|
||||
|
||||
TEST_CASE("definition: rows of a table section reach the vehicle") {
|
||||
auto const pc1{light::headlight_left | light::headlight_upper | light::headlight_right};
|
||||
vehicle wagon{definition::load("wagon")
|
||||
.set("LightsList", "Size", 2)
|
||||
.set("LightsList", "Default", 1)
|
||||
.set("LightsList", "rows", {{0, 0}, {pc1, 0}})};
|
||||
|
||||
CHECK(wagon->LightsPosNo == 2);
|
||||
CHECK(wagon->LightsPos == 1);
|
||||
CHECK(wagon->Lights[end::front][1] == pc1);
|
||||
}
|
||||
39
tests/unit/test_horn.cpp
Normal file
39
tests/unit/test_horn.cpp
Normal file
@@ -0,0 +1,39 @@
|
||||
#include "stdafx.h"
|
||||
#include "doctest.h"
|
||||
|
||||
#include "vehicle/horn.h"
|
||||
|
||||
// the horn keys of the cab: low tone (A) and high tone (shift+A), held to sound
|
||||
|
||||
TEST_CASE("horn: a single lever for both tones sounds one tone at a time (eu07/maszyna#392)") {
|
||||
auto signal{0};
|
||||
signal = horn::pressed(signal, horn::low, true);
|
||||
REQUIRE(signal == horn::low);
|
||||
|
||||
// the lever moved over to the high tone while the low tone key is still held
|
||||
signal = horn::pressed(signal, horn::high, true);
|
||||
CHECK(signal == horn::high);
|
||||
|
||||
// letting go of the low tone key leaves the high tone sounding
|
||||
signal = horn::released(signal, horn::low);
|
||||
CHECK(signal == horn::high);
|
||||
|
||||
signal = horn::released(signal, horn::high);
|
||||
CHECK(signal == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("horn: separate buttons for the tones can sound both at once") {
|
||||
auto signal{0};
|
||||
signal = horn::pressed(signal, horn::low, false);
|
||||
signal = horn::pressed(signal, horn::high, false);
|
||||
CHECK(signal == (horn::low | horn::high));
|
||||
|
||||
signal = horn::released(signal, horn::low);
|
||||
CHECK(signal == horn::high);
|
||||
}
|
||||
|
||||
TEST_CASE("horn: releasing a tone leaves the other bits of the signal alone") {
|
||||
auto const third{4}; // the third horn, operated separately
|
||||
CHECK(horn::released(horn::low | third, horn::low) == third);
|
||||
CHECK(horn::pressed(third, horn::high, true) == (horn::high | third));
|
||||
}
|
||||
62
tests/unit/test_wheelslip.cpp
Normal file
62
tests/unit/test_wheelslip.cpp
Normal file
@@ -0,0 +1,62 @@
|
||||
#include "stdafx.h"
|
||||
#include "doctest.h"
|
||||
#include "vehicle.h"
|
||||
|
||||
using test::definition;
|
||||
using test::track;
|
||||
using test::vehicle;
|
||||
|
||||
namespace {
|
||||
|
||||
struct braking_result {
|
||||
bool skidded{false}; // the wheels locked while braking
|
||||
bool stopped{false}; // came (almost) to a stop on the braking rail
|
||||
};
|
||||
|
||||
// brakes the wagon from 40 km/h on the Braking rail until it's (nearly) stopped, then has it stand on the
|
||||
// Standing one for 30 seconds
|
||||
braking_result brake_to_stop_then_stand(vehicle &Coach, track const &Braking, track const &Standing) {
|
||||
braking_result result;
|
||||
Coach.hold_brake_pipe(5.0);
|
||||
Coach.run(5.0, Braking);
|
||||
Coach.hold_brake_pipe(3.5); // service braking
|
||||
// a skidding wagon brakes on the sliding adhesion only, give it time
|
||||
for (auto step = 0; step < 60000; ++step) { // 600 s in 0.01 s steps
|
||||
result.skidded |= Coach->SlippingWheels;
|
||||
if (auto const speed{std::abs(Coach.speed())}; speed > 0.0 && speed < 0.1) {
|
||||
// almost stopped; a skidding wagon is still skidding at this point
|
||||
result.stopped = true;
|
||||
break;
|
||||
}
|
||||
Coach.step(Braking);
|
||||
}
|
||||
Coach.run(30.0, Standing);
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// the engine doesn't let a stopped, braked vehicle slip (the Vel > 0.1 trap in TMoverParameters::ComputeTotalForce),
|
||||
// so a wagon which stopped in a skid should be no different from one which stopped without it. it used to stay
|
||||
// skidding instead, with its brakes capped by the sliding adhesion, and slid or crept away (eu07/maszyna#479)
|
||||
TEST_CASE("wheelslip: a wagon which stopped in a skid holds like one which stopped without it") {
|
||||
track const dry{0.15, 0.0};
|
||||
track const slippery{0.02, 0.0};
|
||||
// held only by the brakes: the sliding adhesion on this rail is too low for the slope
|
||||
track const slipperyslope{0.02, 0.08};
|
||||
|
||||
vehicle normal{definition::load("wagon"), 40.0};
|
||||
auto const normalbraking{brake_to_stop_then_stand(normal, dry, slipperyslope)};
|
||||
vehicle skidding{definition::load("wagon"), 40.0};
|
||||
auto const skiddingbraking{brake_to_stop_then_stand(skidding, slippery, slipperyslope)};
|
||||
|
||||
// the test means something only when the braking went as planned
|
||||
REQUIRE(normalbraking.stopped);
|
||||
REQUIRE_FALSE(normalbraking.skidded);
|
||||
REQUIRE(skiddingbraking.stopped);
|
||||
REQUIRE(skiddingbraking.skidded);
|
||||
|
||||
CHECK(std::abs(normal.speed()) < 0.1);
|
||||
CHECK(std::abs(skidding.speed()) < 0.1);
|
||||
CHECK_FALSE(skidding->SlippingWheels);
|
||||
}
|
||||
187
tests/unit/vehicle.cpp
Normal file
187
tests/unit/vehicle.cpp
Normal file
@@ -0,0 +1,187 @@
|
||||
#include "stdafx.h"
|
||||
#include "vehicle.h"
|
||||
|
||||
#include <fstream>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace test {
|
||||
|
||||
namespace {
|
||||
|
||||
struct section_format {
|
||||
char separator; // the game's definitions end section names with either '.' or ':'
|
||||
char const *table_end{nullptr}; // the line closing the rows of a table section
|
||||
};
|
||||
|
||||
// the sections TMoverParameters::LoadFIZ() reads, see its issection() calls. a section missing here
|
||||
// would be silently skipped by the loader, and the test would run on the defaults instead
|
||||
std::map<std::string, section_format, std::less<>> const sections{
|
||||
{"Param", {'.'}},
|
||||
{"Load", {':'}},
|
||||
{"Dimensions", {':'}},
|
||||
{"Wheels", {':'}},
|
||||
{"Brake", {':'}},
|
||||
{"Doors", {':'}},
|
||||
{"BuffCoupl", {'.'}},
|
||||
{"BuffCoupl1", {'.'}},
|
||||
{"BuffCoupl2", {'.'}},
|
||||
{"TurboPos", {':'}},
|
||||
{"Cntrl", {'.'}},
|
||||
{"Blending", {':'}},
|
||||
{"DCEMUED", {':'}},
|
||||
{"SpringBrake", {':'}},
|
||||
{"Light", {':'}},
|
||||
{"Headlights", {':'}},
|
||||
{"Clima", {':'}},
|
||||
{"Power", {':'}},
|
||||
{"SpeedControl", {':'}},
|
||||
{"Engine", {':'}},
|
||||
{"Switches", {':'}},
|
||||
{"Security", {':'}},
|
||||
{"Circuit", {':'}},
|
||||
{"AI", {':'}},
|
||||
{"LightsList", {':', "endL"}},
|
||||
{"DimmerList", {':', "endDimmerList"}},
|
||||
{"WiperList", {':', "endwl"}},
|
||||
{"CompressorList", {':', "endCL"}},
|
||||
};
|
||||
|
||||
section_format const &format_of(std::string const &Section) {
|
||||
auto const lookup{sections.find(Section)};
|
||||
if (lookup == sections.end()) {
|
||||
throw definition_error("unknown vehicle definition section " + Section + ", see tests/unit/vehicle.cpp");
|
||||
}
|
||||
return lookup->second;
|
||||
}
|
||||
|
||||
std::string value_text(nlohmann::ordered_json const &Value) {
|
||||
if (Value.is_string()) {
|
||||
return Value.get<std::string>();
|
||||
}
|
||||
if (Value.is_boolean()) {
|
||||
return Value.get<bool>() ? "Yes" : "No";
|
||||
}
|
||||
return Value.dump();
|
||||
}
|
||||
|
||||
// the rows of a table section, each a list of values, and the line closing them
|
||||
std::string table_text(std::string const &Section, section_format const &Format, nlohmann::ordered_json const &Rows) {
|
||||
if (Format.table_end == nullptr) {
|
||||
throw definition_error("vehicle definition section " + Section + " has no rows");
|
||||
}
|
||||
std::string text;
|
||||
for (auto const &row : Rows) {
|
||||
for (auto const &value : row) {
|
||||
text += value_text(value) + " ";
|
||||
}
|
||||
text += "\n";
|
||||
}
|
||||
return text + Format.table_end + "\n";
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
definition definition::load(std::string const &Name) {
|
||||
auto const path{std::string{EU07_UNIT_TESTS_DATA} + "/" + Name + ".json"};
|
||||
std::ifstream file(path);
|
||||
if (false == file.is_open()) {
|
||||
throw definition_error("no test vehicle definition " + path);
|
||||
}
|
||||
definition result;
|
||||
// not brace-initialised: json{other} makes an array of other with GCC and Clang (the initializer list constructor wins)
|
||||
auto const json = nlohmann::ordered_json::parse(file, nullptr, true, true);
|
||||
if (false == json.is_object()) {
|
||||
throw definition_error("test vehicle definition " + path + " isn't a JSON object");
|
||||
}
|
||||
for (auto const &[section, keys] : json.items()) {
|
||||
// keys starting with _ are comments
|
||||
if (section.starts_with('_')) {
|
||||
continue;
|
||||
}
|
||||
format_of(section);
|
||||
result.m_sections[section] = keys;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
definition &definition::set(std::string const &Section, std::string const &Key, nlohmann::ordered_json const &Value) {
|
||||
format_of(Section);
|
||||
m_sections[Section][Key] = Value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
definition &definition::remove(std::string const &Section) {
|
||||
m_sections.erase(Section);
|
||||
return *this;
|
||||
}
|
||||
|
||||
definition &definition::remove(std::string const &Section, std::string const &Key) {
|
||||
if (m_sections.contains(Section)) {
|
||||
m_sections[Section].erase(Key);
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
std::string definition::text() const {
|
||||
std::string text;
|
||||
for (auto const &[section, keys] : m_sections.items()) {
|
||||
auto const &format{format_of(section)};
|
||||
text += section + format.separator;
|
||||
for (auto const &[key, value] : keys.items()) {
|
||||
if (key != "rows") {
|
||||
text += " " + key + "=" + value_text(value);
|
||||
}
|
||||
}
|
||||
text += "\n";
|
||||
if (keys.contains("rows")) {
|
||||
text += table_text(section, format, keys["rows"]);
|
||||
}
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
vehicle::vehicle(definition const &Definition, double Velocity, std::string const &Name) {
|
||||
// the same steps as TDynamicObject::Init()
|
||||
m_mover = std::make_unique<TMoverParameters>(Velocity, Name, Name, 0);
|
||||
if (false == m_mover->LoadFIZ_Text(Definition.text())) {
|
||||
throw definition_error("the test vehicle definition didn't load");
|
||||
}
|
||||
m_mover->AssignLoad("", 0.f);
|
||||
m_mover->ComputeMass();
|
||||
auto const driveractive{Velocity != 0.0};
|
||||
if (false == m_mover->CheckLocomotiveParameters(driveractive, Velocity >= 0.0 ? 1 : -1)) {
|
||||
throw definition_error("parameters mismatch in the test vehicle definition");
|
||||
}
|
||||
m_mover->MainCtrlPos = m_mover->MainCtrlNoPowerPos();
|
||||
m_mover->LocalBrakePosA = 0.0;
|
||||
m_mover->BrakeCtrlPos = static_cast<int>(std::floor(m_mover->Handle->GetPos(driveractive ? bh_RP : bh_NP)));
|
||||
m_mover->BrakeLevelSet(m_mover->BrakeCtrlPos);
|
||||
}
|
||||
|
||||
void vehicle::step(track const &Track, double Dt) {
|
||||
TTrackShape shape;
|
||||
shape.Len = 1000.0;
|
||||
shape.dHtrack = Track.gradient;
|
||||
TTrackParam param;
|
||||
param.Width = 1.435;
|
||||
param.friction = Track.friction;
|
||||
param.CategoryFlag = 1;
|
||||
TTractionParam const traction;
|
||||
TLocation const location{0.0, 0.0, 0.0};
|
||||
TRotation const rotation{0.0, 0.0, 0.0};
|
||||
if (m_pipepressure >= 0.0) {
|
||||
m_mover->Pipe->CreatePress(m_pipepressure);
|
||||
}
|
||||
m_mover->ComputeTotalForce(Dt);
|
||||
m_mover->ComputeMovement(Dt, Dt, shape, param, traction, location, rotation);
|
||||
}
|
||||
|
||||
void vehicle::run(double Seconds, track const &Track, double Dt) {
|
||||
// whole steps: summing up Dt would make the step count depend on the rounding
|
||||
auto const steps{std::lround(Seconds / Dt)};
|
||||
for (long i = 0; i < steps; ++i) {
|
||||
step(Track, Dt);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace test
|
||||
71
tests/unit/vehicle.h
Normal file
71
tests/unit/vehicle.h
Normal file
@@ -0,0 +1,71 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include "McZapkie/MOVER.h"
|
||||
|
||||
namespace test {
|
||||
|
||||
// thrown for a test vehicle definition which can't be used: missing, malformed, or not taken by the loader
|
||||
class definition_error : public std::runtime_error {
|
||||
public:
|
||||
using std::runtime_error::runtime_error;
|
||||
};
|
||||
|
||||
// definition of a test vehicle: one of the JSON files in tests/unit/data, with the sections and keys
|
||||
// of the game's vehicle definitions, changed by the test where it needs to:
|
||||
// auto heavy = definition::load("wagon").set("Param", "M", 60000).set("Brake", "BrakeValve", "ESt3");
|
||||
class definition {
|
||||
public:
|
||||
// tests/unit/data/<Name>.json
|
||||
static definition load(std::string const &Name);
|
||||
|
||||
// sets the key of the section, adding either one when it's missing
|
||||
definition &set(std::string const &Section, std::string const &Key, nlohmann::ordered_json const &Value);
|
||||
// drops the whole section, or one key of it
|
||||
definition &remove(std::string const &Section);
|
||||
definition &remove(std::string const &Section, std::string const &Key);
|
||||
|
||||
// the definition in the form the game's vehicle loader reads
|
||||
std::string text() const;
|
||||
|
||||
private:
|
||||
nlohmann::ordered_json m_sections;
|
||||
};
|
||||
|
||||
// conditions the vehicle runs in: straight track of the given friction and gradient (rise/run)
|
||||
struct track {
|
||||
double friction{0.15};
|
||||
double gradient{0.0};
|
||||
};
|
||||
|
||||
// one vehicle created the way TDynamicObject::Init() creates it, from the given definition
|
||||
class vehicle {
|
||||
public:
|
||||
explicit vehicle(definition const &Definition, double Velocity = 0.0, std::string const &Name = "test");
|
||||
|
||||
TMoverParameters &mover() { return *m_mover; }
|
||||
TMoverParameters *operator->() { return m_mover.get(); }
|
||||
|
||||
// keeps the brake pipe at the given pressure [bar], as a locomotive coupled to the vehicle would;
|
||||
// a negative value leaves the pipe alone
|
||||
void hold_brake_pipe(double Pressure) { m_pipepressure = Pressure; }
|
||||
|
||||
// advances the physics by one step of Dt, the way the simulation loop does
|
||||
void step(track const &Track = {}, double Dt = 0.01);
|
||||
// advances the physics by Seconds in steps of Dt
|
||||
void run(double Seconds, track const &Track = {}, double Dt = 0.01);
|
||||
|
||||
// vehicle speed in kilometres per hour, negative when moving backwards
|
||||
double speed() const { return m_mover->V * 3.6; }
|
||||
|
||||
private:
|
||||
std::unique_ptr<TMoverParameters> m_mover;
|
||||
double m_pipepressure{-1.0};
|
||||
};
|
||||
|
||||
} // namespace test
|
||||
Reference in New Issue
Block a user