Contents

Validation and tooling

This file covers how the engine is checked and driven: the functional test suite’s shape and what it pins, the se developer CLI that resolves the toolchain and runs everything, and the continuous integration checkers that guard the rules a compiler cannot see.

1. Validation and tooling

The engine ships no device code of its own, so there is nothing to test in isolation — a meaningful test must instantiate kernels and run them against the real runtime. The suite in tests/ does exactly that: it follows SushiRuntime’s layout (unit/, integration/, a shared common/ with the GoogleTest entry point and a process-wide runtime fixture) and builds one binary, se_functional_tests — the target is sushiengine_functional_tests — as a SYCL translation-unit set. There are no mocks. Tests carry the Unit_* / Integration_* suite-name prefixes, which tests/CMakeLists.txt turns into CTest labels so a sub-suite is one ctest -L away. The integration tests re-run the sandbox and PGS claims as assertions (scalar-reference agreement, compile_count == 1); the unit tests pin the host bookkeeping (entity directory, swap-remove, command buffer, graph colouring).

tests/common/test_main.cpp installs SushiEngine::Tests::TestAssertionHandler (tests/common/test_assertion_handler.hpp) before RUN_ALL_TESTS. A failed SE_VERIFY anywhere in the binary is reported as a GoogleTest failure on the running test and execution continues; a failed SE_ASSERT or SE_FATAL is reported and then aborts the binary. A test that needs the default handler’s own behaviour, or one it supplies itself, installs it for a scope with ScopedAssertionHandler, which restores whichever handler was active on destruction.

Beyond the ECS/physics core, three areas are now covered directly. The engine/domain/astro/ solar-system model — pure double-precision maths with no device code — is verified against astronomical reference landmarks (J2000 = JD 2451545.0, Earth ~1 AU from the Sun, WGS84 semi-major/minor radii, ~9.8 m/s² surface gravity, Earth’s SOI ~0.9 Gm) and against structural invariants that hold regardless of the exact arithmetic: Kepler’s equation is satisfied by the returned anomaly; the ecliptic↔equatorial, topocentric, body-equatorial, and scene-frame transforms all round-trip to the identity; every basis is right-handed orthonormal; the symplectic integrator conserves orbital energy over a full revolution; and advance_astro_state keeps a low orbit bound and deterministic (Unit_JulianDate, Unit_OrbitalElements, Unit_CelestialBodies, Unit_Gravity, Unit_Topocentric, Unit_BodyOrientation, Unit_SurfaceFrame, Unit_StarCatalog, Unit_SceneFrame, Unit_ReferenceFrame, Integration_AstroDynamics, Integration_Ephemeris).

The camera/transform maths in engine/foundation/core/include/SushiEngine/core/types.hpp (quaternion rotation vs. its matrix form, the reverse-Z infinite-far projection, look_at, compose_transform) is pinned in Unit_MathPrimitives. The determinism guard rails — the seeded xorshift128+ RNG and the fixed-timestep accumulator — are pinned in Unit_RNG (identical seeds replay identically; a snapshot replays the future exactly, as rollback needs) and Unit_FixedTimestep (step count depends only on total elapsed time and not on how it was chunked across frames, for any chunking whose per-frame backlog stays inside the catch-up bound; and that a frame owing more than the bound drops the remainder rather than carrying it).

Build time is read, never instrumented. tools/build/report_build_times.py parses the .ninja_log Ninja writes in a build directory (build/default/ by default, --log for another lane) and prints the wall time, the summed unit time and their ratio, the per-target totals and the slowest outputs; --json writes every record for a before-and-after diff. Only the newest record of each output counts, so a log from an incremental build measures what happened to be stale; a baseline is a se build --clean log. The script invokes nothing, and its own tests run with python -m pytest tools/build.

The se developer CLI (cli/) is the counterpart to the runtime’s sr: a thin Typer layer over a service layer that issues the cmake/ctest calls. It owns no build knowledge the CMake does not — its job is to resolve the toolchain the engine consumes (SushiRuntime’s bundled clang++ and vcpkg) and snapshot the MSVC environment on Windows, then drive configure/build/test/run. The same one-way dependency holds: the CLI resolves the toolchain from the shared SushiStack workspace’s dependencies/ tree (falling back to a runtime-local copy for backward compatibility) but the engine never reaches back into runtime source.

2. The continuous integration checkers

Nine Python checkers guard rules a compiler cannot see. All of them run in one job, Structure and documentation rules (.github/workflows/ci.yml), each as its own step, and every one of them is run from the repository root and exits non-zero on a violation. They are listed here in the order that job runs them, so a red step maps to a row.

Checker Fails on Stated limitation
tools/layering/check_include_layering.py A target_include_directories() call that gives a module another module’s include/ root when the edge points up the tier order in cmake/EngineLayers.cmake. This form creates the same coupling as a declared dependency but the configure-time sushiengine_check_module_edge() never sees it. Every such edge is reported as a note whether legal or not; only an illegal one fails. It reads paths out of target_include_directories() calls in module and application CMakeLists.txt files only.
tools/layering/check_no_shell_invocation.py Any std::system( under engine/, applications/, tools/ or samples/. Every use the tree had was reachable from a user-controlled file name, and the platform APIs that do the same jobs take arguments rather than a command line. The rule is absolute — no exemption list — because a rule with exceptions is one nobody can check. It is a textual match, so it sees no other way of reaching a shell.
tools/layering/check_frame_view_parity.py A std::vector channel on Render::FrameView with no counterpart on Simulation::RenderScene, or the reverse; a stale row in its own SHADOWS pairing table; a stale row in engine/presentation/render/include/SushiEngine/render/frame_view.hpp’s exemption table. It also fails if it cannot find or parse either struct. The pairing table is a third list of the renderable kinds and can drift from the two it describes, so it checks both ends of every row. The table lives in the script because engine/presentation/render/include/SushiEngine/render/frame_view.hpp may not name a Simulation:: type.
tools/layering/check_host_frame_ownership.py A host under applications/editor/ or applications/player/ calling an engine function Frame::compose, Frame::drive_environment, Frame::load_scene_into or Frame::resolve_world_assets owns (OWNED_CALLS, fourteen entries, mutating calls only); a host assembling a Render::CameraView’s view and projection itself instead of calling Frame::camera_view_from_state; a host that has stopped calling Frame::compose or Frame::load_scene_into; a stale row in its dated EXCEPTIONS allowlist. It cannot see the two hosts feeding compose different input (that is Integration_FrameHostParity’s job), where a host reads the extract, duplication that calls nothing, or a third applications/<host>/ not listed in HOST_ROOTS.
tools/layering/check_dependency_provisioning.py A job that runs cmake -B against this tree and cannot find a dependency every configure requires — one a CMakeLists the root reaches with no if() in between locates with a REQUIRED find_package/find_path — by apt, by a vcpkg port with the vcpkg toolchain file on the configure line, or by a source clone the configure points CMAKE_PREFIX_PATH/CMAKE_INCLUDE_PATH at. Also a dependency declared in cli/sushistack.deps.toml and named nowhere in the workflow. It does not model configure options, so an option-gated dependency (the renderer’s Vulkan stack, the editor’s SDL2) is only held to being named somewhere in the file. WAIVED exempts a dependency everywhere and JOB_WAIVED exempts one job, both with a stated reason. It needs PyYAML.
tools/layering/check_settings_serialization_parity.py A field of RenderSettings or SimulationSettings missing from either render_settings_to_json/from_json or simulation_settings_to_json/from_json in engine/world/authoring/source/preferences.cpp — a field silently lost on save or on load. It parses the field list out of the headers and the key list out of each function textually, so it measures presence of a key, not that the key is written and read correctly.
tools/documentation/check_module_documentation.py A directory under engine/<tier>/ with no README.md; a README with a missing or wrong {#module-<name>} heading label, which Doxyfile needs to keep the generated pages apart; a docs/modules/README.md index that misses a module or links a README that is not there. None stated.
tools/documentation/check_documentation_length.py The ceilings in the style guide: prose lines past 100 columns, paragraphs past 1,200 characters, changelog bullets past 240 (fatal past 400), files past 900 lines, and markdown links resolving to no file or to no heading in the file they name. The style guide’s own exemptions apply: table rows, fenced code blocks, a line whose overflow is one unbreakable token, docs/design/, docs/agent/, docs/archive/, docs/api/ and docs/api-site/ from the file and paragraph ceilings, docs/agent/ and docs/archive/ from the column ceiling as well, and the changelog from the file ceiling alone.
tools/documentation/check_design_citations.py A backticked file path cited by a document under docs/design/, docs/architecture/, docs/guides/ or docs/modules/ that resolves to nothing. tools/documentation/check_documentation_length.py resolves markdown links; a design document cites evidence as a backticked path instead. Four categories legitimately do not resolve and are declared per-entry in ALLOWED: external, runtime-artifact, planned and deleted.