diff --git a/docs/build-system.md b/docs/build-system.md index dd942925..7be2d386 100644 --- a/docs/build-system.md +++ b/docs/build-system.md @@ -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 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 1436–1499](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 1661–1697](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 1712–1769](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 208–225, 565–606, and 710–738](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 3–35 and 42–65](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 9–21 and 39–82](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 Five candidates were developed as coherent systems, not as feature bins. diff --git a/docs/spec.md b/docs/spec.md index 59232464..7b5df9d2 100644 --- a/docs/spec.md +++ b/docs/spec.md @@ -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, 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 receive independent child environments and each uses its own source directory. 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 execution-directory effect. A published test binary invoked directly, and the raw single-file compatibility route, inherit the user's invocation cwd, -environment, and standard input; no package directory or input policy is -embedded or forced by the binary. +environment, and three standard descriptors; no package directory, input, or +output policy is embedded or forced by the binary. --- diff --git a/docs/test-system-v2.md b/docs/test-system-v2.md index 288a328a..d4cd95ec 100644 --- a/docs/test-system-v2.md +++ b/docs/test-system-v2.md @@ -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, 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 from production, internal, external, recompiled, support, and generated-main 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 inherited environment beyond former fixed observer sizes, requires one appended 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 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 `stdinpath` opens the null device; an explicit path supplies controlled input, 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, inherited-environment, leader-only compiler, assembler, linker, cleanup, run, and single-file-test calls. The local WW `procrun` implementation is deleted. diff --git a/internal/wwpackage/package.ww b/internal/wwpackage/package.ww index a29c2258..fb709825 100644 --- a/internal/wwpackage/package.ww +++ b/internal/wwpackage/package.ww @@ -35,8 +35,7 @@ type pkggroup = struct { root: str, bin: str, buildok: str, - runout: str, - runerr: str, + runoutput: str, state: i32, runstartfailed: bool, runres: exec.result, @@ -1359,8 +1358,7 @@ fn pkgsetplanpaths(p: *pkgplan, groups: []pkggroup, root: str, index: i32, } else { if (!pkgstring(&g.bin, g.root, "/package.test")) { return false; }; }; - if (!pkgstring(&g.runout, g.root, "/test.stdout") - || !pkgstring(&g.runerr, g.root, "/test.stderr") + if (!pkgstring(&g.runoutput, g.root, "/test.output") || !pkgstring(&g.buildok, g.root, "/build.ok")) { return false; }; i += 1; }; @@ -1374,27 +1372,28 @@ fn pkglabel(g: *pkggroup) void = { pkgput(os.STDOUT_FILENO, "]"); }; -fn pkgreportcommand(kind: str, g: *pkggroup, r: *exec.result) void = { - pkgput(os.STDERR_FILENO, "FAIL "); - pkgput(os.STDERR_FILENO, g.dir); - pkgput(os.STDERR_FILENO, " ["); - pkgput(os.STDERR_FILENO, g.pkg); - pkgput(os.STDERR_FILENO, "] ("); - pkgput(os.STDERR_FILENO, kind); +fn pkgreportcommand(fd: i32, kind: str, g: *pkggroup, + r: *exec.result) void = { + pkgput(fd, "FAIL "); + pkgput(fd, g.dir); + pkgput(fd, " ["); + pkgput(fd, g.pkg); + pkgput(fd, "] ("); + pkgput(fd, kind); if (r.errno != 0 || r.cleanuperrno != 0 || r.termination == exec.termination.ERROR) { - pkgput(os.STDERR_FILENO, " harness error "); + pkgput(fd, " harness error "); let code: i32 = r.errno; 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) { - pkgput(os.STDERR_FILENO, " signal "); - pkgput(os.STDERR_FILENO, strconv.i32tos(r.code, strconv.base.DEC)); + pkgput(fd, " signal "); + pkgput(fd, strconv.i32tos(r.code, strconv.base.DEC)); } else { - pkgput(os.STDERR_FILENO, " exit "); - pkgput(os.STDERR_FILENO, strconv.i32tos(r.code, strconv.base.DEC)); + pkgput(fd, " exit "); + 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, @@ -1542,8 +1541,8 @@ fn pkgstartrun(g: *pkggroup, filters: []str, timeoutarg: str, rcmd.argv = ra; rcmd.env = env; rcmd.dir = g.dir; - rcmd.stdoutpath = g.runout; - rcmd.stderrpath = g.runerr; + rcmd.stdoutpath = g.runoutput; + rcmd.stderrpath = g.runoutput; rcmd.deadline.sec = 0i64; rcmd.deadline.nsec = 0i64; rcmd.grace = 0i64: time.duration; @@ -1580,16 +1579,14 @@ fn pkgemitgroup(g: *pkggroup, compileonly: bool) bool = { return true; }; if (g.runstartfailed) { return false; }; - let runstdout: str; - let runstderr: str; - if (!pkgread(g.runout, &runstdout) || !pkgread(g.runerr, &runstderr)) { + let runoutput: str; + if (!pkgread(g.runoutput, &runoutput)) { pkgfailpath(g.root, "cannot read test capture"); return false; }; - pkgput(os.STDOUT_FILENO, runstdout); - pkgput(os.STDERR_FILENO, runstderr); + pkgput(os.STDOUT_FILENO, runoutput); if (!pkgrunok(g)) { - pkgreportcommand("test", g, &g.runres); + pkgreportcommand(os.STDOUT_FILENO, "test", g, &g.runres); return false; }; pkgput(os.STDOUT_FILENO, "ok "); @@ -1612,7 +1609,8 @@ fn pkgemitplan(p: *pkgplan, groups: []pkggroup, let i: i32 = p.start; for (i < p.end) { if (!pkgproductbuilt(&groups[i], buildonly)) { - pkgreportcommand("build", &groups[i], &p.buildres); + pkgreportcommand(os.STDERR_FILENO, "build", &groups[i], + &p.buildres); failed += 1; } else if (buildonly) { void; diff --git a/lib/os/exec/exec.ww b/lib/os/exec/exec.ww index ea8baf6a..fffa0b4d 100644 --- a/lib/os/exec/exec.ww +++ b/lib/os/exec/exec.ww @@ -29,7 +29,8 @@ export type result = struct { // command is deliberately concrete. argv includes argv[0], env is the full // environment, and an empty dir inherits the caller's working directory. // 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 { path: str, argv: []str, @@ -120,6 +121,16 @@ fn hasnul(s: str) bool = { 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 = { if (c.path.len == 0 || c.argv.len == 0 || c.argv[0].len == 0) { return false; @@ -262,8 +273,14 @@ export fn start(p: *process, c: *command) void = { closefd(&p.result, infd); return; }; - let errfd: i32 = os.open(c.stderrpath, - os.flag.WRONLY | os.flag.CREATE | os.flag.EXCL, 384); + 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); + }; if (errfd < 0) { fail(p, errfd); closefd(&p.result, infd); diff --git a/test/package/package_test.ww b/test/package/package_test.ww index 0c5d45af..1bf9a9bb 100644 --- a/test/package/package_test.ww +++ b/test/package/package_test.ww @@ -627,10 +627,39 @@ fn cwdassertrecord(text: str, label: str, cwd: str, pwd: str, 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, testdata: str) void = { cwdassertrecord(text, label, dir, dir, "1", "yes", data, testdata, "eof"); + cwdassertmergedstreams(text, label, dir); }; 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 cmdno: str = strings.concat(tree, "/cmdno"); - writefile(strings.concat(probe, "/probe.ww"), strings.concat( - "package probe;\n", - "import os;\n", - "import strings;\n", - "fn put(value: str) void = {\n", - " match (os.writeall(os.STDOUT_FILENO, value.ptr, value.len: u64)) {\n", + let streamsource: str = strings.concat( + "fn putfd(fd: i32, value: str) void = {\n", + " match (os.writeall(fd, value.ptr, value.len: u64)) {\n", " case let n: i64 => assert(n == value.len: i64);\n", " case let e: os.oserror => abort(\"write failed\");\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", " put(label); put(\" \" ); put(key); put(\"=\");\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 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", + " streams(label, cwd);\n", " put(label); put(\" cwd=\"); put(cwd); put(\"\\n\");\n", " put(label); put(\" 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 main() void = { probe.run(\"cmdno-main\"); };\n")); - let failnames: []str = ["reject", "p8", "p7"]; + let failnames: []str = ["reject", "p9", "p8", "p7"]; i = 0; for (i < failnames.len) { 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 p8: str = strings.concat(failtree, "/p8"); + let p9: str = strings.concat(failtree, "/p9"); let reject: str = strings.concat(failtree, "/reject"); writefile(strings.concat(p7, "/p7_test.ww"), strings.concat( "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", " time.sleep((2i64 * (time.second: i64)): time.duration, ", "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"), "package reject;\nexport fn value() i32 = { return 1; };\n"); 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.stderr, outw.stderr)); 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"))); 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, "p8-data", "p8-testdata"); 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"))); + 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 // nonempty caller input cannot create a partial persistent generation. 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, "/ww-cwd-inherited-first", "2", "no", "caller-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.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, "/ww-cwd-inherited-first", "2", "yes", "caller-data", "caller-testdata", "data"); + cwdassertsplitstreams(outc.stdout, outc.stderr, + "p4-internal-only", caller); assert(os.remove(strings.concat(caller, "/created.txt")) == 0); cwdassertpackage(multiout, "dep-init", p2, "p2-data", "p2-testdata"); cwdassertpackage(multiout, "p2-external", p2, @@ -1434,7 +1503,7 @@ fn cwdwritedata(dir: str, label: str) void = { (120i64 * (time.second: i64)): time.duration, &out); expectexit(&out, 1); 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"))); assert(!has(out.stdout, "p1-internal cwd=")); cwdassertpackage(out.stdout, "dep-init", p2, @@ -2007,9 +2076,10 @@ fn cwdwritedata(dir: str, label: str) void = { runcommand(root, "assert", av, (30i64 * (time.second: i64)): time.duration, &out); expectexit(&out, 1); - assert(has(out.stdout, - "assert_failure.assertion_failure ... FAIL (exit 1)\n")); - assert(!has(out.stderr, "captures:")); + assert(has(out.stdout, strings.concat( + "assert_failure.assertion_failure ... synthetic assertion failure", + "FAIL (exit 1)\n"))); + assert(out.stderr.len == 0); let nonzeroav: []str = [driver("ww"), "test", packagepath("cases/nonzero")]; @@ -2018,6 +2088,7 @@ fn cwdwritedata(dir: str, label: str) void = { expectexit(&out, 1); assert(has(out.stdout, "nonzero.deliberate_nonzero_exit ... FAIL (exit 7)\n")); + assert(out.stderr.len == 0); let prematureav: []str = [driver("ww"), "test", packagepath("cases/premature")]; @@ -2026,6 +2097,7 @@ fn cwdwritedata(dir: str, label: str) void = { expectexit(&out, 1); assert(has(out.stdout, "premature.clean_exit_without_completion ... HARNESS (incomplete result)\n")); + assert(out.stderr.len == 0); let signalav: []str = [driver("ww"), "test", packagepath("cases/signal")]; @@ -2034,6 +2106,7 @@ fn cwdwritedata(dir: str, label: str) void = { expectexit(&out, 1); assert(has(out.stdout, "signal.signal_is_not_exit ... FAIL (signal 15)\n")); + assert(out.stderr.len == 0); let multiav: []str = [driver("ww"), "test", 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")); assert(has(out.stdout, "3 discovered, 3 selected, 3 started, 3 completed\n")); + assert(out.stderr.len == 0); clean(root); }; diff --git a/test/wwfixture/process/main.ww b/test/wwfixture/process/main.ww index 2b5d023f..91e82ce7 100644 --- a/test/wwfixture/process/main.ww +++ b/test/wwfixture/process/main.ww @@ -83,6 +83,13 @@ fn childmode(args: []str) void = { if (!writeexact(os.STDERR_FILENO, "route stderr\n")) { os.exit(135); }; 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")) { os.kill(os.getpid(), os.SIGKILL); os.exit(120); @@ -251,6 +258,16 @@ fn main() void = { assert(eq(readfile(c.stdoutpath), "route stdout\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", time.second, grace); exec.run(&c, &r); @@ -353,6 +370,16 @@ fn main() void = { || cr.errno != 0 || cr.cleanuperrno != 0) { os.exit(139); }; if (!eq(readfile(cc.stdoutpath), "route stdout\n")) { os.exit(140); }; 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); }; let closedstatus: i32 = waitchild(closedprobe); @@ -382,12 +409,13 @@ fn main() void = { let captures: []str = [ "exit0.out", "exit0.err", "exitdesc.out", "exitdesc.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", "chdirfail.out", "chdirfail.err", "timeout.out", "timeout.err", "exists.out", "runinterrupt.out", "runinterrupt.err", "runinterrupt.ready", - "closed.out", "closed.err", "interrupt.out", "interrupt.err", + "closed.out", "closed.err", "closedmerge.out", + "interrupt.out", "interrupt.err", ]; let i: i32 = 0; for (i < captures.len) {