docs: describe the v2 test architecture

test-system-v2.md is the authoritative owner table and target graph;
CLAUDE.md rule 13 moves from the sizelint gate to authoritative layout
helpers and rule 14 names the new targets; PROJECT_PLAN.md is marked
historical where superseded.
This commit is contained in:
2026-08-07 23:03:55 +09:00
parent 7cc8cee737
commit a87d931096
3 changed files with 300 additions and 14 deletions

View File

@@ -10,5 +10,5 @@
10. Symmetric stages. cstage and wwstage MUST emit byte-identical asm for the same input. When inference power differs, align the richer side DOWN to the leaner side, not the other way.
11. Split commits when they bundle unrelated concerns. Bisect-cleanliness is the default. Multi-fix commits need a body paragraph explaining why they couldn't split.
12. Simple data, simple algorithms. Sea-of-stars style. Mirror Hare's structural choices over clever alternatives.
13. No hardcoded size literals in size-computation contexts. Always route through the type table (tinfo.size / Type.size / ty_*->size / size(T) / primtypesize / tyslicesize). Per Drew's framing of Hare's discipline. `make sizelint` enforces and runs as a dep of `make test`. Exemptions documented inline with `// sizelint-ok: <reason>` (ww) or `/* sizelint-ok: <reason> */` (C). Optional local enforcement: `ln -s ../../tools/sizelint .git/hooks/pre-commit`.
14. Test targets. Inner-loop dev runs `make test-unit` (~3.8s, skips wwstage-driver tests: 950, 990997). Pre-commit runs `make test-commit` (content-keyed tier: the 10 pure gates — 950, 989_lib_byteid, 990997 — report `cached` when the input key matches the last green full run; ~25s on a hit, full gate run on any miss; key = stage-binary md5s + tracked selfhost/lib/cmd/rt/test/examples sources + harness + Makefile; cache in out/.testcache, wiped by make clean; mtime keys and partial keys are vetoed). Pre-push runs full `make test` (~3:40 measured at f940072, all 353 incl. byte-id gates) — the bootstrap-correctness gate, never cached; never push without it green. test-unit is a fast confidence check on unit-level code (stages, stdlib, smoke); a green test-unit doesn't prove bootstrap, only that the unit set is intact. New wwstage-driver tests are no longer confined to the 950/990997 range for correctness: T3 (test-perf arc) made every build's intermediates follow its OUTPUT (`ww build -o <stem>` / `ww run`'s per-pid temp) instead of landing next to every traversed source, so concurrent driver builds no longer race on `selfhost/cmd/<tool>/main.{s,combined.ww,o}` fixtures — the phase-2 byte-id group (incl. 993/995/950) now runs fully parallel. The 950/990997 NNN prefixes still drive test/run's phase-2 membership, the `make test-unit` skip set, and the `make test-commit` cache set, so a heavy 43k self-compile gate should still use that range to be skipped by test-unit and cached by test-commit; a light wwstage-driver test may live at any NNN (just `-o`-redirect its intermediates off the source tree).
13. Authoritative size and layout helpers. Route compiler size and layout computations through resolved type metadata or canonical helpers (tinfo.size / Type.size / ty_*->size / size(T) / primtypesize / tyslicesize). Keep unavoidable external ABI and serialized-format constants localized and explain the contract they encode.
14. Test targets. `make test` is the small developer gate: five in-process compiler units plus one compile-only C/WW compiler-fixture smoke case. `make test-compiler` owns the complete declarative compiler corpus and residual artifact/integration carriers; `make test-package`, `make test-lang`, and `make test-library` own package and in-language behavior. `make test-commit` composes those ordinary behavior suites but excludes byte identity, fixed-point/bootstrap, and platform gates. Run those explicitly with `make test-byteid`, `make test-bootstrap`, and `make test-platform`; `make test-all` is the exhaustive composition. Compiler-fixture subprocess coordination defaults to `JOBS=1`. There is no last-green cache or shell scheduler. A small-target result never proves bootstrap or byte identity.

View File

@@ -1,5 +1,11 @@
# WW Project Continuation Plan
> Historical implementation log. The active test targets and ownership model
> are documented in `docs/test-system-v2.md`. References below to `test/run`,
> `test-run`, phase scheduling, last-green caching, frozen `internal/wwtest`,
> or the 106-fixture prototype describe superseded 2026-08-05 states; they are
> retained only as dated design history and are not current operator guidance.
Status date: 2026-08-05
Canonical baseline: `master` at `3d7973596422f1c9e2275016a5f264a2ae504ceb`
@@ -393,22 +399,34 @@ Remaining:
### 5.4 Package-driver limitations
- `ww test DIR` discovers only immediate `_test.ww` files and does not recurse.
- Files are not sorted deterministically.
- Each discovered file is built separately instead of assembling one package
test unit. Same-package helper sharing therefore does not work as expected.
- Production enumeration excludes all `*test.ww`, while test discovery includes
only `_test.ww`; the naming rules disagree.
- Only the first glob argument reaches the runner even though the runner can
This list records the pre-T1 audit baseline. The 2026-08-05 single-directory
vertical slice closes deterministic sorting, compatible group assembly,
multiple-filter forwarding, no-test/no-match behavior, dependency-test
exclusion, and deterministic `-c` output. Recursive discovery and package
`-o` remain deliberately outside that slice.
- `ww test DIR` discovered only immediate `_test.ww` files and did not recurse.
- Files were not sorted deterministically.
- Each discovered file was built separately instead of assembling one package
test unit, so same-package helper sharing did not work as expected.
- Production enumeration excluded all `*test.ww`, while test discovery included
only `_test.ww`; the naming rules disagreed.
- Only the first glob argument reached the runner even though the runner could
support more than one.
- A single file with no tests can silently return success, while a directory
with no recognized tests can error. Define one consistent contract.
- Imported dependency tests are stripped. This is normally correct, but root
package files containing tests must be assembled into the root test build.
- `-c` without `-o` can leave a temporary test executable behind.
- A single file with no tests could silently return success, while a directory
with no recognized tests could error.
- Imported dependency tests were stripped correctly, but root package files
containing tests were not assembled into the root test build.
- `-c` without `-o` could leave a temporary test executable behind.
### 5.5 Runner-protocol limitations
This list likewise records the pre-slice audit baseline. The first package
vertical slice closes completion proof, failure clamping, timeout/group cleanup,
skip reasons, expected-abort state, and qualified current-test identity. The
remaining invalid-attribute diagnostics and abort/exit-1 ABI ambiguity are
tracked in the later task sections.
- A test can call `os.exit(0)` before completing and the parent reports it as a
pass. The parent needs an explicit completion token from the child.
- The runner returns the raw number of failures. At 256 failures, the Unix exit
@@ -622,6 +640,16 @@ before declaring all of T0 complete.
### T1: Implement package-aware `ww test`
First vertical slice status (2026-08-05): one explicit immediate directory is
planned in byte order; production and same-package tests share a white-box
binary; the exact `<package>_test` group uses a second binary; imported tests do
not leak; qualified IDs, multiple filters, explicit empty outcomes, direct
per-test supervision, completion proof, timeout cleanup, and exit clamping are
covered natively. Both public drivers delegate to the same package planner but
pass their own compiler stage. Recursive `./...`, package concurrency/caching,
package `-o`, universal filename migration, and the remaining attribute-shape
diagnostics are still open, so T1 as a whole is not marked complete.
- Introduce a package test-plan data structure in the driver.
- Unify production-file exclusion and candidate discovery on `*_test.ww`, then
classify candidates by their package declarations.

258
docs/test-system-v2.md Normal file
View File

@@ -0,0 +1,258 @@
# WW test architecture
Status: active architecture as of 2026-08-07. The target graph, corpus
accounting, and complete `test-commit` gate have been rechecked, including its
two concurrency controls separately and together. The separately gated
byte-identity and test-infrastructure proofs have also run. Bootstrap and
platform proofs have not run as part of this revision.
## Why the old suite was slow
The former suite made shell, Make, and one C executable per legacy case share
ownership of discovery, registration, phase selection, scheduling, result
records, timeout classification, and a last-green cache. Language behavior and
byte-identity loops repeatedly rebuilt complete package graphs. The measured
serial result was 411.403 seconds wall time, with a static lower bound of about
11,040 compiler-stage processes and 4,800 archive creations.
The replacement assigns each assertion to one owner and keeps expensive proof
categories out of the ordinary developer target.
## Owners
| Category | Owner |
| --- | --- |
| Arena, lexer, parser, checker, narrow codegen | Five in-process C unit binaries |
| Compile success/rejection, stage-routed diagnostics, and runtime exit | `test/wcc/data/*/case.ww`, executed by `wwfixture` |
| Package semantics | `test/package` and the native package-test coordinator |
| Language behavior | `test/lang/*_test.ww` through the language `@test` runtime |
| Library behavior | Library-owned `@test` sources named by `LIBRARY_TESTS` |
| Standalone library-source compilation | Four real source paths named by `LIBRARY_STANDALONE_SOURCES`, compiled directly by both frontends |
| Compiler-output identity | `test-lang-byteid` and `test-data-byteid` plus residual byte/artifact carriers |
| Fixed point and self-host | `test-bootstrap` |
| Host linker/platform behavior | `test-platform` |
The live declarative compiler corpus has 1,224 fixtures and 2,448 C/WW cells:
314 expected rejections (290 shared and 24 stage-specific), 12 compile-only
successes, 136 exit-zero programs, and 762 explicit-exit programs.
175 native C carriers remain. They are partitioned exactly once as five
in-process units, 27 byte/artifact gates, seven bootstrap gates, one platform
gate, and 135 residual compiler, package-layout, ABI, diagnostic-observer,
driver, linker, or FFI gates. Rows migrated to fixtures or native `@test`
owners were removed from those carriers; there is no compatibility execution
path for retired rows. 61 former byte/artifact carriers whose only assertion
was a cstage-vs-wwstage `.s` byte-compare of corpus fixtures were retired
into the blanket `test-data-byteid` comparator; the 27 survivors observe
something the blanket cannot: assembly patterns, symbol tables, frame
layouts, `.wwi` round-trips, inline sources with no corpus twin, or the
wwstage-driver leg.
## Public targets
| Target | Composition |
| --- | --- |
| `test` | Five in-process units plus one compile-only C/WW compiler-fixture smoke case |
| `test-compiler` | Complete fixture corpus plus residual compiler/integration carriers |
| `test-package` | Package planning, grouping, routing, and package runtime only |
| `test-lang` | Language-owned `@test` behavior |
| `test-library` | Library-owned `@test` behavior plus four direct standalone-source C/WW compilation checks |
| `test-commit` | Unit + compiler + package + language + library behavior |
| `test-byteid` | Compiler-output identity gates |
| `test-bootstrap` | Fixed-point bootstrap plus the 950/990995 native gates |
| `test-platform` | Host-dependent dynamic-link gate |
| `test-wwfixture` | Fixture CLI/process/protocol integration boundary |
| `test-all` | Commit + byte-ID + bootstrap + platform + test-infrastructure checks |
`test-commit` deliberately excludes byte identity, bootstrap, and platform
work. `test` is intentionally smaller than the old target and is the ordinary
developer feedback gate. Its purpose is a short, direct path from a compiler
edit to useful evidence, not compliance with an arbitrary wall-clock cutoff.
Make and fixture scheduling have separate, explicit owners. The Makefile does
not detect CPU count or add `-j` to `MAKEFLAGS`; the caller selects Make
parallelism with the standard `make -jN` option. `JOBS ?= 1` controls only the
`wwfixture -j N` value passed by `test-compiler`. A normal fast gate can use
both layers deliberately:
```sh
make -j4 JOBS=4 test-commit
```
For deterministic failure reproduction, make both layers serial explicitly:
```sh
make -j1 JOBS=1 test-commit
```
Serial execution is a debugging mode, not a correctness requirement. `JOBS`
is not inferred from `MAKEFLAGS`, and there is no jobserver adapter or second
scheduler hidden in Make.
`nocc` remains the separate, explicit reproduction route from a checked-in
stage-0 snapshot. It is not an implicit prerequisite of ordinary tests or of
`test-bootstrap`, because it has an external stage-0 precondition.
## Compiler fixtures
Each fixture is one directory with one `case.ww`. Its first line is exactly one
of:
```text
//ww:error "required diagnostic fragment"
//ww:error c "C-stage fragment" ww "WW-stage fragment"
//ww:compile
//ww:run
//ww:run-exit N
```
- `error` requires normal nonzero frontend termination and the declared stderr
fragment. The labeled form routes distinct fragments to the C and WW cells;
labels are fixed-order and neither fragment is treated as a shared fallback.
- `compile` requires frontend exit zero and produces no executable.
- `run` builds and requires normal program exit zero.
- `run-exit N` builds and requires normal program exit `N`.
Every fixture is run against the C and WW frontends. Signals, launch failures,
timeouts, build failures, and runtime exits are distinct outcomes.
`test-compiler` passes `-j $(JOBS)` and therefore uses one fixture slot by
default. The direct `wwfixture` CLI retains its own four-slot default; pass
`-j N` when its concurrency must be explicit. Its existing
`os.exec.start`/`poll` loop supervises the independent processes. Each cell has
its own working directory; filesystem fixtures use that directory or an
existing `temp.named` path rather than a shared fixed pathname.
The old `TESTS` list and all explicit per-wrapper Make rules are gone. A
surviving C carrier is registered only by its source file and built through one
generic pattern rule. Four arena/frontend units share a static rule; the codegen
unit links the existing private `cgen` and text-emitter objects directly.
`test/wwfixture/integration.sh` remains the direct black-box owner for behavior
that exists only at the command/process/protocol boundary: filtering and list
output, phase and outcome classification, diagnostic routing, malformed-corpus
and identity-drift rejection, signal/timeout/interruption cleanup, publication
failure, and strict result-stream decoding. Semantic fixtures cannot prove
those observations about their coordinator. `test/wwfixture/process/main.ww`
owns the lower-level `os.exec` primitives, not the CLI policy layered over
them, so it is complementary rather than a duplicate owner.
The five unit sources have zero active `system`, `popen`, `fork`, or `exec`
calls. Before direct conversion they contained two `popen` call sites and a full
`test-unit` run launched one `ww -V` plus twelve `w6c` processes. `400_w6c` now
checks the same twelve assembly fragments in process, grouped under ten unique
sources. The CLI version assertion lives with C/WW-driver parity in the existing
`949_driver_flagargs` integration carrier; `000_smoke` retains its arena-growth
and nonempty-version-constant assertions.
## Package and language behavior
`ww test` delegates directory package requests to the native package
coordinator. The coordinator owns discovery, package grouping, same-package and
external-package test composition, filtering, result aggregation, and its
internal temporary workspace. With `-c`, it publishes each exact
`<package>.test` binary and adjacent `<package>.test.sepwork` tree in the
package directory; those become caller-owned artifacts. Without `-c`, it
removes the temporary binary and scratch with its workspace. The language
runtime owns individual `@test` functions.
Separate compilation is the only driver build path; no compatibility mode
switch remains.
`ww build`, an explicit single-file `ww test -o <stem>`, and each successful
directory-package `ww test -c` build publish `<stem>.sepwork` as a caller-owned
artifact directory. The driver acquires it with one fresh `mkdir` and refuses
an existing path; it never clears a collision. A caller keeps only the exact
artifacts it observes and removes that exact tree on every later success or
failure. `ww run` and no-output single-file `ww test` use driver-owned scratch
instead; both driver stages place that scratch and their temporary executable
beneath one freshly acquired directory, remove both after every build result,
and make cleanup failure fail the command. Make recipes build driver-produced
tools in invocation-owned directories and apply the same exact cleanup rule.
`test/lang` currently uses one package per source file, so its complete gate
still performs independent package builds. That remaining source layout is not
hidden behind caching or concurrency; it is outside the small `test` target.
## Compiler-only byte identity
`ww build -S` and `ww test -S -o <stem>` run source discovery, dependency
ordering, unit composition, and each required `w6c -c` invocation. They return
after the complete `.s`/`.wwi` set exists. The producer loop does not invoke
`w6a`, create per-package archives, or invoke `w6l` when `-S` is active.
`test-lang-byteid` runs the 158 selected language files once with C `w6c` and
once with WW `w6c`, requires identical emitted `.s` filename sets including
`__root.s`, and compares those bytes. Relative to the old two-leg full builds,
this removes at least 3,476 assembler launches, 3,160 archive writes, and 316
linker launches from the comparison loop. A cold Make invocation may still
build prerequisite compiler binaries; the compiler-only claim applies to the
per-language-file comparison path.
`test-data-byteid` applies the same comparator to the declarative corpus:
every non-error `case.ww` builds twice through the fixed cstage driver with
only `WW_W6C` swapped, and every emitted per-package `.s` must be
byte-identical. `//ww:error` fixtures have no `.s`; their both-stage reject
parity is owned by the fixture corpus itself. Known cs/ww divergences are
pinned in `DATABYTEID_DIVERGED` with the `989_lib_byteid` discipline: a
pinned fixture must still build on both stages and still differ, so a
compiler fix fails the gate demanding graduation rather than silently
widening coverage. The full sweep compares 910 fixtures in about a minute
and is scratch-rooted under `out/`, not `/tmp`.
Byte identity is an explicit proof gate. It is not a prerequisite of `test` or
`test-commit`.
## Bootstrap, subprocesses, and CSP
Stage-2-through-stage-4 fixed-point proofs and the 950/990995 self-host gates
are reachable through `test-bootstrap` and `test-all`, never through `test` or
`test-commit`. Cold ordinary targets may still build their C- and WW-stage tool
prerequisites once; they do not iterate those tools to a fixed point. The C
bootstrap source and the standalone `nocc` route remain intact.
The bootstrap recipe owns the fixed `out/bootstrap` tree. Make schedules that
target once within one invocation, but two independent `make bootstrap`
invocations are not safe to run concurrently and remain mutually exclusive.
`lib/os/exec` is the sole reusable WW subprocess mechanism. Fixture and package
coordinators use its captured asynchronous path. The WW driver directly uses
`os.exec.runstdio` for inherited-stdio, inherited-environment, leader-only
compiler, assembler, linker, cleanup, run, and single-file-test calls. The
local WW `procrun` implementation is deleted. The C bootstrap retains its C
process implementation because it cannot consume a WW standard-library
module.
WW has tokens and an opaque type for future CSP/channel work, but no mature
production channel operations, task runtime, or scheduler. No channel,
goroutine, thread, worker-runtime, or CSP library was added. Coordinators stay
single-threaded; OS process polling does not require language-level threads.
## Retired mechanisms
The following are deleted, not adapted:
- `test/run`, including its phase classifier, `xargs` scheduler, atomic private
records, timeout-text classifier, result collector, and last-green cache;
- `test/run_test.sh`, the synthetic shell tests for that protocol;
- every explicit `TESTS` registration and all 329 explicit wrapper rules;
- the unregistered `test/runww.ww` corpus runner;
- frozen duplicate compiler-corpus code under `internal/wwtest`, `test/wwtest`,
and `test/compiler`;
- the redundant standalone `smoke`, `test-run`, and `test-harness` routes; and
- library-launcher wrappers whose only assertion was an existing `@test`
source's exit status, including the declarative `900_stdlib.c` launcher.
There is no test cache, daemon, scanner, generated manifest, database, new
framework, compatibility API, concurrency runtime, dependency, or changed
timeout policy in this architecture.
## Validation policy
Use `test-unit` as the inner loop for lexer, parser, checker, and narrow codegen
changes. Run `test` for the ordinary local compiler check, followed by the
focused owner for the changed behavior. Use `test-commit` for ordinary
pre-commit behavior; use `make -j4 JOBS=4 test-commit` when parallel feedback is
desired, and `make -j1 JOBS=1 ...` to reproduce failures deterministically.
Run `test-byteid` and `test-bootstrap` only when those proof categories are
intended. `test-all` is the exhaustive CI/release composition and should not be
launched casually.