ww: put selected toolchain first in test PATH

This commit is contained in:
2026-08-21 05:30:10 +09:00
parent 1b71250ad5
commit 9b6b1100f4
7 changed files with 586 additions and 96 deletions

View File

@@ -16,6 +16,7 @@
#include <fcntl.h> #include <fcntl.h>
#include <libgen.h> #include <libgen.h>
#include <limits.h> #include <limits.h>
#include <stdint.h>
#include <stdarg.h> #include <stdarg.h>
#ifndef PATH_MAX #ifndef PATH_MAX
@@ -82,6 +83,71 @@ run_argv(const char *prog, char *const argv[])
return 1; return 1;
} }
extern char **environ;
struct test_environment {
char **values;
char *path;
};
/* Pinned cmd/go appends PATH=$GOROOT/bin:$PATH to each test command and
* os/exec keeps the appended duplicate. The selected WW driver's sibling
* directory is the local toolchain-bin analogue. Build tools retain the
* caller's environment; only the test binary receives this array. */
static int
make_test_environment(const char *toolbin, struct test_environment *out)
{
const char *oldpath = getenv("PATH");
if (oldpath == NULL) oldpath = "";
size_t dirn = strlen(toolbin), oldn = strlen(oldpath);
if (oldn > SIZE_MAX - 7 || dirn > SIZE_MAX - oldn - 7) return -1;
size_t pathn = 5 + dirn + (oldn != 0 ? 1 + oldn : 0);
char *path = malloc(pathn + 1);
if (path == NULL) return -1;
memcpy(path, "PATH=", 5);
memcpy(path + 5, toolbin, dirn);
size_t at = 5 + dirn;
if (oldn != 0) {
path[at++] = ':';
memcpy(path + at, oldpath, oldn);
at += oldn;
}
path[at] = '\0';
size_t nenv = 0;
while (environ[nenv] != NULL) {
if (nenv == SIZE_MAX - 2) { free(path); return -1; }
nenv++;
}
if (nenv > SIZE_MAX / sizeof(char *) - 2) {
free(path);
return -1;
}
char **values = calloc(nenv + 2, sizeof *values);
if (values == NULL) { free(path); return -1; }
size_t n = 0;
int inserted = 0;
for (size_t i = 0; i < nenv; i++) {
if (strncmp(environ[i], "PATH=", 5) == 0) {
if (!inserted) { values[n++] = path; inserted = 1; }
} else {
values[n++] = environ[i];
}
}
if (!inserted) values[n++] = path;
values[n] = NULL;
out->values = values;
out->path = path;
return 0;
}
static void
free_test_environment(struct test_environment *env)
{
free(env->values);
free(env->path);
}
/* run_test_bin — exec the built test binary with an optional name-filter /* run_test_bin — exec the built test binary with an optional name-filter
* pattern as argv[1] (lib/test run() reads it via os.args). fork+execv * pattern as argv[1] (lib/test run() reads it via os.args). fork+execv
* (not system()) so glob metacharacters in the pattern reach the binary * (not system()) so glob metacharacters in the pattern reach the binary
@@ -91,14 +157,28 @@ run_argv(const char *prog, char *const argv[])
static int static int
run_test_bin(const char *bin, const char *pattern) run_test_bin(const char *bin, const char *pattern)
{ {
char toolbin[PATH_MAX];
if (realpath(self_dir, toolbin) == NULL) {
fputs("ww: cannot prepare test environment\n", stderr);
return -1;
}
struct test_environment env = {0};
if (make_test_environment(toolbin, &env) != 0) {
fputs("ww: cannot prepare test environment\n", stderr);
return -1;
}
pid_t pid = fork(); pid_t pid = fork();
if (pid < 0) { perror("ww: fork"); return -1; } if (pid < 0) {
perror("ww: fork");
free_test_environment(&env);
return -1;
}
if (pid == 0) { if (pid == 0) {
char *xargv[3]; char *xargv[3];
xargv[0] = (char *)bin; xargv[0] = (char *)bin;
if (pattern) { xargv[1] = (char *)pattern; xargv[2] = NULL; } if (pattern) { xargv[1] = (char *)pattern; xargv[2] = NULL; }
else { xargv[1] = NULL; } else { xargv[1] = NULL; }
execv(bin, xargv); execve(bin, xargv, env.values);
perror("ww: exec"); perror("ww: exec");
_exit(127); _exit(127);
} }
@@ -108,6 +188,7 @@ run_test_bin(const char *bin, const char *pattern)
* test PASS. */ * test PASS. */
pid_t got; pid_t got;
do { got = waitpid(pid, &status, 0); } while (got < 0 && errno == EINTR); do { got = waitpid(pid, &status, 0); } while (got < 0 && errno == EINTR);
free_test_environment(&env);
if (got < 0) { perror("ww: waitpid"); return -1; } if (got < 0) { perror("ww: waitpid"); return -1; }
if (WIFEXITED(status)) return WEXITSTATUS(status); if (WIFEXITED(status)) return WEXITSTATUS(status);
return 1; return 1;

View File

@@ -5849,22 +5849,24 @@ reinterpret them. No runtime coordinator-global `chdir` was added; its cwd and
`PWD` remain unchanged. `PWD` remain unchanged.
Each started product also receives a newly allocated run environment. It Each started product also receives a newly allocated run environment. It
retains inherited entries in order except exact `TMPDIR=`, `LC_ALL=`, and retains inherited entries in order except exact `TMPDIR=`, `LC_ALL=`, uppercase
uppercase `PWD=` entries, then appends `LC_ALL=C`, the product-local absolute `PATH=`, and uppercase `PWD=` entries, then appends `LC_ALL=C`, the
`TMPDIR`, and `PWD=<pkggroup.dir>`. Removing prior `PWD` entries reproduces product-local absolute `TMPDIR`, the selected toolchain `PATH`, and
Go's observable last-wins result because WW's executor intentionally preserves `PWD=<pkggroup.dir>`. Removing prior `PWD` and `PATH` entries reproduces Go's
observable last-wins result because WW's executor intentionally preserves
duplicates and WW `os.getenv` returns the first one. Case-distinct and malformed duplicates and WW `os.getenv` returns the first one. Case-distinct and malformed
entries remain untouched. The product observes exactly one uppercase `PWD`, at entries remain untouched. The product observes exactly one uppercase `PWD`, at
the appended position. the appended position, and one effective uppercase `PATH`; section 11.32 owns
the latter rule.
The vector and both generated strings are product-local, dynamically sized, The vector and its generated strings are product-local, dynamically sized,
and published only after every checked allocation succeeds. Partial failure and published only after every checked allocation succeeds. Partial failure
frees only initialized owned storage and never frees borrowed inherited frees only initialized owned storage and never frees borrowed inherited
strings. `exec.start` synchronously deep-copies the command before returning, strings. `exec.start` synchronously deep-copies the command before returning,
after which the coordinator frees its run vector, generated `TMPDIR`, generated after which the coordinator frees its run vector, generated `TMPDIR`, generated
`PWD`, generated `-package` argument, and argv vector. Concurrent children `PATH`, generated `PWD`, generated `-package` argument, and argv vector.
therefore hold independent fork snapshots; no shared environment vector or Concurrent children therefore hold independent fork snapshots; no shared
process-global state is mutated. environment vector or process-global state is mutated.
All dependency initialization occurs inside that product process. A production All dependency initialization occurs inside that product process. A production
or test-only dependency reached by package `p` sees `p`'s directory. If the or test-only dependency reached by package `p` sees `p`'s directory. If the
@@ -5880,15 +5882,19 @@ emission remains byte-sorted and identical to `-j 1`.
`-c -o`, builds or publishes but never enters `pkgstartrun`; no execution cwd `-c -o`, builds or publishes but never enters `pkgstartrun`; no execution cwd
or run environment is allocated. A published binary subsequently invoked by or run environment is allocated. A published binary subsequently invoked by
the user bypasses the coordinator and inherits the user's cwd and environment. the user bypasses the coordinator and inherits the user's cwd and environment.
The raw single-file test compatibility route is likewise unchanged. A The raw single-file test compatibility route retains its caller cwd, `PWD`,
directory with no selected test source still creates no support, generated stdin, and stream behavior, but section 11.32 applies the test-process `PATH`
main, link, run, result, or execution-context state. rule at its driver-owned launch. A directory with no selected test source still
creates no support, generated main, link, run, result, or execution-context
state.
Build-plan commands retain `dir=""` and their existing tool environment. Build-plan commands retain `dir=""` and their existing tool environment.
Compiler, assembler, in-driver archiver, linker, support generation, generated Compiler, assembler, in-driver archiver, linker, support generation, and
main construction, the Cstage driver, and the WWstage driver therefore retain generated-main construction therefore retain their exact prior cwd, argv, and
their exact prior cwd, argv, and environment. The runtime rule required no environment. The directory cwd rule required no `lib/os/exec`, compiler,
`lib/os/exec`, compiler, checker, writer, assembler, linker, or driver change. checker, writer, assembler, linker, or driver change; the later test-only PATH
rule is isolated at launch and changes neither build-plan environment nor those
tools.
Independent Cstage/WWstage compile-only products remain byte-identical, and Independent Cstage/WWstage compile-only products remain byte-identical, and
changing only `data.txt` or `testdata` changes no unit, `.wwi`, assembly, changing only `data.txt` or `testdata` changes no unit, `.wwi`, assembly,
object, archive, generated-main, or binary bytes. object, archive, generated-main, or binary bytes.
@@ -5916,13 +5922,15 @@ The focused native owner is
`directory_test_execution_working_directory` in `directory_test_execution_working_directory` in
`test/package/package_test.ww`. It creates only disposable source trees and `test/package/package_test.ww`. It creates only disposable source trees and
compares Cstage and WWstage across duplicate and large environments; exact compares Cstage and WWstage across duplicate and large environments; exact
`getcwd`, effective/count/position of `PWD`; ordinary data, `testdata`, and `getcwd`, effective/count/position of `PWD`; absent, empty, nonempty, and
duplicate inherited `PATH`; ordinary data, `testdata`, and
relative writes; all production/internal/external/test-only shapes; relative writes; all production/internal/external/test-only shapes;
production and test-only dependency initialization; recompiled external production and test-only dependency initialization; recompiled external
self-import; direct/recursive/redundant/absolute/symlink roots; reversed roots self-import; direct/recursive/redundant/absolute/symlink roots; reversed roots
and creation order; `-j 1`/parallel execution; filters/list/no-match; failure and creation order; `-j 1`/parallel execution; filters/list/no-match; failure
and timeout; no-test and build paths; `-c`, `-c -o`, direct binaries, and raw and timeout; no-test and build paths; `-c`, `-c -o`, running retained tests,
single files; exact tool cwd/argv/locale/TMPDIR; persistent data-only reuse and direct binaries, and raw single files; exact tool cwd/argv/locale/TMPDIR/PATH;
persistent data-only reuse and
artifact/binary identity; and a deterministic post-build directory removal artifact/binary identity; and a deterministic post-build directory removal
where the affected product reports `ENOENT` while its sibling succeeds. where the affected product reports `ENOENT` while its sibling succeeds.
Checked command-global allocation-failure parity remains owned by Checked command-global allocation-failure parity remains owned by
@@ -6904,6 +6912,141 @@ and retained-binary contracts. Existing request-transaction, timeout,
interruption, and concurrent-driver owners continue to cover those unchanged interruption, and concurrent-driver owners continue to cover those unchanged
dimensions. dimensions.
### 11.32 Implemented selected toolchain first in test `PATH`
Every test binary actually started by `ww test` now receives one effective
uppercase `PATH` beginning with the canonical absolute directory of the
selected WW driver. An absent or empty inherited value produces only that
directory. A nonempty first effective inherited value follows it after `:`;
ordinary duplicate `PATH=` entries collapse to the one child value. This is
test-process metadata only: build tools and every request that starts no test
process retain their prior environment.
#### Pinned Go evidence and classification
The sole authority is official Go 1.26.5 at commit
`c19862e5f8415b4f24b189d065ed739517c548ba`:
- The `go test` command documentation states that `$GOROOT/bin` is placed at
the beginning of the test process's `PATH`, so an executed test resolves the
`go` command from the invoking toolchain
([`cmd/go/internal/test/test.go`, lines 8794](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/test/test.go#L87-L94)).
- `(*runTestActor).Act` begins with `cfg.OrigEnv`, applies
`base.AppendPATH`, then `base.AppendPWD`, and assigns that environment to
each test command
([`cmd/go/internal/test/test.go`, lines 16611668](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/test/test.go#L1661-L1668)).
`AppendPATH` appends `PATH=$GOROOT/bin` for an empty effective inherited
value and `PATH=$GOROOT/bin:<old>` otherwise
([`cmd/go/internal/base/env.go`, lines 2945](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/base/env.go#L29-L45)).
- `Cmd.environ` removes duplicate environment keys while preferring the later
entry, making the appended value effective
([`os/exec/exec.go`, lines 12311265](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/os/exec/exec.go#L1231-L1265)).
On the supported Unix model, initial environment lookup uses the first
inherited occurrence
([`syscall/env_unix.go`, lines 2044 and 6684](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/syscall/env_unix.go#L20-L84)).
- Official `test_goroot_PATH.txt` tests both an empty `PATH` and a nonempty
directory containing no executable; the test must find the `go` executable
in the current toolchain's `$GOROOT/bin`
([lines 141](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/testdata/script/test_goroot_PATH.txt#L1-L41)).
Those source rules and official assertions are behavior directly implemented
or asserted by pinned Go. Using the selected WW driver's sibling directory as
the local toolchain-bin analogue is derived from that implementation: WW has
no `GOROOT`, and that directory already supplies the driver's default compiler,
assembler, linker, and package-test coordinator. Applying the rule to WW's raw
single-file compatibility route is also derived; it is a local input extension,
but it executes the same observable test and initialization code. Canonical
absolute spelling is runtime metadata only and prevents a relative driver path
from leaking the coordinator's later package cwd into `PATH`.
#### Fresh four-axis audit and direct pre-fix measurements
The bounded audit considered all four permanent axes and selected only this
test-runtime gap:
- **Build:** pinned `runBuild` loads all roots and checks package errors before
output/action construction
([`cmd/go/internal/work/build.go`, lines 459478](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/work/build.go#L459-L478)); official
`build_json.txt` distinguishes load errors from compiler failures
([lines 1526 and 3945](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/testdata/script/build_json.txt#L15-L45)).
A direct missing-import probe made both WW stages exit `1` with the identical
source diagnostic and no producer or public output. This candidate was
aligned.
- **Test:** direct Cstage and WWstage directory probes, each invoked with
`PATH=/usr/bin:/bin`, both exited `0` while the test printed exactly that
unchanged value. `/home/kimchi/src/ww/out/bin` was absent. These are directly
measured pre-fix WW facts and establish the selected external difference.
- **Package:** pinned `types2.(*Checker).initFiles` rejects package name `_`
([`cmd/compile/internal/types2/check.go`, lines 311355](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/compile/internal/types2/check.go#L311-L355)),
with official anchors in
[`internal/types/testdata/check/blank.go`](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/internal/types/testdata/check/blank.go#L1-L5)
and [`test/blank1.go`](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/test/blank1.go#L1-L10).
Direct production, imported, and test-only probes were rejected identically
by both WW stages. This candidate was aligned.
- **Import:** pinned `unusedImports` requires every nonblank import binding to
be used
([`cmd/compile/internal/types2/resolver.go`, lines 706740](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/compile/internal/types2/resolver.go#L706-L740)); official
`importdecl0` covers default, alias, dot, and blank forms
([lines 531](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/internal/types/testdata/check/importdecl0/importdecl0a.go#L5-L31)).
A direct unused default-import probe produced identical Cstage/WWstage
diagnostics and no committed work. This candidate was aligned.
The build, package, and import observations above are directly measured WW
behavior; the linked rules are behavior directly implemented or asserted by
pinned Go. The conclusion that this slice crosses those axes only when code in
a successfully loaded test variant or initialized dependency observes `PATH`
is derived from the pinned launch placement.
#### Ownership, launch behavior, and preserved boundaries
The environment is synthesized at the three true test-process launch owners:
`internal/wwpackage.pkgstartrun` for directory products, Cstage
`run_test_bin`, and WWstage `runsingletest` for raw single-file tests. Each
uses the selected driver directory's canonical absolute spelling. The first
effective uppercase inherited value is the suffix; absent and empty values
have no suffix; ordinary uppercase duplicates are removed. Unrelated entries,
including case-distinct and malformed names, retain their previous order and
bytes. Directory products retain their established appended locale, temporary
directory, and final `PWD`; raw tests retain caller cwd, `PWD`, stdin, and split
streams.
The rule covers internal, external, and combined directory products;
dependency initialization; filters and list mode; running retained tests; and
raw single-file tests. It is independently materialized for each concurrent
product. `ww build`, `ww run`, compiler/assembler/linker and generated-main
commands, compile-only and assembly-only tests, no-test products, rejected
requests, and later direct execution of a retained binary receive no test
environment transformation.
Loading, graph construction, compilation, assembly, linking, action keys, and
artifact production are unchanged. A load, compile, or link failure starts no
test process, so runtime `PATH` is inapplicable and the existing diagnostic
precedence remains. A started test observes the new environment before normal
success, assertion failure, signal, timeout, or interruption. Existing process
groups, cancellation, output capture, and cleanup own those outcomes; the
environment adds no global mutable state. A running retained request remains
private build, private run, then guarded install, so any unsuccessful run
publishes nothing and preserves prior bytes. Parallel products receive separate
environment arrays and retain existing result isolation.
Canonical physical driver directories do not become package, import, graph,
action, artifact, symbol, `.wwi`, publication, or persistence identity.
Compiled units, interfaces, archives, executables, modes, diagnostics, and
public-output disposition are unchanged. No stored key or byte changed, so
build workdir format remains `18`, test workdir format remains `19`, and
semantic storage format remains `3`.
The extended WW-native `directory_test_execution_working_directory` observer
proves Cstage/WWstage equality for absent, empty, nonempty, and duplicate
caller `PATH`; all directory test shapes and dependency initialization;
filters, listing, raw single-file, and running retained tests; relative selected
driver canonicalization; build/no-test/compile-only/rejection exclusions;
unchanged compiler, assembler, and linker environments; concurrent products;
runtime failure with prior retained-byte rollback; artifact-byte identity; and
transaction, stage, workdir, and generated-file cleanup. Existing signal,
timeout, interruption, concurrent-driver, and public-output transaction owners
cover the unchanged mechanisms at those boundaries.
## 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

@@ -757,6 +757,17 @@ remaining separate actions. Reachable dependency initialization consequently
observes the tested product's directory; a dependency tested as its own product observes the tested product's directory; a dependency tested as its own product
observes its own directory. observes its own directory.
Every test binary actually started by `ww test` receives one effective uppercase
`PATH` whose first element is the canonical absolute directory of the selected
WW driver. An absent or empty caller `PATH` yields only that directory; a
nonempty effective caller value follows it after `:`. Normal duplicate `PATH=`
entries collapse to that one value, using the caller's first effective value as
the suffix. This applies to directory products, their dependency initialization,
filters and list mode, running retained tests, and the raw single-file
compatibility route. It does not apply to build tools, `ww build`, `ww run`,
compile-only or assembly-only test requests, no-test products, or later direct
execution of a retained binary.
The same coordinator-executed product reads standard input from the null The same coordinator-executed product reads standard input from the null
device. Its first read observes EOF regardless of the terminal, pipe, or file 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 connected to the invoking command. Each parallel product owns a separate null
@@ -781,9 +792,11 @@ Relative ordinary files, `testdata`, and writes resolve there for every
executing filter or list path. `ww build`, directory `ww test -c` (including 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 and
environment, and three standard descriptors; no package directory, input, or three standard descriptors; no package directory, input, or output policy is
output policy is embedded or forced by the binary. embedded or forced by the binary. Directly invoked retained binaries inherit the
entire caller environment. Raw single-file tests preserve every caller
environment field except the test-only `PATH` transformation above.
--- ---

View File

@@ -291,6 +291,18 @@ creates a separate process using the dependency directory. Equivalent direct,
recursive, redundant, absolute, and root-symlink spellings converge before recursive, redundant, absolute, and root-symlink spellings converge before
this runtime field is assigned. this runtime field is assigned.
Every test binary actually executed by either driver stage also receives one
effective uppercase `PATH` beginning with the canonical absolute directory of
the selected WW driver. Missing and empty caller values produce only that
directory; a nonempty first effective caller value follows it after `:`.
Normal duplicate `PATH=` entries collapse to the one child value. Directory
products allocate this environment independently per concurrent child; the raw
single-file route performs the same PATH transformation while retaining its
caller cwd, `PWD`, stdin, and split streams. Running retained tests transform
the private run and install only after success. Compile-only, assembly-only,
no-test, rejected, and build requests start no test process and therefore have
no test-PATH state.
Every such product also receives an independently opened null device as fd 0. 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 Caller terminal, pipe, and file bytes remain with the coordinator; serial and
parallel products observe immediate EOF rather than consuming a shared input parallel products observe immediate EOF rather than consuming a shared input
@@ -312,9 +324,10 @@ linker, support, and generated-main commands retain their build-plan cwd and
environment. Captured directory build plans begin with null stdin, which their environment. Captured directory build plans begin with null stdin, which their
inherited-stdio tool descendants retain. `ww build`, `ww test -c`, `-c -o`, and 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 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 directly inherits the user's cwd/environment/stdin. The raw single-file
cwd/environment/stdin and contain no forced package-directory or input compatibility path inherits caller cwd/stdin and all unrelated environment
behavior. entries, with only the test-PATH transformation above; neither route embeds a
package directory or input behavior in the binary.
Before package grouping, both directory drivers and the shared recursive 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 coordinator apply Go 1.26.5's filename OS/architecture rule for WW's fixed
@@ -401,17 +414,20 @@ The same package owner contains the focused
independent temporary directories from an unrelated caller cwd and compares independent temporary directories from an unrelated caller cwd and compares
Cstage/WWstage output for production/internal/external/combined and every Cstage/WWstage output for production/internal/external/combined and every
test-only shape; production and test-only dependency initialization; test-only shape; production and test-only dependency initialization;
recompiled external self-import; duplicate inherited `PWD`; ordinary data, recompiled external self-import; duplicate inherited `PWD`; absent, empty,
nonempty, and duplicate inherited `PATH`; ordinary data,
`testdata`, relative writes, and deliberately nonempty caller stdin; `testdata`, relative writes, and deliberately nonempty caller stdin;
direct/recursive/redundant/absolute/symlink roots; reversed request and creation direct/recursive/redundant/absolute/symlink roots; reversed request and creation
order; serial and parallel products; filters, list, and no-match execution; order; serial and parallel products; filters, list, and no-match execution;
failure and timeout; build/no-test and compile-only paths; direct published failure and timeout; build/no-test and compile-only paths; running retained
binaries and raw single-file execution; build-tool cwd/argv/environment/stdin; success and failed-run rollback; 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 persistent data-only reuse and artifact bytes; input-open failure before capture
creation; source-class rejection with empty workdirs; and deterministic 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`, one toolchain-first child `PATH`, requires EOF for captured
actions and caller data for inherited-
stdio routes, alternates fd-1/fd-2 write syscalls through every executing stdio routes, alternates fd-1/fd-2 write syscalls through every executing
variant and initializer, requires one ordered stdout stream on success, variant and initializer, requires one ordered stdout stream on success,
assertion failure, signal, timeout, and setup failure, requires direct/raw assertion failure, signal, timeout, and setup failure, requires direct/raw
@@ -520,11 +536,12 @@ real directory test products always run, while no-selected-test directories do
not create a process. The byte-identity and bootstrap gates keep building on not create a process. The byte-identity and bootstrap gates keep building on
fresh scratch. `make clean` reclaims every workdir under `out/`. fresh scratch. `make clean` reclaims every workdir under `out/`.
Directory-product cwd and `PWD` are request-time child-process metadata, not Directory-product cwd, `PWD`, and test `PATH` are request-time child-process
persisted action inputs or results. A runtime directory-entry failure therefore metadata, not persisted action inputs or results. A runtime directory-entry
does not erase a successfully committed compilation generation. Changing only failure therefore does not erase a successfully committed compilation
ordinary fixture data causes no unit/export/assembly/object/archive/main/binary generation. Changing only ordinary fixture data causes no
change and no producer work beyond the same established warm final relink, unit/export/assembly/object/archive/main/binary change and no producer work
beyond the same established warm final relink,
while the next always-run product observes the new bytes. Build workdir format while the next always-run product observes the new bytes. Build workdir format
remains 18, test workdir format remains 19, and semantic storage remains 3. remains 18, test workdir format remains 19, and semantic storage remains 3.

View File

@@ -268,17 +268,27 @@ fn pkgfreestrs(values: []str) void = {
}; };
}; };
// Go's AppendPWD appends, then os/exec keeps the last duplicate value. WW's // Go's AppendPATH and AppendPWD append, then os/exec keeps the last duplicate
// executor preserves duplicates, so discard earlier exact PWD entries here. // values. WW's executor preserves duplicates, so discard earlier exact entries
fn runenv(tmpdir: str, pwd: str, out: *[]str, tmpowned: *str, // and materialize the selected driver's sibling directory first in PATH.
pwdowned: *str) bool = { fn runenv(tmpdir: str, pwd: str, builder: str, out: *[]str, tmpowned: *str,
pathowned: *str, pwdowned: *str) bool = {
let toolbin: str;
let oom: bool = false;
if (!pkgcanonicaldir(pkgdirname(builder), &toolbin, &oom)) {
if (!oom) {
pkgputln(os.STDERR_FILENO,
"wwtest package: cannot determine toolchain directory");
};
return false;
};
let inherited: []str = os.getenvs(); let inherited: []str = os.getenvs();
if (inherited.len > PKG_COUNT_MAX - 3) { if (inherited.len > PKG_COUNT_MAX - 4) {
pkgputln(os.STDERR_FILENO, pkgputln(os.STDERR_FILENO,
"wwtest package: package graph is too large"); "wwtest package: package graph is too large");
return false; return false;
}; };
let allocation: ([]str | nomem) = pkgallocstrs(inherited.len + 3); let allocation: ([]str | nomem) = pkgallocstrs(inherited.len + 4);
let env: []str; let env: []str;
match (allocation) { match (allocation) {
case let value: []str => env = value; case let value: []str => env = value;
@@ -291,6 +301,7 @@ fn runenv(tmpdir: str, pwd: str, out: *[]str, tmpowned: *str,
for (i < inherited.len) { for (i < inherited.len) {
if (!strings.hasprefix(inherited[i], "TMPDIR=") if (!strings.hasprefix(inherited[i], "TMPDIR=")
&& !strings.hasprefix(inherited[i], "LC_ALL=") && !strings.hasprefix(inherited[i], "LC_ALL=")
&& !strings.hasprefix(inherited[i], "PATH=")
&& !strings.hasprefix(inherited[i], "PWD=")) { && !strings.hasprefix(inherited[i], "PWD=")) {
append(env, inherited[i]); append(env, inherited[i]);
}; };
@@ -302,22 +313,44 @@ fn runenv(tmpdir: str, pwd: str, out: *[]str, tmpowned: *str,
pkgfreestrs(env); pkgfreestrs(env);
return false; return false;
}; };
let pwdenv: str; let pathenv: str;
if (!pkgstring(&pwdenv, "PWD=", pwd)) { let pathok: bool = false;
match (os.getenv("PATH")) {
case let inheritedpath: str => {
if (inheritedpath.len == 0) {
pathok = pkgstring(&pathenv, "PATH=", toolbin);
} else {
pathok = pkgstring(&pathenv, "PATH=", toolbin, ":",
inheritedpath);
};
};
case void => pathok = pkgstring(&pathenv, "PATH=", toolbin);
};
if (!pathok) {
pkgfreeownedstr(tmpenv); pkgfreeownedstr(tmpenv);
pkgfreestrs(env); pkgfreestrs(env);
return false; return false;
}; };
let pwdenv: str;
if (!pkgstring(&pwdenv, "PWD=", pwd)) {
pkgfreeownedstr(tmpenv);
pkgfreeownedstr(pathenv);
pkgfreestrs(env);
return false;
};
append(env, tmpenv); append(env, tmpenv);
append(env, pathenv);
append(env, pwdenv); append(env, pwdenv);
*out = env; *out = env;
*tmpowned = tmpenv; *tmpowned = tmpenv;
*pathowned = pathenv;
*pwdowned = pwdenv; *pwdowned = pwdenv;
return true; return true;
}; };
fn freerunenv(env: []str, tmpowned: str, pwdowned: str) void = { fn freerunenv(env: []str, tmpowned: str, pathowned: str, pwdowned: str) void = {
pkgfreeownedstr(tmpowned); pkgfreeownedstr(tmpowned);
pkgfreeownedstr(pathowned);
pkgfreeownedstr(pwdowned); pkgfreeownedstr(pwdowned);
pkgfreestrs(env); pkgfreestrs(env);
}; };
@@ -1240,8 +1273,8 @@ fn pkgmakedir(path: str) bool = {
}; };
// Resolve a directory to the kernel's symlink-free absolute spelling. The // Resolve a directory to the kernel's symlink-free absolute spelling. The
// coordinator is single-threaded while planning, so the temporary cwd change // coordinator is single-threaded; each temporary cwd change is restored before
// cannot race a child process launch. // it plans further paths or launches another child.
fn pkgcanonicaldir(path: str, out: *str, oom: *bool) bool = { fn pkgcanonicaldir(path: str, out: *str, oom: *bool) bool = {
*oom = false; *oom = false;
let beforeallocation: ([]u8 | nomem) = pkgallocbytes(os.PATH_MAX); let beforeallocation: ([]u8 | nomem) = pkgallocbytes(os.PATH_MAX);
@@ -1553,7 +1586,7 @@ fn pkgstartbuild(p: *pkgplan, groups: []pkggroup, builder: str, includes: []str,
}; };
fn pkgstartrun(g: *pkggroup, filters: []str, timeoutarg: str, fn pkgstartrun(g: *pkggroup, filters: []str, timeoutarg: str,
list: bool, h: *exec.process) bool = { list: bool, builder: str, h: *exec.process) bool = {
if (filters.len > PKG_COUNT_MAX - 4) { if (filters.len > PKG_COUNT_MAX - 4) {
pkgputln(os.STDERR_FILENO, pkgputln(os.STDERR_FILENO,
"wwtest package: package graph is too large"); "wwtest package: package graph is too large");
@@ -1581,8 +1614,10 @@ fn pkgstartrun(g: *pkggroup, filters: []str, timeoutarg: str,
for (i < filters.len) { append(ra, filters[i]); i += 1; }; for (i < filters.len) { append(ra, filters[i]); i += 1; };
let env: []str; let env: []str;
let tmpowned: str; let tmpowned: str;
let pathowned: str;
let pwdowned: str; let pwdowned: str;
if (!runenv(g.root, g.dir, &env, &tmpowned, &pwdowned)) { if (!runenv(g.root, g.dir, builder, &env, &tmpowned,
&pathowned, &pwdowned)) {
pkgfreeownedstr(packagearg); pkgfreeownedstr(packagearg);
pkgfreestrs(ra); pkgfreestrs(ra);
return false; return false;
@@ -1598,7 +1633,7 @@ fn pkgstartrun(g: *pkggroup, filters: []str, timeoutarg: str,
rcmd.deadline.nsec = 0i64; rcmd.deadline.nsec = 0i64;
rcmd.grace = 0i64: time.duration; rcmd.grace = 0i64: time.duration;
exec.start(h, &rcmd); exec.start(h, &rcmd);
freerunenv(env, tmpowned, pwdowned); freerunenv(env, tmpowned, pathowned, pwdowned);
pkgfreeownedstr(packagearg); pkgfreeownedstr(packagearg);
pkgfreestrs(ra); pkgfreestrs(ra);
return true; return true;
@@ -2561,7 +2596,7 @@ export fn packagecommand(args: []str) int = {
productcompleted += 1; productcompleted += 1;
} else { } else {
if (!pkgstartrun(g, filters, timeoutarg, list, if (!pkgstartrun(g, filters, timeoutarg, list,
&runhandles[gi])) { builder, &runhandles[gi])) {
g.runstartfailed = true; g.runstartfailed = true;
g.state = PKGDONE; g.state = PKGDONE;
productcompleted += 1; productcompleted += 1;

View File

@@ -1632,6 +1632,88 @@ fn sepallocptrs(cap: i32) ([]*u8 | nomem) = {
return value; return value;
}; };
type septestenv = struct {
values: []str,
path: str,
};
// Pinned cmd/go gives each test binary PATH=$GOROOT/bin:$PATH. The selected
// WW driver's sibling directory is the local toolchain-bin analogue. Remove
// normal inherited PATH duplicates because lib/os/exec deliberately preserves
// the concrete environment array while Go's os/exec keeps the appended value.
fn sepmaketestenv(toolbin: str, out: *septestenv) bool = {
let inherited: []str = os.getenvs();
if (inherited.len == SEP_COUNT_MAX) {
cerr("ww: cannot prepare test environment\n");
return false;
};
let oldpath: str = "";
match (os.getenv("PATH")) {
case let value: str => oldpath = value;
case void => void;
};
let total: i64 = 5i64 + (toolbin.len: i64);
if (oldpath.len != 0) { total += 1i64 + (oldpath.len: i64); };
if (total > SEP_COUNT_MAX: i64) {
cerr("ww: cannot prepare test environment\n");
return false;
};
let pathallocation: ([]u8 | nomem) = sepallocbytes(total: i32);
let pathbytes: []u8;
match (pathallocation) {
case let value: []u8 => pathbytes = value;
case nomem => {
cerr("ww: cannot prepare test environment\n");
return false;
};
};
let prefix: str = "PATH=";
let i: i32 = 0;
for (i < prefix.len) { append(pathbytes, prefix[i]); i += 1; };
i = 0;
for (i < toolbin.len) { append(pathbytes, toolbin[i]); i += 1; };
if (oldpath.len != 0) {
append(pathbytes, ':');
i = 0;
for (i < oldpath.len) { append(pathbytes, oldpath[i]); i += 1; };
};
let pathenv: str = strings.frombytes(pathbytes);
let envallocation: ([]str | nomem) = sepallocstrs(inherited.len + 1);
let env: []str;
match (envallocation) {
case let value: []str => env = value;
case nomem => {
os.free(pathenv.ptr: *void, pathenv.len: u64);
cerr("ww: cannot prepare test environment\n");
return false;
};
};
i = 0;
let inserted: bool = false;
for (i < inherited.len) {
if (strings.hasprefix(inherited[i], "PATH=")) {
if (!inserted) { append(env, pathenv); inserted = true; };
} else {
append(env, inherited[i]);
};
i += 1;
};
if (!inserted) { append(env, pathenv); };
out.values = env;
out.path = pathenv;
return true;
};
fn sepfreetestenv(env: *septestenv) void = {
if (env.path.ptr != nil && env.path.len != 0) {
os.free(env.path.ptr: *void, env.path.len: u64);
};
if (env.values.ptr != nil && env.values.cap != 0) {
os.free(env.values.ptr: *void,
(env.values.cap: u64) * (size(str): u64));
};
};
fn sepallocgraph(pkg: []seppkg, context: []sepcontext) (*sepgraph | nomem) = { fn sepallocgraph(pkg: []seppkg, context: []sepcontext) (*sepgraph | nomem) = {
let emptyfolds: []sepfoldentry; let emptyfolds: []sepfoldentry;
let value: *sepgraph = alloc(sepgraph{ let value: *sepgraph = alloc(sepgraph{
@@ -9186,21 +9268,30 @@ fn runsingletest(selfdir: *u8, src: *u8, incs: *u8, compileonly: i32,
if (pattern != nil) { if (pattern != nil) {
append(execargv, pathstr(pattern)); append(execargv, pathstr(pattern));
}; };
let env: []str = os.getenvs();
let result: exec.result; let result: exec.result;
exec.runstdio(pathstr(outp), execargv, env, &result);
let rc: i32 = 1; let rc: i32 = 1;
if (result.termination == exec.termination.EXIT) { let env: septestenv;
rc = result.code; let toolbin: *u8 = canonicaldir(pathstr(selfdir));
} else { if (result.termination == exec.termination.ERROR) { if (toolbin == nil) {
if (result.code == 127) { cerr("ww: cannot prepare test environment\n");
cerr("ww: execve failed\n"); } else { if (sepmaketestenv(pathstr(toolbin), &env)) {
rc = 127; exec.runstdio(pathstr(outp), execargv, env.values, &result);
} else { sepfreetestenv(&env);
cerr("ww: process launch/wait failed\n"); if (result.termination == exec.termination.EXIT) {
rc = -1; rc = result.code;
}; } else { if (result.termination == exec.termination.ERROR) {
if (result.code == 127) {
cerr("ww: execve failed\n");
rc = 127;
} else {
cerr("ww: process launch/wait failed\n");
rc = -1;
};
}; };
}; }; }; };
if (toolbin != nil) {
os.free(toolbin: *void, cstrlen(toolbin) + 1u64);
};
if (rc == 0 && deferredinstall if (rc == 0 && deferredinstall
&& sepinstalltestoutput(outp, outstem) != 0) { rc = 1; }; && sepinstalltestoutput(outp, outstem) != 0) { rc = 1; };
if (owntmp) { if (owntmp) {

View File

@@ -633,10 +633,11 @@ fn hexbytes(value: str) str = {
fn cwdtestenv(stage: str) []str = { fn cwdtestenv(stage: str) []str = {
let inherited: []str = os.getenvs(); let inherited: []str = os.getenvs();
let env: []str = alloc([], (inherited.len + 8): u64)!; let env: []str = alloc([], (inherited.len + 10): u64)!;
let i: i32 = 0; let i: i32 = 0;
for (i < inherited.len) { for (i < inherited.len) {
if (!strings.hasprefix(inherited[i], "PWD=") if (!strings.hasprefix(inherited[i], "PWD=")
&& !strings.hasprefix(inherited[i], "PATH=")
&& !strings.hasprefix(inherited[i], "WW_W6C=") && !strings.hasprefix(inherited[i], "WW_W6C=")
&& !strings.hasprefix(inherited[i], "WW_W6A=") && !strings.hasprefix(inherited[i], "WW_W6A=")
&& !strings.hasprefix(inherited[i], "WW_W6L=") && !strings.hasprefix(inherited[i], "WW_W6L=")
@@ -646,6 +647,7 @@ fn cwdtestenv(stage: str) []str = {
}; };
i += 1; i += 1;
}; };
append(env, "PATH=/ww-path-inherited-first");
append(env, "PWD=/ww-cwd-inherited-first"); append(env, "PWD=/ww-cwd-inherited-first");
if (same(stage, "ww")) { if (same(stage, "ww")) {
append(env, strings.concat("WW_W6C=", driver("w6c"))); append(env, strings.concat("WW_W6C=", driver("w6c")));
@@ -658,10 +660,25 @@ fn cwdtestenv(stage: str) []str = {
append(env, strings.concat("WW_W6L=", driver("w6l_ww"))); append(env, strings.concat("WW_W6L=", driver("w6l_ww")));
}; };
append(env, strings.concat("WW_SRCLIB=", repo(), "/lib")); append(env, strings.concat("WW_SRCLIB=", repo(), "/lib"));
append(env, "PATH=/ww-path-inherited-last");
append(env, "PWD=/ww-cwd-inherited-last"); append(env, "PWD=/ww-cwd-inherited-last");
return env; return env;
}; };
fn cwdassertpath(text: str, label: str, path: str, count: str) void = {
assert(has(text, strings.concat(label, " PATH=", path, "\n")));
assert(has(text, strings.concat(label, " PATH-count=", count, "\n")));
};
fn cwdasserttestpath(text: str, label: str) void = {
cwdassertpath(text, label, strings.concat(repo(),
"/out/bin:/ww-path-inherited-first"), "1");
};
fn cwdassertcallerpath(text: str, label: str) void = {
cwdassertpath(text, label, "/ww-path-inherited-first", "2");
};
fn cwdassertrecord(text: str, label: str, cwd: str, pwd: str, fn cwdassertrecord(text: str, label: str, cwd: str, pwd: str,
pwdcount: str, pwdlast: str, data: str, testdata: str, pwdcount: str, pwdlast: str, data: str, testdata: str,
input: str) void = { input: str) void = {
@@ -707,6 +724,7 @@ 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");
cwdasserttestpath(text, label);
cwdassertmergedstreams(text, label, dir); cwdassertmergedstreams(text, label, dir);
}; };
@@ -883,27 +901,17 @@ fn cwdwritedata(dir: str, label: str) void = {
" let body: str; body.ptr = bytes.ptr; body.len = bytes.len; put(body);\n", " let body: str; body.ptr = bytes.ptr; body.len = bytes.len; put(body);\n",
" if (body.len == 0 || body[body.len - 1] != '\\n') { put(\"\\n\"); };\n", " if (body.len == 0 || body[body.len - 1] != '\\n') { put(\"\\n\"); };\n",
"};\n", "};\n",
"export fn run(label: str) void = {\n", "export fn run(label: str) void = {\n 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 case let value: str => put(value); case void => put(\"<unset>\");\n }; put(\"\\n\");\n put(label); put(\" PATH=\");\n match (os.getenv(\"PATH\")) {\n case let value: str => put(value); case void => put(\"<unset>\");\n }; put(\"\\n\");\n let env: []str = os.getenvs(); let count: i32 = 0; let pathcount: i32 = 0; let last: i32 = -1;\n let i: i32 = 0; for (i < env.len) {\n if (strings.hasprefix(env[i], \"PWD=\")) { count += 1; last = i; };\n if (strings.hasprefix(env[i], \"PATH=\")) { pathcount += 1; };\n i += 1;\n };\n",
" 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",
" case let value: str => put(value); case void => put(\"<unset>\");\n",
" }; put(\"\\n\");\n",
" let env: []str = os.getenvs(); let count: i32 = 0; let last: i32 = -1;\n",
" let i: i32 = 0; for (i < env.len) {\n",
" if (strings.hasprefix(env[i], \"PWD=\")) { count += 1; last = i; };\n",
" i += 1;\n",
" };\n",
" put(label); put(\" PWD-count=\");\n", " put(label); put(\" PWD-count=\");\n",
" if (count == 0) { put(\"0\\n\"); } else { if (count == 1) {\n", " if (count == 0) { put(\"0\\n\"); } else { if (count == 1) {\n",
" put(\"1\\n\"); } else { if (count == 2) { put(\"2\\n\");\n", " put(\"1\\n\"); } else { if (count == 2) { put(\"2\\n\");\n",
" } else { put(\"many\\n\"); }; }; };\n", " } else { put(\"many\\n\"); }; }; };\n",
" put(label); put(\" PWD-last=\");\n", " put(label); put(\" PWD-last=\");\n",
" if (last == env.len - 1) { put(\"yes\\n\"); } else { put(\"no\\n\"); };\n", " if (last == env.len - 1) { put(\"yes\\n\"); } else { put(\"no\\n\"); };\n",
" put(label); put(\" PATH-count=\");\n",
" if (pathcount == 0) { put(\"0\\n\"); } else { if (pathcount == 1) {\n",
" put(\"1\\n\"); } else { if (pathcount == 2) { put(\"2\\n\");\n",
" } else { put(\"many\\n\"); }; }; };\n",
" emitfile(label, \"data\", \"data.txt\");\n", " emitfile(label, \"data\", \"data.txt\");\n",
" emitfile(label, \"testdata\", \"testdata/input.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(\"<error>\\n\"); };\n };\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(\"<error>\\n\"); };\n };\n",
@@ -1051,6 +1059,46 @@ fn cwdwritedata(dir: str, label: str) void = {
assert(same(readfile(strings.concat(caller, "/sentinel")), assert(same(readfile(strings.concat(caller, "/sentinel")),
"parent-sentinel\n")); "parent-sentinel\n"));
// The official Go script exercises both an absent and an explicitly empty
// caller PATH. Each produces only the selected toolchain directory, and the
// two stages expose identical process bytes.
assert(os.remove(strings.concat(p4, "/created.txt")) == 0);
let absentbase: []str = cwdtestenv("ww");
let absentenv: []str = alloc([], absentbase.len: u64)!;
i = 0;
for (i < absentbase.len) {
if (!strings.hasprefix(absentbase[i], "PATH=")) {
append(absentenv, absentbase[i]);
};
i += 1;
};
let absentav: []str = [driver("ww"), "test", "-I", tree, p4];
runcommandinputenvdir(root, "path-absent-c", absentav, absentenv, caller,
stdinpath, (60i64 * (time.second: i64)): time.duration, &outc);
expectexit(&outc, 0);
cwdassertrecord(outc.stdout, "p4-internal-only", p4, p4, "1", "yes",
"p4-data", "p4-testdata", "eof");
cwdassertpath(outc.stdout, "p4-internal-only",
strings.concat(repo(), "/out/bin"), "1");
assert(os.remove(strings.concat(p4, "/created.txt")) == 0);
let emptybase: []str = cwdtestenv("ww_ww");
let emptyenv: []str = alloc([], (emptybase.len + 1): u64)!;
i = 0;
for (i < emptybase.len) {
if (!strings.hasprefix(emptybase[i], "PATH=")) {
append(emptyenv, emptybase[i]);
};
i += 1;
};
append(emptyenv, "PATH=");
let emptyav: []str = [driver("ww_ww"), "test", "-I", tree, p4];
runcommandinputenvdir(root, "path-empty-ww", emptyav, emptyenv, caller,
stdinpath, (60i64 * (time.second: i64)): time.duration, &outw);
expectexit(&outw, 0);
assert(same(outc.stdout, outw.stdout));
assert(same(outc.stderr, outw.stderr));
assert(os.remove(strings.concat(p4, "/created.txt")) == 0);
// Filtering, listing, and a no-match filter all execute the same product // Filtering, listing, and a no-match filter all execute the same product
// binary. Package initialization therefore has the product cwd even when no // binary. Package initialization therefore has the product cwd even when no
// selected test body runs. // selected test body runs.
@@ -1172,6 +1220,7 @@ fn cwdwritedata(dir: str, label: str) void = {
assert(same(outc.stderr, outw.stderr)); assert(same(outc.stderr, outw.stderr));
assert(has(outc.stderr, assert(has(outc.stderr,
"test package must match production package or <package>_test\n")); "test package must match production package or <package>_test\n"));
assert(!has(outc.stdout, " PATH=") && !has(outw.stdout, " PATH="));
assert(directoryisempty(rejectcwork)); assert(directoryisempty(rejectcwork));
assert(directoryisempty(rejectwwork)); assert(directoryisempty(rejectwwork));
@@ -1186,6 +1235,7 @@ fn cwdwritedata(dir: str, label: str) void = {
assert(!has(out.stdout, "plain-init")); assert(!has(out.stdout, "plain-init"));
assert(!has(out.stdout, "cmdno-init")); assert(!has(out.stdout, "cmdno-init"));
assert(!has(out.stdout, "cmdno-main")); assert(!has(out.stdout, "cmdno-main"));
assert(!has(out.stdout, " PATH="));
assert(!os.exists(strings.concat(plain, "/created.txt"))); assert(!os.exists(strings.concat(plain, "/created.txt")));
assert(!os.exists(strings.concat(cmdno, "/created.txt"))); assert(!os.exists(strings.concat(cmdno, "/created.txt")));
let commandbin: str = strings.concat(root, "/cmdno.bin"); let commandbin: str = strings.concat(root, "/cmdno.bin");
@@ -1197,6 +1247,7 @@ fn cwdwritedata(dir: str, label: str) void = {
expectexit(&out, 0); expectexit(&out, 0);
assert(os.exists(commandbin)); assert(os.exists(commandbin));
assert(!has(out.stdout, "cmdno-init") && !has(out.stdout, "cmdno-main")); assert(!has(out.stdout, "cmdno-init") && !has(out.stdout, "cmdno-main"));
assert(!has(out.stdout, " PATH="));
assert(!os.exists(strings.concat(cmdno, "/created.txt"))); assert(!os.exists(strings.concat(cmdno, "/created.txt")));
// Compile-only paths do not run. A published binary has no embedded cwd or // Compile-only paths do not run. A published binary has no embedded cwd or
@@ -1212,6 +1263,7 @@ fn cwdwritedata(dir: str, label: str) void = {
assert(!os.exists(strings.concat(p2, "/p2.test"))); assert(!os.exists(strings.concat(p2, "/p2.test")));
assert(!os.exists(strings.concat(p2, "/created.txt"))); assert(!os.exists(strings.concat(p2, "/created.txt")));
assert(!has(out.stdout, "dep-init") && !has(out.stdout, "p2-external")); assert(!has(out.stdout, "dep-init") && !has(out.stdout, "p2-external"));
assert(!has(out.stdout, " PATH="));
assert(os.remove(defaultbin) == 0); assert(os.remove(defaultbin) == 0);
assert(os.remove(strings.concat(p1, "/created.txt")) == 0); assert(os.remove(strings.concat(p1, "/created.txt")) == 0);
@@ -1232,6 +1284,7 @@ fn cwdwritedata(dir: str, label: str) void = {
assert(!os.exists(strings.concat(p1, "/created.txt"))); assert(!os.exists(strings.concat(p1, "/created.txt")));
assert(same(readfile(p1cbin), readfile(p1wbin))); assert(same(readfile(p1cbin), readfile(p1wbin)));
assert(!has(outc.stdout, "dep-init") && !has(outw.stdout, "dep-init")); assert(!has(outc.stdout, "dep-init") && !has(outw.stdout, "dep-init"));
assert(!has(outc.stdout, " PATH=") && !has(outw.stdout, " PATH="));
let directbin: []str = [p1cbin, "-package=p1"]; let directbin: []str = [p1cbin, "-package=p1"];
runcommandinputenvdir(root, "cwd-published-direct", directbin, cenv, runcommandinputenvdir(root, "cwd-published-direct", directbin, cenv,
@@ -1244,13 +1297,16 @@ 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");
cwdassertcallerpath(out.stdout, "dep-init");
cwdassertcallerpath(out.stdout, "p1-internal");
cwdassertsplitstreams(out.stdout, out.stderr, "dep-init", caller); cwdassertsplitstreams(out.stdout, out.stderr, "dep-init", caller);
cwdassertsplitstreams(out.stdout, out.stderr, "p1-internal", 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);
// Raw single-file compatibility is deliberately outside the directory // Raw single-file compatibility retains its caller cwd/PWD/stdin context,
// coordinator rule and retains its caller execution context. // but it is still a test process and therefore receives the selected
// toolchain directory first in PATH.
let rawc: []str = [driver("ww"), "test", "-I", tree, let rawc: []str = [driver("ww"), "test", "-I", tree,
strings.concat(p4, "/internal_test.ww")]; strings.concat(p4, "/internal_test.ww")];
let raww: []str = [driver("ww_ww"), "test", "-I", tree, let raww: []str = [driver("ww_ww"), "test", "-I", tree,
@@ -1269,9 +1325,52 @@ 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");
cwdasserttestpath(outc.stdout, "p4-internal-only");
cwdassertsplitstreams(outc.stdout, outc.stderr, cwdassertsplitstreams(outc.stdout, outc.stderr,
"p4-internal-only", caller); "p4-internal-only", caller);
assert(os.remove(strings.concat(caller, "/created.txt")) == 0); assert(os.remove(strings.concat(caller, "/created.txt")) == 0);
// A retained running test receives the same environment before guarded
// install. A later runtime failure observes PATH but cannot replace the
// successful binary; both stages preserve identical prior bytes.
let retained: str = strings.concat(root, "/path-retained.test");
let retainc: []str = [driver("ww"), "test", "-o", retained,
"-I", tree, p1];
let retainw: []str = [driver("ww_ww"), "test", "-o", retained,
"-I", tree, p1];
runcommandinputenvdir(root, "path-retain-c", retainc, cenv, caller,
stdinpath, (90i64 * (time.second: i64)): time.duration, &outc);
expectexit(&outc, 0);
cwdassertpackage(outc.stdout, "dep-init", p1, "p1-data", "p1-testdata");
cwdassertpackage(outc.stdout, "p1-internal", p1,
"p1-data", "p1-testdata");
let retainedbytes: str = strings.dup(readfile(retained));
runcommandinputenvdir(root, "path-retain-ww", retainw, wenv, caller,
stdinpath, (90i64 * (time.second: i64)): time.duration, &outw);
expectexit(&outw, 0);
assert(same(outc.stdout, outw.stdout));
assert(same(outc.stderr, outw.stderr));
assert(same(retainedbytes, readfile(retained)));
let retainfailc: []str = [driver("ww"), "test", "-o", retained,
"-I", tree, "-I", failtree, p7];
let retainfailw: []str = [driver("ww_ww"), "test", "-o", retained,
"-I", tree, "-I", failtree, p7];
runcommandinputenvdir(root, "path-retain-fail-c", retainfailc, cenv,
caller, stdinpath,
(90i64 * (time.second: i64)): time.duration, &outc);
expectexit(&outc, 1);
cwdassertpackage(outc.stdout, "p7-fail", p7, "p7-data", "p7-testdata");
assert(same(retainedbytes, readfile(retained)));
runcommandinputenvdir(root, "path-retain-fail-ww", retainfailw, wenv,
caller, stdinpath,
(90i64 * (time.second: i64)): time.duration, &outw);
expectexit(&outw, 1);
assert(same(outc.stdout, outw.stdout));
assert(same(outc.stderr, outw.stderr));
assert(same(retainedbytes, readfile(retained)));
assert(!os.exists(strings.concat(retained, ".new")));
assert(!directoryhasfragment(root, "path-retained.test.wwtxn."));
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,
"p2-data", "p2-testdata"); "p2-data", "p2-testdata");
@@ -1344,8 +1443,8 @@ fn cwdwritedata(dir: str, label: str) void = {
"#!/bin/sh\n", "#!/bin/sh\n",
"printf 'compiler cwd=' >> \"$WW_CWD_TOOL_TRACE\"\n", "printf 'compiler cwd=' >> \"$WW_CWD_TOOL_TRACE\"\n",
"/bin/pwd >> \"$WW_CWD_TOOL_TRACE\"\n", "/bin/pwd >> \"$WW_CWD_TOOL_TRACE\"\n",
"printf 'compiler env=LC_ALL:%s TMPDIR:%s\\n' \"$LC_ALL\" ", "printf 'compiler env=LC_ALL:%s TMPDIR:%s PATH:%s\\n' \"$LC_ALL\" ",
"\"$TMPDIR\" >> \"$WW_CWD_TOOL_TRACE\"\n", "\"$TMPDIR\" \"$PATH\" >> \"$WW_CWD_TOOL_TRACE\"\n",
"if IFS= read -r WW_CWD_STDIN; then\n", "if IFS= read -r WW_CWD_STDIN; then\n",
" printf 'compiler stdin=data\\n' >> \"$WW_CWD_TOOL_TRACE\"\n", " printf 'compiler stdin=data\\n' >> \"$WW_CWD_TOOL_TRACE\"\n",
"else printf 'compiler stdin=eof\\n' >> \"$WW_CWD_TOOL_TRACE\"; fi\n", "else printf 'compiler stdin=eof\\n' >> \"$WW_CWD_TOOL_TRACE\"; fi\n",
@@ -1358,8 +1457,8 @@ fn cwdwritedata(dir: str, label: str) void = {
"#!/bin/sh\n", "#!/bin/sh\n",
"printf 'assembler cwd=' >> \"$WW_CWD_TOOL_TRACE\"\n", "printf 'assembler cwd=' >> \"$WW_CWD_TOOL_TRACE\"\n",
"/bin/pwd >> \"$WW_CWD_TOOL_TRACE\"\n", "/bin/pwd >> \"$WW_CWD_TOOL_TRACE\"\n",
"printf 'assembler env=LC_ALL:%s TMPDIR:%s\\n' \"$LC_ALL\" ", "printf 'assembler env=LC_ALL:%s TMPDIR:%s PATH:%s\\n' \"$LC_ALL\" ",
"\"$TMPDIR\" >> \"$WW_CWD_TOOL_TRACE\"\n", "\"$TMPDIR\" \"$PATH\" >> \"$WW_CWD_TOOL_TRACE\"\n",
"if IFS= read -r WW_CWD_STDIN; then\n", "if IFS= read -r WW_CWD_STDIN; then\n",
" printf 'assembler stdin=data\\n' >> \"$WW_CWD_TOOL_TRACE\"\n", " printf 'assembler stdin=data\\n' >> \"$WW_CWD_TOOL_TRACE\"\n",
"else printf 'assembler stdin=eof\\n' >> \"$WW_CWD_TOOL_TRACE\"; fi\n", "else printf 'assembler stdin=eof\\n' >> \"$WW_CWD_TOOL_TRACE\"; fi\n",
@@ -1372,8 +1471,8 @@ fn cwdwritedata(dir: str, label: str) void = {
"#!/bin/sh\n", "#!/bin/sh\n",
"printf 'linker cwd=' >> \"$WW_CWD_TOOL_TRACE\"\n", "printf 'linker cwd=' >> \"$WW_CWD_TOOL_TRACE\"\n",
"/bin/pwd >> \"$WW_CWD_TOOL_TRACE\"\n", "/bin/pwd >> \"$WW_CWD_TOOL_TRACE\"\n",
"printf 'linker env=LC_ALL:%s TMPDIR:%s\\n' \"$LC_ALL\" ", "printf 'linker env=LC_ALL:%s TMPDIR:%s PATH:%s\\n' \"$LC_ALL\" ",
"\"$TMPDIR\" >> \"$WW_CWD_TOOL_TRACE\"\n", "\"$TMPDIR\" \"$PATH\" >> \"$WW_CWD_TOOL_TRACE\"\n",
"if IFS= read -r WW_CWD_STDIN; then\n", "if IFS= read -r WW_CWD_STDIN; then\n",
" printf 'linker stdin=data\\n' >> \"$WW_CWD_TOOL_TRACE\"\n", " printf 'linker stdin=data\\n' >> \"$WW_CWD_TOOL_TRACE\"\n",
"else printf 'linker stdin=eof\\n' >> \"$WW_CWD_TOOL_TRACE\"; fi\n", "else printf 'linker stdin=eof\\n' >> \"$WW_CWD_TOOL_TRACE\"; fi\n",
@@ -1417,9 +1516,14 @@ fn cwdwritedata(dir: str, label: str) void = {
assert(has(coldtrace, strings.concat("assembler cwd=", caller, "\n"))); assert(has(coldtrace, strings.concat("assembler cwd=", caller, "\n")));
assert(has(coldtrace, strings.concat("linker cwd=", caller, "\n"))); assert(has(coldtrace, strings.concat("linker cwd=", caller, "\n")));
assert(!has(coldtrace, strings.concat("cwd=", p1, "\n"))); assert(!has(coldtrace, strings.concat("cwd=", p1, "\n")));
assert(has(coldtrace, "compiler env=LC_ALL:C TMPDIR:/tmp/")); assert(has(coldtrace,
assert(has(coldtrace, "assembler env=LC_ALL:C TMPDIR:/tmp/")); "compiler env=LC_ALL:C TMPDIR:/tmp/"));
assert(has(coldtrace, "linker 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, " PATH:/ww-path-inherited-last\n"));
assert(!has(coldtrace, strings.concat(" PATH:", repo(), "/out/bin:")));
assert(has(coldtrace, "compiler stdin=eof\n")); assert(has(coldtrace, "compiler stdin=eof\n"));
assert(has(coldtrace, "assembler stdin=eof\n")); assert(has(coldtrace, "assembler stdin=eof\n"));
assert(has(coldtrace, "linker stdin=eof\n")); assert(has(coldtrace, "linker stdin=eof\n"));
@@ -1528,7 +1632,7 @@ fn cwdwritedata(dir: str, label: str) void = {
"\"$WW_CWD_REAL_DRIVER\" \"$@\"\n", "\"$WW_CWD_REAL_DRIVER\" \"$@\"\n",
"status=$?\n", "status=$?\n",
"if [ \"$status\" -eq 0 ]; then\n", "if [ \"$status\" -eq 0 ]; then\n",
" mv -- \"$WW_CWD_HIDE_DIR\" \"$WW_CWD_HIDE_DIR.gone\" || exit 125\n", " /bin/mv -- \"$WW_CWD_HIDE_DIR\" \"$WW_CWD_HIDE_DIR.gone\" || exit 125\n",
" printf 'wrapper-hidden=%s\\n' \"$WW_CWD_HIDE_DIR\" >&2\n", " printf 'wrapper-hidden=%s\\n' \"$WW_CWD_HIDE_DIR\" >&2\n",
"fi\n", "fi\n",
"exit \"$status\"\n")); "exit \"$status\"\n"));
@@ -1545,7 +1649,7 @@ fn cwdwritedata(dir: str, label: str) void = {
driver(hidestages[i]))); driver(hidestages[i])));
append(hideenv, strings.concat("WW_CWD_HIDE_DIR=", p1)); append(hideenv, strings.concat("WW_CWD_HIDE_DIR=", p1));
let hideav: []str = [driver("wwtest"), "package", "--ww-driver", let hideav: []str = [driver("wwtest"), "package", "--ww-driver",
hidewrapper, "-j", "2", "-I", tree, p1, p2]; "../build-then-hide.sh", "-j", "2", "-I", tree, p1, p2];
runcommandinputenvdir(root, strings.concat("cwd-hide-", hidestages[i]), runcommandinputenvdir(root, strings.concat("cwd-hide-", hidestages[i]),
hideav, hideenv, caller, stdinpath, hideav, hideenv, caller, stdinpath,
(120i64 * (time.second: i64)): time.duration, &out); (120i64 * (time.second: i64)): time.duration, &out);
@@ -1554,10 +1658,16 @@ fn cwdwritedata(dir: str, label: str) void = {
assert(has(out.stdout, 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, cwdassertrecord(out.stdout, "dep-init", p2, p2, "1", "yes",
"p2-data", "p2-testdata"); "p2-data", "p2-testdata", "eof");
cwdassertpackage(out.stdout, "p2-external", p2, cwdassertrecord(out.stdout, "p2-external", p2, p2, "1", "yes",
"p2-data", "p2-testdata"); "p2-data", "p2-testdata", "eof");
cwdassertpath(out.stdout, "dep-init", strings.concat(root,
":/ww-path-inherited-first"), "1");
cwdassertpath(out.stdout, "p2-external", strings.concat(root,
":/ww-path-inherited-first"), "1");
cwdassertmergedstreams(out.stdout, "dep-init", p2);
cwdassertmergedstreams(out.stdout, "p2-external", p2);
assert(os.rename(strings.concat(p1, ".gone"), p1) == 0); assert(os.rename(strings.concat(p1, ".gone"), p1) == 0);
assert(!os.exists(strings.concat(p1, ".gone"))); assert(!os.exists(strings.concat(p1, ".gone")));
hidestdout[i] = strings.dup(out.stdout); hidestdout[i] = strings.dup(out.stdout);