ww test: preserve combined product output order

Go 1.26.5 maps each test binary's stdout and stderr to the same product writer. Teach the captured executor to share one open file description for byte-equal output paths, then make directory test products emit that one ordered capture on coordinator stdout. Build captures and inherited-stdio routes remain unchanged.\n\nKeep the executor mechanism, package policy, native Cstage/WWstage proof, and normative contract together so every commit preserves the observable test-output behavior.
This commit is contained in:
2026-08-20 18:29:09 +09:00
parent b996099270
commit f28843f2f5
7 changed files with 310 additions and 48 deletions

View File

@@ -6039,6 +6039,125 @@ source-class rejection creates no persistent state, Cstage/WWstage diagnostics
and output match, compile-only binaries are equal, persisted artifact bytes do and output match, compile-only binaries are equal, persisted artifact bytes do
not change, and no `.new` residue survives. not change, and no `.new` residue survives.
### 11.26 Implemented combined ordered test-product output
Every coordinator-executed directory-package test product now maps its standard
output and standard error to one product-local open capture. The coordinator
emits that capture on stdout after the product completes, preserving the order
in which writes from either descriptor reach the shared output. Runtime and
child-setup status lines are stdout product diagnostics. WW no longer drains
two captures and emits all stdout before all stderr.
#### Pinned Go evidence and pre-fix WW behavior
The authority is official Go 1.26.5 at commit
`c19862e5f8415b4f24b189d065ed739517c548ba`:
- `runTestActor.Act` selects one output writer for the test action: direct
stdout, JSON conversion, stdout plus a buffer, or a private buffer
([`cmd/go/internal/test/test.go`, lines 14361499](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/test/test.go#L1436-L1499)).
- It assigns that same writer to both `cmd.Stdout` and `cmd.Stderr`, then runs
the test binary
([`test.go`, lines 16611697](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/test/test.go#L1661-L1697)).
Success and runtime-failure status text is written through `cmd.Stdout`
([`test.go`, lines 17121769](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/test/test.go#L1712-L1769)).
- `os/exec.Cmd` documents its shared-writer rule; `childStderr` returns the
already prepared stdout child file when the writers compare equal; and
`Start` installs the returned files as descriptors 1 and 2
([`os/exec/exec.go`, lines 208225, 565606, and 710738](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/os/exec/exec.go#L565-L606)).
- Official command testdata has test mains write only to `os.Stderr`, requires
those bytes on `go test` stdout in buffered and streaming forms, and requires
command stderr to remain empty
([`cmd/go/testdata/script/test_fail_newline.txt`, lines 335 and 4265](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/testdata/script/test_fail_newline.txt#L3-L65)).
The adjacent orphan-I/O test forwards a descendant's stderr through the test
process and likewise requires the bytes on command stdout
([`test_timeout_stdin.txt`, lines 921 and 3982](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/testdata/script/test_timeout_stdin.txt#L9-L82)).
The output destination is explicit in the pinned implementation and official
testdata. The ordering conclusion is source-derived: equal writers reuse one
child file or pipe instead of two independently drained pipes. No installed
host Go behavior is authority.
Before this slice, `pkgstartrun` supplied distinct `test.stdout` and
`test.stderr` paths. `exec.start` opened independent files, and
`pkgemitgroup` later emitted the entire stdout file followed by the entire
stderr file on different coordinator descriptors. A direct both-stage probe
that wrote `OUT-1`, `ERR-1`, `OUT-2`, `ERR-2` by alternating syscalls therefore
reported `OUT-1`, `OUT-2` on stdout and `ERR-1`, `ERR-2` on stderr. Cstage and
WWstage had byte-identical pre-fix behavior.
#### Descriptor and product ownership
The reusable captured executor owns only the descriptor mechanism. When
`stdoutpath` and `stderrpath` are byte-equal, it opens the path once with the
existing exclusive mode and obtains a close-on-exec duplicate from the same
open file description. The child maps those owned descriptors to fd 1 and fd 2.
Distinct paths retain independent exclusive opens and their prior behavior.
Path equality here selects an executor resource; it creates no filesystem,
package, import, action, symbol, artifact, or persistence identity.
The package coordinator owns the policy. One `pkggroup` now allocates one
`runoutput`, supplies it for both child paths, reads it once, and writes it to
coordinator stdout. Production, internal, external, recompiled-for-test,
support, generated-main, and test-only actions retain their exact topology and
one directory product still owns one process. Dependency initialization and
test bodies share the product descriptors naturally; no source rewriting or
manual stream forwarding exists.
Individual writes by one process retain syscall order. Descendants inheriting
the descriptors share the same open output, with ordinary kernel scheduling for
concurrent writers. Different products never share a capture. `-j N` may run
products concurrently, but the coordinator still waits for completion and
emits complete captures in canonical group order, so serial and parallel
command byte streams remain identical.
#### Diagnostics, nonexecution, and inherited routes
Successful and failing test-binary bytes, including bytes written to fd 2, are
emitted on stdout. A nonzero product, signal-classified test, timeout, or child
setup failure appends the existing `FAIL DIR [package] (test ...)` status on
stdout. Loader, source, compiler, assembler, linker, build-action, allocation,
capture-read, and cleanup diagnostics keep their established stderr channel;
captured build-plan stdout and stderr remain separate.
`ww build`, directory `ww test -c` (including `-c -o`), and a directory with no
selected test source start no product and allocate no run capture. A published
test binary invoked directly and the raw single-file compatibility route bypass
the coordinator, inherit fd 1 and fd 2 independently, and retain the caller's
stream destinations. `exec.runstdio` is unchanged. An arbitrary
`exec.command` with distinct output paths is also unchanged.
#### Failure, persistence, cleanup, and proof
Input still opens before any output. A merged output open failure creates no
child and no second capture. Duplicate, fork, descriptor-map, `chdir`, and
`execve` failures use the existing checked setup marker and close every owned
descriptor. Product failure does not erase successfully committed compilation;
sibling products retain independent output, process groups, and cleanup. The
coordinator removes its product captures with the existing temporary root, and
rejection or rollback publishes no partial result or `.new` state.
Output routing is request-time process metadata. It changes no unit, export,
assembly, object, archive, generated main, binary, action/storage key, tool
record, stamp, or persistent byte. There is still no test-result cache. Build
workdir format remains `18`, test workdir format remains `19`, and semantic
storage remains `3`.
The executor-level native proof in `test/wwfixture/process/main.ww` requires
distinct captures to stay distinct and equal paths to preserve alternating
fd-1/fd-2 bytes through one file in both compiler stages, including when the
caller closed stdout and stderr. The package owner
`directory_test_execution_working_directory` alternates real writes through
production and test-only dependency initialization; production/internal,
external, combined, recompiled, and test-only products; filters, list, and
no-match execution; success, assertion failure, signal, timeout, and post-build
`chdir` failure; serial/parallel and equivalent-root requests; cold/warm/data-
only persistence; and direct/raw boundaries. It requires Cstage/WWstage output
and diagnostics to match, failure/setup trailers to use stdout, successful
outer stderr to be empty, direct/raw stderr to remain separate, artifacts and
binaries to remain byte-identical, and every temporary or staged path to be
cleaned.
## 12. Candidate architectures and hard-gate decision ## 12. Candidate architectures and hard-gate decision
Five candidates were developed as coherent systems, not as feature bins. Five candidates were developed as coherent systems, not as feature bins.

View File

@@ -665,6 +665,16 @@ listing, no-match execution, failure, and timeout all retain that boundary.
Standard input is runtime process metadata and contributes no package, action, Standard input is runtime process metadata and contributes no package, action,
artifact, or persistence identity. 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.
The coordinator does not change its own cwd or environment. Parallel products The coordinator does not change its own cwd or environment. Parallel products
receive independent child environments and each uses its own source directory. receive independent child environments and each uses its own source directory.
Relative ordinary files, `testdata`, and writes resolve there for every Relative ordinary files, `testdata`, and writes resolve there for every
@@ -672,8 +682,8 @@ executing filter or list path. `ww build`, directory `ww test -c` (including
`-c -o`), and a no-selected-test directory execute no test child and receive no `-c -o`), and a no-selected-test directory execute no test child and receive no
execution-directory effect. A published test binary invoked directly, and the execution-directory effect. A published test binary invoked directly, and the
raw single-file compatibility route, inherit the user's invocation cwd, raw single-file compatibility route, inherit the user's invocation cwd,
environment, and standard input; no package directory or input policy is environment, and three standard descriptors; no package directory, input, or
embedded or forced by the binary. output policy is embedded or forced by the binary.
--- ---

View File

@@ -248,6 +248,14 @@ parallel products observe immediate EOF rather than consuming a shared input
offset. Production and test-only dependency initialization, filters, list mode, offset. Production and test-only dependency initialization, filters, list mode,
no-match execution, failure, and timeout use that same process boundary. no-match execution, failure, and timeout use that same process boundary.
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.
The physical directory remains distinct from exact dotted package identity and The physical directory remains distinct from exact dotted package identity and
from production, internal, external, recompiled, support, and generated-main from production, internal, external, recompiled, support, and generated-main
action identity. Only product execution uses it. Compiler, assembler, archiver, action identity. Only product execution uses it. Compiler, assembler, archiver,
@@ -337,7 +345,11 @@ creation; source-class rejection with empty workdirs; and deterministic
post-build child-`chdir` failure isolated from a successful sibling. It pads the post-build child-`chdir` failure isolated from a successful sibling. It pads the
inherited environment beyond former fixed observer sizes, requires one appended inherited environment beyond former fixed observer sizes, requires one appended
product `PWD`, requires EOF for captured actions and caller data for inherited- product `PWD`, requires EOF for captured actions and caller data for inherited-
stdio routes, and sweeps the persistent workdir for staged residue. stdio routes, alternates fd-1/fd-2 write syscalls through every executing
variant and initializer, requires one ordered stdout stream on success,
assertion failure, signal, timeout, and setup failure, requires direct/raw
inherited routes to keep their streams separate, and sweeps the persistent
workdir for staged residue.
Command-global bounded-memory failure remains independently Command-global bounded-memory failure remains independently
owned by `allocation_failure_is_command_global`. owned by `allocation_failure_is_command_global`.
@@ -513,6 +525,10 @@ invocations are not safe to run concurrently and remain mutually exclusive.
coordinators use its captured asynchronous path. An empty captured-command coordinators use its captured asynchronous path. An empty captured-command
`stdinpath` opens the null device; an explicit path supplies controlled input, `stdinpath` opens the null device; an explicit path supplies controlled input,
and either descriptor is installed before exec with checked setup reporting. and either descriptor is installed before exec with checked setup reporting.
Distinct output paths create independent exclusive captures; byte-equal output
paths open once and duplicate that descriptor so fd 1 and fd 2 share the same
open file description. The executor process proof covers both modes, including
closed caller standard descriptors.
The WW driver directly uses `os.exec.runstdio` for inherited-stdio, The WW driver directly uses `os.exec.runstdio` for inherited-stdio,
inherited-environment, leader-only compiler, assembler, linker, cleanup, run, inherited-environment, leader-only compiler, assembler, linker, cleanup, run,
and single-file-test calls. The local WW `procrun` implementation is deleted. and single-file-test calls. The local WW `procrun` implementation is deleted.

View File

@@ -35,8 +35,7 @@ type pkggroup = struct {
root: str, root: str,
bin: str, bin: str,
buildok: str, buildok: str,
runout: str, runoutput: str,
runerr: str,
state: i32, state: i32,
runstartfailed: bool, runstartfailed: bool,
runres: exec.result, runres: exec.result,
@@ -1359,8 +1358,7 @@ fn pkgsetplanpaths(p: *pkgplan, groups: []pkggroup, root: str, index: i32,
} else { } else {
if (!pkgstring(&g.bin, g.root, "/package.test")) { return false; }; if (!pkgstring(&g.bin, g.root, "/package.test")) { return false; };
}; };
if (!pkgstring(&g.runout, g.root, "/test.stdout") if (!pkgstring(&g.runoutput, g.root, "/test.output")
|| !pkgstring(&g.runerr, g.root, "/test.stderr")
|| !pkgstring(&g.buildok, g.root, "/build.ok")) { return false; }; || !pkgstring(&g.buildok, g.root, "/build.ok")) { return false; };
i += 1; i += 1;
}; };
@@ -1374,27 +1372,28 @@ fn pkglabel(g: *pkggroup) void = {
pkgput(os.STDOUT_FILENO, "]"); pkgput(os.STDOUT_FILENO, "]");
}; };
fn pkgreportcommand(kind: str, g: *pkggroup, r: *exec.result) void = { fn pkgreportcommand(fd: i32, kind: str, g: *pkggroup,
pkgput(os.STDERR_FILENO, "FAIL "); r: *exec.result) void = {
pkgput(os.STDERR_FILENO, g.dir); pkgput(fd, "FAIL ");
pkgput(os.STDERR_FILENO, " ["); pkgput(fd, g.dir);
pkgput(os.STDERR_FILENO, g.pkg); pkgput(fd, " [");
pkgput(os.STDERR_FILENO, "] ("); pkgput(fd, g.pkg);
pkgput(os.STDERR_FILENO, kind); pkgput(fd, "] (");
pkgput(fd, kind);
if (r.errno != 0 || r.cleanuperrno != 0 if (r.errno != 0 || r.cleanuperrno != 0
|| r.termination == exec.termination.ERROR) { || r.termination == exec.termination.ERROR) {
pkgput(os.STDERR_FILENO, " harness error "); pkgput(fd, " harness error ");
let code: i32 = r.errno; let code: i32 = r.errno;
if (code == 0) { code = r.cleanuperrno; }; if (code == 0) { code = r.cleanuperrno; };
pkgput(os.STDERR_FILENO, strconv.i32tos(code, strconv.base.DEC)); pkgput(fd, strconv.i32tos(code, strconv.base.DEC));
} else if (r.termination == exec.termination.SIGNAL) { } else if (r.termination == exec.termination.SIGNAL) {
pkgput(os.STDERR_FILENO, " signal "); pkgput(fd, " signal ");
pkgput(os.STDERR_FILENO, strconv.i32tos(r.code, strconv.base.DEC)); pkgput(fd, strconv.i32tos(r.code, strconv.base.DEC));
} else { } else {
pkgput(os.STDERR_FILENO, " exit "); pkgput(fd, " exit ");
pkgput(os.STDERR_FILENO, strconv.i32tos(r.code, strconv.base.DEC)); pkgput(fd, strconv.i32tos(r.code, strconv.base.DEC));
}; };
pkgputln(os.STDERR_FILENO, ")"); pkgputln(fd, ")");
}; };
fn pkgstartbuild(p: *pkgplan, groups: []pkggroup, builder: str, includes: []str, fn pkgstartbuild(p: *pkgplan, groups: []pkggroup, builder: str, includes: []str,
@@ -1542,8 +1541,8 @@ fn pkgstartrun(g: *pkggroup, filters: []str, timeoutarg: str,
rcmd.argv = ra; rcmd.argv = ra;
rcmd.env = env; rcmd.env = env;
rcmd.dir = g.dir; rcmd.dir = g.dir;
rcmd.stdoutpath = g.runout; rcmd.stdoutpath = g.runoutput;
rcmd.stderrpath = g.runerr; rcmd.stderrpath = g.runoutput;
rcmd.deadline.sec = 0i64; rcmd.deadline.sec = 0i64;
rcmd.deadline.nsec = 0i64; rcmd.deadline.nsec = 0i64;
rcmd.grace = 0i64: time.duration; rcmd.grace = 0i64: time.duration;
@@ -1580,16 +1579,14 @@ fn pkgemitgroup(g: *pkggroup, compileonly: bool) bool = {
return true; return true;
}; };
if (g.runstartfailed) { return false; }; if (g.runstartfailed) { return false; };
let runstdout: str; let runoutput: str;
let runstderr: str; if (!pkgread(g.runoutput, &runoutput)) {
if (!pkgread(g.runout, &runstdout) || !pkgread(g.runerr, &runstderr)) {
pkgfailpath(g.root, "cannot read test capture"); pkgfailpath(g.root, "cannot read test capture");
return false; return false;
}; };
pkgput(os.STDOUT_FILENO, runstdout); pkgput(os.STDOUT_FILENO, runoutput);
pkgput(os.STDERR_FILENO, runstderr);
if (!pkgrunok(g)) { if (!pkgrunok(g)) {
pkgreportcommand("test", g, &g.runres); pkgreportcommand(os.STDOUT_FILENO, "test", g, &g.runres);
return false; return false;
}; };
pkgput(os.STDOUT_FILENO, "ok "); pkgput(os.STDOUT_FILENO, "ok ");
@@ -1612,7 +1609,8 @@ fn pkgemitplan(p: *pkgplan, groups: []pkggroup,
let i: i32 = p.start; let i: i32 = p.start;
for (i < p.end) { for (i < p.end) {
if (!pkgproductbuilt(&groups[i], buildonly)) { if (!pkgproductbuilt(&groups[i], buildonly)) {
pkgreportcommand("build", &groups[i], &p.buildres); pkgreportcommand(os.STDERR_FILENO, "build", &groups[i],
&p.buildres);
failed += 1; failed += 1;
} else if (buildonly) { } else if (buildonly) {
void; void;

View File

@@ -29,7 +29,8 @@ export type result = struct {
// command is deliberately concrete. argv includes argv[0], env is the full // command is deliberately concrete. argv includes argv[0], env is the full
// environment, and an empty dir inherits the caller's working directory. // environment, and an empty dir inherits the caller's working directory.
// An empty stdinpath selects the null device; stdoutpath and stderrpath are // An empty stdinpath selects the null device; stdoutpath and stderrpath are
// created exclusively with mode 0600. A zero deadline means no timeout. // created exclusively with mode 0600. Equal output paths share one open
// capture. A zero deadline means no timeout.
export type command = struct { export type command = struct {
path: str, path: str,
argv: []str, argv: []str,
@@ -120,6 +121,16 @@ fn hasnul(s: str) bool = {
return false; return false;
}; };
fn samepath(a: str, b: str) bool = {
if (a.len != b.len) { return false; };
let i: i32 = 0;
for (i < a.len) {
if (a[i] != b[i]) { return false; };
i += 1;
};
return true;
};
fn valid(c: *command) bool = { fn valid(c: *command) bool = {
if (c.path.len == 0 || c.argv.len == 0 || c.argv[0].len == 0) { if (c.path.len == 0 || c.argv.len == 0 || c.argv[0].len == 0) {
return false; return false;
@@ -262,8 +273,14 @@ export fn start(p: *process, c: *command) void = {
closefd(&p.result, infd); closefd(&p.result, infd);
return; return;
}; };
let errfd: i32 = os.open(c.stderrpath, let errfd: i32;
if (samepath(c.stdoutpath, c.stderrpath)) {
errfd = syscall3(SYS_FCNTL, outfd: i64,
F_DUPFD_CLOEXEC: i64, (os.STDERR_FILENO + 1): i64): i32;
} else {
errfd = os.open(c.stderrpath,
os.flag.WRONLY | os.flag.CREATE | os.flag.EXCL, 384); os.flag.WRONLY | os.flag.CREATE | os.flag.EXCL, 384);
};
if (errfd < 0) { if (errfd < 0) {
fail(p, errfd); fail(p, errfd);
closefd(&p.result, infd); closefd(&p.result, infd);

View File

@@ -627,10 +627,39 @@ fn cwdassertrecord(text: str, label: str, cwd: str, pwd: str,
assert(has(text, strings.concat(label, " create=ok\n"))); assert(has(text, strings.concat(label, " create=ok\n")));
}; };
fn cwdassertmergedstreams(text: str, label: str, dir: str) void = {
let out1: str = strings.concat("STREAM ", label, " out-1 cwd=", dir, "\n");
let err1: str = strings.concat("STREAM ", label, " err-1 cwd=", dir, "\n");
let out2: str = strings.concat("STREAM ", label, " out-2 cwd=", dir, "\n");
let err2: str = strings.concat("STREAM ", label, " err-2 cwd=", dir, "\n");
let p0: i32 = pos(text, out1);
let p1: i32 = pos(text, err1);
let p2: i32 = pos(text, out2);
let p3: i32 = pos(text, err2);
assert(p0 >= 0 && p0 < p1 && p1 < p2 && p2 < p3);
assert(occurrences(text, out1) == 1 && occurrences(text, err1) == 1);
assert(occurrences(text, out2) == 1 && occurrences(text, err2) == 1);
};
fn cwdassertsplitstreams(stdout: str, stderr: str, label: str, dir: str) void = {
let out1: str = strings.concat("STREAM ", label, " out-1 cwd=", dir, "\n");
let err1: str = strings.concat("STREAM ", label, " err-1 cwd=", dir, "\n");
let out2: str = strings.concat("STREAM ", label, " out-2 cwd=", dir, "\n");
let err2: str = strings.concat("STREAM ", label, " err-2 cwd=", dir, "\n");
let p0: i32 = pos(stdout, out1);
let p2: i32 = pos(stdout, out2);
let p1: i32 = pos(stderr, err1);
let p3: i32 = pos(stderr, err2);
assert(p0 >= 0 && p0 < p2 && p1 >= 0 && p1 < p3);
assert(!has(stdout, err1) && !has(stdout, err2));
assert(!has(stderr, out1) && !has(stderr, out2));
};
fn cwdassertpackage(text: str, label: str, dir: str, data: str, fn cwdassertpackage(text: str, label: str, dir: str, data: str,
testdata: str) void = { testdata: str) void = {
cwdassertrecord(text, label, dir, dir, "1", "yes", data, testdata, cwdassertrecord(text, label, dir, dir, "1", "yes", data, testdata,
"eof"); "eof");
cwdassertmergedstreams(text, label, dir);
}; };
fn cwdwritedata(dir: str, label: str) void = { fn cwdwritedata(dir: str, label: str) void = {
@@ -768,16 +797,26 @@ fn cwdwritedata(dir: str, label: str) void = {
let plain: str = strings.concat(tree, "/plain"); let plain: str = strings.concat(tree, "/plain");
let cmdno: str = strings.concat(tree, "/cmdno"); let cmdno: str = strings.concat(tree, "/cmdno");
writefile(strings.concat(probe, "/probe.ww"), strings.concat( let streamsource: str = strings.concat(
"package probe;\n", "fn putfd(fd: i32, value: str) void = {\n",
"import os;\n", " match (os.writeall(fd, value.ptr, value.len: u64)) {\n",
"import strings;\n",
"fn put(value: str) void = {\n",
" match (os.writeall(os.STDOUT_FILENO, value.ptr, value.len: u64)) {\n",
" case let n: i64 => assert(n == value.len: i64);\n", " case let n: i64 => assert(n == value.len: i64);\n",
" case let e: os.oserror => abort(\"write failed\");\n", " case let e: os.oserror => abort(\"write failed\");\n",
" };\n", " };\n",
"};\n", "};\n",
"fn put(value: str) void = { putfd(os.STDOUT_FILENO, value); };\n",
"fn puterr(value: str) void = { putfd(os.STDERR_FILENO, value); };\n",
"export fn streams(label: str, context: str) void = {\n",
" put(strings.concat(\"STREAM \", label, \" out-1 cwd=\", context, \"\\n\"));\n",
" puterr(strings.concat(\"STREAM \", label, \" err-1 cwd=\", context, \"\\n\"));\n",
" put(strings.concat(\"STREAM \", label, \" out-2 cwd=\", context, \"\\n\"));\n",
" puterr(strings.concat(\"STREAM \", label, \" err-2 cwd=\", context, \"\\n\"));\n",
"};\n");
writefile(strings.concat(probe, "/probe.ww"), strings.concat(
"package probe;\n",
"import os;\n",
"import strings;\n",
streamsource,
"fn emitfile(label: str, key: str, path: str) void = {\n", "fn emitfile(label: str, key: str, path: str) void = {\n",
" put(label); put(\" \" ); put(key); put(\"=\");\n", " put(label); put(\" \" ); put(key); put(\"=\");\n",
" let fd: i32 = os.open(path, os.flag.RDONLY, 0i32);\n", " let fd: i32 = os.open(path, os.flag.RDONLY, 0i32);\n",
@@ -800,6 +839,7 @@ fn cwdwritedata(dir: str, label: str) void = {
" let buf: [4096]u8;\n", " let buf: [4096]u8;\n",
" let n: i64 = os.getcwd(&buf[0], size([4096]u8)); assert(n > 1i64);\n", " let n: i64 = os.getcwd(&buf[0], size([4096]u8)); assert(n > 1i64);\n",
" let cwd: str; cwd.ptr = &buf[0]; cwd.len = (n - 1i64): i32;\n", " let cwd: str; cwd.ptr = &buf[0]; cwd.len = (n - 1i64): i32;\n",
" streams(label, cwd);\n",
" put(label); put(\" cwd=\"); put(cwd); put(\"\\n\");\n", " put(label); put(\" cwd=\"); put(cwd); put(\"\\n\");\n",
" put(label); put(\" PWD=\");\n", " put(label); put(\" PWD=\");\n",
" match (os.getenv(\"PWD\")) {\n", " match (os.getenv(\"PWD\")) {\n",
@@ -880,7 +920,7 @@ fn cwdwritedata(dir: str, label: str) void = {
"fn init() void = { probe.run(\"cmdno-init\"); };\n", "fn init() void = { probe.run(\"cmdno-init\"); };\n",
"fn main() void = { probe.run(\"cmdno-main\"); };\n")); "fn main() void = { probe.run(\"cmdno-main\"); };\n"));
let failnames: []str = ["reject", "p8", "p7"]; let failnames: []str = ["reject", "p9", "p8", "p7"];
i = 0; i = 0;
for (i < failnames.len) { for (i < failnames.len) {
let dir: str = strings.concat(failtree, "/", failnames[i]); let dir: str = strings.concat(failtree, "/", failnames[i]);
@@ -890,6 +930,7 @@ fn cwdwritedata(dir: str, label: str) void = {
}; };
let p7: str = strings.concat(failtree, "/p7"); let p7: str = strings.concat(failtree, "/p7");
let p8: str = strings.concat(failtree, "/p8"); let p8: str = strings.concat(failtree, "/p8");
let p9: str = strings.concat(failtree, "/p9");
let reject: str = strings.concat(failtree, "/reject"); let reject: str = strings.concat(failtree, "/reject");
writefile(strings.concat(p7, "/p7_test.ww"), strings.concat( writefile(strings.concat(p7, "/p7_test.ww"), strings.concat(
"package p7;\nimport probe;\n@test fn fails() void = {\n", "package p7;\nimport probe;\n@test fn fails() void = {\n",
@@ -899,6 +940,9 @@ fn cwdwritedata(dir: str, label: str) void = {
" probe.run(\"p8-timeout\");\n", " probe.run(\"p8-timeout\");\n",
" time.sleep((2i64 * (time.second: i64)): time.duration, ", " time.sleep((2i64 * (time.second: i64)): time.duration, ",
"time.clock.monotonic);\n};\n")); "time.clock.monotonic);\n};\n"));
writefile(strings.concat(p9, "/p9_test.ww"), strings.concat(
"package p9;\nimport os;\nimport probe;\n@test fn signals() void = {\n",
" probe.run(\"p9-signal\"); os.kill(os.getpid(), os.SIGTERM);\n};\n"));
writefile(strings.concat(reject, "/reject.ww"), writefile(strings.concat(reject, "/reject.ww"),
"package reject;\nexport fn value() i32 = { return 1; };\n"); "package reject;\nexport fn value() i32 = { return 1; };\n");
writefile(strings.concat(reject, "/bad_test.ww"), writefile(strings.concat(reject, "/bad_test.ww"),
@@ -1016,7 +1060,8 @@ fn cwdwritedata(dir: str, label: str) void = {
assert(same(outc.stdout, outw.stdout)); assert(same(outc.stdout, outw.stdout));
assert(same(outc.stderr, outw.stderr)); assert(same(outc.stderr, outw.stderr));
cwdassertpackage(outc.stdout, "p7-fail", p7, "p7-data", "p7-testdata"); cwdassertpackage(outc.stdout, "p7-fail", p7, "p7-data", "p7-testdata");
assert(same(outc.stderr, strings.concat("FAIL ", p7, assert(outc.stderr.len == 0);
assert(has(outc.stdout, strings.concat("FAIL ", p7,
" [p7] (test exit 1)\n"))); " [p7] (test exit 1)\n")));
let timeoutc: []str = [driver("ww"), "test", "-timeout-ms=50", "-I", let timeoutc: []str = [driver("ww"), "test", "-timeout-ms=50", "-I",
@@ -1036,9 +1081,29 @@ fn cwdwritedata(dir: str, label: str) void = {
cwdassertpackage(outc.stdout, "p8-timeout", p8, cwdassertpackage(outc.stdout, "p8-timeout", p8,
"p8-data", "p8-testdata"); "p8-data", "p8-testdata");
assert(has(outc.stdout, "FAIL(timeout)\n")); assert(has(outc.stdout, "FAIL(timeout)\n"));
assert(same(outc.stderr, strings.concat("FAIL ", p8, assert(outc.stderr.len == 0);
assert(has(outc.stdout, strings.concat("FAIL ", p8,
" [p8] (test exit 1)\n"))); " [p8] (test exit 1)\n")));
let signalc: []str = [driver("ww"), "test", "-I", tree, "-I",
failtree, p9];
let signalw: []str = [driver("ww_ww"), "test", "-I", tree, "-I",
failtree, p9];
runcommandinputenvdir(root, "cwd-signal-c", signalc, cenv, caller,
stdinpath, (60i64 * (time.second: i64)): time.duration, &outc);
runcommandinputenvdir(root, "cwd-signal-ww", signalw, wenv, caller,
stdinpath, (60i64 * (time.second: i64)): time.duration, &outw);
expectexit(&outc, 1);
expectexit(&outw, 1);
assert(same(outc.stdout, outw.stdout));
assert(same(outc.stderr, outw.stderr));
cwdassertpackage(outc.stdout, "p9-signal", p9,
"p9-data", "p9-testdata");
assert(has(outc.stdout, "FAIL (signal 15)\n"));
assert(outc.stderr.len == 0);
assert(has(outc.stdout, strings.concat("FAIL ", p9,
" [p9] (test exit 1)\n")));
// Source-class rejection precedes every captured build or test child, so a // Source-class rejection precedes every captured build or test child, so a
// nonempty caller input cannot create a partial persistent generation. // nonempty caller input cannot create a partial persistent generation.
let rejectcwork: str = strings.concat(root, "/stdin-reject-c"); let rejectcwork: str = strings.concat(root, "/stdin-reject-c");
@@ -1131,6 +1196,8 @@ fn cwdwritedata(dir: str, label: str) void = {
cwdassertrecord(out.stdout, "p1-internal", caller, cwdassertrecord(out.stdout, "p1-internal", caller,
"/ww-cwd-inherited-first", "2", "no", "caller-data", "/ww-cwd-inherited-first", "2", "no", "caller-data",
"caller-testdata", "data"); "caller-testdata", "data");
cwdassertsplitstreams(out.stdout, out.stderr, "dep-init", caller);
cwdassertsplitstreams(out.stdout, out.stderr, "p1-internal", caller);
assert(os.exists(strings.concat(caller, "/created.txt"))); assert(os.exists(strings.concat(caller, "/created.txt")));
assert(os.remove(strings.concat(caller, "/created.txt")) == 0); assert(os.remove(strings.concat(caller, "/created.txt")) == 0);
@@ -1154,6 +1221,8 @@ fn cwdwritedata(dir: str, label: str) void = {
cwdassertrecord(outc.stdout, "p4-internal-only", caller, cwdassertrecord(outc.stdout, "p4-internal-only", caller,
"/ww-cwd-inherited-first", "2", "yes", "caller-data", "/ww-cwd-inherited-first", "2", "yes", "caller-data",
"caller-testdata", "data"); "caller-testdata", "data");
cwdassertsplitstreams(outc.stdout, outc.stderr,
"p4-internal-only", caller);
assert(os.remove(strings.concat(caller, "/created.txt")) == 0); assert(os.remove(strings.concat(caller, "/created.txt")) == 0);
cwdassertpackage(multiout, "dep-init", p2, "p2-data", "p2-testdata"); cwdassertpackage(multiout, "dep-init", p2, "p2-data", "p2-testdata");
cwdassertpackage(multiout, "p2-external", p2, cwdassertpackage(multiout, "p2-external", p2,
@@ -1434,7 +1503,7 @@ fn cwdwritedata(dir: str, label: str) void = {
(120i64 * (time.second: i64)): time.duration, &out); (120i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 1); expectexit(&out, 1);
assert(has(out.stderr, strings.concat("wrapper-hidden=", p1, "\n"))); assert(has(out.stderr, strings.concat("wrapper-hidden=", p1, "\n")));
assert(has(out.stderr, strings.concat("FAIL ", p1, assert(has(out.stdout, strings.concat("FAIL ", p1,
" [p1] (test harness error 2)\n"))); " [p1] (test harness error 2)\n")));
assert(!has(out.stdout, "p1-internal cwd=")); assert(!has(out.stdout, "p1-internal cwd="));
cwdassertpackage(out.stdout, "dep-init", p2, cwdassertpackage(out.stdout, "dep-init", p2,
@@ -2007,9 +2076,10 @@ fn cwdwritedata(dir: str, label: str) void = {
runcommand(root, "assert", av, runcommand(root, "assert", av,
(30i64 * (time.second: i64)): time.duration, &out); (30i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 1); expectexit(&out, 1);
assert(has(out.stdout, assert(has(out.stdout, strings.concat(
"assert_failure.assertion_failure ... FAIL (exit 1)\n")); "assert_failure.assertion_failure ... synthetic assertion failure",
assert(!has(out.stderr, "captures:")); "FAIL (exit 1)\n")));
assert(out.stderr.len == 0);
let nonzeroav: []str = [driver("ww"), "test", let nonzeroav: []str = [driver("ww"), "test",
packagepath("cases/nonzero")]; packagepath("cases/nonzero")];
@@ -2018,6 +2088,7 @@ fn cwdwritedata(dir: str, label: str) void = {
expectexit(&out, 1); expectexit(&out, 1);
assert(has(out.stdout, assert(has(out.stdout,
"nonzero.deliberate_nonzero_exit ... FAIL (exit 7)\n")); "nonzero.deliberate_nonzero_exit ... FAIL (exit 7)\n"));
assert(out.stderr.len == 0);
let prematureav: []str = [driver("ww"), "test", let prematureav: []str = [driver("ww"), "test",
packagepath("cases/premature")]; packagepath("cases/premature")];
@@ -2026,6 +2097,7 @@ fn cwdwritedata(dir: str, label: str) void = {
expectexit(&out, 1); expectexit(&out, 1);
assert(has(out.stdout, assert(has(out.stdout,
"premature.clean_exit_without_completion ... HARNESS (incomplete result)\n")); "premature.clean_exit_without_completion ... HARNESS (incomplete result)\n"));
assert(out.stderr.len == 0);
let signalav: []str = [driver("ww"), "test", let signalav: []str = [driver("ww"), "test",
packagepath("cases/signal")]; packagepath("cases/signal")];
@@ -2034,6 +2106,7 @@ fn cwdwritedata(dir: str, label: str) void = {
expectexit(&out, 1); expectexit(&out, 1);
assert(has(out.stdout, assert(has(out.stdout,
"signal.signal_is_not_exit ... FAIL (signal 15)\n")); "signal.signal_is_not_exit ... FAIL (signal 15)\n"));
assert(out.stderr.len == 0);
let multiav: []str = [driver("ww"), "test", let multiav: []str = [driver("ww"), "test",
packagepath("cases/multi_fail")]; packagepath("cases/multi_fail")];
@@ -2044,6 +2117,7 @@ fn cwdwritedata(dir: str, label: str) void = {
"1 passed, 2 failed, 0 skipped, 0 harness errors\n")); "1 passed, 2 failed, 0 skipped, 0 harness errors\n"));
assert(has(out.stdout, assert(has(out.stdout,
"3 discovered, 3 selected, 3 started, 3 completed\n")); "3 discovered, 3 selected, 3 started, 3 completed\n"));
assert(out.stderr.len == 0);
clean(root); clean(root);
}; };

View File

@@ -83,6 +83,13 @@ fn childmode(args: []str) void = {
if (!writeexact(os.STDERR_FILENO, "route stderr\n")) { os.exit(135); }; if (!writeexact(os.STDERR_FILENO, "route stderr\n")) { os.exit(135); };
os.exit(0); os.exit(0);
}; };
if (eq(mode, "merge")) {
if (!writeexact(os.STDOUT_FILENO, "out 1\n")) { os.exit(146); };
if (!writeexact(os.STDERR_FILENO, "err 1\n")) { os.exit(147); };
if (!writeexact(os.STDOUT_FILENO, "out 2\n")) { os.exit(148); };
if (!writeexact(os.STDERR_FILENO, "err 2\n")) { os.exit(149); };
os.exit(0);
};
if (eq(mode, "signal")) { if (eq(mode, "signal")) {
os.kill(os.getpid(), os.SIGKILL); os.kill(os.getpid(), os.SIGKILL);
os.exit(120); os.exit(120);
@@ -251,6 +258,16 @@ fn main() void = {
assert(eq(readfile(c.stdoutpath), "route stdout\n")); assert(eq(readfile(c.stdoutpath), "route stdout\n"));
assert(eq(readfile(c.stderrpath), "route stderr\n")); assert(eq(readfile(c.stderrpath), "route stderr\n"));
fillcommand(&c, self, "merge", root, "merge",
time.second, grace);
let mergeerr: str = c.stderrpath;
c.stderrpath = c.stdoutpath;
exec.run(&c, &r);
assert(r.termination == exec.termination.EXIT && r.code == 0);
assert(r.errno == 0 && r.cleanuperrno == 0);
assert(eq(readfile(c.stdoutpath), "out 1\nerr 1\nout 2\nerr 2\n"));
assert(!os.exists(mergeerr));
fillcommand(&c, self, "signal", root, "signal", fillcommand(&c, self, "signal", root, "signal",
time.second, grace); time.second, grace);
exec.run(&c, &r); exec.run(&c, &r);
@@ -353,6 +370,16 @@ fn main() void = {
|| cr.errno != 0 || cr.cleanuperrno != 0) { os.exit(139); }; || cr.errno != 0 || cr.cleanuperrno != 0) { os.exit(139); };
if (!eq(readfile(cc.stdoutpath), "route stdout\n")) { os.exit(140); }; if (!eq(readfile(cc.stdoutpath), "route stdout\n")) { os.exit(140); };
if (!eq(readfile(cc.stderrpath), "route stderr\n")) { os.exit(141); }; if (!eq(readfile(cc.stderrpath), "route stderr\n")) { os.exit(141); };
fillcommand(&cc, self, "merge", root, "closedmerge",
time.second, grace);
let closedmergeerr: str = cc.stderrpath;
cc.stderrpath = cc.stdoutpath;
exec.run(&cc, &cr);
if (cr.termination != exec.termination.EXIT || cr.code != 0
|| cr.errno != 0 || cr.cleanuperrno != 0) { os.exit(150); };
if (!eq(readfile(cc.stdoutpath),
"out 1\nerr 1\nout 2\nerr 2\n")) { os.exit(151); };
if (os.exists(closedmergeerr)) { os.exit(152); };
os.exit(0); os.exit(0);
}; };
let closedstatus: i32 = waitchild(closedprobe); let closedstatus: i32 = waitchild(closedprobe);
@@ -382,12 +409,13 @@ fn main() void = {
let captures: []str = [ let captures: []str = [
"exit0.out", "exit0.err", "exitdesc.out", "exitdesc.err", "exit0.out", "exit0.err", "exitdesc.out", "exitdesc.err",
"exit7.out", "exit7.err", "exit127.out", "exit127.err", "exit7.out", "exit7.err", "exit127.out", "exit127.err",
"route.out", "route.err", "signal.out", "signal.err", "route.out", "route.err", "merge.out", "signal.out", "signal.err",
"execfail.out", "execfail.err", "pastfail.out", "pastfail.err", "execfail.out", "execfail.err", "pastfail.out", "pastfail.err",
"chdirfail.out", "chdirfail.err", "timeout.out", "timeout.err", "chdirfail.out", "chdirfail.err", "timeout.out", "timeout.err",
"exists.out", "runinterrupt.out", "runinterrupt.err", "exists.out", "runinterrupt.out", "runinterrupt.err",
"runinterrupt.ready", "runinterrupt.ready",
"closed.out", "closed.err", "interrupt.out", "interrupt.err", "closed.out", "closed.err", "closedmerge.out",
"interrupt.out", "interrupt.err",
]; ];
let i: i32 = 0; let i: i32 = 0;
for (i < captures.len) { for (i < captures.len) {