diff --git a/docs/build-system.md b/docs/build-system.md index a89a9815..16114f10 100644 --- a/docs/build-system.md +++ b/docs/build-system.md @@ -7628,6 +7628,149 @@ unchanged artifact and binary bytes; build/no-test/compile-only/rejection nonexecution; unchanged raw/direct compatibility; exact tool locale/TMPDIR; and stage, transaction, capture, and workdir cleanup. +### 11.37 Implemented empty list-mode output + +A valid directory-owned `ww test -list` request now emits one qualified test +name per selected descriptor and emits no harness list bytes when its filters +select zero tests. The test product still starts, package initialization still +runs, the harness returns success without accounting, and the coordinator still +emits the normal package `ok` result. Ordinary non-list execution with zero +selected tests remains distinct: it keeps `[no matches]` and its +discovered/selected/started/completed accounting. + +#### Pinned Go evidence and fact classification + +The sole authority is official Go 1.26.5 at commit +`c19862e5f8415b4f24b189d065ed739517c548ba`: + +- `cmd/go` registers `-list` among the flags forwarded to the test binary + ([`cmd/go/internal/test/testflag.go`, function `init`, lines 32–80](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/test/testflag.go#L32-L80)). +- `testing.(*M).Run` calls `listTests`, sets exit code 0, and returns before the + ordinary test execution and no-tests warning + ([`testing/testing.go`, lines 2407–2411](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/testing/testing.go#L2407-L2411) + and [2440–2458](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/testing/testing.go#L2440-L2458)). +- `listTests` validates the pattern and prints only inside successful match + branches. It has no zero-match output branch + ([`testing/testing.go`, function `listTests`, lines 2509–2535](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/testing/testing.go#L2509-L2535)). +- `testShowPass` retains successful list output, while the successful run actor + adds a no-tests suffix only if the captured bytes contain the ordinary + no-tests warning + ([`cmd/go/internal/test/test.go`, function `testShowPass`, lines 649–652](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/test/test.go#L649-L652) + and method `(*runTestActor).Act`, lines 1706–1732](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/test/test.go#L1706-L1732)). +- Official `list_test_simple.txt` asserts that list mode emits the matching Test, + Benchmark, and Example names + ([`cmd/go/testdata/script/list_test_simple.txt`, lines 3–14](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/testdata/script/list_test_simple.txt#L3-L14)). + +Those source branches and official positive assertions are behavior directly +implemented or asserted by pinned Go. That a zero-match `go test -list` run has +no list payload, avoids the ordinary no-tests warning, and may still receive the +command's normal successful package result is behavior derived from their +composition. WW retains its local `-list` plus `-run`/`-filter` syntax; only the +applicable selected-name and empty-result behavior is aligned. + +#### Fresh four-axis audit and direct pre-fix measurements + +The bounded audit examined all four permanent axes before selecting this test +runtime difference: + +- **Go-like build:** pinned `(*ErrorReporter).errorUnresolved` gives a missing + `main.main` a dedicated failure + ([`cmd/link/internal/ld/errors.go`, lines 29–67](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/link/internal/ld/errors.go#L29-L67)), + asserted by `TestUndefinedRelocErrors` and official `issue10978` + ([`cmd/link/internal/ld/ld_test.go`, lines 19–45](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/link/internal/ld/ld_test.go#L19-L45), + [`testdata/issue10978/main.go`, lines 5–27](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/link/internal/ld/testdata/issue10978/main.go#L5-L27)). + Both WW stages rejected a selected command package without `fn main`, emitted + zero stdout and the same 50 stderr bytes (SHA-256 + `9ed4d7684412c6d2e615041902072c81e9e09acb3970246d89a2c8bdddd2fcfa`), + and published no output. This audited applicable property was aligned. +- **Go-like test:** for a directory containing one `visible` test, both stages + ran `test -list -run no_such_test`, exited 0 with empty stderr, and emitted the + same 78 stdout bytes (SHA-256 + `d59638ab03a27803ca8e3fd884f341bbb1535ec9604fb69bdff1de4f608a8da0`): + `[no matches]\n` followed by the normal package result. Positive list + selection printed `list_nomatch.visible` once in both stages. This synthetic + empty-result line was the selected difference. +- **Go-like package:** pinned `MultiplePackageError` and directory scanning + reject conflicting declarations + ([`go/build/build.go`, lines 538–549](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/go/build/build.go#L538-L549) + and [939–967](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/go/build/build.go#L939-L967)), + asserted by `TestMultiplePackageImport` and official `testdata/multi` + ([`go/build/build_test.go`, lines 105–133](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/go/build/build_test.go#L105-L133)). + Both WW stages rejected an `alpha`/`beta` directory with zero stdout and the + same 159 stderr bytes (SHA-256 + `eaaa0c91f41b5d3deac4caf4299d5c7c650a1330be9b43edd090b8bfea906076`). + This audited applicable property was aligned. +- **Go-like import:** pinned `unusedImports` and `errorUnusedPkg` reject a + nonblank unused alias + ([`cmd/compile/internal/types2/resolver.go`, lines 706–740](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/compile/internal/types2/resolver.go#L706-L740)), + asserted by official `importdecl0` + ([`internal/types/testdata/check/importdecl0/importdecl0a.go`, lines 9–26](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/internal/types/testdata/check/importdecl0/importdecl0a.go#L9-L26)). + Both WW stages rejected an unused `spare` alias for dotted import `dep` with + zero stdout and the same diagnostic after only private scratch-PID + normalization (SHA-256 + `a429b027e92d52de1c5ec581b1f45c860b0e85b459c3ac7347e2719900cec511`). + This audited applicable property was aligned without changing dotted import + identity. + +The command observations and byte hashes are directly measured WW behavior. +The linked source branches and testdata assertions are behavior directly +implemented or asserted by pinned Go. Applying the empty-list rule to WW's one +local directory-owned test product while retaining its manifest-free input and +filter syntax is behavior derived from that pinned execution boundary. + +Pre-fix `test -c` products were byte-identical between Cstage and WWstage: +112829 bytes, SHA-256 +`d2994ef440d7ceb9be0a7caf53c90dd19d208e847e51ff41ecb89504f825c4c8`. +Directly running either retained product with the same nonmatching list filter +already emitted no stdout or stderr because it had no coordinator-supplied +package prefix. Explicit raw-file requests with package options remained a +separate rejected CLI shape in both stages. + +#### Ownership, final behavior, and preserved boundaries + +`lib/test.run` is the semantic owner. Its existing descriptor loop still +qualifies, filters, and prints every positive list match in order; its list +return now emits nothing extra when the selected count is zero. The package +coordinator does not recognize or strip a magic line, so identical bytes written +by package initialization or user code remain ordinary captured output. + +Loading and platform source selection are unchanged. Production, +internal-test, external-test, recompiled-for-test, support, and generated-main +nodes and actions remain unchanged. Exact dotted import identity, declared +package names, aliases, variants, physical runtime directories, graph edges, +initialization order, symbols, and publication names keep their existing roles. +The product process and package initialization still run in list mode; no +per-test child starts. Positive matching, option diagnostics and precedence, +ordinary non-list no-match output, no-test-file results, and raw-file rejection +are unchanged. + +The shared support implementation change legitimately changes its object, +archive, and linked test-product bytes. Its exported signature and `.wwi` byte +contract do not change. Existing content invalidation rebuilds the affected +support/link actions; there is no test-result cache and no new graph identity. +Running `-o` still executes the private product before guarded publication, and +`-c`, destination safety, transaction rollback, prior-output preservation, and +artifact modes are unchanged. + +Load, compile, assemble, archive, link, initialization, signal, timeout, +interruption, child-start, publication, and cleanup failure paths retain their +existing diagnostics and precedence. Parallel products keep independent +processes, captures, environments, working directories, input descriptors, and +ordered result slots. The changed runtime branch allocates and publishes no +file, and existing cleanup remains responsible for `.new`, `.install`, +`.wwtxn.*`, captures, process groups, and request scratch. + +The WW-native `list_mode_with_no_matches_emits_no_sentinel` observer proves +both stages across concurrent combined and test-only products, package +initialization, cold and warm persistent work, absence of list/accounting +sentinels, positive deterministic selection, running `-o` retention, later +direct execution, stage stdout/stderr equality, retained executable byte +identity, and `.new` cleanup. The existing routing observer separately keeps +ordinary non-list `[no matches]` plus accounting pinned. + +No persisted-byte contract changed: build workdir format remains `18`, test +workdir format remains `19`, and semantic storage format remains `3`. + ## 12. Candidate architectures and hard-gate decision Five candidates were developed as coherent systems, not as feature bins. diff --git a/docs/spec.md b/docs/spec.md index ee5401e9..3613711a 100644 --- a/docs/spec.md +++ b/docs/spec.md @@ -803,6 +803,17 @@ linker, and other build failures retain their diagnostic standard-error channel; build-action stdout and stderr remain separate. Parallel products own independent captures and are still emitted in canonical product order. +List mode starts the same directory-owned product and therefore performs its +ordinary package initialization, but it does not start any selected test child. +The harness emits exactly one newline-terminated qualified name for each test +selected by the command's filters, in descriptor order. If the selected set is +empty, the harness emits no list payload, no `[no matches]` sentinel, and no +accounting, and returns success; the coordinator still emits the ordinary +successful package result. This does not alter non-list execution: a valid +filter selecting no tests continues to emit `[no matches]` and the +discovered/selected/started/completed accounting. A retained binary invoked +directly has no coordinator package result. + After those ordered results, an ordinary explicit `ww test` request that records an attributable setup, build, or execution failure emits exactly one standalone `FAIL\n` on standard output. The line is command-owned: it follows even a later diff --git a/docs/test-system-v2.md b/docs/test-system-v2.md index 5d8708f8..7c3ec1d7 100644 --- a/docs/test-system-v2.md +++ b/docs/test-system-v2.md @@ -219,6 +219,16 @@ every `-j` level, and `-j 1` — the default — matches the former sequential run loop exactly. Measured on the 31-package `lib/...` walk: 7.0s sequential, 2.4s at `-j 4`. +List mode uses that same product process and initialization boundary but starts +no per-test child. The shared language harness emits only selected qualified +test names, one per line in descriptor order. A valid filter selecting no tests +therefore produces no harness list bytes, no `[no matches]` marker, and no +accounting while the process returns success; the coordinator continues with +the package's normal `ok` result. Ordinary non-list zero-selection execution +retains its `[no matches]` marker and accounting. Concurrent products keep +independent empty or nonempty captures, and persistent work never caches a test +result. + Directory test binaries are always linked under the coordinator's temporary product root. `-c` independently requests a caller-visible executable copy and suppresses execution. `-o` independently requests a copy and still runs the diff --git a/lib/test/run.ww b/lib/test/run.ww index 76f1641b..cb22e25e 100644 --- a/lib/test/run.ww +++ b/lib/test/run.ww @@ -113,9 +113,6 @@ export fn run(tests: [](str, *fn() void)) i32 = { }; if (list) { - if (selected == 0 && pkgprefix.len != 0) { - tstputs("[no matches]\n"); - }; return 0; }; if (selected == 0) { diff --git a/test/package/package_test.ww b/test/package/package_test.ww index dce8bc23..01c9aab9 100644 --- a/test/package/package_test.ww +++ b/test/package/package_test.ww @@ -844,7 +844,132 @@ fn cwdwritedata(dir: str, label: str) void = { runcommand(root, "list-nomatch", lnav, (30i64 * (time.second: i64)): time.duration, &out); expectexit(&out, 0); - assert(occurrences(out.stdout, "[no matches]\n") == 1); + assert(occurrences(out.stdout, "[no matches]\n") == 0); + assert(!has(out.stdout, " discovered, ")); + clean(root); +}; + +@test fn list_mode_with_no_matches_emits_no_sentinel() void = { + let root: str = fresh(); + let alpha: str = strings.concat(root, "/alpha"); + let beta: str = strings.concat(root, "/beta"); + mkdirall(alpha); + mkdirall(beta); + writefile(strings.concat(alpha, "/alpha.ww"), strings.concat( + "package list_alpha;\nimport os;\n", + "fn init() void = { os.write(os.STDOUT_FILENO,", + " \"alpha-init\\n\".ptr, 11u64); };\n")); + writefile(strings.concat(alpha, "/alpha_internal_test.ww"), + strings.concat( + "package list_alpha;\n", + "@test fn first() void = { abort(\"selected first\"); };\n", + "@test fn second() void = { abort(\"selected second\"); };\n")); + writefile(strings.concat(alpha, "/alpha_external_test.ww"), + strings.concat( + "package list_alpha_test;\n", + "@test fn external() void = { abort(\"selected external\"); };\n")); + writefile(strings.concat(beta, "/beta_test.ww"), strings.concat( + "package list_beta_test;\nimport os;\n", + "fn init() void = { os.write(os.STDOUT_FILENO,", + " \"beta-init\\n\".ptr, 10u64); };\n", + "@test fn only() void = { abort(\"selected only\"); };\n")); + + let stages: []str = ["ww", "ww_ww"]; + let tags: []str = ["c", "ww"]; + let works: []str = [strings.concat(root, "/work-c"), + strings.concat(root, "/work-ww")]; + let coldout: []str = ["", ""]; + let colderror: []str = ["", ""]; + let positiveout: []str = ["", ""]; + let positiveerror: []str = ["", ""]; + let retainedout: []str = ["", ""]; + let retainederror: []str = ["", ""]; + let manualout: []str = ["", ""]; + let manualerror: []str = ["", ""]; + let retained: []str = [strings.concat(root, "/retained-c.test"), + strings.concat(root, "/retained-ww.test")]; + let out: commandout; + let i: i32 = 0; + for (i < stages.len) { + let emptyav: []str = [driver(stages[i]), "test", "-j", "2", + "-w", works[i], "-list", "-run", "no-such-*", alpha, + beta]; + runcommand(root, strings.concat("list-empty-cold-", tags[i]), + emptyav, (90i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stderr.len == 0); + assert(occurrences(out.stdout, "alpha-init\n") == 1); + assert(occurrences(out.stdout, "beta-init\n") == 1); + assert(occurrences(out.stdout, "ok ") == 2); + assert(!has(out.stdout, "[no matches]")); + assert(!has(out.stdout, " discovered, ")); + assert(!has(out.stdout, "list_alpha.first")); + assert(!has(out.stdout, "list_alpha.second")); + assert(!has(out.stdout, "list_alpha.external")); + assert(!has(out.stdout, "list_beta.only")); + coldout[i] = strings.dup(out.stdout); + colderror[i] = strings.dup(out.stderr); + + runcommand(root, strings.concat("list-empty-warm-", tags[i]), + emptyav, (90i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(same(out.stdout, coldout[i])); + assert(same(out.stderr, colderror[i])); + assert(!directoryhasnew(works[i])); + + let positiveav: []str = [driver(stages[i]), "test", "-j", "2", + "-w", works[i], "-list", alpha, beta]; + runcommand(root, strings.concat("list-positive-", tags[i]), + positiveav, (90i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stderr.len == 0); + let first: i32 = pos(out.stdout, "list_alpha.first\n"); + let second: i32 = pos(out.stdout, "list_alpha.second\n"); + let external: i32 = pos(out.stdout, "list_alpha.external\n"); + let only: i32 = pos(out.stdout, "list_beta.only\n"); + assert(first >= 0 && first < second && second < external + && external < only); + assert(occurrences(out.stdout, "list_alpha.first\n") == 1); + assert(occurrences(out.stdout, "list_alpha.second\n") == 1); + assert(occurrences(out.stdout, "list_alpha.external\n") == 1); + assert(occurrences(out.stdout, "list_beta.only\n") == 1); + positiveout[i] = strings.dup(out.stdout); + positiveerror[i] = strings.dup(out.stderr); + + let retainav: []str = [driver(stages[i]), "test", "-w", + works[i], "-o", retained[i], "-list", "-run", + "no-such-*", alpha]; + runcommand(root, strings.concat("list-retained-", tags[i]), + retainav, (90i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stderr.len == 0); + assert(occurrences(out.stdout, "alpha-init\n") == 1); + assert(occurrences(out.stdout, "ok ") == 1); + assert(!has(out.stdout, "[no matches]")); + assert(!has(out.stdout, "list_alpha.")); + assert(os.exists(retained[i])); + retainedout[i] = strings.dup(out.stdout); + retainederror[i] = strings.dup(out.stderr); + + let manualav: []str = [retained[i], "-list", "no-such-*"]; + runcommand(root, strings.concat("list-manual-", tags[i]), manualav, + (30i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(same(out.stdout, "alpha-init\n")); + assert(out.stderr.len == 0); + manualout[i] = strings.dup(out.stdout); + manualerror[i] = strings.dup(out.stderr); + i += 1; + }; + assert(same(coldout[0], coldout[1])); + assert(same(colderror[0], colderror[1])); + assert(same(positiveout[0], positiveout[1])); + assert(same(positiveerror[0], positiveerror[1])); + assert(same(retainedout[0], retainedout[1])); + assert(same(retainederror[0], retainederror[1])); + assert(same(manualout[0], manualout[1])); + assert(same(manualerror[0], manualerror[1])); + assert(same(readfile(retained[0]), readfile(retained[1]))); clean(root); };