18
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-11-15 02:29:07 +01:00

feat(simulation): test mode, --test runs a scenery unattended and exits with the result (eu07/maszyna#541)

eu07 --test -s <scenery> [--test-timeout <s>] runs the scenery in ghostview with
the null renderer (models loaded, no textures), a hidden window, no sound,
network or python, and in fixed steps of 0.03 s of simulation time per frame
(three 10 ms physics steps) as fast as it can. Without -seed it uses a fixed
seed, so two runs give byte-identical physicslogs.

The scenery reports its result to the __test.result memory cell, which the
simulator always creates: value 1 passes once every vehicle has stood still for
1 s, value 0 fails at once. The run ends with exit code
  0 pass, 1 fail,
  2 a train passed a signal at stop (SetVelocity 0 0, as seen in its driver's
    speed table; SetVelocity 0 x on automatic block doesn't count),
  3 timeout (test.timeout, default 3600 s of simulation time),
  4 standstill (every vehicle still for test.standstill s, default 300, with no
    result; a pass with test.pass_on_standstill yes),
  5 error (no -s, the scenery didn't load or has no vehicles, an exception).
The test.* keys go in the scenery's config block; test.fail_if_stop_overran no
only logs a signal passed at danger. Outside the test mode a signal passed at
danger is logged.

The null renderer gets a ui backend which draws nothing, hands out the null
texture and material instead of throwing, and ends the ui frame. A GL context
is still made for the hidden window, without asking for a version, as parts
of the ui create textures with gl calls of their own. No crash dialog or
message box in test mode; the window code no longer needs a monitor.
This commit is contained in:
jerrrrycho
2026-11-07 13:17:26 +01:00
parent 6209e16558
commit 02e29f6971
16 changed files with 458 additions and 26 deletions

View File

@@ -378,7 +378,7 @@ int eu07_application::init(int Argc, char *Argv[])
return result;
}
if (crashreport_is_pending())
if (crashreport_is_pending() && false == Global.test.enabled)
{ // run crashgui as early as possible
if ((result = run_crashgui()) != 0)
{
@@ -951,14 +951,17 @@ GLFWwindow *eu07_application::window(int const Windowindex, bool visible, int wi
}
// for index -1 create a new child window
auto const *vmode{glfwGetVideoMode(monitor ? monitor : glfwGetPrimaryMonitor())};
// match requested video mode to current to allow for
// fullwindow creation when resolution is the same
glfwWindowHint(GLFW_RED_BITS, vmode->redBits);
glfwWindowHint(GLFW_GREEN_BITS, vmode->greenBits);
glfwWindowHint(GLFW_BLUE_BITS, vmode->blueBits);
glfwWindowHint(GLFW_REFRESH_RATE, vmode->refreshRate);
auto *videomonitor{monitor ? monitor : glfwGetPrimaryMonitor()};
// a machine without a display (e.g. a test runner) has no monitor to match
if (auto const *vmode{videomonitor ? glfwGetVideoMode(videomonitor) : nullptr})
{
// match requested video mode to current to allow for
// fullwindow creation when resolution is the same
glfwWindowHint(GLFW_RED_BITS, vmode->redBits);
glfwWindowHint(GLFW_GREEN_BITS, vmode->greenBits);
glfwWindowHint(GLFW_BLUE_BITS, vmode->blueBits);
glfwWindowHint(GLFW_REFRESH_RATE, vmode->refreshRate);
}
glfwWindowHint(GLFW_VISIBLE, visible);
@@ -1195,6 +1198,19 @@ int eu07_application::init_settings(int Argc, char *Argv[])
WriteLog(std::format("seed given on the command line: {} -> {}", Argv[i], Global.random_seed));
}
}
else if (token == "--test")
{
// integration test: the scenery runs unattended and the exit code is the result (eu07/maszyna#541)
Global.test.enabled = true;
}
else if (token == "--test-timeout")
{
if (i + 1 < Argc)
{
Global.test.timeout = std::max(0.0, std::atof(Argv[++i]));
Global.test.timeout_from_command_line = true;
}
}
else if (token == "-ini")
{
// extra settings file read on top of eu07.ini, so a run can override a few keys
@@ -1210,13 +1226,45 @@ int eu07_application::init_settings(int Argc, char *Argv[])
<< " [-v vehiclename]"
<< " [-edit [sceneryfilepath]]"
<< " [-seed number|word]"
<< " [-ini settingsfile]" << std::endl;
<< " [-ini settingsfile]"
<< " [--test [--test-timeout seconds]]" << std::endl;
return -1;
}
}
if (editwithoutscenery && false == sceneryfromcommandline)
Global.SceneryFile.clear();
if (Global.test.enabled)
{
if (false == sceneryfromcommandline || Global.SceneryFile.empty())
{
ErrorLog("test: --test needs a scenery given with -s");
return -1;
}
// nobody watches nor listens: no player vehicle, nothing drawn, no sound, no window in the way
Global.editor_session = false;
Global.local_start_vehicle = "ghostview";
Global.GfxRenderer = "null";
Global.NvRenderer = false;
Global.bSoundEnabled = false;
Global.python_enabled = false;
Global.bInactivePause = false;
Global.bFullScreen = false;
Global.fullscreen_windowed = false;
Global.VSync = false;
Global.iMultisampling = 0;
Global.gfx_usegles = false;
Global.minframetime = std::chrono::duration<float>(0.0f);
Global.extra_viewports.clear();
Global.headtrack_conf.joy.clear();
// a test runs on its own
Global.network_servers.clear();
Global.network_client.reset();
// and the same way every time, unless given another seed (-seed, or randomseed in the settings)
if (!Global.random_seed)
Global.random_seed = seed_of("test");
}
return 0;
}
@@ -1290,6 +1338,12 @@ int eu07_application::init_glfw()
{
glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
}
else if (Global.test.enabled)
{
// the null renderer draws nothing, but parts of the ui make textures with gl calls of their own.
// any context will do for them, so none is asked for in particular: a machine without a graphics
// driver (a test runner) has only the basic one of the system
}
else
{
if (!Global.LegacyRenderer)
@@ -1341,7 +1395,8 @@ int eu07_application::init_glfw()
Global.bFullScreen = true;
}
auto *mainwindow = window(-1, true, Global.window_size.x, Global.window_size.y, Global.bFullScreen ? monitor : nullptr, true, false);
// test mode keeps its window hidden: glfw, the ui and the input still need one, but nothing is shown in it
auto *mainwindow = window(-1, false == Global.test.enabled, Global.window_size.x, Global.window_size.y, Global.bFullScreen ? monitor : nullptr, true, false);
if (mainwindow == nullptr)
{
@@ -1428,6 +1483,11 @@ int eu07_application::init_gfx()
// default render path
GfxRenderer = gfx_renderer_factory::get_instance()->create("modern");
}
else if (Global.GfxRenderer == "null")
{
// draws nothing and loads no textures (test mode)
GfxRenderer = gfx_renderer_factory::get_instance()->create("null");
}
else if (Global.GfxRenderer == "experimental")
{
GfxRenderer = gfx_renderer_factory::get_instance()->create(Global.GfxRenderer);