ww: report no test files for empty test packages

This commit is contained in:
2026-08-21 11:59:46 +09:00
parent 394e46ff2f
commit bcc3aa9336
6 changed files with 195 additions and 50 deletions

View File

@@ -3180,7 +3180,7 @@ still checked; that is a real zero-test package and runs one empty combined
harness. A directory with no selected test files instead follows the no-real-run
path: validate/compile ordinary production as needed, publish status only, and
create no support action, generated main, link, binary, result, or process. The
coordinator alone emits its `[no tests]` report.
coordinator alone emits its `[no test files]` report.
Persistent workdirs keep a global driver/compiler/assembler/stamp identity and
per-action committed units. When that global identity is stale, the command
@@ -5593,12 +5593,14 @@ precise body/export and target-removal/re-addition invalidation, combined-graph
allocation failure, repeated late internal/external/main/link rollback, and a
complete work-directory sweep for staged residue.
Deliberately unchanged or unsupported behavior includes the raw single-file
compatibility path, WW's `[no tests]` presentation text, Go modules and build
cache, network resolution, manifests, coverage/vet/fuzz/benchmark generation,
source-level build expressions, quoted/grouped/dot imports, and targets other
than the separately specified fixed `linux/amd64` filename selection. None is
used to define canonical package or directory-product identity.
At completion of this earlier topology slice, deliberately unchanged or
unsupported behavior included the raw single-file compatibility path, the
then-current `[no tests]` presentation text, Go modules and build cache, network
resolution, manifests, coverage/vet/fuzz/benchmark generation, source-level
build expressions, quoted/grouped/dot imports, and targets other than the
separately specified fixed `linux/amd64` filename selection. Section 11.35
subsequently closes only that presentation-text gap. None is used to define
canonical package or directory-product identity.
### 11.23 Implemented case-fold collision preflight
@@ -6271,9 +6273,9 @@ selected package; missing parents are created with `0777` subject to umask. A
non-directory destination accepts exactly one selected package. Exact
`/dev/null` suppresses retained copies, permits duplicate visible names, and
does not suppress execution unless `-c` is also present. A no-test package
performs ordinary production validation, reports `[no tests]`, and creates no
binary or otherwise-unused output directory. Successful compile-only products
are silent, matching Go's no-op print action.
performs ordinary production validation, reports `[no test files]`, and creates
no binary or otherwise-unused output directory. Successful test-bearing
compile-only products are silent, matching Go's no-op print action.
For `-c`, the driver copies the private runnable bytes to a distinct `.new`
inode opened with executable mode `0777` subject to umask. Temporary runnable,
@@ -7343,6 +7345,127 @@ Existing directory execution observers cover signals, timeouts, child cleanup,
and canonical-order behavior with the new final line, while build-mode controls
prove that the other command axis remains silent.
### 11.35 Implemented Go-like no-test-files package result
A source-bearing directory test product with no selected `*_test.ww` file now
reports exactly `? <package> [no test files]\n` after ordinary production
validation. It continues to create no test-support action, generated main,
link, runnable, retained binary, captured runtime result, or process. The rule
is shared by explicit directories, implicit current-directory selection,
logical/dotted targets, recursive discovery, and compile-only or retained-output
requests. Platform-ineligible test filenames do not prevent the result. A
selected helper-only test file remains a real test product with an empty
harness; the raw single-file compatibility route remains outside the
directory-owned selected-test-file classification.
#### Pinned Go evidence and fact classification
The sole authority is official Go 1.26.5 at commit
`c19862e5f8415b4f24b189d065ed739517c548ba`:
- `builderTest` detects
`len(p.TestGoFiles)+len(p.XTestGoFiles) == 0`, keeps ordinary production as a
dependency, constructs output-preparation and print actions, and creates no
real test binary or run action
([`cmd/go/internal/test/test.go`, lines 11331169](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/test/test.go#L1133-L1169)).
- `(*runTestActor).Act` owns that print action and writes exactly
`? \t%s\t[no test files]\n` in the ordinary non-coverage case
([`cmd/go/internal/test/test.go`, lines 15241557, especially 15511552](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/test/test.go#L1524-L1557)).
- Official `test_no_tests.txt` invokes `go test testnorun`, requires the
`[no test files]` result, and gives production an initializer that panics if a
test binary is linked and executed
([`cmd/go/testdata/script/test_no_tests.txt`, lines 114](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/testdata/script/test_no_tests.txt#L1-L14)).
Those source rules and the script expectation are behavior directly implemented
or asserted by pinned Go. That the ordinary case compiles production, prints the
package result, and does not execute initialization is behavior derived from the
pinned action graph. Applying the bracketed status to WW's established local
package presentation is likewise derived: WW has no module import path, but its
directory product already owns the corresponding selected-test-file decision
and no-process action branch. This does not import Go's coverage behavior,
module loader, cache, manifest, registry, or network resolution.
#### Fresh four-axis audit and direct pre-fix measurements
The bounded audit considered all four permanent axes and selected only this
test-result wording gap:
- **Go-like build:** pinned unresolved-symbol handling and its special missing
`main` case are implemented by `(*ErrorReporter).errorUnresolved`
([`cmd/link/internal/ld/errors.go`, lines 2967](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/link/internal/ld/errors.go#L29-L67))
and asserted by `TestUndefinedRelocErrors`
([`cmd/link/internal/ld/ld_test.go`, lines 1945](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/link/internal/ld/ld_test.go#L19-L45))
using `issue10978/main.go`
([lines 527](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/link/internal/ld/testdata/issue10978/main.go#L5-L27)).
Both WW stages rejected a selected `package main` without `fn main`, emitted
identical 50-byte linker diagnostics, and created no output. This candidate
was aligned.
- **Go-like test:** a production directory with an aborting initializer and no
test file made both stages exit 0 with empty stderr and byte-identical 55-byte
stdout (SHA-256
`36adf30792e2900b60ec8cd02ba86e0387acebd41c4d9aab186af2c649ffe67c`):
`? /tmp/ww-go1265-four-axis.Wq8d8H/notest [no tests]\n`.
Bare implicit, logical `-I`, and `-c` forms produced the same bytes; `-c`
created no binary. A platform-excluded test file produced the same old status
class without observing its missing import or test body. These are directly
measured pre-fix WW facts and establish the selected external difference.
- **Go-like package:** `MultiplePackageError` and the package scan reject
conflicting selected declarations
([`go/build/build.go`, lines 538548 and 931967](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/go/build/build.go#L538-L548));
`TestMultiplePackageImport` asserts the file/name pairs
([`go/build/build_test.go`, lines 105133](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/go/build/build_test.go#L105-L133)).
Both WW stages rejected an `alpha`/`beta` production directory identically
before tools. This candidate was aligned.
- **Go-like import:** `unusedImports` and `errorUnusedPkg` implement unused
ordinary and renamed-import diagnostics
([`cmd/compile/internal/types2/resolver.go`, lines 706740](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/compile/internal/types2/resolver.go#L706-L740));
official `importdecl0` asserts both forms
([`internal/types/testdata/check/importdecl0/importdecl0a.go`, lines 927](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/internal/types/testdata/check/importdecl0/importdecl0a.go#L9-L27)).
After normalizing only PID-bearing scratch roots, both WW stages rejected an
unused renamed dotted import with the same diagnostic and no output. This
candidate was aligned.
The command observations in that list are directly measured WW behavior. The
linked source and testdata are behavior directly implemented or asserted by
pinned Go. Selecting only the no-test-files presentation while keeping build,
package, and import semantics fixed is behavior derived from the pinned action
boundary and WW's already aligned no-process topology.
#### Ownership, final behavior, and preserved boundaries
`internal/wwpackage.pkgemitgroup` is the sole semantic owner of the directory
package result. The loader still sets `g.notests` only after exact filename and
platform eligibility have selected the source set. Product construction still
compiles ordinary production and omits support/main/link/output/status actions;
the scheduler still skips execution. The successful result literal changes
only after that work succeeds. Loading, import, package, graph, compiler,
assembler, archiver, linker, or publication failure therefore retains its prior
diagnostic and precedence and cannot be hidden by a no-test-files result.
Canonical dotted package/import identity, declared names, file-local import
bindings, graph nodes and edges, action keys, physical-directory metadata,
symbols, `.wwi`, source units, assembly, objects, archives, and executable bytes
are unchanged. There is no new artifact or publication destination. `-c` and
`-o` still omit a binary and do not create an otherwise-unused output hierarchy;
prior caller state and persistent generations are preserved on every producer
failure. Warm reuse and invalidation still concern production actions only.
Concurrent products retain canonical ordered emission because the text remains
inside the existing group emitter. Interruption before emission makes no new
promise; the branch starts no child that can be signalled or timed out. Cleanup
still removes only request-private plan state, creates no `.new` or `.install`
stage, and leaves no test-process residue. Cstage and WWstage use the same
coordinator owner and therefore emit byte-identical diagnostics and results.
The WW-native `empty_and_invalid_package_classes` observer now requires the
exact explicit and implicit result in both stages and uses an aborting production
initializer to prove no process starts. Existing package observers cover
logical and recursive selection, platform filtering, helper-only selected test
files, compile-only/output omission, persistent cold/warm/invalidation behavior,
large scheduling sets, failure rollback, stage parity, and artifact-byte
identity. 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.

View File

@@ -710,7 +710,9 @@ runtime lets, and init declarations found only in `*_test.ww` never enter an
ordinary production build. Test-only same-package, external-package, and valid
mixed directories are accepted from their selected test files. A directory
with no selected test file validates ordinary production but creates no test
support, generated main, link, binary, result, or process.
support, generated main, link, binary, captured runtime result, or process. The
coordinator reports that successful validation exactly as
`? <package> [no test files]\n`.
Every test-bearing directory product links one request-private runnable.
`-c` retains an executable copy and suppresses its execution; `-o` retains a

View File

@@ -207,9 +207,10 @@ not followed. A wildcard cannot consume a non-terminal exact `vendor` element,
while an explicitly vendor-rooted pattern remains legal. Every selected
test-bearing directory becomes exactly one package run, binary, and result even
when both internal and external actions exist. Selected source-bearing
directories without selected test files report the usual `?` line after
ordinary production validation, with no support, generated main, link, binary,
result, or process. The coordinator first
directories without selected test files report exactly
`? <package> [no test files]\n` after ordinary production validation, with
no support, generated main, link, binary, captured runtime result, or process.
The coordinator first
launches one driver plan for the complete command-global package/action union.
After that shared build completes, `-j N` schedules up to N successful selected
test binaries concurrently under `os.exec` start/poll supervision (no threads);
@@ -230,9 +231,10 @@ names reject before tools; exact `/dev/null` discards every copy and permits
duplicate names. The private test link still runs unless `-c` suppresses it;
the raw single-file compatibility route also links into driver-owned private
storage instead of using `/dev/null` as an artifact or `.sepwork` stem. A
package with no selected test source validates production
but publishes nothing and does not create an otherwise unneeded output
directory. Successful `-c` output is silent apart from no-test reporting.
package with no selected test source validates production, reports the same
`[no test files]` result, publishes nothing, and does not create an otherwise
unneeded output directory. Successful `-c` output is silent apart from that
no-test-files report.
The package suite also pins the adjacent `ww build` output-directory branch.
After normal loading, an existing directory (including a symlink to one) or an

View File

@@ -1720,7 +1720,7 @@ fn pkgemitgroup(g: *pkggroup, compileonly: bool,
if (g.notests) {
pkgput(os.STDOUT_FILENO, "? ");
pkgput(os.STDOUT_FILENO, g.dir);
pkgputln(os.STDOUT_FILENO, " [no tests]");
pkgputln(os.STDOUT_FILENO, " [no test files]");
return true;
};
if (compileonly) {

View File

@@ -1,3 +1,5 @@
package no_tests;
fn init() void = { abort(); };
export fn value() int = { return 1; };

View File

@@ -1407,8 +1407,8 @@ fn cwdwritedata(dir: str, label: str) void = {
expectexit(&outw, 0);
assert(same(outc.stdout, outw.stdout));
assert(same(outc.stderr, outw.stderr) && outc.stderr.len == 0);
assert(has(outc.stdout, strings.concat("? ", plain, " [no tests]\n")));
assert(has(outc.stdout, strings.concat("? ", cmdno, " [no tests]\n")));
assert(has(outc.stdout, strings.concat("? ", plain, " [no test files]\n")));
assert(has(outc.stdout, strings.concat("? ", cmdno, " [no test files]\n")));
assert(!has(outc.stdout, "plain-init"));
assert(!has(outc.stdout, "cmdno-init"));
assert(!has(outc.stdout, "cmdno-main"));
@@ -1968,13 +1968,29 @@ fn cwdwritedata(dir: str, label: str) void = {
@test fn empty_and_invalid_package_classes() void = {
let root: str = fresh();
let av: []str = [driver("ww"), "test", packagepath("no_tests")];
let stages: []str = ["ww", "ww_ww"];
let out: commandout;
runcommand(root, "notests", av,
(30i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
assert(has(out.stdout, " [no tests]\n"));
assert(!has(out.stdout, " ... ok"));
let nofiles: str = strings.concat("? ", packagepath("no_tests"),
" [no test files]\n");
let si: i32 = 0;
for (si < stages.len) {
let av: []str = [driver(stages[si]), "test", packagepath("no_tests")];
runcommand(root, strings.concat("notests-explicit-", stages[si]), av,
(30i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
assert(same(out.stdout, nofiles));
assert(out.stderr.len == 0);
assert(!has(out.stdout, " ... ok"));
let implicit: []str = [driver(stages[si]), "test"];
runcommanddir(root, strings.concat("notests-implicit-", stages[si]),
packagepath("no_tests"), implicit,
(30i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
assert(same(out.stdout, nofiles));
assert(out.stderr.len == 0);
si += 1;
};
let bad: []str = [driver("ww"), "test", packagepath("bad_external")];
runcommand(root, "badexternal", bad,
@@ -2000,7 +2016,7 @@ fn cwdwritedata(dir: str, label: str) void = {
(30i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 1);
assert(has(out.stderr, "cannot find package nowhere"));
assert(!has(out.stdout, "[no tests]"));
assert(!has(out.stdout, "[no test files]"));
// A test file is part of its variant even when it only supplies helpers.
// Its imports and package clause must therefore reach the shared loader;
@@ -2011,8 +2027,7 @@ fn cwdwritedata(dir: str, label: str) void = {
"package helperbad;\nexport fn value() i32 = { return 1; };\n");
writefile(strings.concat(helperbad, "/helper_test.ww"),
"package helperbad_test;\nimport nowhere;\nfn helper() void = { };\n");
let stages: []str = ["ww", "ww_ww"];
let si: i32 = 0;
si = 0;
for (si < stages.len) {
let helperav: []str = [driver(stages[si]), "test", helperbad];
runcommand(root, strings.concat("helper-only-", stages[si]), helperav,
@@ -2078,7 +2093,7 @@ fn cwdwritedata(dir: str, label: str) void = {
(30i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
assert(has(out.stdout, "multiline.actually_runs ... ok\n"));
assert(!has(out.stdout, "[no tests]"));
assert(!has(out.stdout, "[no test files]"));
let commented: str = strings.concat(root, "/commented-test");
assert(os.mkdir(commented, 448i32) == 0);
@@ -2093,7 +2108,7 @@ fn cwdwritedata(dir: str, label: str) void = {
(30i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
assert(has(out.stdout, "ok "));
assert(!has(out.stdout, " [no tests]\n"));
assert(!has(out.stdout, " [no test files]\n"));
assert(!has(out.stdout, "phantom"));
// Go's complete empty-package matrix: production-only, internal-only,
@@ -2163,9 +2178,9 @@ fn cwdwritedata(dir: str, label: str) void = {
};
shi += 1;
};
assert(has(shaperefs[0], " [no tests]\n"));
assert(has(shaperefs[0], " [no test files]\n"));
assert(occurrences(shaperefs[0], strings.concat(
"? ", prodonly, " [no tests]\n")) == 1);
"? ", prodonly, " [no test files]\n")) == 1);
assert(has(shaperefs[1], "shapeinternal.internal_runs ... ok\n"));
assert(has(shaperefs[2], "shapeexternal.external_runs ... ok\n"));
assert(occurrences(shaperefs[3], " ... ok\n") == 2);
@@ -3979,7 +3994,7 @@ fn cwdwritedata(dir: str, label: str) void = {
expectexit(&out, 0);
assert(occurrences(out.stdout, "built ") == 0);
assert(has(out.stdout, strings.concat(
"? ", gamma, " [no tests]\n")));
"? ", gamma, " [no test files]\n")));
assert(os.exists(publicalpha) && os.exists(publicbeta));
assert(!os.exists(publicgamma));
assert(!os.exists(strings.concat(alpha, "/alpha_test.test")));
@@ -5039,7 +5054,7 @@ fn cwdwritedata(dir: str, label: str) void = {
assert(has(out.stdout, strings.concat("ok ", suite,
"/p051 [p051]\n")));
assert(has(out.stdout, strings.concat("? ", suite,
"/p256 [no tests]\n")));
"/p256 [no test files]\n")));
let beforecompiler: i32 = readfile(compilertraces[si]).len;
runcommandenv(root, strings.concat("dynamic-coordinator-warm-",
@@ -8228,7 +8243,7 @@ fn cwdwritedata(dir: str, label: str) void = {
assert(has(out.stdout, "selected_internal_pair ... ok\n"));
assert(has(out.stdout, "selected_external_pair ... ok\n"));
assert(has(out.stdout, strings.concat("? ", notests,
" [no tests]\n")));
" [no test files]\n")));
assert(!has(out.stdout, "wrong_target_test"));
assert(!has(out.stderr, "only_windows"));
assert(!has(readfile(compilertrace), "dep.wrong"));
@@ -9307,11 +9322,11 @@ fn cwdwritedata(dir: str, label: str) void = {
assert(has(seq, strings.concat("ok ", tree,
"/domain/app [app]\n")));
assert(has(seq, strings.concat("? ", tree,
"/gamma [no tests]\n")));
"/gamma [no test files]\n")));
assert(has(seq, strings.concat("? ", tree,
"/vendor [no tests]\n")));
"/vendor [no test files]\n")));
assert(has(seq, strings.concat("? ", tree,
"/vendorx [no tests]\n")));
"/vendorx [no test files]\n")));
let rootreport: i32 = pos(seq, strings.concat("ok ", tree,
" [rootpkg]\n"));
let alphareport: i32 = pos(seq, strings.concat("ok ", tree,
@@ -9323,11 +9338,11 @@ fn cwdwritedata(dir: str, label: str) void = {
let domainreport: i32 = pos(seq, strings.concat("ok ", tree,
"/domain/app [app]\n"));
let gammareport: i32 = pos(seq, strings.concat("? ", tree,
"/gamma [no tests]\n"));
"/gamma [no test files]\n"));
let vendorreport: i32 = pos(seq, strings.concat("? ", tree,
"/vendor [no tests]\n"));
"/vendor [no test files]\n"));
let vendorxreport: i32 = pos(seq, strings.concat("? ", tree,
"/vendorx [no tests]\n"));
"/vendorx [no test files]\n"));
assert(rootreport >= 0 && rootreport < alphareport
&& alphareport < appreport && appreport < innerreport
&& innerreport < domainreport && domainreport < gammareport
@@ -9342,11 +9357,11 @@ fn cwdwritedata(dir: str, label: str) void = {
assert(occurrences(seq, strings.concat("ok ", tree,
"/domain/app [app]\n")) == 1);
assert(occurrences(seq, strings.concat("? ", tree,
"/gamma [no tests]\n")) == 1);
"/gamma [no test files]\n")) == 1);
assert(occurrences(seq, strings.concat("? ", tree,
"/vendor [no tests]\n")) == 1);
"/vendor [no test files]\n")) == 1);
assert(occurrences(seq, strings.concat("? ", tree,
"/vendorx [no tests]\n")) == 1);
"/vendorx [no test files]\n")) == 1);
assert(!has(seq, ".hidden"));
assert(!has(seq, "_skip"));
assert(!has(seq, "/testdata"));
@@ -9488,7 +9503,7 @@ fn cwdwritedata(dir: str, label: str) void = {
assert(same(outc.stdout, outw.stdout));
assert(same(outc.stderr, outw.stderr));
assert(has(outc.stdout, strings.concat("? ", tree,
"/vendor [no tests]\n")));
"/vendor [no test files]\n")));
assert(has(outc.stdout, strings.concat("ok ", tree,
"/vendor/bad [bad]\n")));
assert(!has(outc.stdout, "/vendor/bad/vendor/deep"));
@@ -9506,14 +9521,14 @@ fn cwdwritedata(dir: str, label: str) void = {
assert(same(outc.stdout, outw.stdout));
assert(same(outc.stderr, outw.stderr));
assert(has(outc.stdout, strings.concat("? ", tree,
"/app/vendor/lib/math [no tests]\n")));
"/app/vendor/lib/math [no test files]\n")));
let vendorroots: []str = [driver("ww"), "test", "-I", tree,
strings.concat(tree, "/app/vendor/...")];
runcommand(root, "tree-pattern-below-vendor", vendorroots,
(30i64 * (time.second: i64)): time.duration, &outc);
expectexit(&outc, 0);
assert(has(outc.stdout, strings.concat("? ", tree,
"/app/vendor/lib/math [no tests]\n")));
"/app/vendor/lib/math [no test files]\n")));
// A child symlink is never traversed. The same symlink is followed when it
// is itself the explicit traversal root, then canonical output names it once.
@@ -12690,7 +12705,8 @@ fn runtimepath(relative: str) str = {
let fanpkg: str = strings.concat(fanarg, "pkg.test");
assert(os.exists(fanabin) && os.exists(fanpkg));
assert(!os.exists(strings.concat(fanarg, "znone.test")));
assert(has(out.stdout, strings.concat("? ", none, " [no tests]\n")));
assert(has(out.stdout, strings.concat("? ", none,
" [no test files]\n")));
if (i == 0) {
fanabytes = readfile(fanabin);
fanpkgbytes = readfile(fanpkg);
@@ -12945,7 +12961,7 @@ fn runtimepath(relative: str) str = {
(60i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
assert(has(out.stdout, strings.concat("? ", none,
" [no tests]\n")));
" [no test files]\n")));
assert(out.stderr.len == 0 && !os.exists(noneroot));
// Pinned builderTest makes the install action depend on the run action.