Testing¶
citsy uses Catch2 v3 for unit tests. All core tests run headlessly with MockHost — no GPU, window, or audio device required.
Running tests¶
Full suite (recommended)¶
After building:
Add --output-on-failure to see assertion details when a test fails:
Run the test binary directly¶
Catch2 supports filtering by tag or test name:
# All tests
./build/citsy_tests
# Parser tests only
./build/citsy_tests "[parser]"
# Engine + mock tests
./build/citsy_tests "[engine]"
# Single test by name
./build/citsy_tests "parser: simple tile"
# List all registered tests
./build/citsy_tests --list-tests
Parallel execution¶
CTest runs tests sequentially by default. For faster local runs:
Test layout¶
tests/
├── unit/
│ ├── test_parser.cpp # Parser and Game model tests
│ ├── test_engine.cpp # Engine lifecycle and MockHost integration
│ ├── test_dialog.cpp # Dialog page extraction and script interpreter
│ ├── test_simulation.cpp # Movement, collision, render, exits, dialog
│ └── test_phase3.cpp # Animation, fonts, RTL, sound, transitions, dialog VM
└── data/
├── minimal.bitsy # Full game with all entity types
├── animated.bitsy # Multi-frame tile animation
├── two_rooms.bitsy # Two rooms with exits and palettes
├── playable.bitsy # Walls, NPC dialog, item, room exit
├── mossland.bitsy # Real Bitsy 6.4 game
├── scripted.bitsy # Variables, conditional dialog, inventory, ending
└── phase3.bitsy # Bitsy 8.15 tune/blip/AVA/FX sample
| File | Tags | What it covers |
|---|---|---|
test_parser.cpp |
[parser], [palette], [tile], [sprite], [item], [room], [dialogue], [variable], [ending], [model], [errors], [fixture] |
.bitsy parsing, entity fields, error handling, fixture files |
test_engine.cpp |
[engine], [mock] |
Engine construction, lifecycle, buffer sizes, palette output, MockHost behavior |
test_dialog.cpp |
[dialog], [script] |
Page extraction and script evaluation (variables, lists, items, {end}/{exit}) |
test_simulation.cpp |
[engine], [sim], [render], [dialog], [fixture] |
Movement, walls, map/video compose, sprite/item drawing, exits, linear dialog |
test_phase3.cpp |
[phase3], [font], [sound], [transition], [inventory], [dialog] |
Animation, fonts, RTL, blips/tunes, {item}/{property}, titles, 8.15 extras |
As of Phase 3, the suite covers parser, simulation, dialog scripting, fonts, sound, and transitions.
MockHost¶
Tests that exercise the engine use MockHost (backends/mock/mock_host.hpp), a header-only test double that implements citsy::Host.
MockHost records:
- Whether
on_engine_ready()was called - Every
present()call as aPresentSnapshot(palette, video, map1, map2, textbox, sound channels) - Log messages passed to
log()
Basic usage in a test:
#include "backends/mock/mock_host.hpp"
citsy::Engine engine(kMinimalGame);
citsy::MockHost host;
engine.start(host);
engine.update(host);
const auto* snap = host.last_snapshot();
REQUIRE(snap != nullptr);
CHECK(snap->video.size() == citsy::kVideoSize * citsy::kVideoSize);
CHECK(snap->gfx_mode == citsy::GraphicsMode::Map);
MockHost helpers:
| Member / method | Purpose |
|---|---|
snapshots |
Vector of every present() call |
last_snapshot() |
Pointer to most recent snapshot, or nullptr |
set_button(Button, bool) |
Simulate input |
dt_ms |
Configurable frame delta (default ≈ 16.667 ms) |
reset() |
Clear recorded state |
Fixture files¶
Tests in tests/data/ are real .bitsy files used for integration-style parser checks. The helper in test_parser.cpp resolves paths relative to the source tree:
static std::string load_fixture(const char* name) {
fs::path here = fs::path(__FILE__).parent_path(); // tests/unit/
fs::path data = here.parent_path() / "data" / name; // tests/data/<name>
// ...
}
This works regardless of the build directory because it anchors on __FILE__.
When adding a new fixture:
- Place the
.bitsyfile intests/data/ - Add a
TEST_CASEwith tag[fixture]that loads and asserts on key entities - Keep fixtures small and focused on the feature under test
Writing new tests¶
Parser / model test (inline string)¶
White-box tests include the internal parser header directly:
#include "src/parser/parser.hpp"
TEST_CASE("parser: my new feature", "[parser]") {
constexpr std::string_view src = R"(
TIL x
00000000
...
)";
auto game = citsy::parse(src);
CHECK(/* assertions on game model */);
}
Engine test (MockHost)¶
Use an inline .bitsy string or load a fixture:
#include <citsy/engine.hpp>
#include "backends/mock/mock_host.hpp"
TEST_CASE("engine: my behavior", "[engine]") {
citsy::Engine engine(kMinimalGame);
citsy::MockHost host;
engine.start(host);
engine.update(host);
// assert on host.last_snapshot()
}
Error tests¶
Parser and engine constructors throw citsy::ParseError on bad input:
CHECK_THROWS_AS(citsy::parse(bad_src), citsy::ParseError);
CHECK_THROWS_AS(citsy::Engine(bad_src), citsy::ParseError);
Register a new test file¶
- Create
tests/unit/test_myfeature.cpp - Add it to the
citsy_teststarget inCMakeLists.txt:
add_executable(citsy_tests
tests/unit/test_parser.cpp
tests/unit/test_engine.cpp
tests/unit/test_dialog.cpp
tests/unit/test_simulation.cpp
tests/unit/test_myfeature.cpp # add here
)
- Reconfigure and rebuild — Catch2 auto-discovers new
TEST_CASEmacros
Catch2 tags¶
Tests use tags for filtering. Current tags:
| Tag | Scope |
|---|---|
[parser] |
All parser tests |
[engine] |
Engine lifecycle and output |
[mock] |
MockHost-specific tests |
[dialog] |
Linear dialog extraction and playback |
[sim] |
Movement, collision, exits, room state |
[render] |
map1 / map2 / video composition |
[fixture] |
Tests loading tests/data/*.bitsy |
[errors] |
Parse error handling |
Sub-tags like [palette], [tile], [room] |
Entity-specific parser tests |
Run a subset:
CI considerations¶
The test suite is designed for headless CI:
- No display server required
- No audio device required
- Catch2 fetched at configure time (needs network on first build)
- All tests complete in well under one second
Minimal CI recipe:
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build
ctest --test-dir build --output-on-failure
To skip tests in a CI job that only validates compilation:
Debugging failed tests¶
- Run with verbose output:
- Run the specific test directly for a full Catch2 report:
The -s flag shows successful assertions too (useful for diagnosing unexpected passes).
- For parser issues, reproduce with the smallest inline
.bitsysnippet before reaching for a fixture file.