ww: put test result trailers on a new line

This commit is contained in:
2026-08-21 06:19:26 +09:00
parent 9b6b1100f4
commit b5fb478613
5 changed files with 281 additions and 9 deletions

View File

@@ -7047,6 +7047,149 @@ transaction, stage, workdir, and generated-file cleanup. Existing signal,
timeout, interruption, concurrent-driver, and public-output transaction owners
cover the unchanged mechanisms at those boundaries.
### 11.33 Implemented newline before directory test result trailers
When a directory-owned test product has emitted a nonempty combined capture
whose final byte is not newline, `ww test` now emits exactly one newline before
its existing `ok` or run-status `FAIL` trailer. Empty and already
newline-terminated captures gain no byte. The rule belongs only to the
coordinator boundary between completed test-process output and its result
trailer; it does not rewrite the capture or affect a route with no coordinator
trailer.
#### Pinned Go evidence and classification
The sole authority is official Go 1.26.5 at commit
`c19862e5f8415b4f24b189d065ed739517c548ba`:
- `(*runTestActor).Act` selects one output writer/buffer for a package test
([`cmd/go/internal/test/test.go`, lines 14361499](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/test/test.go#L1436-L1499)),
then assigns that same writer to the test command's stdout and stderr and
retains the resulting bytes as `out`
([lines 16611708](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/test/test.go#L1661-L1708)).
- On success, a nonempty `out` without a trailing newline receives one before
the `ok` record
([lines 17121732](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/test/test.go#L1712-L1732)).
On failure with partial output, the same check inserts one before `FAIL`
([lines 17331769](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/test/test.go#L1733-L1769)).
- Official `test_fail_newline.txt` asserts that buffered partial failure output
and `FAIL` begin on different lines, and that buffered verbose partial
success output and `ok` begin on different lines. It also records the
deliberate streaming-mode exception
([lines 335](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/testdata/script/test_fail_newline.txt#L3-L35)).
Those source rules and official script assertions are behavior directly
implemented or asserted by pinned Go. Applying the buffered-package boundary
to WW's directory-product capture is derived from that implementation: WW's
manifest-free directory coordinator likewise owns the completed combined bytes
and immediately appends a package result trailer. Excluding raw single-file
tests and later manual execution of retained binaries is also derived from the
pinned distinction: those WW routes have no coordinator-owned `ok` or `FAIL`
trailer to separate.
#### Fresh four-axis audit and direct pre-fix measurements
The bounded audit considered all four permanent axes and selected only this
test-output gap:
- **Go-like build:** pinned `(*ErrorReporter).errorUnresolved` gives an
undeclared main function its dedicated link error
([`cmd/link/internal/ld/errors.go`, lines 2965](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/link/internal/ld/errors.go#L29-L65)).
`TestUndefinedRelocErrors` directly requires build failure and that message
([`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 official `issue10978/main.go`, whose main function is absent
([lines 527](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/link/internal/ld/testdata/issue10978/main.go#L5-L27)).
Direct Cstage and WWstage `ww build -o /dev/null` probes of a selected WW
command package without `main` both exited `1`, produced the identical
`w6l: undefined reference to 'main'` then `ww: w6l failed` diagnostics, and
published nothing. This candidate was aligned.
- **Go-like test:** a selected external `*_test.ww` initializer wrote exactly
`partial-success` without newline and exited `0`. Both stages exited `0`,
wrote no stderr, and emitted 72 stdout bytes beginning
`partial-successok `. The corresponding initializer wrote exactly
`partial-failure` to stderr and exited `7`; both commands exited `1`, wrote
no coordinator stderr, and emitted 86 stdout bytes beginning
`partial-failureFAIL `. Full Cstage and WWstage captures were byte-identical.
These are directly measured pre-fix WW facts and establish the selected
externally observable difference.
- **Go-like package:** `MultiplePackageError` represents conflicting selected
package clauses and formats the two declarations
([`go/build/build.go`, lines 538548](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/go/build/build.go#L538-L548));
the scanner creates it when selected files disagree
([lines 930967](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/go/build/build.go#L930-L967)),
and `TestMultiplePackageImport` asserts the typed result and files
([`go/build/build_test.go`, lines 105124](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/go/build/build_test.go#L105-L124)).
Direct WW directories declaring `alpha` and `beta` were rejected before any
producer by both stages with the same positioned conflict diagnostic. This
candidate was aligned.
- **Go-like import:** `loadImport` rejects a package declared `main` when it is
imported from another directory
([`cmd/go/internal/load/pkg.go`, lines 787805](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/load/pkg.go#L787-L805));
official `import_main.txt` asserts the rule for builds and internal/external
tests
([lines 335](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/testdata/script/import_main.txt#L3-L35)).
Both WW stages rejected a direct dotted import of a package declared `main`
with the identical `ww: package cmdpkg is a program, not an importable
package` diagnostic. This candidate was aligned.
The direct Go source and official-test statements above are behavior directly
implemented or asserted by pinned Go. The WW command results are directly
measured behavior. The conclusion that the selected change is a runtime
presentation boundary, with no build, package, or import identity effect, is
derived from the pinned placement after command completion and before the
result record.
#### Ownership, final behavior, and preserved boundaries
`internal/wwpackage.pkgemitgroup` is the true owner because it alone has both
the completed product-local combined capture and knowledge that an existing
directory result trailer follows. It first emits the capture unchanged, then
emits one separator only when the capture is nonempty and its last byte is not
newline, then follows the established success or failure branch. The check is
shared by Cstage and WWstage and is independent for every canonically ordered
product, including internal, external, and combined variants; dependency
initialization; filters and list mode; concurrent products; and the private run
of a retained request.
Empty output does not acquire a leading blank line, and output already ending
in newline does not acquire a second one. A nonzero exit or signal retains its
existing process classification and `FAIL` text; only a preceding partial line
is terminated. Test-harness timeout and ordinary assertion output already end
in newline and therefore remain byte-identical. A child that cannot start has
no completed capture/trailer boundary in this function. Parent interruption,
producer failure, and load, compile, assemble, archive, link, or install
failure retain their existing diagnostics and precedence. Raw single-file
tests and later direct execution of retained binaries have no package
coordinator result trailer and retain their exact process bytes.
Loading, graph construction, action construction and scheduling, compiler,
assembler, archiver, linker, generated main, test executable, and retained
artifact bytes are unchanged. The separator is emitted after the private
process completes; it is not written into the capture, executable, interface,
archive, work record, or public destination. Running retention remains private
build, private run, then guarded install. Failed, signalled, interrupted, or
unstartable runs still publish nothing and preserve prior retained bytes.
Concurrent products retain separate capture files and canonical emission;
there is no shared mutable newline state. Existing process-group cleanup,
transaction rollback, temporary-root removal, and staged-file cleanup are
unchanged.
Physical source directories remain runtime/loader metadata only and do not
become package, import, graph, action, artifact, symbol, `.wwi`, publication,
or persistence identity. No persisted byte or key changes, so build workdir
format remains `18`, test workdir format remains `19`, and semantic storage
format remains `3`.
The WW-native `directory_test_trailer_starts_on_new_line` observer proves both
stages for unterminated stdout success, unterminated stderr failure,
unterminated signal output, already terminated output, and empty output. It
also covers concurrent products, a filter, list mode, running retained
publication, retained executable byte identity, and the unchanged raw/manual
routes; complete stdout and stderr from the concurrent Cstage and WWstage runs
must match exactly. Existing package execution, timeout, interruption,
transaction, persistence, byte-identity, and cleanup owners continue to prove
the mechanisms this slice does not alter.
## 12. Candidate architectures and hard-gate decision
Five candidates were developed as coherent systems, not as feature bins.

View File

@@ -779,12 +779,16 @@ artifact, or persistence identity.
The product's standard output and standard error refer to one product-local
capture. Bytes from either descriptor retain the order in which their writes
reach that shared open output, and the coordinator emits the completed capture
on its standard output. A runtime failure or child-setup failure appends the
product status diagnostic to standard output as well. Loader, source,
compiler, assembler, 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.
on its standard output. Immediately before an existing directory-product
`ok` or run-status `FAIL` trailer, a nonempty capture whose final byte is not
newline receives exactly one newline; an empty or already newline-terminated
capture receives none. The capture itself is unchanged, and raw single-file or
later direct retained-binary execution has no coordinator trailer and no such
separator. A runtime failure or child-setup failure appends the product status
diagnostic to standard output as well. Loader, source, compiler, assembler,
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.
The coordinator does not change its own cwd or environment. Parallel products
receive independent child environments and each uses its own source directory.

View File

@@ -313,9 +313,14 @@ The same product maps fd 1 and fd 2 to one open product-local capture. This
matches Go's test-command use of one writer for `exec.Cmd.Stdout` and
`exec.Cmd.Stderr`: writes are not drained into two files and regrouped later.
The coordinator emits the completed combined bytes and any product run-status
line on stdout. Each parallel product has a distinct capture, while canonical
group-order emission remains byte-stable across `-j` values. Build-plan stdout
and stderr stay distinct, and loader/build diagnostics stay on stderr.
line on stdout. Before its existing directory-product `ok` or run-status
`FAIL` trailer, it emits exactly one newline when that capture is nonempty and
does not already end in newline. Empty and newline-terminated captures gain no
byte. Raw single-file and later direct retained-binary execution have no
coordinator trailer, so their bytes remain untouched. Each parallel product
has a distinct capture, while canonical group-order emission remains
byte-stable across `-j` values. Build-plan stdout and stderr stay distinct, and
loader/build diagnostics stay on stderr.
The physical directory remains distinct from exact dotted package identity and
from production, internal, external, recompiled, support, and generated-main
@@ -436,6 +441,14 @@ workdir for staged residue.
Command-global bounded-memory failure remains independently
owned by `allocation_failure_is_command_global`.
The focused `directory_test_trailer_starts_on_new_line` observer generates
empty, already terminated, and unterminated combined captures. Across both
stages it covers stdout success, stderr failure, signal termination,
concurrent products, filtering, listing, running retention, retained artifact
bytes, and the raw/manual routes that have no coordinator trailer. It requires
complete concurrent stdout/stderr parity and forbids both an adjacent trailer
and a doubled separator.
The same package owner contains the focused
`platform_filename_source_selection` observer. It independently generates the
suffix matrix, production/internal/external sources, wrong-target import and

View File

@@ -1724,6 +1724,9 @@ fn pkgemitgroup(g: *pkggroup, compileonly: bool) bool = {
return false;
};
pkgput(os.STDOUT_FILENO, runoutput);
if (runoutput.len != 0 && runoutput[runoutput.len - 1] != '\n') {
pkgput(os.STDOUT_FILENO, "\n");
};
if (!pkgrunok(g)) {
pkgreportcommand(os.STDOUT_FILENO, "test", g, &g.runres);
return false;

View File

@@ -1683,6 +1683,115 @@ fn cwdwritedata(dir: str, label: str) void = {
clean(root);
};
@test fn directory_test_trailer_starts_on_new_line() void = {
let root: str = fresh();
let pass: str = strings.concat(root, "/pass");
let fail: str = strings.concat(root, "/fail");
let complete: str = strings.concat(root, "/complete");
let empty: str = strings.concat(root, "/empty");
let signal: str = strings.concat(root, "/signal");
mkdirall(pass);
mkdirall(fail);
mkdirall(complete);
mkdirall(empty);
mkdirall(signal);
writefile(strings.concat(pass, "/partial_test.ww"), strings.concat(
"package trailer_pass_test;\nimport os;\n",
"fn init() void = { os.write(os.STDOUT_FILENO,",
" \"partial-success\".ptr, 15u64); os.exit(0); };\n",
"@test fn unreachable() void = { void; };\n"));
writefile(strings.concat(fail, "/partial_test.ww"), strings.concat(
"package trailer_fail_test;\nimport os;\n",
"fn init() void = { os.write(os.STDERR_FILENO,",
" \"partial-failure\".ptr, 15u64); os.exit(7); };\n",
"@test fn unreachable() void = { void; };\n"));
writefile(strings.concat(complete, "/complete_test.ww"), strings.concat(
"package trailer_complete_test;\nimport os;\n",
"fn init() void = { os.write(os.STDOUT_FILENO,",
" \"complete\\n\".ptr, 9u64); os.exit(0); };\n",
"@test fn unreachable() void = { void; };\n"));
writefile(strings.concat(empty, "/empty_test.ww"), strings.concat(
"package trailer_empty_test;\nimport os;\n",
"fn init() void = { os.exit(0); };\n",
"@test fn unreachable() void = { void; };\n"));
writefile(strings.concat(signal, "/partial_test.ww"), strings.concat(
"package trailer_signal_test;\nimport os;\n",
"fn init() void = { os.write(os.STDOUT_FILENO,",
" \"partial-signal\".ptr, 14u64);",
" os.kill(os.getpid(), os.SIGTERM); };\n",
"@test fn unreachable() void = { void; };\n"));
let stages: []str = ["ww", "ww_ww"];
let tags: []str = ["c", "ww"];
let multiout: []str = ["", ""];
let multierr: []str = ["", ""];
let retained: []str = ["", ""];
let out: commandout;
let i: i32 = 0;
for (i < stages.len) {
let mav: []str = [driver(stages[i]), "test", "-j", "5", "-run",
"unreachable", pass, fail, complete, empty, signal];
runcommand(root, strings.concat("trailer-multi-", tags[i]), mav,
(90i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 1);
assert(out.stderr.len == 0);
assert(has(out.stdout, "partial-success\nok "));
assert(!has(out.stdout, "partial-successok "));
assert(has(out.stdout, "partial-failure\nFAIL "));
assert(!has(out.stdout, "partial-failureFAIL "));
assert(has(out.stdout, "partial-signal\nFAIL "));
assert(!has(out.stdout, "partial-signalFAIL "));
assert(has(out.stdout, "complete\nok "));
assert(!has(out.stdout, "complete\n\nok "));
multiout[i] = strings.dup(out.stdout);
multierr[i] = strings.dup(out.stderr);
let eav: []str = [driver(stages[i]), "test", empty];
runcommand(root, strings.concat("trailer-empty-", tags[i]), eav,
(60i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
assert(out.stderr.len == 0);
assert(pos(out.stdout, "ok ") == 0);
let lav: []str = [driver(stages[i]), "test", "-list", pass];
runcommand(root, strings.concat("trailer-list-", tags[i]), lav,
(60i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
assert(out.stderr.len == 0);
assert(has(out.stdout, "partial-success\nok "));
retained[i] = strings.concat(root, "/retained-", tags[i]);
let rav: []str = [driver(stages[i]), "test", "-o", retained[i],
pass];
runcommand(root, strings.concat("trailer-retained-", tags[i]), rav,
(60i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
assert(out.stderr.len == 0);
assert(has(out.stdout, "partial-success\nok "));
assert(os.exists(retained[i]));
let runav: []str = [retained[i]];
runcommand(root, strings.concat("trailer-manual-", tags[i]), runav,
(30i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
assert(same(out.stdout, "partial-success"));
assert(out.stderr.len == 0);
let rawav: []str = [driver(stages[i]), "test",
strings.concat(pass, "/partial_test.ww")];
runcommand(root, strings.concat("trailer-raw-", tags[i]), rawav,
(60i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
assert(same(out.stdout, "partial-success"));
assert(out.stderr.len == 0);
i += 1;
};
assert(same(multiout[0], multiout[1]));
assert(same(multierr[0], multierr[1]));
assert(same(readfile(retained[0]), readfile(retained[1])));
clean(root);
};
@test fn imported_dependency_tests_do_not_leak() void = {
let root: str = fresh();
let base: str = packagepath("dependency");