What I do
I define how tests are written in this repo:
- the two test kinds (fixture-replay, analytic) and when each applies
- the assert surface and the suite shape
- where fixtures come from, how a new model gets its test tree
- the linters that verify all of it before anything commits
When to use me
Use this skill when doing any of these:
Related docs, each with its own job:
Test kinds
| kind |
expected side |
home |
| fixture-replay |
recorded sidecar statistics + decision records, produced once by the python reference |
q_bf16/, q_exl3/ |
| analytic |
truth computed in-process: closed forms, exact order statistics, in-process reference ops |
kvcache/, samplers/, batch_invariance/ |
A fixture-replay suite can never compare bit-exactly.
Honest rounding between recording and replay is the checked quantity.
An analytic suite compares against values computed in the same process,
where bit-exact is available and expected whenever the math is exact.
Assert surface
Two functions, that is all, full contract in the harness README.
assertStats(actual, record, kind) checks the statistical property:
quantiles, histogram, boundary elements, mean and tail bands
assertArgMax(actual, record) checks the decision: argmax id,
top-k pair, margin, tail
kind names a statistical property, never an operation:
| kind |
error model |
kElementwise |
reduction-free sequence, tight band, zero mismatch |
kReduction |
one accumulation point, moderate band, small mismatch fraction, histogram L1 |
| composed chains |
kReduction plus the depth argument |
Ban list, enforced by the linters:
- ad-hoc
rtol or atol at call sites, the band comes from the kind
- raw
doAssert for value comparisons in suites
try/except/discard that eats a failed check
check* enforcement procs outside tests/harness/
- PASS emission and verdict printing, a failed assert raises, rc=0 is green
Suite shape
- one file, one flat
main, setup then asserts, no framework sections
- every random tensor is seeded or replaced by a closed form
const blocks carry filepaths only, model geometry arrives over
config.json through the loader, never a literal
- setup helpers import
tests/layer_utils.nim for layer setup,
tests/stateful_utils.nim for the stateful context
- ulp math imports
tests/ulp_utils.nim
- headers carry the run command and the contract, never the journey
std/unittest (suite/test/check) is retired for suites,
existing conversions remove it
Adding a new model, the short version
- the model module + registry entry (ARCHITECTURE.md)
- one generator per fixture tier, named
gen_<dtype>_<model>_<NN>_<tier>.py
- record the families (
FIXTURE_GENERATION.md)
- the suites per tier, per the suite-shape rules above
- register the granular task inside
config.nims
Linters, run before commit, always
| linter |
checks |
.agents/skills/writing-docs/tools/lint_docs.py |
doc comments everywhere |
tests/linters/lint_gen_scripts.py |
generators: docs, config over consts, entry shape, directory allowlist |
tests/linters/lint_fixtures.py |
fixtures: size caps, dir tiers, symlinks, record schema, provenance |
tests/linters/lint_nim_fixtures_consumers.py |
suites: assert allowlist, flat main, consts, shared helpers, docs |
Each linter header carries its rules table, the golden doc-comment
rules, and pointers to both doc skills.
Read them, they are always forgotten.
A finding is fixed, never silenced and never widened into acceptance.
Nim mechanics that bite (libtorch FFI)
- wrap test code in procs: module-scope
TorchTensor variables fail
compilation, cppNonPod types reject brace initialization
- every branch of a
case must assign result
- a parameter shadowing an accessed field is a compile error, rename it
- import
workspace/libtorch_testutils for tensor test utilities
runCppTest stays for the transitional suites only
Kernel tests against a reference
A kernel test against a reference has three parts, so a reader always
sees which side is which:
- a proc that computes with the function under check
- a proc that computes the reference: an independent implementation,
libtorch math over the same rounded inputs, or a closed form
- the comparison, naming both sides at the assert
Rules:
- name the two results
actual and expected at the assert site
- both sides derive from the same seeded inputs, rounding applied
identically, never round the reference from the kernel's output
- a Nim reimplementation of the kernel's arithmetic as the reference
is banned, the duplication can carry the same misunderstanding twice
- a reference sharing the kernel's code path proves nothing, it must
be able to diverge
- data-preparation helpers that mirror a storage format are allowed,
once, in a shared helper, never copied per test
- the ceiling for a kernel test is around 80 lines
1---2name: testing3description: How tests are written in this repo — fixture suites, analytic suites, the assert surface, the linters4license: MIT5---67## What I do89I define how tests are written in this repo:1011- the two test kinds (fixture-replay, analytic) and when each applies12- the assert surface and the suite shape13- where fixtures come from, how a new model gets its test tree14- the linters that verify all of it before anything commits1516## When to use me1718Use this skill when doing any of these:1920- writing or extending a test under `workspace/*/tests/`21- adding a new model, quantization scheme, or layer ([ARCHITECTURE.md](../../workspace/transformers/ARCHITECTURE.md))22- recording a new fixture family ([FIXTURE_GENERATION.md](../../workspace/transformers/tests/testgen/FIXTURE_GENERATION.md))2324Related docs, each with its own job:2526| doc | job |27|---|---|28| [`tests/README.md`](../../workspace/transformers/tests/README.md) | test tree, check ladder, the stakes |29| [`tests/harness/README.md`](../../workspace/transformers/tests/harness/README.md) | assert API: assertStats, assertArgMax, the record |30| [`ARCHITECTURE.md` Extension Points](../../workspace/transformers/ARCHITECTURE.md) | where a new model or layer plugs in, test side included |31| [`FIXTURE_GENERATION.md`](../../workspace/transformers/tests/testgen/FIXTURE_GENERATION.md) | recording: tiers, payload rules, provenance |32| [`writing-docs` skill](../writing-docs/SKILL.md) | doc comments, together with the global writing-code-doc skill |3334## Test kinds3536| kind | expected side | home |37|---|---|---|38| fixture-replay | recorded sidecar statistics + decision records, produced once by the python reference | `q_bf16/`, `q_exl3/` |39| analytic | truth computed in-process: closed forms, exact order statistics, in-process reference ops | `kvcache/`, `samplers/`, `batch_invariance/` |4041A fixture-replay suite can never compare bit-exactly.4243Honest rounding between recording and replay is the checked quantity.4445An analytic suite compares against values computed in the same process,46where bit-exact is available and expected whenever the math is exact.4748## Assert surface4950Two functions, that is all, full contract in [the harness README](../../workspace/transformers/tests/harness/README.md).5152- `assertStats(actual, record, kind)` checks the statistical property:53 quantiles, histogram, boundary elements, mean and tail bands54- `assertArgMax(actual, record)` checks the decision: argmax id,55 top-k pair, margin, tail5657`kind` names a statistical property, never an operation:5859| kind | error model |60|---|---|61| `kElementwise` | reduction-free sequence, tight band, zero mismatch |62| `kReduction` | one accumulation point, moderate band, small mismatch fraction, histogram L1 |63| composed chains | `kReduction` plus the depth argument |6465Ban list, enforced by the linters:6667- ad-hoc `rtol` or `atol` at call sites, the band comes from the kind68- raw `doAssert` for value comparisons in suites69- `try`/`except`/`discard` that eats a failed check70- `check*` enforcement procs outside `tests/harness/`71- PASS emission and verdict printing, a failed assert raises, rc=0 is green7273## Suite shape7475- one file, one flat `main`, setup then asserts, no framework sections76- every random tensor is seeded or replaced by a closed form77- `const` blocks carry filepaths only, model geometry arrives over78 `config.json` through the loader, never a literal79- setup helpers import `tests/layer_utils.nim` for layer setup,80 `tests/stateful_utils.nim` for the stateful context81- ulp math imports `tests/ulp_utils.nim`82- headers carry the run command and the contract, never the journey83- `std/unittest` (`suite`/`test`/`check`) is retired for suites,84 existing conversions remove it8586## Adding a new model, the short version87881. the model module + registry entry ([ARCHITECTURE.md](../../workspace/transformers/ARCHITECTURE.md#extension-points))892. one generator per fixture tier, named `gen_<dtype>_<model>_<NN>_<tier>.py`903. record the families ([`FIXTURE_GENERATION.md`](../../workspace/transformers/tests/testgen/FIXTURE_GENERATION.md))914. the suites per tier, per the suite-shape rules above925. register the granular task inside `config.nims`9394## Linters, run before commit, always9596| linter | checks |97|---|---|98| `.agents/skills/writing-docs/tools/lint_docs.py` | doc comments everywhere |99| `tests/linters/lint_gen_scripts.py` | generators: docs, config over consts, entry shape, directory allowlist |100| `tests/linters/lint_fixtures.py` | fixtures: size caps, dir tiers, symlinks, record schema, provenance |101| `tests/linters/lint_nim_fixtures_consumers.py` | suites: assert allowlist, flat main, consts, shared helpers, docs |102103Each linter header carries its rules table, the golden doc-comment104rules, and pointers to both doc skills.105106Read them, they are always forgotten.107108A finding is fixed, never silenced and never widened into acceptance.109110## Nim mechanics that bite (libtorch FFI)111112- wrap test code in procs: module-scope `TorchTensor` variables fail113 compilation, `cppNonPod` types reject brace initialization114- every branch of a `case` must assign `result`115- a parameter shadowing an accessed field is a compile error, rename it116- import `workspace/libtorch_testutils` for tensor test utilities117- `runCppTest` stays for the transitional suites only118119## Kernel tests against a reference120121A kernel test against a reference has three parts, so a reader always122sees which side is which:1231241. a proc that computes with the function under check1252. a proc that computes the reference: an independent implementation,126 libtorch math over the same rounded inputs, or a closed form1273. the comparison, naming both sides at the assert128129Rules:130131- name the two results `actual` and `expected` at the assert site132- both sides derive from the same seeded inputs, rounding applied133 identically, never round the reference from the kernel's output134- a Nim reimplementation of the kernel's arithmetic as the reference135 is banned, the duplication can carry the same misunderstanding twice136- a reference sharing the kernel's code path proves nothing, it must137 be able to diverge138- data-preparation helpers that mirror a storage format are allowed,139 once, in a shared helper, never copied per test140- the ceiling for a kernel test is around 80 lines