From b5fb478613afe642c00a7561387ccdeda4795bf0 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Fri, 21 Aug 2026 06:19:26 +0900 Subject: [PATCH] ww: put test result trailers on a new line --- docs/build-system.md | 143 ++++++++++++++++++++++++++++++++++ docs/spec.md | 16 ++-- docs/test-system-v2.md | 19 ++++- internal/wwpackage/package.ww | 3 + test/package/package_test.ww | 109 ++++++++++++++++++++++++++ 5 files changed, 281 insertions(+), 9 deletions(-) diff --git a/docs/build-system.md b/docs/build-system.md index 67e34a99..9146bec5 100644 --- a/docs/build-system.md +++ b/docs/build-system.md @@ -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 1436–1499](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 1661–1708](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 1712–1732](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 1733–1769](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 3–35](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 29–65](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 19–45](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 5–27](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 538–548](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/go/build/build.go#L538-L548)); + the scanner creates it when selected files disagree + ([lines 930–967](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 105–124](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 787–805](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 3–35](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. diff --git a/docs/spec.md b/docs/spec.md index e514fd59..2c8c5fee 100644 --- a/docs/spec.md +++ b/docs/spec.md @@ -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. diff --git a/docs/test-system-v2.md b/docs/test-system-v2.md index 13f4cbe8..2efb3ecd 100644 --- a/docs/test-system-v2.md +++ b/docs/test-system-v2.md @@ -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 diff --git a/internal/wwpackage/package.ww b/internal/wwpackage/package.ww index 030b2fec..3d6665a7 100644 --- a/internal/wwpackage/package.ww +++ b/internal/wwpackage/package.ww @@ -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; diff --git a/test/package/package_test.ww b/test/package/package_test.ww index bb09fab0..5cb226ba 100644 --- a/test/package/package_test.ww +++ b/test/package/package_test.ww @@ -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");