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:
@@ -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, 990–997). Pre-commit runs `make test-commit` (content-keyed tier: the 10 pure gates — 950, 989_lib_byteid, 990–997 — 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/990–997 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/990–997 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.
|
||||
|
||||
@@ -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
258
docs/test-system-v2.md
Normal 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/990–995 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/990–995 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.
|
||||
Reference in New Issue
Block a user