From 17b2879c3eda59c143cacfe44a1939b85a604765 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Fri, 21 Aug 2026 16:15:12 +0900 Subject: [PATCH] ww test: report no tests to run --- docs/build-system.md | 183 ++++++++++++++++++++++++++- docs/spec.md | 18 ++- docs/test-system-v2.md | 18 ++- internal/wwpackage/package.ww | 12 ++ lib/test/run.ww | 7 +- test/package/package_test.ww | 229 +++++++++++++++++++++++++++++++++- 6 files changed, 443 insertions(+), 24 deletions(-) diff --git a/docs/build-system.md b/docs/build-system.md index 3b4911d1..74a17c8a 100644 --- a/docs/build-system.md +++ b/docs/build-system.md @@ -7635,8 +7635,9 @@ 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. +selected tests remains distinct: it keeps its +discovered/selected/started/completed accounting and, as implemented later in +section 11.39, uses the pinned no-tests warning and package-result suffix. #### Pinned Go evidence and fact classification @@ -7695,7 +7696,9 @@ runtime difference: ([`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)). + ([`go/build/build_test.go`, lines 105–133](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/go/build/build_test.go#L105-L133), + [`testdata/multi/file.go`, lines 1–5](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/go/build/testdata/multi/file.go#L1-L5), + and [`file_appengine.go`, lines 1–5](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/go/build/testdata/multi/file_appengine.go#L1-L5)). Both WW stages rejected an `alpha`/`beta` directory with zero stdout and the same 159 stderr bytes (SHA-256 `eaaa0c91f41b5d3deac4caf4299d5c7c650a1330be9b43edd090b8bfea906076`). @@ -7742,7 +7745,8 @@ 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. +were unchanged by this list-only slice; section 11.39 subsequently changes only +the ordinary no-match warning and successful result annotation. The shared support implementation change legitimately changes its object, archive, and linked test-product bytes. Its exported signature and `.wwi` byte @@ -7766,7 +7770,8 @@ 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. +the section-11.37 baseline for ordinary non-list reporting; section 11.39 +supersedes that private marker while retaining the accounting. No persisted-byte contract changed: build workdir format remains `18`, test workdir format remains `19`, and semantic storage format remains `3`. @@ -7946,6 +7951,174 @@ check introduces no independently interruptible or shared state. No valid persisted-byte contract changed. Build workdir format remains `18`, test workdir format remains `19`, and semantic storage format remains `3`. +### 11.39 Implemented ordinary zero-match test results + +An ordinary successful directory `ww test` whose valid `-run`/`-filter` +selection starts no registered test now uses Go's externally visible no-tests +protocol. The shared test runtime writes exactly +`testing: warning: no tests to run\n` through standard error, retains WW's +discovered/selected/started/completed accounting, and returns success. The +directory coordinator recognizes that exact line at capture byte zero or after +a newline and appends ` [no tests to run]` to the corresponding successful +package `ok` result. WW's former `[no matches]` sentinel is no longer emitted. + +This is deliberately distinct from the completed list-mode rule: list mode +returns before the warning/accounting branch and retains an unsuffixed package +result. A source-bearing directory without test files also retains its separate +`? [no test files]` result and starts no runtime product. + +#### Pinned Go evidence and fact classification + +The sole semantic authority is official Go 1.26.5 at commit +`c19862e5f8415b4f24b189d065ed739517c548ba`: + +- `testing.(*M).Run` returns directly from list mode at lines 2407–2411. In + ordinary execution it gathers whether tests, examples, or fuzz targets ran, + writes exactly `testing: warning: no tests to run` to stderr when none did, + and keeps the outcome successful when no independent failure occurred + ([`testing/testing.go`, lines 2432–2485](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/testing/testing.go#L2432-L2485)). +- `cmd/go` defines the line-delimited `noTestsToRun` marker + ([`cmd/go/internal/test/test.go`, line 1385](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/test/test.go#L1385)) + and, after a successful test process, recognizes it at capture byte zero or + after a newline and appends ` [no tests to run]` to the package result + ([method `(*runTestActor).Act`, lines 1706–1732](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/test/test.go#L1706-L1732)). +- Official script/testdata `test_match_no_tests.txt` runs one registered test + through a nonmatching filter and asserts the suffixed successful package + result + ([lines 1–11](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/testdata/script/test_match_no_tests.txt#L1-L11)). +- Official precedence script/testdata `test_match_no_tests_build_failure.txt` + asserts that a build failure under a nonmatching filter produces `FAIL` and + does not acquire a successful no-tests result + ([lines 1–18](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/testdata/script/test_match_no_tests_build_failure.txt#L1-L18)). + +Those branches, exact bytes, success conditions, delimiter checks, result +suffix, and script assertions are **behavior directly implemented or asserted +by pinned Go**. That the runtime owns whether a test ran, the coordinator owns +the package-result annotation, a build failure precedes both, and an arbitrary +mid-line substring is not the marker are **behavior derived from the pinned +implementation**. + +WW retains its fnmatch-based local `-run`/`-filter` language rather than +adopting Go regular expressions. WW also has an established always-visible +harness report rather than Go's quiet/`-v` presentation switch, so this slice +does not suppress every successful product capture or replace WW accounting +with Go's `PASS` line. Within that honest local presentation boundary, the +zero-execution warning, stream owner, success classification, marker delimiter, +and package annotation apply directly. + +#### Fresh four-axis audit and direct pre-fix measurements + +The bounded audit examined all four permanent axes before selecting this test +runtime/coordinator difference. Commands used Cstage `out/bin/ww` and WWstage +`out/bin/ww_ww` against identical sources: + +- **Go-like build:** pinned linker method + `(*ErrorReporter).errorUnresolved` gives 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 declared-main package with no entry, emitted empty + stdout and the same 50 stderr bytes (SHA-256 + `9ed4d7684412c6d2e615041902072c81e9e09acb3970246d89a2c8bdddd2fcfa`), + and published nothing. This applicable build property was aligned. +- **Go-like test:** with one registered test, both stages exited 0 for + `test -run no-such-*`, emitted empty stderr, and emitted the same 120 stdout + bytes (SHA-256 + `ca12f88ebf1f94d3a2ca63ed1bc1e9b5a4811a660df3af4e70c7b9624ef97c40`): + `[no matches]`, zero-selection accounting, and an unsuffixed package `ok`. + This private marker and missing result annotation were the selected gap. + Empty list selection and positive ordinary selection were already aligned + controls and stayed outside the changed branch. +- **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 one `first`/`second` directory with empty stdout and + byte-identical 161-byte stderr (SHA-256 + `a2d95681b8a97d55c26367084cd294632fa015882d1aa53b0df63f24bcf24ced`). + This applicable package property was aligned. +- **Go-like import:** pinned `unusedImports` and `errorUnusedPkg` reject every + nonblank unused import + ([`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 `importdecl0a.go` + ([lines 9–26](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/internal/types/testdata/check/importdecl0/importdecl0a.go#L9-L26)). + Both stages rejected an unused dotted `fmt` import with empty stdout and the + same semantic diagnostic; + raw stderr differed only in the deliberately stage-named private output path. + This applicable import property was aligned. + +The WW statuses, streams, lengths, hashes, and diagnostics are **directly +measured WW behavior**. The linked official implementation and testdata are +**behavior directly implemented or asserted by pinned Go**. Applying the +runtime/coordinator split without changing WW's filter syntax or harness report +is **behavior derived from the pinned implementation**. + +#### Ownership, final behavior, and preserved boundaries + +`lib/test/run.ww` is the runtime owner. Its existing `selected == 0` branch now +writes the pinned warning through the same EINTR-safe fd writer used elsewhere, +targeting stderr, then writes the unchanged accounting to stdout and returns 0. +Its earlier list return is untouched. A directly invoked retained binary +therefore exposes the warning on stderr and accounting on stdout. + +`internal/wwpackage/package.ww` is the directory result owner. It already gives +each product one combined stdout/stderr capture and emits that capture in +canonical group order. Its new predicate accepts only the exact warning at byte +zero or following `\n`; after `pkgrunok` succeeds, `pkgemitgroup` appends the +suffix to the result it already owns. Text embedded mid-line in a running test +does not match. A failed, signalled, timed-out, interrupted, or unstartable test +does not reach the successful result. A producer failure never starts the +runtime and cannot synthesize the warning or suffix. + +Both selected driver stages compile the same `lib/test` code into test products +and delegate directory execution to the same WW-native coordinator, so no +C-only or self-host-only semantic fork was introduced. Direct post-fix probes +through both stages exited 0, emitted empty coordinator stderr, and emitted the +same 159 stdout bytes (SHA-256 +`a67242ab79b9bd9bca1a32073c1fccbb9aa4fa9d8ad52ced667b25c54dc1be08`): +the warning, unchanged accounting, and suffixed package result. + +Loading and Go-platform source selection are unchanged. Production, +internal-test, external-test, recompiled-for-test, support, and generated-main +graph nodes and actions are unchanged. Compilers, assemblers, archivers, and +linkers retain their diagnostics and scheduling. The support implementation +change legitimately changes its object/archive and linked test-product bytes, +but its exported signature and `.wwi` contract do not change; valid Cstage and +WWstage retained products remain byte-identical. + +Declared package names and canonical dotted import identities remain separate. +The coordinator annotates an already-owned result; it derives no identity from +the warning, alias, declared name, path leaf, filename, physical directory, +output path, artifact name, or linker order. Physical directories remain test +cwd and result-label metadata only, never package/import/graph/action/artifact/ +symbol/`.wwi`/publication/persistence identity. + +Cold and warm persistent work produce identical result bytes and still run the +test product because there is no test-result cache. A successful retained run +publishes the privately tested executable through the existing guarded install. +A later producer failure preserves prior public bytes, and restored valid reuse +reproduces the same warning/suffix without rewriting an identical executable. +Publication rejection, rollback, existing-output preservation, concurrency, +interruption, process-group cancellation, capture separation, final `FAIL`, and +cleanup remain with their existing owners. No active `.new`, `.install`, +`.wwtxn.*`, adjacent `.sepwork`, process, or capture residue is introduced. + +The WW-native `ordinary_no_match_uses_go_warning_and_result_suffix` observer +proves both stages; cold/warm reuse; concurrent reverse-requested packages and +ordered per-product markers; exact mid-line rejection; positive, list, and +no-test-files controls; retained publication and direct stderr ownership; +producer-failure precedence; cold/repeated failure; prior-output preservation; +restored reuse; normalized diagnostic parity; retained executable byte identity; +and transaction/output-scratch cleanup. Existing signal, timeout, interruption, +and process-group observers continue to prove those unchanged mechanisms. + +This is runtime/coordinator presentation, not a persisted-byte contract. 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 36cbdb8c..ed24db7e 100644 --- a/docs/spec.md +++ b/docs/spec.md @@ -828,12 +828,18 @@ 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. +empty, the harness emits no list payload, warning, or accounting and returns +success; the coordinator still emits the ordinary successful package result +without a no-tests suffix. Ordinary non-list execution is distinct: a valid +filter selecting no tests writes exactly +`testing: warning: no tests to run\n` through the harness's standard error, +preserves the discovered/selected/started/completed accounting and successful +exit, and causes a successful directory package result to end in +` [no tests to run]`. The coordinator recognizes that exact warning only at +capture byte zero or after a newline. Directory execution combines the warning +and accounting in its product-local capture; a retained binary invoked directly +keeps the warning on standard error, its accounting on standard output, and 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 diff --git a/docs/test-system-v2.md b/docs/test-system-v2.md index 4a0b8abf..62bb4054 100644 --- a/docs/test-system-v2.md +++ b/docs/test-system-v2.md @@ -222,12 +222,18 @@ run loop exactly. Measured on the 31-package `lib/...` walk: 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. +therefore produces no harness list bytes, warning, or accounting while the +process returns success; the coordinator continues with the package's normal +unsuffixed `ok` result. Ordinary non-list zero-selection execution instead +writes the exact line `testing: warning: no tests to run` through harness +stderr, retains its accounting and successful status, and gives the successful +package `ok` result the suffix ` [no tests to run]`. Marker recognition is +line-delimited, at capture byte zero or after a newline; an arbitrary substring +in user output does not annotate the result. Concurrent products keep +independent empty or nonempty captures, markers, and suffixes, and persistent +work never caches a test result. A directly invoked retained binary exposes the +warning on stderr and accounting on stdout because no coordinator combines its +descriptors or emits a package result. Directory test binaries are always linked under the coordinator's temporary product root. `-c` independently requests a caller-visible executable copy and diff --git a/internal/wwpackage/package.ww b/internal/wwpackage/package.ww index 202dfcef..f2e67caa 100644 --- a/internal/wwpackage/package.ww +++ b/internal/wwpackage/package.ww @@ -1819,6 +1819,15 @@ fn pkgemitinstall(g: *pkggroup) bool = { return true; }; +// Pinned cmd/go recognizes the testing package's exact no-tests warning only +// at capture byte zero or after a newline. The harness owns whether a test ran; +// this coordinator owns only the corresponding package-result suffix. +fn pkgnoteststorun(output: str) bool = { + let first: str = "testing: warning: no tests to run\n"; + let later: str = "\ntesting: warning: no tests to run\n"; + return strings.hasprefix(output, first) || strings.contains(output, later); +}; + fn pkgemitgroup(g: *pkggroup, compileonly: bool, statusfailed: *bool) bool = { if (g.notests) { @@ -1850,6 +1859,9 @@ fn pkgemitgroup(g: *pkggroup, compileonly: bool, }; pkgput(os.STDOUT_FILENO, "ok "); pkglabel(g); + if (pkgnoteststorun(runoutput)) { + pkgput(os.STDOUT_FILENO, " [no tests to run]"); + }; pkgput(os.STDOUT_FILENO, "\n"); return true; }; diff --git a/lib/test/run.ww b/lib/test/run.ww index cb22e25e..79e82c91 100644 --- a/lib/test/run.ww +++ b/lib/test/run.ww @@ -116,8 +116,7 @@ export fn run(tests: [](str, *fn() void)) i32 = { return 0; }; if (selected == 0) { - if (pkgprefix.len != 0) { tstputs("[no matches]\n"); } - else { tstputs("No tests run\n"); }; + tsterrputs("testing: warning: no tests to run\n"); tstputaccounting(tests.len, 0, 0, 0); return 0; }; @@ -626,6 +625,10 @@ fn tstputs(s: str) void = { tstfdwriteall(os.STDOUT_FILENO, s.ptr, s.len: u64); }; +fn tsterrputs(s: str) void = { + tstfdwriteall(os.STDERR_FILENO, s.ptr, s.len: u64); +}; + fn tstputuint(n: i32) void = { let buf: [16]u8; let i: i32 = 16; diff --git a/test/package/package_test.ww b/test/package/package_test.ww index 55506083..e9f1bc67 100644 --- a/test/package/package_test.ww +++ b/test/package/package_test.ww @@ -835,16 +835,21 @@ fn cwdwritedata(dir: str, label: str) void = { runcommand(root, "nomatch", nav, (30i64 * (time.second: i64)): time.duration, &out); expectexit(&out, 0); - assert(occurrences(out.stdout, "[no matches]\n") == 1); + assert(out.stderr.len == 0); + assert(occurrences(out.stdout, + "testing: warning: no tests to run\n") == 1); assert(occurrences(out.stdout, "3 discovered, 0 selected, 0 started, 0 completed\n") == 1); + assert(occurrences(out.stdout, strings.concat("ok ", target, + " [routing] [no tests to run]\n")) == 1); let lnav: []str = [driver("ww"), "test", "-list", "-run", "no-such-*", "-I", sourceroot, target]; runcommand(root, "list-nomatch", lnav, (30i64 * (time.second: i64)): time.duration, &out); expectexit(&out, 0); - assert(occurrences(out.stdout, "[no matches]\n") == 0); + assert(!has(out.stdout, "testing: warning: no tests to run")); + assert(!has(out.stdout, " [no tests to run]")); assert(!has(out.stdout, " discovered, ")); clean(root); }; @@ -901,7 +906,8 @@ fn cwdwritedata(dir: str, label: str) void = { 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, "testing: warning: no tests to run")); + assert(!has(out.stdout, " [no tests to run]")); assert(!has(out.stdout, " discovered, ")); assert(!has(out.stdout, "list_alpha.first")); assert(!has(out.stdout, "list_alpha.second")); @@ -945,7 +951,8 @@ fn cwdwritedata(dir: str, label: str) void = { 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, "testing: warning: no tests to run")); + assert(!has(out.stdout, " [no tests to run]")); assert(!has(out.stdout, "list_alpha.")); assert(os.exists(retained[i])); retainedout[i] = strings.dup(out.stdout); @@ -973,6 +980,216 @@ fn cwdwritedata(dir: str, label: str) void = { clean(root); }; +@test fn ordinary_no_match_uses_go_warning_and_result_suffix() void = { + let root: str = fresh(); + let alpha: str = strings.concat(root, "/alpha"); + let beta: str = strings.concat(root, "/beta"); + let none: str = strings.concat(root, "/none"); + let broken: str = strings.concat(root, "/broken"); + mkdirall(alpha); + mkdirall(beta); + mkdirall(none); + mkdirall(broken); + writefile(strings.concat(alpha, "/alpha.ww"), + "package zero_alpha;\nfn value() i32 = { return 1; };\n"); + writefile(strings.concat(alpha, "/alpha_test.ww"), strings.concat( + "package zero_alpha;\nimport os;\n", + "@test fn first() void = {};\n", + "@test fn marker_text() void = {\n", + " let text: str = \"not-a-marker testing: warning: no tests to run\\n\";\n", + " os.write(os.STDOUT_FILENO, text.ptr, text.len: u64);\n", + "};\n")); + writefile(strings.concat(beta, "/beta.ww"), + "package zero_beta;\nfn value() i32 = { return 2; };\n"); + writefile(strings.concat(beta, "/beta_test.ww"), + "package zero_beta;\n@test fn only() void = {};\n"); + writefile(strings.concat(none, "/none.ww"), + "package zero_none;\nfn value() i32 = { return 3; };\n"); + writefile(strings.concat(broken, "/broken.ww"), + "package zero_broken;\nfn value() missing_type = { return 4; };\n"); + writefile(strings.concat(broken, "/broken_test.ww"), + "package zero_broken;\n@test fn registered() void = {};\n"); + + let warning: str = "testing: warning: no tests to run\n"; + let suffix: str = " [no tests to run]"; + 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 retained: []str = [strings.concat(root, "/retained-c.test"), + strings.concat(root, "/retained-ww.test")]; + let coldout: []str = ["", ""]; + let colderror: []str = ["", ""]; + let positiveout: []str = ["", ""]; + let positiveerror: []str = ["", ""]; + let listout: []str = ["", ""]; + let listerror: []str = ["", ""]; + let retainedout: []str = ["", ""]; + let retainederror: []str = ["", ""]; + let manualout: []str = ["", ""]; + let manualerror: []str = ["", ""]; + let failurediagnostic: []str = ["", ""]; + let out: commandout; + let i: i32 = 0; + for (i < stages.len) { + // Reverse request order and run two products concurrently. Result bytes + // remain directory-sorted and each product owns its own marker/suffix. + let emptyav: []str = [driver(stages[i]), "test", "-j", "2", + "-w", works[i], "-run", "no-such-*", beta, alpha]; + runcommand(root, strings.concat("ordinary-empty-cold-", tags[i]), + emptyav, (90i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stderr.len == 0); + assert(occurrences(out.stdout, warning) == 2); + assert(!has(out.stdout, "[no matches]")); + assert(occurrences(out.stdout, + "2 discovered, 0 selected, 0 started, 0 completed\n") == 1); + assert(occurrences(out.stdout, + "1 discovered, 0 selected, 0 started, 0 completed\n") == 1); + let alphaok: str = strings.concat("ok ", alpha, + " [zero_alpha] [no tests to run]\n"); + let betaok: str = strings.concat("ok ", beta, + " [zero_beta] [no tests to run]\n"); + let alphacount: i32 = pos(out.stdout, + "2 discovered, 0 selected, 0 started, 0 completed\n"); + let alphareport: i32 = pos(out.stdout, alphaok); + let betacount: i32 = pos(out.stdout, + "1 discovered, 0 selected, 0 started, 0 completed\n"); + let betareport: i32 = pos(out.stdout, betaok); + assert(alphacount >= 0 && alphacount < alphareport + && alphareport < betacount && betacount < betareport); + assert(occurrences(out.stdout, alphaok) == 1); + assert(occurrences(out.stdout, betaok) == 1); + coldout[i] = strings.dup(out.stdout); + colderror[i] = strings.dup(out.stderr); + + runcommand(root, strings.concat("ordinary-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])); + + // The same bytes inside a running test, but not at a line boundary, are + // ordinary user output and must not annotate a successful package result. + let positiveav: []str = [driver(stages[i]), "test", "-w", + works[i], "-run", "marker_text", alpha]; + runcommand(root, strings.concat("ordinary-positive-", tags[i]), + positiveav, (90i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stderr.len == 0); + assert(occurrences(out.stdout, warning) == 1); + assert(has(out.stdout, strings.concat("not-a-marker ", warning))); + assert(!has(out.stdout, suffix)); + assert(has(out.stdout, strings.concat("ok ", alpha, + " [zero_alpha]\n"))); + positiveout[i] = strings.dup(out.stdout); + positiveerror[i] = strings.dup(out.stderr); + + // The already-closed list behavior remains an early return: no warning, + // accounting, old sentinel, or package suffix. + let listav: []str = [driver(stages[i]), "test", "-w", works[i], + "-list", "-run", "no-such-*", alpha]; + runcommand(root, strings.concat("ordinary-list-control-", tags[i]), + listav, (90i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(same(out.stdout, strings.concat("ok ", alpha, + " [zero_alpha]\n"))); + assert(out.stderr.len == 0); + assert(!has(out.stdout, warning) && !has(out.stdout, suffix)); + assert(!has(out.stdout, " discovered, ")); + listout[i] = strings.dup(out.stdout); + listerror[i] = strings.dup(out.stderr); + + let noneav: []str = [driver(stages[i]), "test", "-w", works[i], + none]; + runcommand(root, strings.concat("ordinary-no-files-", tags[i]), + noneav, (90i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(same(out.stdout, strings.concat("? ", none, + " [no test files]\n"))); + assert(out.stderr.len == 0); + assert(!has(out.stdout, warning) && !has(out.stdout, suffix)); + + // A successful zero-selection run may publish its retained test binary. + let retainav: []str = [driver(stages[i]), "test", "-w", works[i], + "-o", retained[i], "-run", "no-such-*", alpha]; + runcommand(root, strings.concat("ordinary-retained-", tags[i]), + retainav, (90i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stderr.len == 0); + assert(occurrences(out.stdout, warning) == 1); + assert(occurrences(out.stdout, alphaok) == 1); + assert(os.exists(retained[i])); + assert(!os.exists(strings.concat(retained[i], ".sepwork"))); + let prior: str = strings.dup(readfile(retained[i])); + retainedout[i] = strings.dup(out.stdout); + retainederror[i] = strings.dup(out.stderr); + + // Direct execution exposes the harness-owned stream boundary: accounting + // remains stdout while the exact pinned warning is stderr. + let manualav: []str = [retained[i], "-package=zero_alpha", + "no-such-*"]; + runcommand(root, strings.concat("ordinary-manual-", tags[i]), manualav, + (30i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(same(out.stdout, + "2 discovered, 0 selected, 0 started, 0 completed\n")); + assert(same(out.stderr, warning)); + manualout[i] = strings.dup(out.stdout); + manualerror[i] = strings.dup(out.stderr); + + // A producer failure wins before runtime and preserves a prior public + // binary. Cold and repeated failure have no warning, ok line, or suffix. + let failav: []str = [driver(stages[i]), "test", "-w", works[i], + "-o", retained[i], "-run", "no-such-*", broken]; + runcommand(root, strings.concat("ordinary-failure-cold-", tags[i]), + failav, (90i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(same(out.stdout, "FAIL\n")); + assert(out.stderr.len != 0); + assert(!has(out.stdout, warning) && !has(out.stderr, warning)); + assert(!has(out.stdout, suffix) && !has(out.stderr, suffix)); + assert(!has(out.stdout, "ok ") && !has(out.stderr, "ok ")); + assert(same(prior, readfile(retained[i]))); + failurediagnostic[i] = strings.dup(primarydiagnostic(out.stderr)); + let failstderr: str = strings.dup(out.stderr); + + runcommand(root, strings.concat("ordinary-failure-warm-", tags[i]), + failav, (90i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(same(out.stdout, "FAIL\n")); + assert(same(out.stderr, failstderr)); + assert(same(prior, readfile(retained[i]))); + assert(!has(out.stdout, warning) && !has(out.stderr, warning)); + + // Restored valid reuse still emits the success marker/suffix and never + // rewrites an already-identical retained binary. + runcommand(root, strings.concat("ordinary-restored-", tags[i]), retainav, + (90i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(same(out.stdout, retainedout[i])); + assert(same(out.stderr, retainederror[i])); + assert(same(prior, readfile(retained[i]))); + assert(!directoryhasnew(works[i])); + assert(!os.exists(strings.concat(retained[i], ".sepwork"))); + 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(listout[0], listout[1])); + assert(same(listerror[0], listerror[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(failurediagnostic[0], failurediagnostic[1])); + assert(same(readfile(retained[0]), readfile(retained[1]))); + clean(root); +}; + // Directory-package execution context is one product-level runtime property. // This fixture keeps every source tree disposable while covering the variant, // dependency, environment, routing, concurrency, persistence, and failure @@ -1317,7 +1534,9 @@ fn cwdwritedata(dir: str, label: str) void = { cwdassertpackage(out.stdout, "dep-init", p3, "p3-data", "p3-testdata"); cwdassertpackage(out.stdout, "testdep-init", p3, "p3-data", "p3-testdata"); - assert(has(out.stdout, "[no matches]\n")); + assert(has(out.stdout, "testing: warning: no tests to run\n")); + assert(has(out.stdout, strings.concat("ok ", p3, + " [p3] [no tests to run]\n"))); assert(!has(out.stdout, "p3-internal cwd=")); assert(!has(out.stdout, "p3-external cwd="));