From b996099270d02b9e512947144ed5321e4ae833df Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Thu, 20 Aug 2026 17:39:10 +0900 Subject: [PATCH] test: prove captured action stdin isolation --- docs/build-system.md | 111 +++++++++++++++++++ docs/spec.md | 13 ++- docs/test-system-v2.md | 51 +++++---- test/package/package_test.ww | 200 +++++++++++++++++++++++++++++------ 4 files changed, 321 insertions(+), 54 deletions(-) diff --git a/docs/build-system.md b/docs/build-system.md index ee6459b0..dd942925 100644 --- a/docs/build-system.md +++ b/docs/build-system.md @@ -5928,6 +5928,117 @@ Checked command-global allocation-failure parity remains owned by crosses the former fixed environment-size boundary and verifies that no partial execution environment or staged `.new` state is published. +### 11.25 Implemented null standard input for captured actions + +Every process launched through WW's captured asynchronous executor now receives +an explicit fd 0. An empty `exec.command.stdinpath`, which is the production +default, opens the null device read-only; a nonempty value opens that exact path. +Consequently every coordinator-executed directory test product observes +immediate EOF instead of inheriting and consuming the invoking terminal, pipe, +or file. Captured directory build plans and the compiler, assembler, and linker +processes that inherit their stdio receive the same noninteractive boundary. + +#### Pinned Go evidence and pre-fix WW behavior + +The authority is official Go 1.26.5 at commit +`c19862e5f8415b4f24b189d065ed739517c548ba`: + +- `runTestActor.Act` constructs an `exec.Cmd`, assigns its package directory, + environment, stdout, stderr, cancellation, and wait delay, and invokes + `Run` without assigning `Stdin` + ([`cmd/go/internal/test/test.go`, lines 1661–1697](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/test/test.go#L1661-L1697)). +- `Cmd.Stdin` specifies that a nil value reads from `os.DevNull`; + `childStdin` opens that device and retains the file for the child; and + `Start` installs it as the first child file before process creation + ([`os/exec/exec.go`, lines 193–206, 531–538, and 710–738](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/os/exec/exec.go#L193-L206)). +- The ordinary build-command path has the same default. `Shell.runOut` creates + an `exec.Cmd`, assigns output, directory, and environment, and runs it without + assigning `Stdin` + ([`cmd/go/internal/work/shell.go`, lines 600–663](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/work/shell.go#L600-L663)). +- Official `os/exec` tests define a `cat` helper that copies stdin to EOF and + require that helper to terminate successfully when run with no `Stdin` + assignment + ([`os/exec/exec_test.go`, lines 201–204 and 416–459](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/os/exec/exec_test.go#L416-L459)). + The command testdata separately exercises deliberately supplied stdin-pipe + lifetime and closure for orphaned test descendants + ([`cmd/go/testdata/script/test_timeout_stdin.txt`, lines 1–21 and 39–88](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/testdata/script/test_timeout_stdin.txt#L1-L21)); + that script is adjacent stream-lifetime evidence, while the default null-fd + conclusion comes directly from the implementation chain above. + +Before this slice, `lib/os/exec.start` redirected only stdout and stderr. A +directory driver invoked with a nonempty stdin file passed the same open file +description through the top-level inherited-stdio handoff, the package +coordinator, its captured builder, and the generated product. Serial products +could consume caller data; parallel products raced on the shared file offset; +a test that waited for input could wait on an interactive caller. Direct +measurement with a one-byte pipe made the same directory `@test` fail under +both Cstage and WWstage because its first read returned that byte. The raw +single-file route also read the byte and failed, but that route intentionally +remains inherited-stdio compatibility behavior. + +#### Descriptor ownership, action boundaries, and concurrency + +`exec.start` validates `stdinpath`, selects `/dev/null` for the empty value, and +opens the input before creating either output capture. `safefd` moves all three +standard streams above fd 2 when a caller had closed a standard descriptor. +After fork, the child maps the owned input to fd 0 before mapping the captures +to fd 1 and fd 2; setup failures travel through the existing close-on-exec +marker. The parent closes its input copy immediately after fork. Every +pre-fork error path closes every successfully acquired descriptor. + +The package coordinator does not read or mutate its own fd 0. Each captured +build or run child opens an independent null descriptor, so `-j N` products +share neither readable caller data nor an input offset. Production, internal, +external, recompiled-for-test, support, and generated-main actions still form +the same graph and the one directory product still owns one process. Package +and test-only dependency initialization observes EOF inside that process. +Filters, list mode, no-match execution, failure, and timeout use the same +boundary. + +Standard input is request-time process metadata only. It does not enter +canonical dotted identity, declared-name binding, actions, units, exports, +symbols, archives, generated main, executable bytes, product names, storage +keys, or diagnostics. The source path accepted by `stdinpath` is an executor +resource, not a package or filesystem-identity input. + +#### Inherited-stdio routes, failure, persistence, and proof + +`exec.runstdio` remains unchanged. The top-level driver therefore preserves +inherited stdin for raw single-file tests and runs, and a published test binary +invoked directly receives its invoker's fd 0. Directory `ww test -c`, including +`-c -o`, starts no product; the compiled binary acquires no embedded stdin +policy. No-selected-test packages likewise start no product. Directory build +and compile-only plans are captured actions and therefore noninteractive, but +their output, cwd, environment, graph, and publication rules are unchanged. + +Failure to open an explicit input path or the default null device is a +pre-fork `termination.ERROR` with positive errno. Because input opens first, +neither output capture exists. A child-side `dup2` or close failure is reported +through the setup marker, distinguished from exit 127, and follows the existing +process-group cleanup path. Test failures, timeouts, post-build directory +removal, sibling isolation, transaction rollback, and scratch removal retain +their prior contracts. + +No test-result cache exists. Caller stdin bytes never affect source actions or +persistent artifacts, and changing only the explicit proof input causes no +compile or assemble work beyond the established warm final relink. Build +workdir format remains `18`, test workdir format remains `19`, and semantic +storage remains `3` because no persisted byte schema changed. + +The focused native owner remains +`directory_test_execution_working_directory` in +`test/package/package_test.ww`. It now drives every relevant command with a +known nonempty input file and requires EOF across all directory action/test +variants, production and test-only dependency initialization, serial and +parallel products, filters/list/no-match, recursive and equivalent roots, +failure, timeout, persistent cold/warm/data-only runs, and post-build child +setup failure. Tool wrappers require EOF without changing cwd, argv, locale, or +`TMPDIR`. Direct published and raw single-file binaries must instead read the +supplied data. The observer also proves input-open failure creates no captures, +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. + ## 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 9bc42234..59232464 100644 --- a/docs/spec.md +++ b/docs/spec.md @@ -657,14 +657,23 @@ remaining separate actions. Reachable dependency initialization consequently observes the tested product's directory; a dependency tested as its own product observes its own directory. +The same coordinator-executed product reads standard input from the null +device. Its first read observes EOF regardless of the terminal, pipe, or file +connected to the invoking command. Each parallel product owns a separate null +descriptor, and production or test-only dependency initialization, filtering, +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 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 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 and -environment; no package directory is embedded or forced by the binary. +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. --- diff --git a/docs/test-system-v2.md b/docs/test-system-v2.md index c80159ef..288a328a 100644 --- a/docs/test-system-v2.md +++ b/docs/test-system-v2.md @@ -242,14 +242,22 @@ creates a separate process using the dependency directory. Equivalent direct, recursive, redundant, absolute, and root-symlink spellings converge before this runtime field is assigned. +Every such product also receives an independently opened null device as fd 0. +Caller terminal, pipe, and file bytes remain with the coordinator; serial and +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 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, linker, support, and generated-main commands retain their build-plan cwd and -environment. `ww build`, `ww test -c`, `-c -o`, and no-selected-test products -start no test child. A published test binary run directly and the raw -single-file compatibility path inherit the user's cwd/environment and contain -no forced package-directory behavior. +environment. Captured directory build plans begin with null stdin, which their +inherited-stdio tool descendants retain. `ww build`, `ww test -c`, `-c -o`, and +no-selected-test products start no test child. A published test binary run +directly and the raw single-file compatibility path inherit the user's +cwd/environment/stdin and contain no forced package-directory or input +behavior. Before package grouping, both directory drivers and the shared recursive coordinator apply Go 1.26.5's filename OS/architecture rule for WW's fixed @@ -319,15 +327,18 @@ independent temporary directories from an unrelated caller cwd and compares Cstage/WWstage output for production/internal/external/combined and every test-only shape; production and test-only dependency initialization; recompiled external self-import; duplicate inherited `PWD`; ordinary data, -`testdata`, and relative writes; direct/recursive/redundant/absolute/symlink -roots; reversed request and creation order; serial and parallel products; -filters, list, and no-match execution; failure and timeout; build/no-test and -compile-only paths; direct published binaries and raw single-file execution; -build-tool cwd/argv/environment; persistent data-only reuse and artifact bytes; -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`, and sweeps the persistent workdir for -staged residue. Command-global bounded-memory failure remains independently +`testdata`, relative writes, and deliberately nonempty caller stdin; +direct/recursive/redundant/absolute/symlink roots; reversed request and creation +order; serial and parallel products; filters, list, and no-match execution; +failure and timeout; build/no-test and compile-only paths; direct published +binaries and raw single-file execution; build-tool cwd/argv/environment/stdin; +persistent data-only reuse and artifact bytes; input-open failure before capture +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. +Command-global bounded-memory failure remains independently owned by `allocation_failure_is_command_global`. The same package owner contains the focused @@ -499,12 +510,14 @@ target once within one invocation, but two independent `make bootstrap` invocations are not safe to run concurrently and remain mutually exclusive. `lib/os/exec` is the sole reusable WW subprocess mechanism. Fixture and package -coordinators use its captured asynchronous path. 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. The C bootstrap retains its C -process implementation because it cannot consume a WW standard-library -module. +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. +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. +The C bootstrap retains its C process implementation because it cannot consume +a WW standard-library module. WW has tokens and an opaque type for future CSP/channel work, but no mature production channel operations, task runtime, or scheduler. No channel, diff --git a/test/package/package_test.ww b/test/package/package_test.ww index 91e66773..0c5d45af 100644 --- a/test/package/package_test.ww +++ b/test/package/package_test.ww @@ -174,6 +174,29 @@ fn runcommandenvdir(root: str, name: str, argv: []str, env: []str, out.stderr = readfile(c.stderrpath); }; +// A direct descriptor mapping keeps the caller's duplicate environment and cwd +// intact while giving the nested driver known nonempty input. +fn runcommandinputenvdir(root: str, name: str, argv: []str, env: []str, + dir: str, input: str, lifetime: time.duration, out: *commandout) void = { + let c: exec.command; + c.path = argv[0]; + c.argv = argv; + c.env = env; + c.dir = dir; + c.stdinpath = input; + c.stdoutpath = strings.concat(root, "/", name, ".stdout"); + c.stderrpath = strings.concat(root, "/", name, ".stderr"); + c.deadline = time.add(time.now(time.clock.monotonic), lifetime); + c.grace = (100i64 * (time.millisecond: i64)): time.duration; + let r: exec.result; + exec.run(&c, &r); + assert(r.errno == 0 && r.cleanuperrno == 0); + out.termination = r.termination; + out.code = r.code; + out.stdout = readfile(c.stdoutpath); + out.stderr = readfile(c.stderrpath); +}; + fn clean(root: str) void = { let av: []str = ["/bin/rm", "-rf", "--", root]; let c: exec.command; @@ -592,19 +615,22 @@ fn cwdtestenv(stage: str) []str = { }; fn cwdassertrecord(text: str, label: str, cwd: str, pwd: str, - pwdcount: str, pwdlast: str, data: str, testdata: str) void = { + pwdcount: str, pwdlast: str, data: str, testdata: str, + input: str) void = { assert(has(text, strings.concat(label, " cwd=", cwd, "\n"))); assert(has(text, strings.concat(label, " PWD=", pwd, "\n"))); assert(has(text, strings.concat(label, " PWD-count=", pwdcount, "\n"))); assert(has(text, strings.concat(label, " PWD-last=", pwdlast, "\n"))); assert(has(text, strings.concat(label, " data=", data, "\n"))); assert(has(text, strings.concat(label, " testdata=", testdata, "\n"))); + assert(has(text, strings.concat(label, " stdin=", input, "\n"))); assert(has(text, strings.concat(label, " create=ok\n"))); }; fn cwdassertpackage(text: str, label: str, dir: str, data: str, testdata: str) void = { - cwdassertrecord(text, label, dir, dir, "1", "yes", data, testdata); + cwdassertrecord(text, label, dir, dir, "1", "yes", data, testdata, + "eof"); }; fn cwdwritedata(dir: str, label: str) void = { @@ -689,6 +715,35 @@ fn cwdwritedata(dir: str, label: str) void = { mkdirall(failtree); cwdwritedata(caller, "caller"); writefile(strings.concat(caller, "/sentinel"), "parent-sentinel\n"); + let stdinpath: str = strings.concat(root, "/stdin.txt"); + let stdinbytes: []u8 = alloc([], 4096u64)!; + let inputi: i32 = 0; + for (inputi < 2048) { + append(stdinbytes, 'X'); + append(stdinbytes, '\n'); + inputi += 1; + }; + writefile(stdinpath, strings.frombytes(stdinbytes)); + let missingstdinout: str = strings.concat(root, "/missing-stdin.stdout"); + let missingstdinerr: str = strings.concat(root, "/missing-stdin.stderr"); + let missingstdinav: []str = ["/bin/true"]; + let missingstdincmd: exec.command; + missingstdincmd.path = missingstdinav[0]; + missingstdincmd.argv = missingstdinav; + missingstdincmd.env = os.getenvs(); + missingstdincmd.dir = caller; + missingstdincmd.stdinpath = strings.concat(root, "/missing-stdin"); + missingstdincmd.stdoutpath = missingstdinout; + missingstdincmd.stderrpath = missingstdinerr; + missingstdincmd.deadline.sec = 0i64; + missingstdincmd.deadline.nsec = 0i64; + missingstdincmd.grace = 0i64: time.duration; + let missingstdinresult: exec.result; + exec.run(&missingstdincmd, &missingstdinresult); + assert(missingstdinresult.termination == exec.termination.ERROR); + assert(missingstdinresult.errno == 2 + && missingstdinresult.cleanuperrno == 0); + assert(!os.exists(missingstdinout) && !os.exists(missingstdinerr)); // Create in reverse lexical order; discovery and product emission may not // inherit filesystem or request order. @@ -763,6 +818,7 @@ fn cwdwritedata(dir: str, label: str) void = { " if (last == env.len - 1) { put(\"yes\\n\"); } else { put(\"no\\n\"); };\n", " emitfile(label, \"data\", \"data.txt\");\n", " emitfile(label, \"testdata\", \"testdata/input.txt\");\n", + " let input: [1]u8;\n let inputn: i64 = os.read(os.STDIN_FILENO, &input[0], 1u64);\n put(label); put(\" stdin=\");\n if (inputn == 0i64) { put(\"eof\\n\"); } else {\n if (inputn == 1i64) { put(\"data\\n\"); } else { put(\"\\n\"); };\n };\n", " let fd: i32 = os.open(\"created.txt\", os.flag.WRONLY |\n", " os.flag.CREATE | os.flag.TRUNC, 384i32);\n", " put(label); put(\" create=\");\n", @@ -824,7 +880,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 = ["p8", "p7"]; + let failnames: []str = ["reject", "p8", "p7"]; i = 0; for (i < failnames.len) { let dir: str = strings.concat(failtree, "/", failnames[i]); @@ -834,6 +890,7 @@ fn cwdwritedata(dir: str, label: str) void = { }; let p7: str = strings.concat(failtree, "/p7"); let p8: str = strings.concat(failtree, "/p8"); + 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", " probe.run(\"p7-fail\"); assert(false);\n};\n")); @@ -842,6 +899,10 @@ 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(reject, "/reject.ww"), + "package reject;\nexport fn value() i32 = { return 1; };\n"); + writefile(strings.concat(reject, "/bad_test.ww"), + "package unrelated;\n@test fn unreachable() void = { assert(false); };\n"); let out: commandout; let lnav: []str = ["/bin/ln", "-s", tree, treelink]; @@ -868,9 +929,11 @@ fn cwdwritedata(dir: str, label: str) void = { p6, p5, p4, p3, p2, p1, testdep, dep]; let outc: commandout; let outw: commandout; - runcommandenvdir(root, "cwd-multi-c", forward, cenv, caller, + runcommandinputenvdir(root, "cwd-multi-c", forward, cenv, caller, + stdinpath, (120i64 * (time.second: i64)): time.duration, &outc); - runcommandenvdir(root, "cwd-multi-ww", reverse, wenv, caller, + runcommandinputenvdir(root, "cwd-multi-ww", reverse, wenv, caller, + stdinpath, (120i64 * (time.second: i64)): time.duration, &outw); expectexit(&outc, 0); expectexit(&outw, 0); @@ -901,7 +964,8 @@ fn cwdwritedata(dir: str, label: str) void = { // selected test body runs. let filterav: []str = [driver("ww"), "test", "-run", "cwd_external", "-I", tree, p3]; - runcommandenvdir(root, "cwd-filter", filterav, cenv, caller, + runcommandinputenvdir(root, "cwd-filter", filterav, cenv, caller, + stdinpath, (60i64 * (time.second: i64)): time.duration, &out); expectexit(&out, 0); cwdassertpackage(out.stdout, "dep-init", p3, "p3-data", "p3-testdata"); @@ -912,7 +976,8 @@ fn cwdwritedata(dir: str, label: str) void = { assert(!has(out.stdout, "p3-internal cwd=")); let listav: []str = [driver("ww_ww"), "test", "-list", "-I", tree, p3]; - runcommandenvdir(root, "cwd-list", listav, wenv, caller, + runcommandinputenvdir(root, "cwd-list", listav, wenv, caller, + stdinpath, (60i64 * (time.second: i64)): time.duration, &out); expectexit(&out, 0); cwdassertpackage(out.stdout, "dep-init", p3, "p3-data", "p3-testdata"); @@ -925,7 +990,8 @@ fn cwdwritedata(dir: str, label: str) void = { let nomatchav: []str = [driver("ww"), "test", "-run", "no-such-test", "-I", tree, p3]; - runcommandenvdir(root, "cwd-no-match", nomatchav, cenv, caller, + runcommandinputenvdir(root, "cwd-no-match", nomatchav, cenv, caller, + stdinpath, (60i64 * (time.second: i64)): time.duration, &out); expectexit(&out, 0); cwdassertpackage(out.stdout, "dep-init", p3, "p3-data", "p3-testdata"); @@ -939,9 +1005,11 @@ fn cwdwritedata(dir: str, label: str) void = { p7]; let failw: []str = [driver("ww_ww"), "test", "-I", tree, "-I", failtree, p7]; - runcommandenvdir(root, "cwd-fail-c", failc, cenv, caller, + runcommandinputenvdir(root, "cwd-fail-c", failc, cenv, caller, + stdinpath, (60i64 * (time.second: i64)): time.duration, &outc); - runcommandenvdir(root, "cwd-fail-ww", failw, wenv, caller, + runcommandinputenvdir(root, "cwd-fail-ww", failw, wenv, caller, + stdinpath, (60i64 * (time.second: i64)): time.duration, &outw); expectexit(&outc, 1); expectexit(&outw, 1); @@ -955,9 +1023,11 @@ fn cwdwritedata(dir: str, label: str) void = { tree, "-I", failtree, p8]; let timeoutw: []str = [driver("ww_ww"), "test", "-timeout-ms=50", "-I", tree, "-I", failtree, p8]; - runcommandenvdir(root, "cwd-timeout-c", timeoutc, cenv, caller, + runcommandinputenvdir(root, "cwd-timeout-c", timeoutc, cenv, caller, + stdinpath, (60i64 * (time.second: i64)): time.duration, &outc); - runcommandenvdir(root, "cwd-timeout-ww", timeoutw, wenv, caller, + runcommandinputenvdir(root, "cwd-timeout-ww", timeoutw, wenv, caller, + stdinpath, (60i64 * (time.second: i64)): time.duration, &outw); expectexit(&outc, 1); expectexit(&outw, 1); @@ -969,11 +1039,35 @@ fn cwdwritedata(dir: str, label: str) void = { assert(same(outc.stderr, strings.concat("FAIL ", p8, " [p8] (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"); + let rejectwwork: str = strings.concat(root, "/stdin-reject-ww"); + assert(os.mkdir(rejectcwork, 448i32) == 0); + assert(os.mkdir(rejectwwork, 448i32) == 0); + let rejectcav: []str = [driver("ww"), "test", "-w", rejectcwork, + "-I", failtree, reject]; + let rejectwav: []str = [driver("ww_ww"), "test", "-w", rejectwwork, + "-I", failtree, reject]; + runcommandinputenvdir(root, "stdin-reject-c", rejectcav, cenv, caller, + stdinpath, (60i64 * (time.second: i64)): time.duration, &outc); + runcommandinputenvdir(root, "stdin-reject-ww", rejectwav, wenv, caller, + stdinpath, (60i64 * (time.second: i64)): time.duration, &outw); + expectexit(&outc, 1); + expectexit(&outw, 1); + assert(outc.stdout.len == 0 && outw.stdout.len == 0); + assert(same(outc.stderr, outw.stderr)); + assert(has(outc.stderr, + "test package must match production package or _test\n")); + assert(directoryisempty(rejectcwork)); + assert(directoryisempty(rejectwwork)); + // Packages without selected test sources and `ww build` never start a test // process, even if production initialization would make that visible. let notestav: []str = [driver("ww_ww"), "test", "-I", tree, plain, cmdno]; - runcommandenvdir(root, "cwd-no-tests", notestav, wenv, caller, + runcommandinputenvdir(root, "cwd-no-tests", notestav, wenv, caller, + stdinpath, (60i64 * (time.second: i64)): time.duration, &out); expectexit(&out, 0); assert(!has(out.stdout, "plain-init")); @@ -984,7 +1078,8 @@ fn cwdwritedata(dir: str, label: str) void = { let commandbin: str = strings.concat(root, "/cmdno.bin"); let buildav: []str = [driver("ww"), "build", "-I", tree, "-o", commandbin, cmdno]; - runcommandenvdir(root, "cwd-build", buildav, cenv, caller, + runcommandinputenvdir(root, "cwd-build", buildav, cenv, caller, + stdinpath, (60i64 * (time.second: i64)): time.duration, &out); expectexit(&out, 0); assert(os.exists(commandbin)); @@ -995,7 +1090,8 @@ fn cwdwritedata(dir: str, label: str) void = { // wrapper: direct invocation keeps the caller's cwd and duplicate PWDs. assert(os.remove(strings.concat(p2, "/created.txt")) == 0); let defaultcompile: []str = [driver("ww"), "test", "-c", "-I", tree, p2]; - runcommandenvdir(root, "cwd-compile-default", defaultcompile, cenv, caller, + runcommandinputenvdir(root, "cwd-compile-default", defaultcompile, cenv, + caller, stdinpath, (90i64 * (time.second: i64)): time.duration, &out); expectexit(&out, 0); let defaultbin: str = strings.concat(p2, "/p2.test"); @@ -1012,9 +1108,11 @@ fn cwdwritedata(dir: str, label: str) void = { "-I", tree, p1]; let compilew: []str = [driver("ww_ww"), "test", "-c", "-o", p1wbin, "-I", tree, p1]; - runcommandenvdir(root, "cwd-compile-c", compilec, cenv, caller, + runcommandinputenvdir(root, "cwd-compile-c", compilec, cenv, caller, + stdinpath, (90i64 * (time.second: i64)): time.duration, &outc); - runcommandenvdir(root, "cwd-compile-ww", compilew, wenv, caller, + runcommandinputenvdir(root, "cwd-compile-ww", compilew, wenv, caller, + stdinpath, (90i64 * (time.second: i64)): time.duration, &outw); expectexit(&outc, 0); expectexit(&outw, 0); @@ -1023,15 +1121,16 @@ fn cwdwritedata(dir: str, label: str) void = { assert(!has(outc.stdout, "dep-init") && !has(outw.stdout, "dep-init")); let directbin: []str = [p1cbin, "-package=p1"]; - runcommandenvdir(root, "cwd-published-direct", directbin, cenv, caller, + runcommandinputenvdir(root, "cwd-published-direct", directbin, cenv, + caller, stdinpath, (60i64 * (time.second: i64)): time.duration, &out); expectexit(&out, 0); cwdassertrecord(out.stdout, "dep-init", caller, "/ww-cwd-inherited-first", "2", "no", "caller-data", - "caller-testdata"); + "caller-testdata", "data"); cwdassertrecord(out.stdout, "p1-internal", caller, "/ww-cwd-inherited-first", "2", "no", "caller-data", - "caller-testdata"); + "caller-testdata", "data"); assert(os.exists(strings.concat(caller, "/created.txt"))); assert(os.remove(strings.concat(caller, "/created.txt")) == 0); @@ -1041,10 +1140,12 @@ fn cwdwritedata(dir: str, label: str) void = { strings.concat(p4, "/internal_test.ww")]; let raww: []str = [driver("ww_ww"), "test", "-I", tree, strings.concat(p4, "/internal_test.ww")]; - runcommandenvdir(root, "cwd-raw-c", rawc, cbase, caller, + runcommandinputenvdir(root, "cwd-raw-c", rawc, cbase, caller, + stdinpath, (60i64 * (time.second: i64)): time.duration, &outc); assert(os.remove(strings.concat(caller, "/created.txt")) == 0); - runcommandenvdir(root, "cwd-raw-ww", raww, wenv, caller, + runcommandinputenvdir(root, "cwd-raw-ww", raww, wenv, caller, + stdinpath, (60i64 * (time.second: i64)): time.duration, &outw); expectexit(&outc, 0); expectexit(&outw, 0); @@ -1052,7 +1153,7 @@ fn cwdwritedata(dir: str, label: str) void = { assert(same(outc.stderr, outw.stderr)); cwdassertrecord(outc.stdout, "p4-internal-only", caller, "/ww-cwd-inherited-first", "2", "yes", "caller-data", - "caller-testdata"); + "caller-testdata", "data"); assert(os.remove(strings.concat(caller, "/created.txt")) == 0); cwdassertpackage(multiout, "dep-init", p2, "p2-data", "p2-testdata"); cwdassertpackage(multiout, "p2-external", p2, @@ -1076,9 +1177,11 @@ fn cwdwritedata(dir: str, label: str) void = { strings.concat(tree, "/...")]; let linkrec: []str = [driver("ww_ww"), "test", "-j", "4", "-I", "../tree-link", "../tree-link/..."]; - runcommandenvdir(root, "cwd-recursive-real", realrec, cenv, caller, + runcommandinputenvdir(root, "cwd-recursive-real", realrec, cenv, caller, + stdinpath, (120i64 * (time.second: i64)): time.duration, &outc); - runcommandenvdir(root, "cwd-recursive-link", linkrec, wenv, caller, + runcommandinputenvdir(root, "cwd-recursive-link", linkrec, wenv, caller, + stdinpath, (120i64 * (time.second: i64)): time.duration, &outw); expectexit(&outc, 0); expectexit(&outw, 0); @@ -1096,9 +1199,11 @@ fn cwdwritedata(dir: str, label: str) void = { let equivalent: []str = [driver("ww_ww"), "test", "-j", "3", "-I", "../tree-link", "../tree/p1", p1, "../tree/./p1/../p1", "../tree-link/p1"]; - runcommandenvdir(root, "cwd-direct", direct, cenv, caller, + runcommandinputenvdir(root, "cwd-direct", direct, cenv, caller, + stdinpath, (60i64 * (time.second: i64)): time.duration, &outc); - runcommandenvdir(root, "cwd-equivalent", equivalent, wenv, caller, + runcommandinputenvdir(root, "cwd-equivalent", equivalent, wenv, caller, + stdinpath, (60i64 * (time.second: i64)): time.duration, &outw); expectexit(&outc, 0); expectexit(&outw, 0); @@ -1124,6 +1229,9 @@ fn cwdwritedata(dir: str, label: str) void = { "/bin/pwd >> \"$WW_CWD_TOOL_TRACE\"\n", "printf 'compiler env=LC_ALL:%s TMPDIR:%s\\n' \"$LC_ALL\" ", "\"$TMPDIR\" >> \"$WW_CWD_TOOL_TRACE\"\n", + "if IFS= read -r WW_CWD_STDIN; then\n", + " printf 'compiler stdin=data\\n' >> \"$WW_CWD_TOOL_TRACE\"\n", + "else printf 'compiler stdin=eof\\n' >> \"$WW_CWD_TOOL_TRACE\"; fi\n", "printf 'compiler argv' >> \"$WW_CWD_TOOL_TRACE\"\n", "for arg in \"$@\"; do printf ' <%s>' \"$arg\" >> ", "\"$WW_CWD_TOOL_TRACE\"; done\n", @@ -1135,6 +1243,9 @@ fn cwdwritedata(dir: str, label: str) void = { "/bin/pwd >> \"$WW_CWD_TOOL_TRACE\"\n", "printf 'assembler env=LC_ALL:%s TMPDIR:%s\\n' \"$LC_ALL\" ", "\"$TMPDIR\" >> \"$WW_CWD_TOOL_TRACE\"\n", + "if IFS= read -r WW_CWD_STDIN; then\n", + " printf 'assembler stdin=data\\n' >> \"$WW_CWD_TOOL_TRACE\"\n", + "else printf 'assembler stdin=eof\\n' >> \"$WW_CWD_TOOL_TRACE\"; fi\n", "printf 'assembler argv' >> \"$WW_CWD_TOOL_TRACE\"\n", "for arg in \"$@\"; do printf ' <%s>' \"$arg\" >> ", "\"$WW_CWD_TOOL_TRACE\"; done\n", @@ -1146,6 +1257,9 @@ fn cwdwritedata(dir: str, label: str) void = { "/bin/pwd >> \"$WW_CWD_TOOL_TRACE\"\n", "printf 'linker env=LC_ALL:%s TMPDIR:%s\\n' \"$LC_ALL\" ", "\"$TMPDIR\" >> \"$WW_CWD_TOOL_TRACE\"\n", + "if IFS= read -r WW_CWD_STDIN; then\n", + " printf 'linker stdin=data\\n' >> \"$WW_CWD_TOOL_TRACE\"\n", + "else printf 'linker stdin=eof\\n' >> \"$WW_CWD_TOOL_TRACE\"; fi\n", "printf 'linker argv' >> \"$WW_CWD_TOOL_TRACE\"\n", "for arg in \"$@\"; do printf ' <%s>' \"$arg\" >> ", "\"$WW_CWD_TOOL_TRACE\"; done\n", @@ -1174,7 +1288,8 @@ fn cwdwritedata(dir: str, label: str) void = { assert(os.mkdir(work, 448i32) == 0); let persistav: []str = [driver("ww"), "test", "-w", work, "-I", tree, p1]; - runcommandenvdir(root, "cwd-persist-cold", persistav, traceenv, caller, + runcommandinputenvdir(root, "cwd-persist-cold", persistav, traceenv, + caller, stdinpath, (120i64 * (time.second: i64)): time.duration, &outc); expectexit(&outc, 0); cwdassertpackage(outc.stdout, "dep-init", p1, "p1-data", "p1-testdata"); @@ -1188,6 +1303,10 @@ fn cwdwritedata(dir: str, label: str) void = { assert(has(coldtrace, "compiler env=LC_ALL:C TMPDIR:/tmp/")); assert(has(coldtrace, "assembler env=LC_ALL:C TMPDIR:/tmp/")); assert(has(coldtrace, "linker env=LC_ALL:C TMPDIR:/tmp/")); + assert(has(coldtrace, "compiler stdin=eof\n")); + assert(has(coldtrace, "assembler stdin=eof\n")); + assert(has(coldtrace, "linker stdin=eof\n")); + assert(!has(coldtrace, "stdin=data\n")); assert(has(coldtrace, "compiler argv <")); assert(has(coldtrace, ".unit.new>")); assert(has(coldtrace, "assembler argv <")); @@ -1221,7 +1340,8 @@ fn cwdwritedata(dir: str, label: str) void = { }; rewritefile(tooltrace, ""); - runcommandenvdir(root, "cwd-persist-warm", persistav, traceenv, caller, + runcommandinputenvdir(root, "cwd-persist-warm", persistav, traceenv, + caller, stdinpath, (120i64 * (time.second: i64)): time.duration, &outw); expectexit(&outw, 0); assert(same(outc.stdout, outw.stdout)); @@ -1230,6 +1350,8 @@ fn cwdwritedata(dir: str, label: str) void = { assert(!has(warmtrace, "assembler cwd=")); assert(occurrences(warmtrace, "linker cwd=") == 1); assert(has(warmtrace, strings.concat("linker cwd=", caller, "\n"))); + assert(has(warmtrace, "linker stdin=eof\n")); + assert(!has(warmtrace, "stdin=data\n")); // Non-source data does not enter graph/action/storage bytes. The established // warm final relink remains one invocation, while the next uncached runtime @@ -1237,8 +1359,17 @@ fn cwdwritedata(dir: str, label: str) void = { rewritefile(strings.concat(p1, "/data.txt"), "p1-data-v2\n"); rewritefile(strings.concat(p1, "/testdata/input.txt"), "p1-testdata-v2\n"); + let changedstdin: []u8 = alloc([], 4096u64)!; + inputi = 0; + for (inputi < 2048) { + append(changedstdin, 'Y'); + append(changedstdin, '\n'); + inputi += 1; + }; + rewritefile(stdinpath, strings.frombytes(changedstdin)); rewritefile(tooltrace, ""); - runcommandenvdir(root, "cwd-persist-data", persistav, traceenv, caller, + runcommandinputenvdir(root, "cwd-persist-data", persistav, traceenv, + caller, stdinpath, (120i64 * (time.second: i64)): time.duration, &out); expectexit(&out, 0); cwdassertpackage(out.stdout, "dep-init", p1, @@ -1250,6 +1381,8 @@ fn cwdwritedata(dir: str, label: str) void = { assert(!has(datatrace, "assembler cwd=")); assert(occurrences(datatrace, "linker cwd=") == 1); assert(has(datatrace, strings.concat("linker cwd=", caller, "\n"))); + assert(has(datatrace, "linker stdin=eof\n")); + assert(!has(datatrace, "stdin=data\n")); i = 0; for (i < artifactpaths.len) { assert(same(artifacts[i], readfile(artifactpaths[i]))); @@ -1263,7 +1396,8 @@ fn cwdwritedata(dir: str, label: str) void = { let p1after: str = strings.concat(root, "/p1-after-data.test"); let afterav: []str = [driver("ww_ww"), "test", "-c", "-o", p1after, "-I", tree, p1]; - runcommandenvdir(root, "cwd-after-data-binary", afterav, wenv, caller, + runcommandinputenvdir(root, "cwd-after-data-binary", afterav, wenv, + caller, stdinpath, (90i64 * (time.second: i64)): time.duration, &out); expectexit(&out, 0); assert(same(readfile(p1cbin), readfile(p1after))); @@ -1295,8 +1429,8 @@ fn cwdwritedata(dir: str, label: str) void = { append(hideenv, strings.concat("WW_CWD_HIDE_DIR=", p1)); let hideav: []str = [driver("wwtest"), "package", "--ww-driver", hidewrapper, "-j", "2", "-I", tree, p1, p2]; - runcommandenvdir(root, strings.concat("cwd-hide-", hidestages[i]), - hideav, hideenv, caller, + runcommandinputenvdir(root, strings.concat("cwd-hide-", hidestages[i]), + hideav, hideenv, caller, stdinpath, (120i64 * (time.second: i64)): time.duration, &out); expectexit(&out, 1); assert(has(out.stderr, strings.concat("wrapper-hidden=", p1, "\n")));