Files
cthangover/docs/concepts/testing.md
T
cthangover a0207d500b System concepts and rewrite analysis
docs/concepts: one concept per engine system (how it works, invariants and
traps, tests, what the review fixed), with an index. docs/analysis/
rewrite-proposals.md: the structural causes behind the review's defects and
how to replace them, with reasons, costs and an order; analysis only.
AGENTS.md points to both and lists the mods with unit tests.
2026-10-21 11:26:20 +02:00

3.1 KiB

Testing infrastructure (tests/, Core/Mods/Testing, mod test_suites)

Four layers, from the cheapest to the most complete. tests/Cthangover.TestRunner runs all of them as one command: build, then xUnit, then every integration test of tests/tests.manifest.json, then a summary and a JSON report.

Layer Where Runs in Use for
xUnit tests/Cthangover.Tests plain .NET, no Godot (Godot calls must be guarded by GameEnvironment.IsRuntime) pure core logic: pipeline, resolvers, patches, saves, time, DSL parsing
Mod unit tests mods/<mod>/tests via test_sources, [ModTest("group/case")] + ModAssert the real game process (--run-tests [--mod=]), compiled into <mod>__tests against the mod and its dependencies mod code that needs Godot or mod types: dialog runtime, inventory, quests, status effects, map, audio
Integration scenes tests/Cthangover.Tests.Integration (dev build), one setup node per scene printing [TEST_RESULT] a headless (or xvfb) Godot process per test core runtime behaviour: event bus bridge, scene context, saves, coverage guards
Mod test suites mods/<host>/test_suites/<id> (virtual mod <host>__<id>, mounted by activate args) full game, driven like a player GUI flows: menu, inventory, hot reload, studio, tools, battles, dialog autoplay

Mechanics worth knowing

  • Mod unit tests. Each test runs, then the runner recovers state:

    • IResettables reset;
    • registries rebuild;
    • root nodes the test added are removed (removing a core node is an error);
    • Engine.TimeScale is restored;
    • the per-mod sandbox is wiped.

    It then checks invariants. Async tests can await frames, and TimeoutSeconds applies to them. The report goes to user://mod_tests/last-report.json. Mods with tests: cooking, locale_atlas, dialog_base, items_base, quests_base, turn_battle_base, map_base, audio_base.

  • Coverage guards. coverage (battle cores) and coverage_subsystems (migrated subsystems) fail when a registered core or subsystem has no declared test, or when the declared test is not in tests.manifest.json.

  • Dialog autoplay (dialog_base__autoplay):

    • --choices= / --timeout= mount a driver that clicks through dialogs;
    • by default it passes when the box goes idle;
    • --dialogs=N passes after N finished dialogs instead (hub games never go idle).
  • Runner conventions. It adds --all-mods to tests without a game, and adds --no-hot-reload unless a test asks for --hot-reload. It checks scene path case before Godot starts. Logs go to tests/artifacts/logs/<name>.log.

  • Speed. A full run takes about 12 minutes on the 9p workspace. Most of each integration test's time is the game boot; mod compilation is cached and recompiles after every core rebuild.

Rules (skill-code / skill-test)

  • Test the public contract, check data rather than calls, and run JSON round trips for saved types.
  • Use reflection only to set up static singletons.
  • A failing test is never weakened. When it fails for a reason other than your change, ask.
  • Read the logs: every Error line is a red flag even when the test passes.