I need to add a regression test
Evidence basis: source-verified at the pinned commit; tests exist (not executed for this page). Claims on this page were checked by reading the CNA source at commit 009d40f5; unless a sentence says otherwise, nothing here was built or executed. Read from UnitTests.cmake, TestHelpers.cmake, the example helpers and existing tests at 009d40f5; the two worked procedures are illustrative and were not executed, and no test was configured, built or run.
A regression test in CNA has to do three jobs at once: fail for the reason the bug exists, join the right executable and CTest routes without a hand edit, and say only what it can honestly prove in the configurations that run it. This recipe is the procedure: choose the layer (GoogleTest object group, renderer example program or configuration gate), place the file so the CMake globs and filters pick it up, write a narrow failing case and its negative twin, run it through the focused target, and check that it is actually registered and not silently skipped. It ends with two worked, not executed, procedures in different modules. Everything was read from the CNA source at snapshot 009d40f5; no test was configured, built or run for this page.
How the executables are assembled, what the labels and gates mean and what each CI workflow covers are on Test architecture and change recipes; working commands are on the testing handbook; the change-to-test matrix is What to test after changing X; the registered inventory is the Test target index. This page is the procedure that ties them together.
Find the owner: which layer proves this?
| The behaviour is | Write | Lives in | Runs as |
|---|---|---|---|
| Pure logic in one module (math, core, content readers, input state, storage rules) | A GoogleTest case | modules/<name>/tests/, mirroring the namespace path | The group's focused binary (CnaMathTests, CnaContentTests, ...) and the aggregate CnaTests; every case is also a discovered CTest entry |
Neutral GraphicsDevice or resource behaviour that must hold on whichever renderer is compiled in | A GoogleTest case gated at run time | modules/graphics/tests, with CNA_SKIP_IF_RENDERER_IS_NONE_OF(...) from RendererTestGate.hpp | CnaGraphicsTests and CnaTests, against the configuration's renderer (STUB in the unit preset, so many cases skip there) |
| Real pixels, swapchains, a native API, a window | A standalone example program with its own main() | The family's examples/ directory, sharing sources from modules/graphics/examples | A named CTest entry registered by cna_register_renderer_test; only in a tree where that family's block is entered |
| The same behaviour on several renderers | A parity fixture | ParityFixtures.cmake: write parity_<name>.cpp, append <name> to CNA_PARITY_FIXTURES | Every renderer that calls cna_register_parity_fixtures() registers <Prefix>_Parity_<name> automatically (EasyGL, WebGPU, SDL_GPU, OpenGL4) |
| A build or selection rule (an option, a refusal, a link closure) | A script-mode or Python gate | cmake/Tests, scripts/check_*.py, registered from UnitTests.cmake or the module probes | Cheap, GPU-free CTest entries (label configuration for many) |
| A backend contract across implementations | A parameterised case | PlatformConformanceTests.cpp | Modify a platform backend |
Read first
UnitTests.cmake: theCNA_TEST_SOURCESglob and its filters, the group tables (CNA_TEST_GROUP_DEPENDENCY_<group>,CNA_TEST_FOCUSED_TARGET_<group>), thegtest_discover_testscall and the named routes.TestHelpers.cmake: whatcna_register_renderer_testsets by default and what its output gates do.- The neighbouring test file for the same subject; copy its fixture idiom rather than inventing one.
CMakePresets.json: which preset builds which focused target, so the loop you run is the loop CI's presets describe.
Place the file so it is picked up
- Location decides membership. Sources are globbed with
CONFIGURE_DEPENDSfrommodules/*/tests/*.cpp,modules/renderers/*/tests/*.cpp,modules/renderers/common/*/tests/*.cppand the top-leveltests/*.cpp. A new.cppunder an existing module'stests/joins that module's object group (cna_<group>_test_objects, with-in the module name becoming_) and therefore both the focused binary andCnaTests, with no CMake edit. Every renderer family's tests form the singlerenderersgroup; anything under top-leveltests/isintegration. - A brand-new module tests directory needs an entry in
CNA_TEST_FOCUSED_TARGET_<group>; without it the configure stops with "No focused test target name is defined for group". - Link closure. A focused executable links only its group's dependencies (for example
CnaMathTestslinkscna_math). Compile-only include roots are visible everywhere, so a math test that calls graphics code compiles and then fails to link; add the implementation to the group's dependency list, as the content group does forcna_graphics_ext. Therenderers,graphics_extandintegrationgroups link the whole umbrella. - Files with their own
main()or a helper process are not GoogleTest units. They are excluded from the glob (the C API tests, the module probes, the Apple smoke app) and registered as their own executables; a test that spawns a helper gets the helper's path through a compile definition and a dependency from the group, as the tool-spawning content tests do. - Configuration filters. Whole files leave the corpus when the configuration cannot support them: platform-specific suites unless that backend is selected, the inspector suites without the inspector, network suites without networking, the XML-serialisation tests where the component is absent. A file that needs a family's headers must be guarded, and the guard should read
defined(CNA_RENDERER_<X>) || defined(CNA_RENDERER_PRESENT_<X>): the plain macro names only the build's default renderer, so it would compile the body to nothing for the other families of a multi-renderer build. Absence fromctest -Nis information. - Naming. Files are
<Subject>Tests.cpp; suites are usually<Subject>Test(RectangleTest,GraphicsAdapterTest,GraphicsProfileDrawLimitTest), with exceptions such asNpotTexture; a case name states the behaviour as a sentence (DifferentTypeSameNameThrowsContentLoadExceptionNotBadAnyCast). The discovered CTest name isSuite.Case, so a stable name also makesctest -R '^Suite.Case$'and--gtest_filterreliable. A file header that names the plan task or defect it pins is the local convention. - Named filters that must be extended. If your suite belongs to a route selected by a filter of name tokens, extend the filter: the input label runs one canonical
CNA_INPUT_TEST_FILTER(ctest -L input, shuffled and repeated five times), the platform entries run token filters, and the glTF ladder (CnaGltfConformanceL0toL6,Perf,Ledger,Tool) partitions everyGltf*suite into exactly one rung throughCNA_GLTF_CONFORMANCE_RUNGS; a new glTF suite that fits no rung fails the run throughGltfConformanceLadder.
Write the narrow failing case
- Reproduce first, in the smallest form. Take the bug to a single call sequence with the fewest objects. A test that needs a window, a GPU or a second thread is a different test; make sure the bug really needs them.
- Assert on the value that proves the cause, not on "did not throw". Where the expected value has a reference (XNA, an oracle file, a hand-derived table), quote its source in a comment. A golden produced by running CNA proves only that CNA did not change.
- Make it fail for the right reason. Run it against the unfixed code and read the failure. Then check the reverse where you can: break the production code the test claims to cover and see it fail.
NpotTextureTests.cpprecords why: plainTexture2D::GetDatais served from the framework's own CPU shadow, so those cases cannot reach a renderer's readback at all, and breaking one renderer's stride left every one of them green; only a render-target readback reaches the renderer. A test that cannot reach the code it names is worse than none. - Add the negative and boundary cases the bug lives beside: the wrong-type request that must throw the documented exception, the unsupported capability that must refuse, the exact limit, the empty and null inputs, disposal while bound, a second frame after a resize. Prefer several small cases to one long one, so a failure names the rule.
- Keep fixtures self-contained. Use a unique scratch directory per test (the content tests use a
ScratchContentRootunder the system temporary directory and remove it in the destructor); take shared read-only assets fromtests/assets, resolved relative to the repository root because discovered cases run there; never point a destructive fixture at a shared or user location. The storage fixture, for instance, deletes the resolved storage root on teardown (storage changes). - Reset process-wide state you touch. Input, the renderer selection and the ambient platform are process-wide: use the reset hooks (
InputManager::ResetAllForTests,GraphicsRendererSelection::ResetForTestingEXT) and leave no residue, or the test passes alone and fails in the shuffled, repeated routes. - Do not require a real desktop or device when a headless one is under test. Do not call
SDL_Initfrom a unit case, sleep for wall-clock time, or depend on execution order.
Renderer example programs versus CTest
An example program is a standalone executable: it creates a real Game and device, draws, reads pixels back and returns an exit code. Single-frame pixel tests derive from CNA::Examples::PixelTestGame in PixelTestGame.hpp: override RunTest(), call ExpectPixel(label, rect, colour, tolerance) or CompareGoldenImage(label, rect, path, tolerance), and finish with return CNA::Examples::RunPixelTest<MyTest>();. Exit 0 is pass, 1 is failure, and 77 (kSkipExitCode) is "no display or GPU here", which CTest reports as skipped. A golden image is created or refreshed only by running once with CNA_UPDATE_GOLDEN=1, reviewing the PNG, and committing it; its path is relative to the process working directory, so its registration must name WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" (the EasyGL golden entries do). Multi-frame state-machine tests hand-roll their own Game subclass.
The registration is a separate step in the family's examples/CMakeLists.txt: its own cna_<family>_test(target source) macro for linking, then cna_register_renderer_test(NAME <Family>_<Behaviour> COMMAND <target> TIMEOUT <s> LABELS "<Family>" ENVIRONMENT "SDL_VIDEODRIVER=x11;DISPLAY=${CNA_TEST_DISPLAY}"), and the file ends with cna_apply_skip_convention(). Three rules govern it.
- Working directory. Renderer-example registrations default to the top build directory; discovered GoogleTest cases and golden-image tests run in the repository root. A test that passes when run by hand and fails under CTest is usually this.
- Displays. Never hard-code
DISPLAY=:0or awayland-0socket; useDISPLAY=${CNA_TEST_DISPLAY}and let the launcher choose.CnaTestDisplayIsolation(check_test_display_isolation.py) fails a tree whose registrations could reach the live desktop, and a leftover emptyDISPLAY=entry. - Which tree runs it. A family's block registers only under its guard (see the
EasyGL_*,Headless_*andVulkan_*notes in Fix a renderer bug); several suites need an SDL3 target and return early without one. Output gates (Vulkan and OpenGL4) are added by the helper; a registration that overwritesFAIL_REGULAR_EXPRESSIONmust re-apply them last.
The unit route is fast and display-free but says nothing about pixels, and the example route is the opposite. Decide which you need for the failure, and add both only when the neutral rule and its native translation can fail independently.
Run it, and prove it ran
cmake --preset unit # Debug, STUB, tests on; examples off
cmake --build --preset unit-math # or unit-core, unit-content, unit-graphics
./cmake-build-unit/CnaMathTests --gtest_list_tests | grep -A3 RectangleTest # is it in the binary?
./cmake-build-unit/CnaMathTests --gtest_filter='RectangleTest.*' # run from the repository root
cmake --build --preset unit # the aggregate CnaTests (a separate build)
ctest --test-dir cmake-build-unit -N -R '^RectangleTest\.' # discovered once CnaTests exists
ctest --test-dir cmake-build-unit -R '^RectangleTest\.' --output-on-failure
The unit configure preset writes cmake-build-unit; the four focused build presets (unit-core, unit-math, unit-content, unit-graphics) build one focused target each, and other groups are built by target name (cmake --build cmake-build-unit --target CnaStorageTests). The discovered CTest cases appear only after CnaTests itself is built (the discovery mode is PRE_TEST; read from the GoogleTest module, not observed here). Then check the result the way the evidence supports it: the case appears in --gtest_list_tests and ctest -N, it ran rather than skipped (read the skip count), it failed before the fix and passes after, and, for anything with process-wide state or ordering risk, it survives --gtest_shuffle --gtest_repeat=5. For a case that needs a rendering renderer, say which one and on which host it ran, and reach for the private display wrapper for anything that opens a window (private displays and host caveats).
Worked procedures (not executed)
A. A value-type regression in the math module
Suppose a report says Rectangle.Intersect returns a wrong origin for disjoint inputs. The steps, without claiming the bug exists:
- Open
RectangleTests.cpp, which already holds theRectangleTestsuite with section comments and cases such asIntersectReturnsOverlapRegionandIntersectOfDisjointReturnsEmpty. Read what those assert first; a regression case pins what they do not, for example a disjoint pair with non-zero origins whose whole result must equalRectangle::Empty, named as a sentence such asIntersectOfDisjointRectanglesWithNonZeroOriginsIsTheEmptyRectangle. Quote the expected value's source (XNA's documented result or an oracle value) in a comment. No CMake edit is needed: the file is already in themathgroup. - Build and run only that case:
cmake --build --preset unit-math, then./cmake-build-unit/CnaMathTests --gtest_filter='RectangleTest.Intersect*'. Confirm it fails against the unfixed code and read why. - Add the neighbours the bug lives beside: touching edges, one rectangle inside another, negative sizes, and the out-parameter overload separately from the value-returning one (CNA's rules require each overload to be covered).
- Fix the implementation in
modules/math/src/Rectangle.cpp, re-run the focused binary, then buildCnaTestsand runctest -R '^RectangleTest\.'to prove the discovered entries agree. - Consider the blast radius before finishing: rectangles are used by graphics and input, so run
cmake --build --preset unit-graphicsand its filter for scissor and viewport cases, and check Change public XNA behavior if the fix moves a documented XNA result.
B. A cache-key regression in the content module
Suppose a report says loading the same logical name as two different types throws std::bad_any_cast instead of a ContentLoadException. That behaviour is already pinned by CnjAssetCacheTypeSafetyTests.cpp, which is the model to copy for a new neighbour.
- Place the file under
modules/content/tests/Microsoft/Xna/Framework/Content/, so the namespace path is mirrored and the file joins thecontentgroup (focused targetCnaContentTests, presetunit-content). Reuse the file's idiom: aScratchContentRootwith a unique directory, aWriteFilehelper, andContentManager cm(nullptr, root.path().string()). - Write the smallest failing sequence: register two loaders for two types under different
.cnjtype strings, load the name as the first type, then request it as the second andEXPECT_THROW(..., ContentLoadException). Add the control case that the same type and name still returns the cached instance, so a fix that simply disables caching fails. - Run
cmake --build --preset unit-contentand./cmake-build-unit/CnaContentTests --gtest_filter='CnjAssetCacheTypeSafetyTest.*'from the repository root; several texture-reader suites in this group are renderer-gated and skip under STUB, so read the skip count rather than the exit colour. - If the change touches the tier order or the loose-file resolver, also run the resolver-order and cache tests named on Modify ContentManager, and the XNB and CNB conformance suites.
C. A pixel program in a renderer family (outline)
For a bug that only shows in pixels, put the shared logic in a PixelTestGame source under modules/graphics/examples/ if it is renderer-neutral, or beside the family if it is not; add a cna_<family>_test line and a cna_register_renderer_test entry with a 30 to 60 second timeout, the family label and the display environment above; ensure the file's guard matches the way the family's other tests are guarded; then run it under the private display wrapper with ctest -N -R '^<Family>_' first to confirm it registered. If the behaviour is neutral, add it to CNA_PARITY_FIXTURES instead and every parity-registering renderer gains it.
Keep the blast radius small
- Every new source in a group is compiled into both the focused binary and
CnaTests; a compile or link failure in your file stops both. Keep includes narrow and avoid new heavy dependencies. - A test that mutates process-wide state or writes outside its scratch directory can break unrelated suites in the same process (the aggregate runs one process; discovered cases run one process each, which hides such leaks until the shuffled routes run).
- A registration property that is too broad (a label, an environment variable, a working directory) changes how other tests are selected; prefer the helper's defaults and set only what the test needs.
- A filter edit (input tokens, glTF rungs, a platform token list) changes what a named route runs for everyone; extend it, and check that the route still contains what it contained.
- Do not turn a skip into a pass, and do not add a renderer-conditional assertion that silently weakens the test on the renderers that cannot satisfy it; skip explicitly with the gate macro so the coverage drop is visible.
Review checklist
- Does the test fail before the fix, for the reason in the report, and did anyone try breaking the production code to see it fail?
- Is it in the module and namespace path that mirrors the code, and is it visible in
--gtest_list_testsandctest -Nof a tree that should run it? - Are negative, boundary and lifetime cases present, not only the happy path?
- Does the fixture avoid shared locations, real desktops and process-wide residue?
- For an example program: exit codes, skip convention, working directory, display environment and family guard are right; a golden image was regenerated only deliberately.
- Does the description state the configuration, the renderer, the host, and the skip count, and say what was not run?
Deep dives on this topic
Long-form pages that explain the exact semantics, invariants and evidence behind this subject.
- Curve evaluation: keys, tangents, loop types and the XNA reference — How CNA's Curve evaluates: sorted keys, the per-segment Hermite basis, Step continuity, the five loop types, smooth tangents and degenerate curves, each compared with the XNA 4.0 algorithm.
- Planes, rays and bounding volumes: exact containment and intersection semantics — Half-space conventions, plane transforms, ray tolerances, box corner order, sphere and frustum containment rules in CNA, compared function by function with XNA 4.0, with workarounds for every mismatch.
- SpriteBatch sorting, flushing and renderer batching — How CNA's SpriteBatch flushes and sorts (XNA's unstable quicksort, reproduced), what each renderer kind does with the sprite stream, the viewport-local projection and the Direct3D 9 half-pixel offset.
- Test populations, counts and structural gates — Which test layer produced a CNA result, what each static count at 009d40f5 counts, why no CTest total exists, and how identity, containment, inventory and mutation gates work.
- Vector, Matrix and MathHelper numerics: interpolation, clamping and degenerate inputs — What CNA's vector, matrix and MathHelper functions return for out-of-range amounts, inverted clamps, NaN, zero vectors, singular matrices and bad camera input, compared with XNA 4.0, plus the precision and test evidence.
Known issues in this area
Current defects, gaps and limitations at this snapshot that touch this subject.
- CNA-BUG-001: Plane::Transform(plane, matrix) transposes the inverse in place through a non-aliasing-safe Matrix::Transpose — Plane::Transform with a Matrix passes one object as both source and destination of Matrix::Transpose, which is not aliasing-safe, so any transform whose inverse is not symmetric (a translation, most rotations) yields a w
- CNA-BUG-002: BoundingSphere::Contains(const BoundingFrustum&) never returns Disjoint — When any frustum corner lies outside the sphere, BoundingSphere::Contains(frustum) always answers Intersects, because its distance test compares a constant zero, so a frustum nowhere near the sphere is reported as overla
- CNA-BUG-003: BoundingBox::Contains(const BoundingFrustum&) never returns Disjoint and answers Contains when only the first frustum corner is outside — BoundingBox::Contains(frustum) classifies the frustum from its corners only, so a frustum wholly separate from the box is reported as Intersects, and one whose first corner alone lies outside is reported as Contains.
- CNA-BUG-004: BoundingFrustum::Intersects(Ray) never computes an entry distance: rays starting outside always miss, and an origin on a plane throws — BoundingFrustum::Intersects(Ray) and Ray::Intersects(BoundingFrustum) report no hit for every ray whose origin is outside the frustum, whatever its direction, return 0 for an origin inside, and throw NotImplementedExcept
- CNA-BUG-026: Matrix::CreatePerspectiveFieldOfView accepts a field of view of exactly MathHelper::Pi — The upper guard compares against the literal 3.141593f, which rounds to the float one step above MathHelper::Pi, so a field of view of exactly Pi passes validation and builds a degenerate projection instead of throwing a
- CNA-BUG-028: Curve::ComputeTangent Smooth tangents test the key spacing against two different epsilons, where XNA tests the value difference against one — For CurveTangent::Smooth, ComputeTangent zeroes the in tangent when the neighbour span is below 2^-24 but the out tangent only below the smallest denormal, while XNA zeroes both when the neighbours' value difference is b
- CNA-BUG-064: Curve::Evaluate reads the wrong key for Step continuity away from position 1 and for a Linear post-loop — A Step segment returns the next key's value whenever the evaluated position is at least 1.0 (an absolute constant) instead of at the segment's end, and a Linear post-loop extrapolates with the first key's TangentOut inst
- CNA-BUG-065: Curve::Evaluate divides by zero for coincident key positions, returning NaN and converting infinity to int in the cyclic loop modes — GetNumberOfCycle and GetCurvePosition divide by the key span and the segment width without a guard, so curves with coincident keys yield NaN or undefined behaviour where XNA returns the first key's value.
- CNA-BUG-066: BoundingFrustum::Contains(Vector3) answers Intersects for a point exactly on a plane and skips the remaining planes — BoundingFrustum::Contains(point) returns Disjoint for any positive plane distance and, for a distance of exactly zero, Intersects without testing the remaining planes; XNA never answers Intersects for a point and uses a
Related pages
The same subsystem is explained at four altitudes. These are the neighbouring pages at each one.
- User guide
- Building CNA: running the tests · Building CNA: CMake presets · Verification: what the test counts count · Verification: what CI covers
- Architecture
- Architecture overview
- Maintainer workflow
- What to test after changing X · Fix a renderer bug · Modify a platform backend
- Tests and validation
- Test architecture and change recipes · Testing handbook
- Reference
- Test target index · CMake option index