test: make T0 harness results complete

This commit is contained in:
2026-08-05 18:19:40 +09:00
parent 3d79735964
commit 0bb1f86c14
14 changed files with 1944 additions and 281 deletions

View File

@@ -606,7 +606,7 @@ $(BIN)/test_test_filter: test/wcc/989_test_filter.c $(BIN)/ww $(BIN)/ww_ww \
$(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \
$(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \
$(LIB)/libwwrt.a | $(BIN)
$(CC) $(CFLAGS) -o $@ $<
$(CC) $(CFLAGS) -Werror -o $@ $<
# 989_callarg_typecheck drives `ww build` + `ww test -c` then `<comp> -T`
# on BOTH driver/compiler twins (#24 general call-arg typecheck + #34
@@ -2740,10 +2740,13 @@ sizelint:
peellint:
@sh tools/peellint
test: all sizelint peellint $(TESTS) test-lang test-lang-byteid test-run
test-harness: test/run test/run_test.sh
@sh test/run_test.sh
test: all sizelint peellint test-harness $(TESTS) test-lang test-lang-byteid test-run
@WW=$(BIN)/ww BIN=$(BIN) sh test/run
test-unit: all sizelint peellint $(TESTS)
test-unit: all sizelint peellint test-harness $(TESTS)
@WW=$(BIN)/ww BIN=$(BIN) UNIT=1 sh test/run
# ---- test-run: ww-native behavior harness (fold 2) ---------------------
@@ -2840,12 +2843,13 @@ test-lang-byteid: all
# ---- test-commit: per-commit default (task #10 T1) ---------------------
# Full suite, but content-key-SKIPS the expensive wwstage byte-id /
# self-compile gates (950/989_lib_byteid/990-997) when their input set is
# self-compile gates (950/989_lib_byteid/990-995/997) when their input set is
# byte-identical to the last green run — reported as `cached`, never a
# silent skip. NOT a substitute for the push bar: `make test` stays the
# no-skip bootstrap gate. Cache lives in out/.testcache (gitignored, wiped
# by `make clean`); a green `make test` primes it. See test/run.
test-commit: all sizelint peellint $(TESTS) test-lang
# by `make clean`); a green `make test` primes it. The host-/usr/lib dynamic
# gate 996 always runs and is never cached. See test/run.
test-commit: all sizelint peellint test-harness $(TESTS) test-lang
@WW=$(BIN)/ww BIN=$(BIN) COMMIT=1 sh test/run
# ---- smoke: inner-loop cross-stage byte-id check on a SMALL input ------
@@ -3028,4 +3032,4 @@ nocc:
@echo
@echo "NOCC OK: $(STAGE0)/* reproduces itself from source. cc not invoked."
.PHONY: all cstage wwstage test test-unit test-commit test-run test-lang test-lang-byteid smoke sizelint peellint install clean bootstrap nocc bootstrap-snapshot
.PHONY: all cstage wwstage test test-unit test-commit test-harness test-run test-lang test-lang-byteid smoke sizelint peellint install clean bootstrap nocc bootstrap-snapshot

827
PROJECT_PLAN.md Normal file
View File

@@ -0,0 +1,827 @@
# WW Project Continuation Plan
Status date: 2026-08-05
Canonical baseline: `master` at `3d7973596422f1c9e2275016a5f264a2ae504ceb`
This is the durable handoff for the next Codex session. Read this file before
starting implementation. It records the repository audit, the test-suite audit,
the branch cleanup, the first T0 implementation slice, and the recommended order
of work.
## 0. T0 first-slice update
The focused topic branch is `t0-harness-integrity`, based directly on the
canonical `master` commit above. This slice did not touch compiler features,
CSP, bootstrap design, the cast-diagnostic worktree, recovery artifacts, or the
existing unrelated generated/untracked paths.
Completed in this slice:
- `test/run` now materializes the complete 337-source corpus before fan-out,
records atomic started and terminal records, captures launcher failures, and
checks discovered, started, completed, classified, cached, excluded, and
not-run counts by identity.
- Worker results distinguish pass, fail, explicit exit-77 skip, timeout, and
harness error. Every C wrapper has a 900-second outer GNU `timeout` with a
kill-after grace period. A private child-status record plus a separate GNU
`timeout --verbose` diagnostic distinguishes actual expiration from tests
that themselves return reserved codes 124-127 or 137.
- `make test-unit` prints all eight intentional phase-2 exclusions. Unexpected
skips, timeouts, missing binaries, malformed/incomplete records, launcher
failures, empty corpora, and corpus shrinkage make required targets fail.
- Successful wrapper output is visible. The whole-wrapper prerequisite skips in
`530_w6a_parsenum`, `810_dyn`, `989_dynentry_run`, and
`989_structlocal_frame` now return the explicit skip code instead of success.
- The cache key includes tracked and relevant nonignored untracked inputs,
unstaged-deletion markers, built tools, the runtime archive, cached gate
executables, and harness configuration. Ambient `WW_W6*`/library/cache
overrides are removed from required-suite workers. Inputs are fingerprinted
again after both cache hits and misses, publication is atomic, custom workers
disable caching, and the host-dependent `996_dyn_ww` gate is never cached.
- `test/run_test.sh`, wired as `make test-harness`, exercises pass, fail, skip,
TERM-resistant timeout, genuine reserved exit codes, timeout-wrapper failure,
missing binary/capture/result, empty/shrunken corpora, failed launchers,
zero-duration rejection, cache publication/hits, unstaged tracked deletion,
uncached host-dependent gates, and visible `test-unit` exclusions entirely in
temporary directories.
- The regex gate now runs the external black-box unit and the in-package module
root as two separate `-T` binaries. It requires all 33 black-box tests and all
15 named white-box tests exactly once. The obsolete zero-test
`lib/regex/wb/regex_test.ww` scaffold and its duplicate byte-ID row are gone.
- `989_test_filter.c` uses one named four-entry bound for both expectation arrays
and loops; the redundant fifth initializer is gone, and its dedicated Make
rule compiles with `-Werror`.
- `989_lib_byteid.c` now pins its enrolled corpus at 42 entries, so deleting a
byte-ID row cannot silently shrink that cross-stage gate.
Verification on 2026-08-05:
| Command | Result | Reported scope and time |
| --- | --- | --- |
| `sh test/run_test.sh` | Pass | All 16 synthetic harness scenarios |
| `make test-harness out/bin/test_regex_run out/bin/test_test_filter` | Pass | Harness self-test plus focused binaries |
| `BIN="$PWD/out/bin" timeout 300 ./out/bin/test_regex_run` | Pass | Regex black-box 33/33; white-box 15/15 |
| `BIN="$PWD/out/bin" timeout 60 ./out/bin/test_test_filter` | Pass | Five filter rows; `ww == ww_ww` output |
| `BIN=out/bin out/bin/test_lib_byteid` | Pass | 36 byte-identical, four pinned divergences, two pinned WW rejects across 42 units |
| `make test-unit` | Pass | Discovered 337; started/completed/pass 329; fail/skip/timeout/harness-error 0; eight explicit exclusions; 76 seconds |
| `make test` | Pass | C harness 337/337 with zero fail/skip/timeout/harness-error; 159 language files containing 1,224 tests; 153 language byte-ID files; 106/106 `runww` cases; 308 seconds |
The pre/post generated-artifact path scan contained the same 533 paths; neither
required run introduced a new source-tree artifact path. Final logs are in
`/tmp/ww-t0-test-unit-final.log` and `/tmp/ww-t0-test-full-final.log`, which are
not durable.
Remaining T0 and adjacent risks:
- `runww` still uses shared `/tmp/runww.err` and `/tmp/runww.bin`, has no per-case
timeout, and has no pinned 106-case minimum. Close these paths in the first
replacement-runner vertical slice rather than extending the legacy wrapper.
- The WW-native `lib/test` runner still needs T2's child completion token,
timeout/capture protocol, skip reason API, and failure-count clamping; an
explicit stale-record/256-failure regression therefore remains outstanding.
- Several legacy wrappers have partial stage-missing branches that print a skip
and continue after other assertions. Required Make prerequisites made none of
those branches fire in either verified run, and their output is now visible,
but converting them to structured whole-test/subtest outcomes remains work.
- Warning-fatal compilation is local to `989_test_filter.c`; broader harness
`-Werror` requires resolving existing unrelated missing-initializer and path
truncation warnings first.
- The regex module-root route is deliberately temporary and C-driver-only.
T1 must implement package-aware white-box/external test planning symmetrically
in `ww` and `ww_ww`, after which `regex_whitebox.ww` can become a canonical
`*_test.ww` file.
- Manifest and planning appends still rely on the fresh temporary filesystem
remaining writable, and cached gates treat ordinary host `PATH` utilities as
part of the machine baseline rather than fingerprinting each utility. These
are reasons not to expand the legacy cache/harness design.
Strategic direction recorded on 2026-08-05: the 337-wrapper/manual-Make test
architecture is a temporary migration oracle, not a design to keep extending.
After landing this branch, design the declarative replacement and migrate
`runww` as its first vertical slice; the new runner must provide unique temp
roots, per-case timeouts, a pinned corpus, and structured outcomes from the
start. Retire old wrappers and manual rules only after coverage-equivalence
checks pass. Do not integrate the cast-diagnostic WIP into this branch.
## 1. Executive decision
The project is alive and its current test commands pass. The first T0 slice has
closed the host C harness's missing-worker and vacuous-regex false greens, but the
remaining `runww`, package-driver, and WW-native runner contracts still prevent
the test system from being the final trustworthy development gate. The largest
immediate risk remains test architecture, not a single compiler feature.
Proceed in this order:
1. Preserve the current state and make the test result trustworthy.
2. Split compiler unit tests, compiler conformance tests, package unit tests,
integrations, differential tests, and bootstrap tests into explicit suites.
3. Finish the small compiler-stability tail, including the active cast-diagnostic
work.
4. Produce a usable developer release: SDK installation, documentation, CI, and
import/export and error-handling cleanup.
5. Start the CSP/runtime vertical slice only after renewed explicit approval.
6. Treat bootstrap independence as the final milestone, not the next task.
Do not start by rewriting the compiler, changing the backend, or doing a large
test-directory rename. First eliminate false-green paths and define stable suite
contracts.
## 2. Preservation and branch cleanup
### 2.1 Repository state
- There is no configured Git remote. Local history has no off-machine safety net.
- The main worktree is `/home/kimchi/src/ww` on `t0-harness-integrity`, based
directly on `master` at the canonical commit above.
- `docs/spec.md` exists but is untracked. It appears to be intended as a
normative language specification and must not be discarded.
- `ref/hare` and `ref/plan9front` are gitlinks, but there is no `.gitmodules`
file. `ref/inferno-os` and `ref/qbe` are untracked trees. Make their provenance
reproducible before calling the repository portable.
- Ignored bootstrap binaries exist under `bootstrap/amd64`.
- Generated output currently exists under `build/`, several
`test/wcc/data/*/out/` directories, `scratch/`, and other untracked paths.
Classify these paths before deleting them; they were not part of the branch
cleanup.
- `.ai/` is ignored and contains older handoffs. Use this document as the current
source of truth unless an older note is explicitly cited.
### 2.2 Recovery artifacts created before cleanup
The following files are in the repository root and are currently untracked:
| File | Purpose | SHA-256 |
| --- | --- | --- |
| `branch-backup-20260805.bundle` | Complete pre-cleanup Git history and refs | `a96f48588aa406ab74c52ba8e94fdd635bfbb4e80c2602811d33c5b28a7793aa` |
| `wip-29-castdiag-20260805.patch` | Tracked dirty changes from the active cast-diagnostic worktree | `c7a5de8606ee8d9cbba32dbc5fd420c694b0c2929b673f35155db4f7066ba2d7` |
| `wip-29-cast-fixture-20260805.tar.gz` | Untracked `cast_unknown_type` fixture | `27a16b43ee83dde28c207f22050738591c1414ab917509529fb2d9d5519b5192` |
`git bundle verify branch-backup-20260805.bundle` reports a complete history.
The bundle contains the original 107 local branch refs plus the stash, tag, and
worktree refs. Copy these three artifacts to a different disk or remote storage;
an untracked backup on the same disk is not a permanent backup.
To recover one deleted branch without disturbing current refs:
```sh
git bundle verify branch-backup-20260805.bundle
git fetch ./branch-backup-20260805.bundle \
refs/heads/OLD_NAME:refs/heads/restore/OLD_NAME
```
To reconstruct the dirty cast work on a fresh worktree:
```sh
git worktree add ../ww-cast-restore -b restore/castdiag master
git -C ../ww-cast-restore apply \
/home/kimchi/src/ww/wip-29-castdiag-20260805.patch
tar -xzf /home/kimchi/src/ww/wip-29-cast-fixture-20260805.tar.gz \
-C ../ww-cast-restore
```
### 2.3 Branch cleanup result
The repository started with 107 local branches. Cleanup deleted 105:
- 90 branch tips were direct ancestors of `master`.
- 14 branches looked unmerged by graph ancestry but were audited and found to be
patch-equivalent to, or superseded by, later work on `master`.
- `wip-buga` was already merged; its clean worktree was removed and the branch
was deleted.
Before T0 implementation, only these branches remained. The focused T0 branch
was then added:
| Branch | State | Decision |
| --- | --- | --- |
| `master` | Clean tracked baseline at `3d79735` | Keep |
| `wip-29-castdiag` | Checked out at `/home/kimchi/ww-29`; contains uncommitted source and fixture changes | Keep until reviewed and integrated |
| `t0-harness-integrity` | Checked out at `/home/kimchi/src/ww`; first T0 gate-integrity slice | Review and merge separately |
The active WIP adds a clean diagnostic for a cast to an unknown type in
`selfhost/cmd/wcc/check.ww`, plus a fixture under
`test/wcc/data/cast_unknown_type/`. Do not delete this worktree merely because
its branch tip is already reachable from `master`; the valuable part is the
dirty working tree. The patch and fixture archive above are secondary recovery
copies.
A detached worktree remains at `/home/kimchi/src/ww-f0` on `e8977a4`. It is not
a branch and was therefore not removed. Audit it separately before deciding
whether to remove it.
The 14 non-ancestor branches were matched to later `master` work as follows:
- `e3-c1-flip`, `e3-oracle`: separate-compilation flip later landed around
`33edc38`.
- `f1-stack-v1`, `f1-stack-v2`, `f1-stack-v3`: superseded by `9bd0d8b`,
`b9dd297`, `c138605`, `ac63951`, and later type-chase work.
- `impl-26`: superseded by `9fcb3be`.
- `impl-ascii-bytes-cut`, `impl-ascii2-complete`: superseded by `07fed80` and
`2b893b9`.
- `impl-strconv`: finalized through `6a5cdbd`, `a117852`, and `db5c5b6`.
- `impl-strings-cutrcut`: patch-equivalent to `38a906c`.
- `t28`, `t28-parked`: superseded by `c40b2df` and `330792b`.
- `wip/io5-handle-seeker`: superseded by `06c00e0` and `532b0a8`.
- `worktree-impl-e8`: superseded by `b842f93`, `10cb835`, and `7d39f6d`.
The bundle remains the authoritative record if any of those classifications
must be revisited.
## 3. Verified build and test baseline
The following commands were run on `master` at the baseline commit before this
document and branch-only cleanup:
| Command | Result | Observed scope and time |
| --- | --- | --- |
| `make test-unit` | Pass, 329/329 C harness entries | About 95 seconds |
| `make test` | Pass | C harness 337 entries; `test/lang` 159 files; language byte-ID pass over 153 files; `runww` 106 cases; about 313 seconds total |
The full audit log was written to `/tmp/ww-tests-health-full.log`; `/tmp` is not
durable, so rerun the commands when a fresh log is needed.
These historical green results prove that the canonical baseline is buildable.
They did not prove that every named test executed useful assertions; the first
T0 slice above closes the missing-worker and empty regex-table paths without
changing this preserved baseline record.
No source code changed during the branch cleanup and English-document rewrite,
so the full suite was not rerun afterward.
## 4. Current test architecture
The project already contains substantial test material. The problem is that
several different kinds of tests are mixed behind numeric C wrappers and broad
Make targets.
### 4.1 C harness under `test/wcc`
- 337 C test sources are registered through roughly 337 manual Make rules.
- The sources contain about 89,519 lines.
- Static inspection found subprocess orchestration in approximately 333 tests
(`fork`, `exec`, `system`, or `popen`).
- Only a handful, approximately the `000`, `100`, `200`, `300`, and `738`
families, resemble direct compiler-component unit tests.
- Most files are integration wrappers around compiler commands, generated
programs, output matching, byte-ID checks, or bootstrap behavior.
Therefore `make test-unit` is currently misnamed. It omits a few heavy phase-2
tests, but still runs most integrations and includes `989_lib_byteid`, which
alone took roughly 42 seconds in the audit. It does not run the 1,224-test
`test/lang` corpus or the `runww` fixtures, but C wrappers do indirectly launch
the library `@test` files. The first T0 slice makes all 458 library tests
effective, including the 15 regex white-box tests.
### 4.2 Language tests under `test/lang`
- 159 source files contain exactly 1,224 source-level `@test` functions.
- Six files are `_runonly_test.ww` variants.
- The current Make targets compile/run this corpus and also perform byte-ID
comparison for most of it.
This is valuable language and compiler coverage, but file layout and target
naming do not clearly separate package unit behavior from compiler conformance
behavior.
### 4.3 Library tests
- 35 test-named library source files contain 443 `@test` functions. Eleven use
`_test.ww`; 24 use other names ending in `test.ww`.
- `lib/regex/regex_whitebox.ww`, a production-named source file, contains 15
additional `@test` functions.
- There are therefore 36 files that actually contain tests and 458 source-level
library tests in total.
Together, `test/lang` and `lib` contain 1,682 real source `@test` functions,
excluding generated combined files.
### 4.4 Directive fixtures through `runww`
There are 106 directive-driven cases:
- 100 `//ww:error` cases.
- 5 `//ww:run-exit` cases.
- 1 `//ww:run` case.
- 0 `//ww:compile` cases.
The rejection coverage is useful, but this is not yet a balanced compiler
conformance suite. Rejections do not identify whether failure was expected in
lexing, parsing, checking, code generation, assembly, or linking. Accept/run
cases use only the C stage, while rejects exercise two stages.
### 4.5 Current `@test` implementation
The compiler's `-T` path synthesizes a `__wwtests` value table and test main.
`@test` functions are expected to have the shape `fn() void`. Normal builds
type-check tests and then strip them. The runtime in `lib/test` forks once per
test, so an abort, signal, or nonzero exit does not stop subsequent tests. The
runner also supports glob filtering and treats zero matches as success.
The per-test fork model and compiler-generated value table are good foundations
and should be preserved. The package driver and child/parent result protocol need
to be completed.
### 4.6 Target coupling and differential caveats
- Despite its name, `make test-run` is not an isolated `runww` target. Its
order-only prerequisites build the C harness and run `test-lang` plus
`test-lang-byteid` first. The redesigned target graph must remove this
surprising coupling.
- `test-lang-byteid` reads only `test/lang`. The six positive `runww` fixtures
are not checked for stage-by-stage assembly identity.
- `989_lib_byteid` has 42 rows: 36 require identity, four are declared
divergences, and two are expected WW-stage rejects. A green result does not
mean complete standard-library stage parity.
- `runww` itself is compiled by the C-stage compiler and then invokes that same
stage. It is useful coverage but not an independent sole oracle. Retain an
independent host-side runner or cross-validate the harness during migration.
## 5. Correctness risks in the current test result
Treat the following as gate-integrity bugs, not cosmetic cleanup.
### 5.1 Vacuous regex test: closed in the first T0 slice
The former `lib/regex/wb/regex_test.ww` root contained no tests while importing
regex as a dependency, so `-T` stripped all 15 intended white-box tests. The T0
gate now resolves `regex` itself as the root module, keeps it separate from the
external `regex_test` binary, and asserts every expected name plus the exact
15/15 summary. The empty scaffold was removed. T1 still needs to replace this
temporary module-root route with ordinary package-aware test assembly.
### 5.2 Harness false-green paths
Closed in the first T0 slice:
- `test/run` no longer masks either `xargs` launcher. It collects from a complete
manifest and fails on missing starts, missing/malformed terminal records,
launcher errors, or any count/identity mismatch.
- Pass, fail, explicit skip, timeout, and harness error are distinct outcomes;
required targets fail on every unexpected non-pass outcome.
- Successful wrapper output and every expected `test-unit` exclusion are visible.
- The cache hashes nonignored untracked inputs plus effective built inputs,
represents tracked deletions, revalidates hits and misses at run end, and
publishes atomically only after complete green accounting. The host-dependent
dynamic gate is deliberately uncached.
- The dead nested `test/lang` scan is gone.
- `989_test_filter.c` has one named expectation count, no excess initializer,
and a rule-local `-Werror` gate.
Remaining:
- Existing generated artifacts outside the new harness result/temp directories
have not been broadly relocated or classified.
- Partial prerequisite skips inside legacy wrappers are not yet structured
subtest outcomes. None fired in the recorded runs, and whole-wrapper skips now
use exit 77.
- Broad warning-fatal harness compilation remains blocked by pre-existing
warnings outside `989_test_filter.c`.
### 5.3 `runww` limitations
- It uses shared fixed paths such as `/tmp/runww.err` and a fixed binary name,
which prevents safe parallel execution and risks stale-file interference.
- It supplies a nil environment to subprocesses.
- Negative cases check a diagnostic substring but do not identify the failure
phase.
- There is no per-case timeout.
- Missing arguments fail, but there is no expected/minimum corpus count. A suite
accidentally reduced from 106 fixtures to one can still pass without a
corpus-shrinkage alarm.
### 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
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.
### 5.5 Runner-protocol limitations
- 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
status can wrap to zero.
- There is no timeout, structured capture, duration reporting, skip reason,
expected-abort protocol, cleanup hook, or per-test temporary directory.
- Invalid `@test` declarations are checked too narrowly and mainly under `-T`.
Exported tests, prototypes, arguments, variadics, non-void returns, duplicate
annotations, and other invalid forms need deterministic diagnostics in normal
and test builds.
## 6. Target model: Go/Hare-style separation
The desired result is not a copy of either project, but it should adopt their
separation of concerns.
### 6.1 Lessons from Go
The local Go 1.25.4 source tree demonstrates two distinct systems:
- Compiler/runtime conformance fixtures live outside ordinary package unit
tests and use explicit execution/error directives.
- Ordinary `*_test.go` files are assembled per package into a generated test
main, with support for same-package tests, external-package tests, filtering,
caching, timeouts, parallelism, and structured output.
- Compiler internals also have direct component unit tests rather than wrapping
every assertion in a compiler subprocess.
WW needs the same conceptual split: compiler correctness fixtures are not the
same thing as user-facing package unit tests.
### 6.2 Lessons from Hare and harec
The vendored `ref/hare` test command demonstrates useful package-test behavior:
- Recursive module discovery when no explicit module is given.
- Deterministic sorting, multiple globs, and a test-only build tag.
- First-class skip, require, expected-abort, current-test, capture, timing, and
backtrace behavior.
The upstream Hare compiler suite uses feature-grouped positive and negative
fixtures and distinguishes outcomes such as success, user error, lex failure,
parse failure, check failure, and abnormal failure. The vendored `ref/harec`
copy is incomplete and lacks the full compiler test corpus; pin an upstream
version or import a documented reference snapshot before using it as a durable
oracle.
Keep WW's fork isolation and generated source value table. Do not copy Hare's
same-process recovery mechanism or unrelated linker-specific structure.
## 7. Proposed suite contracts
Use semantic directories and explicit metadata. Numeric prefixes may remain as
stable case IDs during migration, but they must no longer determine what a test
means or when it runs.
```text
test/
unit/ Direct host-side compiler/library API tests; no subprocesses
compiler/ Source conformance fixtures grouped by language feature
package/ Fixtures for `ww test` discovery and package assembly
integration/ CLI, import graph, archive, ELF, FFI, and toolchain behavior
differential/ C-stage versus WW-stage and byte-ID comparisons
bootstrap/ Fixed-point, self-host, and no-C-compiler scenarios
harness/ Shared runners, manifests, and assertion helpers
```
Actual package unit tests should stay beside their code as canonical
`*_test.ww` files. `test/package` is for testing the package-test driver itself,
including multi-file layouts, helper sharing, external test packages, empty
packages, recursion, and failures.
Define these Make targets:
| Target | Contract |
| --- | --- |
| `test-unit` | Direct in-process compiler and library API tests only; zero child compiler processes |
| `test-compiler` | Positive, negative, run, compile, assembly, and multi-file language conformance fixtures |
| `test-package` | Real `ww test` package tests and package-driver fixtures |
| `test-integration` | CLI, separate compilation, archives, ELF, FFI, and graph behavior |
| `test-differential` | C-stage/WW-stage output and byte-identity checks |
| `test-bootstrap` | Fixed point, self-host, and `nocc`; explicitly slow |
| `test-lint` | Harness integrity, fixture metadata, duplicate IDs, empty corpora, and warning checks |
| `test-fast` | Unit tests plus a documented compiler/package smoke set |
| `test` | Complete required pre-merge set, with no hidden skips |
| `test-all` | `test` plus bootstrap and any explicitly labeled slow suites |
Every target must print discovered, passed, failed, skipped, timed out, and not
run counts. A target may intentionally exclude another named suite, but it may
not silently skip entries within its declared corpus.
Compiler fixtures need explicit metadata for at least:
- Mode: check, compile, run, run-with-exit, reject, assembly match, or multi-file.
- Expected stage: lex, parse, check, codegen, assemble, link, run, signal, or
success.
- Expected diagnostic, stdout, stderr, exit status, or signal.
- Timeout and optional platform/tool prerequisites.
- Stable feature group and case ID.
Use one declarative manifest or one clearly documented directive grammar. Do not
continue adding one manual Make rule and one custom C subprocess wrapper for
every source fixture.
## 8. Required `ww test` semantics
The language specification requires both same-package white-box tests and an
external `<package>_test` black-box package. A package-under-test invocation must
produce one top-level result covering both, but the implementation cannot simply
link two current `-T` units: every `-T` unit owns global `main` and `__wwtests`
symbols, dependencies are compiled without `-T`, and `.wwi` does not carry test
descriptors.
Use this implementation sequence:
1. Treat `*_test.ww` only as the candidate-file discovery rule. Classify a file
as white-box or black-box from its declared package (`package foo` versus
`package foo_test`), not from its suffix alone.
2. Compile production files plus same-package test files into one white-box root
unit.
3. Compile the external package against only the public API. Initially, build a
second test binary and aggregate both binary results in the driver. A later
design may instead emit linkable test descriptors and generate one aggregator
main, but it must first solve the current symbol and interface limitations.
4. Exclude test bodies from imported dependencies unless that dependency is the
package under test.
5. Give each test a canonical package-qualified ID such as
`<import-path>.<function>`. Bare leaf names are ambiguous across the white-box
and black-box units and are not sufficient for output or filtering. The
generated descriptor must also refer to the callable through its owning
package rather than assuming a bare function reference.
6. Produce one top-level package result even if it is internally implemented by
two test binaries.
7. Support multiple glob filters and a recursive package pattern equivalent in
purpose to `./...`.
8. Use one canonical file naming rule: `*_test.ww`.
Normal production enumeration must exclude the entire `*_test.ww` file,
including test-only helpers and imports. An `@test` declaration placed in a
production file must still be fully checked in a normal build before its function
is stripped. These are separate driver and checker responsibilities.
Make ordering an explicit compatibility decision. The recommended first
contract is to sort package and source discovery paths, then preserve declaration
order within each source. Do not silently change the existing documented
source/collection behavior to a global name sort.
Define all empty cases separately:
- An explicitly requested package with no tests reports `[no tests]` and
succeeds.
- An explicitly requested test file containing no `@test` declaration is an
error because it is likely a fixture mistake.
- A filter matching zero tests reports `[no matches]` and succeeds, preserving
the current useful filtering behavior.
- Recursive discovery reports packages with no tests visibly, but a pattern that
resolves to zero packages is an error.
- A required suite with an unexpectedly empty or shrunken corpus is a harness
error regardless of the user-facing command rules.
Validate every `@test` declaration during normal checking, even when test bodies
are later stripped. At minimum reject parameters, variadics, non-void returns,
prototypes without bodies, invalid export combinations, `@test(...)`, unknown
attributes, placement on non-functions, and duplicate attributes with stable
diagnostic text. Put both the C-stage and self-hosted parser/checker through the
same acceptance fixtures.
Define compile-only behavior rather than deleting its output: `ww test -c`
should preserve a deterministic `<package>.test` binary by default, and `-o`
should select its path. Specify whether `-o` without `-c` is invalid; the
recommended contract is to require `-c` so output preservation is unambiguous.
Keep one process per test, but add a control pipe from child to parent. A normal
return must write a completion record. `os.exit(0)` without that record is a
failure. The same protocol can carry skip, expected-abort, failure reason, and
captured-output metadata. Clamp the process exit code to `0` for no failures and
`1` for any failure instead of returning the raw failure count.
## 9. Test-system implementation plan
### T0: Make the existing green result trustworthy
Do this before moving directories. Status after the first focused slice:
- [x] Make `test/run` compare discovered, started, and completed test counts.
- [x] Remove `|| true` masking around worker execution; collect worker failure
as a harness failure.
- [x] Represent pass, fail, skip, timeout, and harness error separately.
- [x] Print skip reasons/output and fail required targets on unexpected skips.
- [~] Harness records and new regression artifacts are private temporary files,
and relevant source/configuration inputs are in the cache key. Broad legacy
source-tree artifact relocation remains.
- [x] Give every host C-harness subprocess an outer timeout and kill its process
group on expiry. WW-native enforcement remains T2 work.
- [ ] Replace shared `runww` paths with a unique per-case temporary directory.
- [x] Fail the C-harness target on zero or shrunken corpora and incomplete worker
results. Add the equivalent pinned minimum to `runww` next.
- [x] Fix the `mustnot[4]` initializer and related loops through one named count.
- [x] Prove all 15 regex white-box tests execute in a separate root-module
binary without combining conflicting `-T` units.
- [~] Sixteen harness scenarios cover fail, skip, TERM-resistant timeout,
reserved child exits, timeout-wrapper failure, lost/incomplete workers,
failed launchers, missing binary/result/captures, empty/shrunken corpora,
invalid bounds, cache safety, and expected exclusion. A fresh per-run result
directory prevents stale reuse; the WW-native 256-failure case remains.
The host C-harness portion of the completion criterion is met: a killed or
skipped worker cannot leave a required target green, and the regex test reports
15 discovered and 15 passed white-box tests. Finish the unchecked `runww` items
before declaring all of T0 complete.
### T1: Implement package-aware `ww test`
- 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.
- Assemble production files and same-package tests into one white-box root.
- Build the required external `<package>_test` unit as a second test binary first,
and aggregate its result with the white-box binary at the driver level. Do not
attempt to link two current `-T` mains together.
- Keep imported dependency tests stripped.
- Add package-qualified test IDs, deterministic recursive discovery, and
multiple filter arguments.
- Implement the documented `-c` output contract and all four no-test/no-match
cases above.
- Add C-stage and self-hosted compiler diagnostics for all invalid `@test` and
attribute shapes.
- Convert the regex white-box case into an ordinary, non-vacuous package test.
Completion criterion: two test files in one package can share a private helper,
dependency tests do not leak in, filters work deterministically, and empty or
invalid packages produce the documented result. The C-stage `ww` and self-hosted
`ww_ww` must discover the same fixtures and produce matching output classes and
exit statuses.
### T2: Finish runner semantics
- Add the required OS/runtime wrappers for pipes, process-group creation, and
process-group signals before depending on them in the WW-native runner.
- Add the child completion/control protocol.
- Add skip and require helpers, expected abort, current test identity, cleanup,
per-test temporary directories, timeouts, captured output, and duration.
- Drain stdout, stderr, and control pipes while the child is running; waiting
before draining can deadlock when a pipe buffer fills. Add polling/nonblocking
support as needed.
- Ensure signals and premature `os.exit(0)` are classified correctly.
- Offer a stable human-readable output and a machine-readable event stream.
Completion criterion: the runner's own conformance suite covers every outcome
and never wraps failures into exit status zero.
### T3: Build the compiler conformance engine
- Create the explicit fixture schema and stage-aware outcome model.
- Migrate `runww` first because it is already directive-driven.
- Keep an independent host-side fixture runner as the gate, or cross-check the
WW-native harness so the compiler under test is not its own sole oracle.
- Add balanced positive/check/compile/run cases beside rejection cases.
- Add multi-file, diagnostic location, exit, signal, stdout/stderr, and assembly
matching support.
- Pin the external Hare/harec reference version used for design comparison.
This is a replacement program, not a permanent adapter around `test/run`.
Freeze the current C wrapper layer as a comparison oracle, migrate by declared
suite and outcome, and remove each old path when the new engine proves the same
coverage. Do not add new numeric wrappers or hand-written Make registrations for
cases that fit the new fixture schema.
Completion criterion: adding a language fixture normally requires source plus
metadata, not a new C wrapper or Make rule.
### T4: Migrate and shrink `test/wcc`
Classify every existing C wrapper as unit, compiler fixture, package, integration,
differential, bootstrap, or obsolete duplicate. Move direct API tests first.
Mechanically migrate simple subprocess wrappers into fixtures. Keep C only where
the assertion genuinely needs host APIs, ABI inspection, or custom orchestration.
Completion criterion: `test-unit` launches no compiler subprocess, suite
membership is discoverable without numeric-prefix knowledge, and the giant
manual Make registry is replaced by generated discovery plus a small manifest.
### T5: Add CI cadence
- Pre-commit/local: `test-fast`.
- Required pre-merge: `test` plus harness lint.
- Scheduled or release: `test-all`, bootstrap, sanitizers, and expensive
differential/byte-ID jobs.
- Archive structured results and fail on unexpected skips or corpus shrinkage.
Completion criterion: a fresh checkout can reproduce the declared test matrix
without ignored local binaries or undocumented reference trees.
## 10. Broader project roadmap
### P0: Preserve and normalize the repository
- Copy the recovery artifacts off-machine.
- Add a remote or another durable backup.
- Decide which of `docs/spec.md`, reference trees, probes, and scratch artifacts
are source, vendored dependencies, or disposable output.
- Track reference provenance through submodules, pinned archives, or documented
fetch scripts.
- Commit this plan and the chosen specification files. Do not commit generated
output or the bundle merely to make `git status` quiet.
### P1: Establish the test foundation
Execute T0 through at least T3 before broad compiler feature work. T4 can proceed
incrementally after the contracts stabilize.
### P2: Finish the compiler-stability tail
- Review and integrate `wip-29-castdiag`, including a regression fixture.
- Finish or explicitly close the large aggregate return (`sret`, especially
values over 24 bytes) edge cases.
- Re-audit stale issue-number tests and convert unresolved cases into explicit
fixtures.
- Finish type-information (`tinfo`) consistency work and remove duplicated or
stage-specific behavior where possible.
- Preserve separate-compilation and byte-ID guarantees while changing these
paths.
Each item needs a focused branch, a regression test in the correct new suite,
and a clean full required test run before merge.
### P3: Produce a developer-usable v0.1 release
- Finish import/export membership rules and diagnostics.
- Normalize `errno` and error propagation across the standard library, especially
the existing error/I/O tail.
- Turn `make install` into an actual SDK installation. It currently copies only
the `ww` executable and `libwcc.a`; define and install the compiler, standard
library sources/artifacts, headers or metadata, and version information.
- Add a README with bootstrap prerequisites, build, test, install, and first
program instructions.
- Track the language specification and label implemented, reserved, and planned
behavior.
- Add CI from a fresh checkout and publish one reproducible release artifact.
- Document the current allocation lifetime honestly: both the compiler's
`free(x)` lowering and `os.free(p, n)` ultimately reach the current no-op
`rt_free`, so neither reclaims memory today. Decide and test a real reclamation
policy separately before promising `free` semantics.
Release criterion: a developer can clone, build, run the required tests, install
to a staging prefix, and compile a small program using the installed SDK without
knowledge of this worktree.
### P4: CSP/runtime vertical slice, only after explicit approval
The existing design direction is a cooperative M:1 runtime with `swapctx`, a
run queue, and an unbuffered channel implemented library-first, followed by
syntax. `epoll`, `alt`/`select`, broader scheduling, and optimization come later.
However, the recorded project status says CSP work was explicitly put on hold on
2026-06-18. Do not infer that a general request to resume the project cancels
that hold. Ask for renewed explicit approval before implementing CSP/runtime
features. The specification may reserve the design in the meantime.
When approved, implement one narrow end-to-end slice: spawn two cooperative
tasks, rendezvous over one unbuffered channel, shut down deterministically, and
cover it in package, integration, and differential tests.
### P5: Bootstrap independence last
`make bootstrap` already checks a fixed point, `make nocc` exists, and ignored
stage-0 binaries are present. The process is not yet a clean independent bootstrap
because the C driver and local artifacts still orchestrate important steps.
Freeze bootstrap only after the language, package graph, tests, SDK layout, and
runtime surface are stable. Then pin stage 0, document trust inputs, build stage
1 and stage 2 in a clean environment, compare the intended artifacts, and prove
the process without a C compiler where promised.
## 11. Next-session checklist
The next Codex session should do the following, in order:
1. Read `CLAUDE.md`, this file, and `docs/spec.md`; do not rely on `.ai/` notes
without checking their date and status.
2. Inspect `t0-harness-integrity` and its final commit/diff against `master` at
`3d79735`; do not repeat the repository-wide audit unless that state changed.
3. Verify the three recovery artifact hashes and arrange an off-machine copy.
4. Inspect `/home/kimchi/ww-29` and compare its working tree with
`wip-29-castdiag-20260805.patch` and the fixture archive. Do not delete it.
5. Review and merge the first T0 branch separately, preserving all unrelated
untracked/generated paths. Re-run required targets if the diff changes.
6. Specify the replacement test manifest, structured result protocol, suite
boundaries, and coverage-equivalence report; keep fork-per-test isolation.
7. Migrate `runww` as the first vertical slice, implementing unique temp roots,
per-case timeout, and the pinned 106-case corpus in the new engine rather
than further entrenching the old wrapper architecture.
8. Begin package-aware `ww test` assembly after that first replacement slice;
keep white-box and external-package binaries separate while generated
`main`/`__wwtests` symbols still conflict.
9. Integrate the cast-diagnostic WIP as its own reviewed branch once the new test
placement is available, then remove `wip-29-castdiag` and its worktree.
10. Do not start CSP or bootstrap redesign unless the prerequisites and approval
above have been satisfied.
## 12. Working rules for future branches
- One branch should represent one reviewable outcome, not an indefinite work
queue.
- Record the intended suite and completion criterion before implementing.
- Rebase or merge promptly, then delete the branch after verifying reachability
and a durable backup.
- Never classify a branch only with `git branch --merged`; inspect dirty
worktrees, patch equivalence, fixture files, and later superseding commits.
- Keep generated output out of topic branches.
- A green target is acceptable only when it reports a complete corpus and
explicit skips.
The immediate success condition is not more test count. It is the ability to
state exactly which compiler unit tests, compiler fixtures, package tests, and
integrations ran—and to know that none of them passed vacuously.

View File

@@ -7,8 +7,8 @@
// unexported internals (thread/newmatch, run_thread / add_thread /
// delete_thread / search, find_last_groupstart / shift /
// parse_repetition, is_consuming_inst) — lives in
// lib/regex/regex_whitebox.ww (`package regex`, unified with regex.ww),
// run via the lib/regex/wb/ driver.
// lib/regex/regex_whitebox.ww (`package regex`, unified with regex.ww) and
// runs as a separate root-package binary.
//
// Fold 2a ports compile()'s lit/any/match arms only; exec lives in
// later folds, so the compile_* cases pin the emitted inst PROGRAM

View File

@@ -10,14 +10,12 @@
// the non-T @test drop, #6). Load-bearing ww-compiler coverage (drove
// #34/#38/#44/#45/#48), not black-box-reachable.
//
// `_whitebox.ww` (NOT *_test.ww) is the ww convention for an in-package
// white-box test file: the enumerator skips *test.ww, so the non-suffixed
// name is what keeps this a bundled regex module source. It is exercised
// by the sibling external driver lib/regex/wb/regex_test.ww, whose
// different-dir `import regex` dir-resolves and bundles this file (a
// same-dir import file-resolves regex.ww only — probe E; task #33 would
// delete the driver). The public-API @tests stay black-box in
// lib/regex/regex_test.ww (Go's foo / foo_test split; CLAUDE.md rule-9).
// `_whitebox.ww` (NOT *_test.ww) keeps this file in the production
// enumerator's root-package assembly until package-aware testing can classify
// same-package `*_test.ww` files (T1). The T0 gate resolves `regex` as the root
// directory so -T sees these tests directly; importing regex as a dependency
// would strip them. Public-API tests remain a separate `package regex_test`
// binary in lib/regex/regex_test.ww (CLAUDE.md rule 9).
package regex;
import io;

View File

@@ -1,20 +0,0 @@
// wb/regex_test — DOCUMENTED-TEMPORARY scaffolding (rule-7), NOT part of
// the blessed white-box convention: the permanent shape is the colocated
// lib/regex/regex_whitebox.ww (`package regex`) file alone. This driver
// only exists to RUN it, and task #33 ("resolver: dir-enumerate same-dir
// same-package siblings on import") DELETES this file when it lands — the
// true E-endpoint.
//
// Why it is load-bearing until then, not incidental: a `import regex` from
// a file sitting IN lib/regex/ (e.g. regex_test.ww) FILE-resolves to the
// sibling regex.ww and pulls ONLY that file, so the colocated
// regex_whitebox.ww is never bundled and its @tests are never collected
// (probe E, negative). Placed in a DIFFERENT dir with no regex.ww to
// shadow, this driver's `import regex` instead DIRECTORY-resolves the
// lib/regex package and bundles every non-*test.ww member — regex.ww +
// regex_whitebox.ww — unifying them so -T collects the white-box @tests.
// The dir is `wb/`, NOT `test/`: a lib/<mod>/test/ subdir would shadow the
// lib/test runner module in the -T auto-bundle search path (task #32).
package regex_test;
import regex;

756
test/run
View File

@@ -1,126 +1,331 @@
#!/bin/sh
# test/run — driver for `make test`. Plan 9 rc-flavoured but plain sh.
#
# Walks test/wcc/<NNN>_<name>.c → out/bin/test_<name> binary, fans out
# across $JOBS (default $(nproc)) workers via xargs -P. Each worker
# writes its status to a per-job file; the collector enumerates them in
# lexicographic order so output stays deterministic across runs even
# though execution interleaves. Exit non-zero if any worker failed.
# A manifest-backed collector prevents a lost parallel worker from shrinking a
# required corpus into a false green.
BIN=${BIN:-out/bin}
WW=${WW:-$BIN/ww}
TEST_DIR=${TEST_DIR:-test/wcc}
TEST_EXPECTED_MIN=${TEST_EXPECTED_MIN:-337}
TEST_TIMEOUT=${TEST_TIMEOUT:-900s}
TEST_KILL_AFTER=${TEST_KILL_AFTER:-10s}
TEST_WORKER=${TEST_WORKER:-$0}
# Worker mode: invoked once per test by xargs. Argv: --one <prefix> <test.c>.
# Writes <prefix>.status; touches <prefix>.fail on a non-zero binary exit.
if [ "$1" = "--one" ]; then
# Required-suite results describe the toolchain under BIN, not ambient driver
# overrides from a developer shell. Individual tests still set overrides
# explicitly when that behavior is what they exercise.
unset WW_W6C WW_W6A WW_W6L WW_LIB WW_SRCLIB WW_PKGCACHE
atomic_record() {
record_path=$1
record_text=$2
record_tmp=$record_path.tmp.$$
if ! printf '%s\n' "$record_text" > "$record_tmp"; then
rm -f "$record_tmp"
return 1
fi
if ! mv "$record_tmp" "$record_path"; then
rm -f "$record_tmp"
return 1
fi
return 0
}
# Worker mode owns one result prefix. A result becomes visible only after every
# capture and timing file is complete, so the collector cannot accept a torn
# terminal record as a finished test.
if [ "${1:-}" = "--one" ]; then
[ "$#" -eq 3 ] || exit 64
prefix=$2
t=$3
name=${t##*/}
name=${name%.c}
short=${name#[0-9][0-9][0-9]_}
bin=$BIN/test_$short
if [ ! -x "$bin" ]; then
# A missing binary means an unwired test/wcc file: `make test`
# builds every $(TESTS) target before this runs, so the only way
# to get here is a .c with no Makefile rule. That used to SKIP
# and still count toward "all N tests passed" — 953_arrlit_slice
# sat dark for weeks under a green gate. Fail loud instead.
{
printf 'FAIL %s (no binary %s)\n' "$name" "$bin"
printf 'unwired test: add $(BIN)/test_%s to TESTS + a build rule in Makefile\n' "$short"
} > "$prefix.status"
: > "$prefix.fail"
exit 0
if ! atomic_record "$prefix.started" "WWTEST_STARTED 1"; then
exit 70
fi
# Per-test wall-clock (task #10 baseline). Stamp around the binary run
# only; build cost lives in `make all`, not here. Duration lands in
# <prefix>.dur as "<sec>\t<name>" for the collector's timing section —
# kept out of <prefix>.status so the ok/FAIL log format is untouched.
if ! : > "$prefix.out" || ! : > "$prefix.err"; then
exit 70
fi
t0=$(date +%s.%N)
if "$bin" > "$prefix.out" 2> "$prefix.err"; then
t1=$(date +%s.%N)
printf 'ok %s\n' "$name" > "$prefix.status"
outcome=pass
rc=0
if [ ! -x "$bin" ]; then
printf 'unwired test: no executable %s\n' "$bin" > "$prefix.err"
outcome=harness_error
rc=127
else
rc=$?
t1=$(date +%s.%N)
{
printf 'FAIL %s (rc=%d)\n' "$name" "$rc"
echo '--- stdout ---'
cat "$prefix.out"
echo '--- stderr ---'
cat "$prefix.err"
} > "$prefix.status"
: > "$prefix.fail"
# The child records its own status before the timeout wrapper exits.
# That distinguishes a real child exit 124/137 from expiration, and
# child exits 125-127 from timeout's reserved infrastructure codes.
command_status=$prefix.command-status
timeout_diag=$prefix.timeout-diag
if LC_ALL=C timeout --verbose --kill-after="$TEST_KILL_AFTER" \
"$TEST_TIMEOUT" sh -c '
"$1" > "$2" 2> "$3"
command_rc=$?
status_tmp=$4.tmp.$$
printf "%s\n" "$command_rc" > "$status_tmp" || exit 70
mv "$status_tmp" "$4" || exit 70
' sh "$bin" "$prefix.out" "$prefix.err" "$command_status" \
2> "$timeout_diag"; then
timeout_rc=0
else
timeout_rc=$?
fi
deadline_expired=0
if grep -Eq '^timeout: sending signal (TERM|KILL) to command ' \
"$timeout_diag"; then
deadline_expired=1
fi
if [ -s "$timeout_diag" ]; then
cat "$timeout_diag" >> "$prefix.err"
fi
if [ "$deadline_expired" -eq 1 ]; then
outcome=timeout
rc=$timeout_rc
elif [ -f "$command_status" ]; then
rc="" extra=""
IFS=' ' read -r rc extra < "$command_status"
case $rc in
''|*[!0-9]*) rc_valid=0 ;;
*) rc_valid=1 ;;
esac
if [ "$rc_valid" -ne 1 ] || [ -n "$extra" ] \
|| [ "$timeout_rc" -ne 0 ]; then
printf 'timeout wrapper returned %s with child status %s\n' \
"$timeout_rc" "${rc:-missing}" >> "$prefix.err"
outcome=harness_error
rc=125
elif [ "$rc" -eq 0 ]; then
outcome=pass
elif [ "$rc" -eq 77 ]; then
outcome=skip
else
outcome=fail
fi
else
printf 'timeout wrapper produced no child status (rc=%s)\n' \
"$timeout_rc" >> "$prefix.err"
outcome=harness_error
case $timeout_rc in
125|126|127) rc=$timeout_rc ;;
*) rc=125 ;;
esac
fi
fi
t1=$(date +%s.%N)
dur_tmp=$prefix.dur.tmp.$$
if ! awk -v a="$t0" -v b="$t1" -v n="$name" \
'BEGIN{printf "%.3f\t%s\n", b-a, n}' > "$dur_tmp"; then
rm -f "$dur_tmp"
exit 70
fi
if ! mv "$dur_tmp" "$prefix.dur"; then
rm -f "$dur_tmp"
exit 70
fi
if ! atomic_record "$prefix.result" "WWTEST_RESULT 1 $outcome $rc"; then
exit 70
fi
awk -v a="$t0" -v b="$t1" -v n="$name" 'BEGIN{printf "%.3f\t%s\n", b-a, n}' \
> "$prefix.dur"
exit 0
fi
set -e
RESULTS=$(mktemp -d "${TMPDIR:-/tmp}/wwtest.XXXXXX")
RESULTS=$(mktemp -d "${TMPDIR:-/tmp}/wwtest.XXXXXX") || exit 2
trap 'rm -rf "$RESULTS"' EXIT
trap 'exit 1' HUP INT TERM
JOBS=${JOBS:-$(nproc 2>/dev/null || echo 4)}
ALL_MANIFEST=$RESULTS/all.manifest
PHASE1_MANIFEST=$RESULTS/phase1.manifest
PHASE2_MANIFEST=$RESULTS/phase2.manifest
RUN1_MANIFEST=$RESULTS/run1.manifest
RUN2_MANIFEST=$RESULTS/run2.manifest
SELECTED_NAMES=$RESULTS/selected.names
CACHED_NAMES=$RESULTS/cached.names
ALL_NAMES=$RESULTS/all.names
for manifest in "$ALL_MANIFEST" "$PHASE1_MANIFEST" "$PHASE2_MANIFEST" \
"$RUN1_MANIFEST" "$RUN2_MANIFEST" "$SELECTED_NAMES" \
"$CACHED_NAMES" "$ALL_NAMES"; do
if ! : > "$manifest"; then
printf 'HARNESS suite (cannot initialize manifest %s)\n' "$manifest"
exit 2
fi
done
# ---- content-keyed gate cache (task #10 T1; .ai/rob-testperf-go.md L1) -------
# `make test-commit` (COMMIT=1) reports the expensive wwstage byte-id /
# self-compile gates as `cached` when their full input set is byte-identical to
# the last green run. Those gate outcomes are a PURE function of (stage binaries
# + selfhost/lib/cmd/rt sources + test fixtures + this harness + the Makefile);
# if that content hash equals the recorded last-green key the result is provably
# unchanged, so `cached` is sound — not a guess. `make test` NEVER reads the
# cache (it stays the no-skip pre-push bar) but a green run of the gates DOES
# record the key, as the natural primer. A red run never records: the write is
# guarded on fail=0 (and the fail>0 path exits before it). Cache lives under
# out/.testcache (out/ is gitignored); `make clean` wipes out/ and thus the
# cache, which is correct — the key is keyed on the just-built binaries, so a
# from-scratch rebuild must start from a clean cache.
OUT_DIR=$(dirname "$BIN")
CACHE_DIR=$OUT_DIR/.testcache
CACHE_FILE=$CACHE_DIR/last-green
# The exact cached set, confirmed from the Makefile/test/run: the UNIT-guarded
# phase-2 gates (950 + 990-997) plus the single phase-1 lib byte-id gate
# (989_lib_byteid). The other 989_*/998/999 files are cheap functional _run
# tests, NOT byte-id gates, and stay always-on. Every member is a deterministic
# pure function of the hashed inputs (no /tmp/env/clock/network reads); a future
# gate that reads outside that set must be kept OUT of this list (Go marks such
# tests uncacheable for the same reason).
is_cached_gate() {
is_phase2() {
case $1 in
950_selfcheck.c|989_lib_byteid.c|\
990_*|991_*|992_*|993_*|994_*|995_*|996_*|997_*) return 0 ;;
950_*|990_*|991_*|992_*|993_*|994_*|995_*|996_*|997_*) return 0 ;;
esac
return 1
}
emit_cached() {
printf 'cached %s\n' "$1" > "$RESULTS/$1.status"
: > "$RESULTS/$1.cached"
is_cached_gate() {
case $1 in
950_selfcheck.c|989_lib_byteid.c|\
990_*|991_*|992_*|993_*|994_*|995_*|997_*) return 0 ;;
esac
return 1
}
# Content key over EVERYTHING that determines the cached gates' outcomes: the
# stage binary md5s first (fixed order), then tracked source/fixture/harness
# content (git ls-files is sorted → deterministic; md5sum hashes working-tree
# bytes, so uncommitted edits flip the key too). Any changed byte flips the key
# → the gates RUN. A missed input would be a false green (rob's veto), so the
# set is a deliberate conservative superset (wwstage binary md5s overlap the
# selfhost/lib source hashes, rt/ is hashed though it only reaches the gates via
# libwwrt); an extra input only ever costs a needless honest re-run. examples/
# is hashed because 996_dyn_ww links examples/mandelbrot/mandelbrot.{ww,o} — a
# real gate input outside the source tree.
for t in "$TEST_DIR"/*.c; do
[ -f "$t" ] || continue
printf '%s\n' "$t" >> "$ALL_MANIFEST"
name=${t##*/}
name=${name%.c}
printf '%s\n' "$name" >> "$ALL_NAMES"
if is_phase2 "${t##*/}"; then
printf '%s\n' "$t" >> "$PHASE2_MANIFEST"
else
printf '%s\n' "$t" >> "$PHASE1_MANIFEST"
fi
done
discovered=$(awk 'END{print NR + 0}' "$ALL_MANIFEST")
excluded=0
preflight_error=0
harness_error=0
case $TEST_EXPECTED_MIN in
''|*[!0-9]*)
printf 'HARNESS suite (invalid TEST_EXPECTED_MIN: %s)\n' "$TEST_EXPECTED_MIN"
preflight_error=1
harness_error=$((harness_error + 1))
;;
*)
if [ "$discovered" -eq 0 ]; then
echo 'HARNESS suite (empty corpus)'
preflight_error=1
harness_error=$((harness_error + 1))
elif [ "$discovered" -lt "$TEST_EXPECTED_MIN" ]; then
printf 'HARNESS suite (corpus shrank: %d < %d)\n' \
"$discovered" "$TEST_EXPECTED_MIN"
preflight_error=1
harness_error=$((harness_error + 1))
fi
;;
esac
duplicate_names=$(sort "$ALL_NAMES" | uniq -d)
if [ -n "$duplicate_names" ]; then
echo 'HARNESS suite (duplicate test identifiers)'
printf '%s\n' "$duplicate_names"
preflight_error=1
harness_error=$((harness_error + 1))
fi
JOBS=${JOBS:-$(nproc 2>/dev/null || echo 4)}
case $JOBS in
''|*[!0-9]*|0)
printf 'HARNESS suite (invalid JOBS: %s)\n' "$JOBS"
preflight_error=1
harness_error=$((harness_error + 1))
;;
esac
if ! command -v timeout >/dev/null 2>&1; then
echo 'HARNESS suite (GNU timeout is unavailable)'
preflight_error=1
harness_error=$((harness_error + 1))
fi
valid_duration() {
awk -v duration="$1" 'BEGIN {
if (duration !~ /^[0-9]+([.][0-9]+)?[smhd]?$/) exit 1
sub(/[smhd]$/, "", duration)
exit !(duration + 0 > 0)
}'
}
if ! valid_duration "$TEST_TIMEOUT"; then
printf 'HARNESS suite (invalid TEST_TIMEOUT: %s)\n' "$TEST_TIMEOUT"
preflight_error=1
harness_error=$((harness_error + 1))
fi
if ! valid_duration "$TEST_KILL_AFTER"; then
printf 'HARNESS suite (invalid TEST_KILL_AFTER: %s)\n' "$TEST_KILL_AFTER"
preflight_error=1
harness_error=$((harness_error + 1))
fi
OUT_DIR=$(dirname "$BIN")
CACHE_DIR=$OUT_DIR/.testcache
CACHE_FILE=$CACHE_DIR/last-green
# The cache includes nonignored untracked inputs because a local fixture or
# source file can change a gate even before it is added to the index.
testcache_key() {
cache_inputs=$RESULTS/cache-inputs
if ! : > "$cache_inputs"; then
return 1
fi
# Hash the effective built tools, runtime archive, and every executable
# that the last-green policy may replace with a cached result.
for b in ww w6c w6a w6l wwdump ww_ww w6c_ww w6a_ww w6l_ww wwdump_ww; do
[ -f "$BIN/$b" ] && md5sum "$BIN/$b"
printf '%s\0' "$BIN/$b" >> "$cache_inputs" || return 1
done
git ls-files selfhost lib cmd rt examples test/wcc test/run Makefile | xargs md5sum
printf '%s\0' "$OUT_DIR/lib/libwwrt.a" >> "$cache_inputs" || return 1
while IFS= read -r t; do
[ -n "$t" ] || continue
if is_cached_gate "${t##*/}"; then
name=${t##*/}
name=${name%.c}
short=${name#[0-9][0-9][0-9]_}
printf '%s\0' "$BIN/test_$short" >> "$cache_inputs" || return 1
fi
printf '%s\0' "$t" >> "$cache_inputs" || return 1
done < "$ALL_MANIFEST"
if ! git ls-files --cached -z -- \
selfhost lib cmd rt examples test/wcc test/run test/run_test.sh Makefile \
>> "$cache_inputs"; then
return 1
fi
# Generated package-cache state is not fixture input; hashing it would make
# a gate mutate its own key. Other untracked files under data/ remain input.
if ! git ls-files --others --exclude-standard -z -- \
selfhost lib cmd rt examples test/wcc test/run test/run_test.sh Makefile \
':(exclude,glob)test/wcc/data/**/.pkgcache/**' >> "$cache_inputs"; then
return 1
fi
if ! sort -z -u "$cache_inputs" -o "$cache_inputs"; then
return 1
fi
for setting in \
"TEST_DIR=$TEST_DIR" \
"TEST_EXPECTED_MIN=$TEST_EXPECTED_MIN" \
"TEST_TIMEOUT=$TEST_TIMEOUT" \
"TEST_KILL_AFTER=$TEST_KILL_AFTER" \
"TEST_WORKER=$TEST_WORKER" \
"JOBS=$JOBS"; do
setting_key=$(printf '%s' "$setting" | md5sum) || return 1
printf 'setting %s\n' "${setting_key%% *}"
done
# The index can name an unstaged deletion. Preserve that state in the
# stream instead of letting md5sum abort and silently disable caching.
xargs -0 -r sh -c '
for cache_file do
path_key=$(printf "%s" "$cache_file" | md5sum) || exit
path_key=${path_key%% *}
if [ -f "$cache_file" ]; then
content_key=$(md5sum < "$cache_file") || exit
content_key=${content_key%% *}
mode=-
[ -x "$cache_file" ] && mode=x
printf "file %s %s %s\n" \
"$path_key" "$mode" "$content_key"
elif [ ! -e "$cache_file" ] && [ ! -L "$cache_file" ]; then
printf "missing %s\n" "$path_key"
else
printf "unsupported cache input %s\n" "$path_key" >&2
exit 1
fi
done
' sh < "$cache_inputs"
}
KEY=""
if [ "${UNIT:-0}" != "1" ]; then
KEY=$(testcache_key 2>/dev/null | md5sum 2>/dev/null | cut -d' ' -f1) || KEY=""
if [ "${UNIT:-0}" != "1" ] && [ "$TEST_WORKER" = "$0" ]; then
if key_stream=$(testcache_key 2>/dev/null); then
KEY=$(printf '%s\n' "$key_stream" | md5sum | cut -d' ' -f1)
fi
fi
CACHE_HIT=0
if [ "${COMMIT:-0}" = "1" ] && [ -n "$KEY" ] && [ -f "$CACHE_FILE" ] \
@@ -128,122 +333,311 @@ if [ "${COMMIT:-0}" = "1" ] && [ -n "$KEY" ] && [ -f "$CACHE_FILE" ] \
CACHE_HIT=1
fi
# Phase wall-clock stamps (task #10 baseline). Each phase records start/end
# so the collector can report wall shares onto ken's three poles.
run_start=$(date +%s.%N)
# Phase 2 runs the wwstage byte-id / self-compile gates (990-997) +
# 950_selfcheck. They're kept out of phase 1 only so the heavy 43k
# self-compiles don't contend with the phase-1 fixture swarm; within
# phase 2 they all run in parallel (post-T3 each build's intermediates
# follow its output via `-o`/temp, so no member writes a path another
# reads — see the phase-2 block below).
for t in test/wcc/*.c; do
[ -f "$t" ] || continue
case ${t##*/} in
950_*|990_*|991_*|992_*|993_*|994_*|995_*|996_*|997_*) continue ;;
esac
name=${t##*/}; name=${name%.c}
# The only cached gate that lives in phase 1 is 989_lib_byteid; on a
# content-key hit report it cached instead of re-running it.
if [ "$CACHE_HIT" = 1 ] && is_cached_gate "${t##*/}"; then
emit_cached "$name"
continue
emit_cached() {
cached_name=$1
cached_prefix=$RESULTS/$cached_name
if ! atomic_record "$cached_prefix.result" "WWTEST_RESULT 1 cached 0"; then
return 1
fi
printf '%s/%s\0%s\0' "$RESULTS" "$name" "$t"
done | xargs -0 -n2 -P "$JOBS" "$0" --one || true
p1_end=$(date +%s.%N)
printf '%s\n' "$cached_name" >> "$CACHED_NAMES"
return 0
}
# UNIT=1 (make test-unit) is the inner-loop short path: skip the wwstage
# byte-id gates (990-997) and 950_selfcheck entirely. Pre-push uses
# `make test` for the full suite.
if [ "${UNIT:-0}" != "1" ]; then
if [ "$CACHE_HIT" = 1 ]; then
# Content key matched last-green → the wwstage byte-id / self-compile
# gates are provably unchanged. Report them cached; do NOT re-run.
for t in test/wcc/990_*.c test/wcc/991_*.c test/wcc/992_*.c \
test/wcc/993_*.c test/wcc/994_*.c test/wcc/995_*.c \
test/wcc/996_*.c test/wcc/997_*.c test/wcc/950_*.c; do
[ -f "$t" ] || continue
name=${t##*/}; name=${name%.c}
emit_cached "$name"
done
plan_manifest() {
plan_input=$1
plan_run=$2
while IFS= read -r t; do
[ -n "$t" ] || continue
name=${t##*/}
name=${name%.c}
printf '%s\n' "$name" >> "$SELECTED_NAMES"
if [ "$CACHE_HIT" = 1 ] && is_cached_gate "${t##*/}"; then
if ! emit_cached "$name"; then
printf 'HARNESS %s (cannot record cached result)\n' "$name"
harness_error=$((harness_error + 1))
fi
else
printf '%s\n' "$t" >> "$plan_run"
fi
done < "$plan_input"
}
if [ "$preflight_error" -eq 0 ]; then
plan_manifest "$PHASE1_MANIFEST" "$RUN1_MANIFEST"
if [ "${UNIT:-0}" = "1" ]; then
while IFS= read -r t; do
[ -n "$t" ] || continue
name=${t##*/}
name=${name%.c}
printf 'excluded %s (test-unit phase-2)\n' "$name"
excluded=$((excluded + 1))
done < "$PHASE2_MANIFEST"
else
p2par_start=$(date +%s.%N)
# Phase 2 — the wwstage byte-id / self-compile gates, all parallel.
# Pre-T3 the source-tree writers (993/995/950) ran in a serial tail
# because ww_ww wrote intermediates next to every traversed source, so
# they raced each other AND the 990/991/992/994 readers. T3 made every
# build's .combined.ww/.s/.o follow its OUTPUT (`-o`/per-pid temp): 993
# → its dc/dw workdirs, 995 → /tmp/wwsr_<pid>_<tool>, 950 → /tmp only.
# No member now writes a stem another reads (the readers see only the
# stable make-all regen), so the whole group fans out via xargs -P.
for t in test/wcc/990_*.c test/wcc/991_*.c test/wcc/992_*.c \
test/wcc/993_*.c test/wcc/994_*.c test/wcc/995_*.c \
test/wcc/996_*.c test/wcc/997_*.c test/wcc/950_*.c; do
[ -f "$t" ] || continue
name=${t##*/}; name=${name%.c}
plan_manifest "$PHASE2_MANIFEST" "$RUN2_MANIFEST"
fi
fi
launch_manifest() {
launch_file=$1
[ -s "$launch_file" ] || return 0
while IFS= read -r t; do
name=${t##*/}
name=${name%.c}
printf '%s/%s\0%s\0' "$RESULTS" "$name" "$t"
done | xargs -0 -n2 -P "$JOBS" "$0" --one || true
p2par_end=$(date +%s.%N)
done < "$launch_file" | \
xargs -0 -r -n2 -P "$JOBS" "$TEST_WORKER" --one
}
run_start=$(date +%s.%N)
launcher_error=0
p1_end=$run_start
p2par_start=""
p2par_end=""
if [ "$preflight_error" -eq 0 ]; then
if launch_manifest "$RUN1_MANIFEST"; then
:
else
rc=$?
printf 'HARNESS launcher (phase 1 xargs rc=%d)\n' "$rc"
launcher_error=1
harness_error=$((harness_error + 1))
fi
p1_end=$(date +%s.%N)
if [ "${UNIT:-0}" != "1" ] && [ -s "$RUN2_MANIFEST" ]; then
p2par_start=$(date +%s.%N)
if launch_manifest "$RUN2_MANIFEST"; then
:
else
rc=$?
printf 'HARNESS launcher (phase 2 xargs rc=%d)\n' "$rc"
launcher_error=1
harness_error=$((harness_error + 1))
fi
p2par_end=$(date +%s.%N)
fi
fi
fail=0
ran=0
cached=0
for s in "$RESULTS"/*.status; do
[ -f "$s" ] || continue
cat "$s"
prefix=${s%.status}
if [ -f "$prefix.cached" ]; then
cached=$((cached + 1))
else
ran=$((ran + 1))
emit_output() {
output_prefix=$1
output_name=$2
if [ -s "$output_prefix.out" ]; then
printf '%s\n' "--- stdout: $output_name ---"
cat "$output_prefix.out"
fi
[ -f "$prefix.fail" ] && fail=$((fail + 1))
done
if [ -s "$output_prefix.err" ]; then
printf '%s\n' "--- stderr: $output_name ---"
cat "$output_prefix.err"
fi
}
# ---- ww source-level tests ----------------------------------------------
if [ -d test/lang ]; then
for f in test/lang/*/*.ww; do
[ -f "$f" ] || continue
echo "skip $f (ww run not online yet)"
started=0
completed=0
pass=0
fail=0
skip=0
timed_out=0
cached=0
worker_harness_error=0
if [ "$preflight_error" -eq 0 ]; then
while IFS= read -r name; do
[ -n "$name" ] || continue
prefix=$RESULTS/$name
expect_cached=0
if grep -Fqx "$name" "$CACHED_NAMES"; then
expect_cached=1
fi
has_started=0
if [ -f "$prefix.started" ]; then
if grep -qx 'WWTEST_STARTED 1' "$prefix.started"; then
has_started=1
started=$((started + 1))
else
printf 'HARNESS %s (malformed started record)\n' "$name"
harness_error=$((harness_error + 1))
fi
fi
if [ ! -f "$prefix.result" ]; then
if [ "$has_started" -eq 1 ]; then
printf 'HARNESS %s (started but no terminal result)\n' "$name"
else
printf 'HARNESS %s (worker never started)\n' "$name"
fi
harness_error=$((harness_error + 1))
continue
fi
magic="" version="" outcome="" result_rc="" extra=""
IFS=' ' read -r magic version outcome result_rc extra < "$prefix.result"
if [ "$magic" != WWTEST_RESULT ] || [ "$version" != 1 ] \
|| [ -z "$outcome" ] || [ -z "$result_rc" ] || [ -n "$extra" ]; then
printf 'HARNESS %s (malformed terminal result)\n' "$name"
harness_error=$((harness_error + 1))
continue
fi
case $result_rc in
''|*[!0-9]*)
printf 'HARNESS %s (nonnumeric terminal rc)\n' "$name"
harness_error=$((harness_error + 1))
continue
;;
esac
if [ "$outcome" = cached ]; then
if [ "$expect_cached" -ne 1 ] || [ "$has_started" -ne 0 ] \
|| [ "$result_rc" != 0 ]; then
printf 'HARNESS %s (invalid cached result)\n' "$name"
harness_error=$((harness_error + 1))
continue
fi
printf 'cached %s\n' "$name"
cached=$((cached + 1))
continue
fi
capture_error=0
for suffix in out err; do
if [ ! -f "$prefix.$suffix" ]; then
printf 'HARNESS %s (missing %s capture)\n' "$name" "$suffix"
capture_error=1
fi
done
if [ ! -s "$prefix.dur" ]; then
printf 'HARNESS %s (missing duration record)\n' "$name"
capture_error=1
fi
if [ "$capture_error" -ne 0 ]; then
harness_error=$((harness_error + 1))
continue
fi
completed=$((completed + 1))
if [ "$expect_cached" -eq 1 ] || [ "$has_started" -ne 1 ]; then
printf 'HARNESS %s (terminal result without matching start)\n' "$name"
harness_error=$((harness_error + 1))
fi
case $outcome:$result_rc in
pass:0)
printf 'ok %s\n' "$name"
pass=$((pass + 1))
;;
skip:77)
printf 'skip %s (rc=77)\n' "$name"
skip=$((skip + 1))
if [ ! -s "$prefix.out" ] && [ ! -s "$prefix.err" ]; then
echo 'skip reason: exit 77 (no output)'
fi
;;
timeout:124|timeout:137)
printf 'TIMEOUT %s\n' "$name"
timed_out=$((timed_out + 1))
;;
fail:*)
printf 'FAIL %s (rc=%s)\n' "$name" "$result_rc"
fail=$((fail + 1))
;;
harness_error:125|harness_error:126|harness_error:127)
printf 'HARNESS %s (worker rc=%s)\n' "$name" "$result_rc"
worker_harness_error=$((worker_harness_error + 1))
harness_error=$((harness_error + 1))
;;
*)
printf 'HARNESS %s (invalid outcome/rc %s/%s)\n' \
"$name" "$outcome" "$result_rc"
worker_harness_error=$((worker_harness_error + 1))
harness_error=$((harness_error + 1))
;;
esac
emit_output "$prefix" "$name"
done < "$SELECTED_NAMES"
for record in "$RESULTS"/*.result "$RESULTS"/*.started; do
[ -f "$record" ] || continue
record_name=${record##*/}
record_name=${record_name%.result}
record_name=${record_name%.started}
if ! grep -Fqx "$record_name" "$SELECTED_NAMES"; then
printf 'HARNESS %s (unexpected worker record)\n' "$record_name"
harness_error=$((harness_error + 1))
fi
done
fi
# ---- timing (task #10 baseline) -----------------------------------------
# Per-test durations + phase walls. Emitted after the per-test ok/FAIL dump
# but before the summary line's exit checks, so timing prints even on the
# failure path; the "all N tests passed" summary stays the last line. No log
# consumer is disturbed: the "---" delimiters and "<sec>\t<name>" rows match
# neither the "all N tests passed" nor the "FAIL" grep keys.
selected=$((discovered - excluded))
planned=$((selected - cached))
terminal=$((pass + fail + skip + timed_out + worker_harness_error))
if [ "$preflight_error" -eq 0 ]; then
if [ "$started" -ne "$planned" ]; then
printf 'HARNESS accounting (started %d != planned %d)\n' \
"$started" "$planned"
harness_error=$((harness_error + 1))
fi
if [ "$completed" -ne "$planned" ]; then
printf 'HARNESS accounting (completed %d != planned %d)\n' \
"$completed" "$planned"
harness_error=$((harness_error + 1))
fi
if [ "$terminal" -ne "$completed" ]; then
printf 'HARNESS accounting (terminal outcomes %d != completed %d)\n' \
"$terminal" "$completed"
harness_error=$((harness_error + 1))
fi
if [ $((completed + cached + excluded)) -ne "$discovered" ]; then
printf 'HARNESS accounting (discovered corpus is incomplete)\n'
harness_error=$((harness_error + 1))
fi
fi
# A cache decision is valid only if its complete input fingerprint is still
# current after all workers have finished. This applies to hits as well as to
# newly published keys.
if [ "${UNIT:-0}" != "1" ] && [ -n "$KEY" ]; then
end_key=""
if end_stream=$(testcache_key 2>/dev/null); then
end_key=$(printf '%s\n' "$end_stream" | md5sum | cut -d' ' -f1)
fi
if [ -z "$end_key" ] || [ "$end_key" != "$KEY" ]; then
echo 'HARNESS cache (inputs changed during test run)'
harness_error=$((harness_error + 1))
fi
fi
not_run=$((discovered - completed - cached))
run_end=$(date +%s.%N)
echo '--- timing: per-test (sec, desc) ---'
cat "$RESULTS"/*.dur 2>/dev/null | sort -rn
echo '--- timing: phase walls (sec) ---'
awk -v rs="$run_start" -v p1e="$p1_end" \
-v ppar_s="$p2par_start" -v ppar_e="$p2par_end" \
-v pser_s="$p2ser_start" -v pser_e="$p2ser_end" \
-v re="$run_end" 'BEGIN{
printf "phase1_parallel\t%.3f\n", p1e - rs
if (ppar_e != "") printf "phase2_parallel\t%.3f\n", ppar_e - ppar_s
if (pser_e != "") printf "phase2_serial_tail\t%.3f\n", pser_e - pser_s
printf "total\t%.3f\n", re - rs
}'
if [ $ran -eq 0 ] && [ $cached -eq 0 ]; then
echo "no tests were run"
exit 1
printf 'summary: discovered=%d started=%d completed=%d pass=%d fail=%d skip=%d timeout=%d harness_error=%d cached=%d not_run=%d\n' \
"$discovered" "$started" "$completed" "$pass" "$fail" "$skip" \
"$timed_out" "$harness_error" "$cached" "$not_run"
green=1
if [ "$preflight_error" -ne 0 ] || [ "$launcher_error" -ne 0 ] \
|| [ "$fail" -ne 0 ] || [ "$skip" -ne 0 ] || [ "$timed_out" -ne 0 ] \
|| [ "$harness_error" -ne 0 ]; then
green=0
fi
if [ $fail -gt 0 ]; then
echo "$fail test(s) failed"
exit 1
if [ "$preflight_error" -eq 0 ] && [ "$not_run" -ne "$excluded" ]; then
green=0
fi
# Green and the gates actually RAN (not a cache hit, not test-unit): record the
# content key as the last-green primer. A red run exits above, never here.
if [ "${UNIT:-0}" != "1" ] && [ "$CACHE_HIT" != 1 ] && [ -n "$KEY" ]; then
mkdir -p "$CACHE_DIR" && printf '%s\n' "$KEY" > "$CACHE_FILE"
if [ "$green" -eq 1 ] && [ "${UNIT:-0}" != "1" ] \
&& [ "$CACHE_HIT" != 1 ] && [ -n "$KEY" ]; then
if ! mkdir -p "$CACHE_DIR" \
|| ! atomic_record "$CACHE_FILE" "$KEY"; then
echo 'HARNESS cache (cannot publish last-green key)'
exit 1
fi
fi
[ $cached -gt 0 ] && echo "$cached gate(s) cached (content key matched last green)"
echo "all $ran tests passed"
[ "$green" -eq 1 ] || exit 1
exit 0

316
test/run_test.sh Normal file
View File

@@ -0,0 +1,316 @@
#!/bin/sh
# Synthetic corpora keep harness failure-path coverage independent of compiler builds.
set -eu
ROOT=$(cd "$(dirname "$0")/.." && pwd)
RUN=$ROOT/test/run
WORK=$(mktemp -d "${TMPDIR:-/tmp}/ww-run-test.XXXXXX")
trap 'rm -rf "$WORK"' EXIT
trap 'exit 1' HUP INT TERM
fail()
{
printf 'run_test: FAIL: %s\n' "$*" >&2
exit 1
}
new_case()
{
CASE=$WORK/$1
mkdir -p "$CASE/corpus" "$CASE/bin"
}
add_source()
{
: > "$CASE/corpus/$1.c"
}
add_binary()
{
name=$1
body=$2
{
printf '%s\n' '#!/bin/sh'
printf '%s\n' "$body"
} > "$CASE/bin/test_$name"
chmod +x "$CASE/bin/test_$name"
}
run_case()
{
want=$1
shift
set +e
env BIN="$CASE/bin" JOBS=2 "$@" sh "$RUN" > "$CASE/output" 2>&1
GOT=$?
set -e
[ "$GOT" -eq "$want" ] || {
cat "$CASE/output" >&2
fail "$(basename "$CASE"): exit $GOT, want $want"
}
}
run_case_from()
{
want=$1
run_dir=$2
shift 2
set +e
(
cd "$run_dir"
env BIN="$CASE/bin" JOBS=2 "$@" sh "$RUN"
) > "$CASE/output" 2>&1
GOT=$?
set -e
[ "$GOT" -eq "$want" ] || {
cat "$CASE/output" >&2
fail "$(basename "$CASE"): exit $GOT, want $want"
}
}
assert_line()
{
grep -Fqx "$1" "$CASE/output" || {
cat "$CASE/output" >&2
fail "$(basename "$CASE"): missing line: $1"
}
}
assert_text()
{
grep -Fq "$1" "$CASE/output" || {
cat "$CASE/output" >&2
fail "$(basename "$CASE"): missing text: $1"
}
}
assert_summary_field()
{
key=$1
want=$2
summary=$(grep '^summary:' "$CASE/output" || true)
[ "$(printf '%s\n' "$summary" | grep -c '^summary:')" -eq 1 ] || {
cat "$CASE/output" >&2
fail "$(basename "$CASE"): expected one summary line"
}
case " $summary " in
*" $key=$want "*) ;;
*)
cat "$CASE/output" >&2
fail "$(basename "$CASE"): summary $key is not $want"
;;
esac
}
assert_summary_nonzero()
{
key=$1
summary=$(grep '^summary:' "$CASE/output" || true)
[ "$(printf '%s\n' "$summary" | grep -c '^summary:')" -eq 1 ] || {
cat "$CASE/output" >&2
fail "$(basename "$CASE"): expected one summary line"
}
case " $summary " in
*" $key=0 "*)
cat "$CASE/output" >&2
fail "$(basename "$CASE"): summary $key is zero"
;;
*" $key="*) ;;
*)
cat "$CASE/output" >&2
fail "$(basename "$CASE"): summary has no $key field"
;;
esac
}
new_case all_pass
add_source 001_alpha
add_source 002_beta
add_binary alpha 'exit 0'
add_binary beta 'exit 0'
run_case 0 TEST_DIR="$CASE/corpus" TEST_EXPECTED_MIN=2 UNIT=1
assert_line 'ok 001_alpha'
assert_line 'ok 002_beta'
assert_line 'summary: discovered=2 started=2 completed=2 pass=2 fail=0 skip=0 timeout=0 harness_error=0 cached=0 not_run=0'
new_case cache_publish
add_source 990_cache
add_source 996_dynamic
add_binary cache '[ -z "${WW_W6C+x}${WW_W6A+x}${WW_W6L+x}${WW_LIB+x}${WW_SRCLIB+x}${WW_PKGCACHE+x}" ] || exit 9; exit 0'
add_binary dynamic 'exit 0'
run_case 0 TEST_DIR="$CASE/corpus" TEST_EXPECTED_MIN=2 COMMIT=1 \
WW_W6C=/first/w6c WW_W6A=/first/w6a WW_W6L=/first/w6l \
WW_LIB=/first/lib WW_SRCLIB=/first/src WW_PKGCACHE=/first/cache
assert_summary_field pass 2
assert_summary_field harness_error 0
assert_summary_field cached 0
[ -s "$CASE/.testcache/last-green" ] || fail "cache_publish: no cache key"
run_case 0 TEST_DIR="$CASE/corpus" TEST_EXPECTED_MIN=2 COMMIT=1 \
WW_W6C=/second/w6c WW_W6A=/second/w6a WW_W6L=/second/w6l \
WW_LIB=/second/lib WW_SRCLIB=/second/src WW_PKGCACHE=/second/cache
assert_line 'cached 990_cache'
assert_line 'ok 996_dynamic'
assert_line 'summary: discovered=2 started=1 completed=1 pass=1 fail=0 skip=0 timeout=0 harness_error=0 cached=1 not_run=0'
new_case cache_deleted_tracked
add_source 990_cache
add_binary cache 'exit 0'
mkdir -p "$CASE/repo"
: > "$CASE/repo/Makefile"
git -C "$CASE/repo" init -q
git -C "$CASE/repo" add Makefile
rm -f "$CASE/repo/Makefile"
run_case_from 0 "$CASE/repo" TEST_DIR="$CASE/corpus" \
TEST_EXPECTED_MIN=1 COMMIT=1
assert_summary_field pass 1
[ -s "$CASE/.testcache/last-green" ] || \
fail "cache_deleted_tracked: no cache key"
run_case_from 0 "$CASE/repo" TEST_DIR="$CASE/corpus" \
TEST_EXPECTED_MIN=1 COMMIT=1
assert_summary_field cached 1
assert_summary_field started 0
new_case fail
add_source 001_fail
add_binary fail 'printf "%s\n" "synthetic failure" >&2; exit 3'
run_case 1 TEST_DIR="$CASE/corpus" TEST_EXPECTED_MIN=1 UNIT=1
assert_line 'FAIL 001_fail (rc=3)'
assert_text 'synthetic failure'
assert_summary_field fail 1
assert_summary_field harness_error 0
new_case skip
add_source 001_skip
add_binary skip 'printf "%s\n" "synthetic prerequisite unavailable"; exit 77'
run_case 1 TEST_DIR="$CASE/corpus" TEST_EXPECTED_MIN=1 UNIT=1
assert_line 'skip 001_skip (rc=77)'
assert_text 'synthetic prerequisite unavailable'
assert_summary_field skip 1
assert_summary_field pass 0
new_case timeout
add_source 001_slow
add_binary slow 'trap "" TERM; sleep 5; exit 0'
run_case 1 TEST_DIR="$CASE/corpus" TEST_EXPECTED_MIN=1 \
TEST_TIMEOUT=1 TEST_KILL_AFTER=1 UNIT=1
assert_line 'TIMEOUT 001_slow'
assert_summary_field timeout 1
assert_summary_field pass 0
new_case reserved_exit_codes
for item in 001:124 002:125 003:126 004:127 005:137; do
prefix=${item%%:*}
rc=${item#*:}
add_source "${prefix}_exit${rc}"
add_binary "exit${rc}" "exit $rc"
done
run_case 1 TEST_DIR="$CASE/corpus" TEST_EXPECTED_MIN=5 UNIT=1
assert_summary_field fail 5
assert_summary_field timeout 0
assert_summary_field harness_error 0
new_case timeout_wrapper_failure
add_source 001_probe
add_binary probe 'exit 0'
mkdir -p "$CASE/path"
{
printf '%s\n' '#!/bin/sh'
printf '%s\n' 'exit 124'
} > "$CASE/path/timeout"
chmod +x "$CASE/path/timeout"
run_case 1 PATH="$CASE/path:$PATH" TEST_DIR="$CASE/corpus" \
TEST_EXPECTED_MIN=1 UNIT=1
assert_summary_field timeout 0
assert_summary_nonzero harness_error
new_case missing_binary
add_source 001_missing
run_case 1 TEST_DIR="$CASE/corpus" TEST_EXPECTED_MIN=1 UNIT=1
assert_text 'HARNESS 001_missing'
assert_summary_field harness_error 1
assert_summary_field completed 1
new_case empty
run_case 1 TEST_DIR="$CASE/corpus" TEST_EXPECTED_MIN=0 UNIT=1
assert_summary_field discovered 0
assert_summary_field started 0
assert_summary_field completed 0
assert_summary_field harness_error 1
new_case below_minimum
add_source 001_only
add_binary only 'exit 0'
run_case 1 TEST_DIR="$CASE/corpus" TEST_EXPECTED_MIN=2 UNIT=1
assert_summary_field discovered 1
assert_summary_field harness_error 1
new_case zero_timeout
add_source 001_bounded
add_binary bounded 'exit 0'
run_case 1 TEST_DIR="$CASE/corpus" TEST_EXPECTED_MIN=1 \
TEST_TIMEOUT=0 UNIT=1
assert_summary_field discovered 1
assert_summary_field started 0
assert_summary_field harness_error 1
new_case launcher_failure
add_source 001_lost
add_binary lost 'exit 0'
mkdir -p "$CASE/path"
{
printf '%s\n' '#!/bin/sh'
printf '%s\n' 'exit 23'
} > "$CASE/path/xargs"
chmod +x "$CASE/path/xargs"
run_case 1 PATH="$CASE/path:$PATH" TEST_DIR="$CASE/corpus" \
TEST_EXPECTED_MIN=1 UNIT=1
assert_summary_field discovered 1
assert_summary_field started 0
assert_summary_field completed 0
assert_summary_nonzero harness_error
new_case lost_result
add_source 001_lost
add_binary lost 'exit 0'
{
printf '%s\n' '#!/bin/sh'
printf '%s\n' 'prefix=$2'
printf '%s\n' "printf '%s\\n' 'WWTEST_STARTED 1' > \"\$prefix.started\""
printf '%s\n' 'exit 0'
} > "$CASE/worker"
chmod +x "$CASE/worker"
run_case 1 TEST_DIR="$CASE/corpus" TEST_EXPECTED_MIN=1 \
TEST_WORKER="$CASE/worker" UNIT=1
assert_summary_field discovered 1
assert_summary_field started 1
assert_summary_field completed 0
assert_summary_nonzero harness_error
new_case incomplete_result
add_source 001_incomplete
add_binary incomplete 'exit 0'
{
printf '%s\n' '#!/bin/sh'
printf '%s\n' 'prefix=$2'
printf '%s\n' "printf '%s\\n' 'WWTEST_STARTED 1' > \"\$prefix.started\""
printf '%s\n' "printf '%s\\n' 'WWTEST_RESULT 1 pass 0' > \"\$prefix.result\""
printf '%s\n' 'exit 0'
} > "$CASE/worker"
chmod +x "$CASE/worker"
run_case 1 TEST_DIR="$CASE/corpus" TEST_EXPECTED_MIN=1 \
TEST_WORKER="$CASE/worker" UNIT=1
assert_summary_field discovered 1
assert_summary_field completed 0
assert_summary_field pass 0
assert_summary_nonzero harness_error
new_case unit_exclusion
add_source 001_fast
add_source 990_phase
add_binary fast 'exit 0'
run_case 0 TEST_DIR="$CASE/corpus" TEST_EXPECTED_MIN=2 UNIT=1
assert_line 'excluded 990_phase (test-unit phase-2)'
assert_line 'summary: discovered=2 started=1 completed=1 pass=1 fail=0 skip=0 timeout=0 harness_error=0 cached=0 not_run=1'
printf 'run_test: all harness regressions passed\n'

View File

@@ -106,7 +106,7 @@ main(void)
if (access(wtool, X_OK) != 0) {
fprintf(stderr, "w6a_parsenum: skip (no %s)\n", wtool);
printf("w6a_parsenum: skipped\n");
return 0;
return 77;
}
int nid = (int)(sizeof idrows / sizeof idrows[0]);

View File

@@ -13,7 +13,7 @@
* the loader requires the right .gnu.version_r entry to bind
* the default vDSO-aware impl.
*
* Skipped (passing trivially) on systems without /usr/lib/libc.so.6.
* Reports the harness skip status on systems without a supported libc.so.6.
*/
#include <stdio.h>
#include <stdlib.h>
@@ -108,7 +108,7 @@ main(void)
&& access("/lib/x86_64-linux-gnu/libc.so.6", 0) != 0
&& access("/lib64/libc.so.6", 0) != 0) {
puts("dyn: no libc.so.6 on this system — skipping");
return 0;
return 77;
}
const char *bin = getenv("BIN");

View File

@@ -23,8 +23,9 @@
* small pins the headers-fit-page path stays correct and byte-identical.
*
* Generates the dyn-sym set from `nm -D <libc>` (default-versioned text
* exports) so it adapts to whatever glibc is present; skips (passing) when
* libc / nm are unavailable or too few symbols to overflow the first page.
* exports) so it adapts to whatever glibc is present; reports the harness
* skip status when libc / nm are unavailable or too few symbols are available
* to overflow the first page.
*/
#include <stdio.h>
#include <stdlib.h>
@@ -87,14 +88,14 @@ main(void)
if (access("/usr/lib/libc.so.6", 0) == 0) { libc = "/usr/lib/libc.so.6"; libdir = "/usr/lib"; }
else if (access("/lib/x86_64-linux-gnu/libc.so.6", 0) == 0) { libc = "/lib/x86_64-linux-gnu/libc.so.6"; libdir = "/lib/x86_64-linux-gnu"; }
else if (access("/lib64/libc.so.6", 0) == 0) { libc = "/lib64/libc.so.6"; libdir = "/lib64"; }
if (!libc) { puts("dynentry: no libc.so.6 — skipping"); return 0; }
if (!libc) { puts("dynentry: no libc.so.6 — skipping"); return 77; }
static char syms[200][32];
int ns = harvest_syms(libc, syms, 200);
if (ns < 130) {
printf("dynentry: only %d libc syms harvested (<130, can't overflow "
"first page) — skipping\n", ns);
return 0;
return 77;
}
const char *bin = getenv("BIN");

View File

@@ -53,7 +53,7 @@
#include <sys/wait.h>
#include <sys/stat.h>
enum { M_ID, M_DIVERGE, M_WWREJECT };
enum { M_ID, M_DIVERGE, M_WWREJECT, NENT_EXPECT = 42 };
struct ent {
const char *fixture; /* repo-relative .ww; NULL → probe entry */
@@ -95,10 +95,6 @@ static const struct ent ents[] = {
{ .fixture = "lib/memio/memiotest.ww", .mode = M_ID },
{ .fixture = "lib/os/ostest.ww", .mode = M_ID },
{ .fixture = "lib/regex/regex_test.ww", .mode = M_ID },
/* in-package white-box half (#9): the wb/ driver dir-resolves
* `import regex` and bundles lib/regex/regex_whitebox.ww (package
* regex), so this fixture byte-ids the moved engine probes. */
{ .fixture = "lib/regex/wb/regex_test.ww", .mode = M_ID },
{ .fixture = "lib/strconv/test/ftostest.ww", .mode = M_ID },
/* graduated from #59.10 DIVERGE by the #62 float-literal fold fix
* (wwstage lexer now folds through strconv.stof64, matching
@@ -457,6 +453,12 @@ main(void)
if (!bin) return 1;
char cwd[1024];
if (getcwd(cwd, sizeof cwd) == NULL) return 1;
int nent = (int)(sizeof ents / sizeof ents[0]) - 1;
if (nent != NENT_EXPECT) {
fprintf(stderr, "lib_byteid FAIL: corpus has %d entries, want %d\n",
nent, NENT_EXPECT);
return 1;
}
int fail = 0, n = 0, nid = 0, ndiv = 0, nrej = 0;
if (corpus_complete(cwd) != 0) fail++;

View File

@@ -1,67 +1,201 @@
/*
* 989_regex_run — execute BOTH lib/regex @test halves under the C-side
* `ww test` driver and assert exit 0:
* 1. lib/regex/regex_test.ww — BLACK-BOX, package regex_test.
* 2. lib/regex/wb/regex_test.ww — the WHITE-BOX driver (#9): its
* `import regex` dir-resolves and bundles lib/regex/regex_whitebox.ww
* (package regex), so -T collects the in-package engine probes.
* Both are main-less @test files; -T synthesizes the entry, runs the
* fork-per-test record-and-continue harness, and exits nonzero iff any
* @test fails.
*
* Sibling to 984_base64_run / 989_sha256_run (9xx is full so this
* shares the 989 prefix — the `short` name keys the binary, cf the
* 949_* / 989_sha256 precedent).
* The white-box half must be the -T root: dependency packages lose @test
* declarations before code generation. Keep it separate from the external
* regex_test binary because each -T unit owns main and __wwtests.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
static const char *whitebox[] = {
"thread_shape",
"newmatch_discriminates",
"is_consuming_kinds",
"delete_thread_middle",
"add_thread_dedup_inherit",
"add_thread_dup_independence",
"run_thread_literal_program",
"run_thread_anchored_route",
"search_matches",
"search_early_exit",
"search_no_match",
"find_last_groupstart_cases",
"shift_direct",
"run_thread_group_arms",
"parse_repetition_cases",
};
static int
runwait(const char *cmd)
{
int rc = system(cmd);
if (rc == -1) return -1;
if (WIFEXITED(rc)) return WEXITSTATUS(rc);
int rc;
rc = system(cmd);
if (rc == -1)
return -1;
if (WIFEXITED(rc))
return WEXITSTATUS(rc);
return 1;
}
static int
runcapture(const char *cmd, char *out, size_t outsz)
{
FILE *p;
size_t n;
int ch, overflow, status;
p = popen(cmd, "r");
if (p == NULL)
return -1;
n = 0;
overflow = 0;
while ((ch = fgetc(p)) != EOF) {
if (n + 1 < outsz)
out[n++] = ch;
else
overflow = 1;
}
out[n] = '\0';
status = pclose(p);
if (overflow || status == -1)
return -1;
if (WIFEXITED(status))
return WEXITSTATUS(status);
return 1;
}
static int
linecount(const char *out, const char *want)
{
const char *p, *end;
size_t n;
int count;
n = strlen(want);
count = 0;
for (p = out; *p != '\0'; p = *end == '\0' ? end : end + 1) {
end = strchr(p, '\n');
if (end == NULL)
end = p + strlen(p);
if ((size_t)(end - p) == n && memcmp(p, want, n) == 0)
count++;
}
return count;
}
static int
okcount(const char *out)
{
const char *p, *end;
static const char suffix[] = " ... ok";
size_t n, slen;
int count;
slen = sizeof suffix - 1;
count = 0;
for (p = out; *p != '\0'; p = *end == '\0' ? end : end + 1) {
end = strchr(p, '\n');
if (end == NULL)
end = p + strlen(p);
n = (size_t)(end - p);
if (n >= slen && memcmp(end - slen, suffix, slen) == 0)
count++;
}
return count;
}
static int
checkresult(const char *label, const char *out, int expected,
const char *const *names, size_t nnames)
{
char summary[64], row[256];
int bad;
bad = 0;
snprintf(summary, sizeof summary, "%d passed, 0 failed", expected);
if (linecount(out, summary) != 1 || okcount(out) != expected
|| strstr(out, "FAIL") != NULL || strstr(out, "No tests run") != NULL) {
fprintf(stderr, "regex_run FAIL: %s result was incomplete\n%s",
label, out);
bad = 1;
}
for (size_t i = 0; i < nnames; i++) {
snprintf(row, sizeof row, "%s ... ok", names[i]);
if (linecount(out, row) != 1) {
fprintf(stderr, "regex_run FAIL: %s did not execute exactly once\n",
names[i]);
bad = 1;
}
}
return bad ? -1 : 0;
}
int
main(void)
{
const char *bin = getenv("BIN");
if (!bin) bin = "out/bin";
char absbin[1024];
const char *bin;
char absbin[1024], cwd[1024], tmp[] = "/tmp/wwregex.XXXXXX";
char cmd[4096], output[32768], cleanup[1200];
int rc, failed;
bin = getenv("BIN");
if (bin == NULL)
bin = "out/bin";
if (bin[0] != '/') {
char cwd[1024];
if (getcwd(cwd, sizeof cwd) == NULL) return 1;
snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin);
if (getcwd(cwd, sizeof cwd) == NULL)
return 1;
if (snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin)
>= (int)sizeof absbin)
return 1;
bin = absbin;
}
char cwd[1024];
if (getcwd(cwd, sizeof cwd) == NULL) return 1;
if (getcwd(cwd, sizeof cwd) == NULL)
return 1;
if (mkdtemp(tmp) == NULL)
return 1;
const char *srcs[] = {
"lib/regex/regex_test.ww",
"lib/regex/wb/regex_test.ww",
};
for (size_t i = 0; i < sizeof srcs / sizeof srcs[0]; i++) {
const char *src = srcs[i];
char path[1024], cmd[2048];
snprintf(path, sizeof path, "%s/%s", cwd, src);
/* timeout 180 per repo convention (732/775; test/run does not
* bound runtime): the fold-2c zero-length findall rows turn a
* regression of the ha:946-952 rune-advancement guard into an
* infinite loop (frees are no-ops, so OOM is the only other
* exit) — timeout converts the hang into a loud 124. */
snprintf(cmd, sizeof cmd, "timeout 180 %s/ww test %s", bin, path);
int rc = runwait(cmd);
if (rc != 0) {
fprintf(stderr, "regex_run FAIL: %s exited %d\n", src, rc);
return 1;
failed = 0;
/* A unique cwd prevents an unrelated local `regex` path from winning
* the module lookup; the private cache keeps the source tree untouched. */
if (snprintf(cmd, sizeof cmd,
"cd '%s' && WW_PKGCACHE='%s/cache-black' timeout 180 '%s/ww' "
"test -I '%s/lib' '%s/lib/regex/regex_test.ww'",
tmp, tmp, bin, cwd, cwd) >= (int)sizeof cmd) {
failed = 1;
} else {
rc = runcapture(cmd, output, sizeof output);
if (rc != 0 || checkresult("black-box", output, 33, NULL, 0) != 0) {
fprintf(stderr, "regex_run FAIL: black-box exited %d\n", rc);
failed = 1;
} else {
printf("regex_run: black-box 33 discovered, 33 passed, 0 failed\n");
}
printf("regex_run: %s ok\n", src);
}
return 0;
/* Resolving the package directory as the root keeps -T off imported
* dependencies while making every in-package probe visible to the synth. */
if (snprintf(cmd, sizeof cmd,
"cd '%s' && WW_PKGCACHE='%s/cache-white' timeout 180 '%s/ww' "
"test -I '%s/lib' regex", tmp, tmp, bin, cwd) >= (int)sizeof cmd) {
failed = 1;
} else {
rc = runcapture(cmd, output, sizeof output);
if (rc != 0 || checkresult("white-box", output, 15, whitebox,
sizeof whitebox / sizeof whitebox[0]) != 0) {
fprintf(stderr, "regex_run FAIL: white-box exited %d\n", rc);
failed = 1;
} else {
printf("regex_run: white-box 15 discovered, 15 passed, 0 failed\n");
}
}
snprintf(cleanup, sizeof cleanup, "rm -rf -- '%s'", tmp);
if (runwait(cleanup) != 0) {
fprintf(stderr, "regex_run FAIL: cannot clean temporary directory\n");
failed = 1;
}
return failed ? 1 : 0;
}

View File

@@ -169,7 +169,7 @@ main(void)
if (access(wdrv, X_OK) != 0) {
fprintf(stderr, "structlocal_frame: skip (no %s)\n", wdrv);
printf("structlocal_frame: skipped (no wwstage)\n");
return 0;
return 77;
}
int n = (int)(sizeof rows / sizeof rows[0]);

View File

@@ -27,11 +27,13 @@
#include <unistd.h>
#include <sys/wait.h>
enum { Nexpect = 4 };
struct row {
const char *pat; /* pattern arg, or NULL for none */
int rc; /* expected exit code */
const char *must[4]; /* substrings required in stdout */
const char *mustnot[4]; /* substrings forbidden in stdout */
const char *must[Nexpect]; /* substrings required in stdout */
const char *mustnot[Nexpect]; /* substrings forbidden in stdout */
};
static const struct row rows[] = {
@@ -46,7 +48,7 @@ static const struct row rows[] = {
{ "alpha", "beta", NULL } },
/* no match → "No tests run", success */
{ "zzz", 0, { "No tests run", NULL },
{ "alpha", "beta", "gamma", "passed", NULL } },
{ "alpha", "beta", "gamma", "passed" } },
/* star matches everything */
{ "*", 0, { "alpha ... ok", "beta ... ok", "gamma ... ok",
"3 passed, 0 failed" }, { NULL } },
@@ -93,7 +95,12 @@ main(void)
if (bin[0] != '/') {
char cwd[1024];
if (getcwd(cwd, sizeof cwd) == NULL) return 1;
snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin);
size_t ncwd = strlen(cwd);
size_t nbin = strlen(bin);
if (nbin >= sizeof absbin - ncwd - 1) return 1;
memcpy(absbin, cwd, ncwd);
absbin[ncwd] = '/';
memcpy(absbin + ncwd + 1, bin, nbin + 1);
bin = absbin;
}
@@ -110,13 +117,13 @@ main(void)
pat, crc, rows[i].rc);
fail = 1;
}
for (int k = 0; k < 4 && rows[i].must[k]; k++)
for (int k = 0; k < Nexpect && rows[i].must[k]; k++)
if (!strstr(cout, rows[i].must[k])) {
fprintf(stderr, "989 FAIL: ww pat=%s stdout missing "
"'%s' (got '%s')\n", pat, rows[i].must[k], cout);
fail = 1;
}
for (int k = 0; k < 4 && rows[i].mustnot[k]; k++)
for (int k = 0; k < Nexpect && rows[i].mustnot[k]; k++)
if (strstr(cout, rows[i].mustnot[k])) {
fprintf(stderr, "989 FAIL: ww pat=%s stdout has "
"forbidden '%s' (got '%s')\n",