From 9a2812347c94e124fa6454b36636ce71cc571697 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Thu, 13 Aug 2026 23:42:54 +0900 Subject: [PATCH] test: prove recursive local package patterns --- docs/build-system.md | 329 +++++++- test/package/package_test.ww | 1422 ++++++++++++++++++++++++++++++++-- 2 files changed, 1631 insertions(+), 120 deletions(-) diff --git a/docs/build-system.md b/docs/build-system.md index 5cb5d1fd..6beaf004 100644 --- a/docs/build-system.md +++ b/docs/build-system.md @@ -2806,8 +2806,11 @@ imports-only parser, unions duplicate imports, byte-sorts direct edges, interns canonical directory actions, and reports self-imports and stable cycle chains before compilation. -A directory package consists of its immediate regular non-symlink `.ww` files, -excluding `*_test.ww`, in byte-sorted filename order. Every selected file must +A directory package consists of its immediate `.ww` entries whose basenames do +not begin `.` or `_`: regular files and symlinks targeting regular files are +included under the entry name, while symlinks targeting directories are +ignored. The production variant excludes `*_test.ww`; each variant retains +byte-sorted filename order. Every selected file must declare the same package. An ordinary importable directory's declaration must equal the final component of its import path. A selected command directory instead declares `package main` to validate command kind while retaining its @@ -2925,11 +2928,13 @@ importer under the existing propagation rule. ### 11.7 Implemented directory package-test slice Directory tests now enter that same local package loader and build path. The -supported manifest-free commands are `ww test DIR`, `ww test DIR/...`, and -their existing `-run`, `-filter`, `-list`, `-timeout-ms`, `-j`, `-c`, and `-w` -forms. `ww test -c -o test.bin DIR` names the result when the selected directory -has one test variant; the coordinator rejects one output name for a multi-variant -or recursive request. Explicit `ww test FILE` retains its compatibility path. +supported manifest-free forms include `ww build DIR...`, `ww build DIR/...`, +`ww test DIR...`, and `ww test DIR/...`, with overlapping direct and recursive +roots. Test retains its existing `-run`, `-filter`, `-list`, `-timeout-ms`, +`-j`, `-c`, and `-w` forms. `ww test -c -o test.bin DIR` names the result when +the selected directory has one test variant; the coordinator rejects one output +name for a multi-variant or recursive test request. Explicit `ww test FILE` +retains its compatibility path. The test coordinator still discovers requested directories, enumerates the test package names, selects variants, executes independent binaries, and emits @@ -2978,9 +2983,8 @@ invocation, each still-unbound directory is finalized by this exact algorithm: 1. For every import-resolution context that reached the directory, walk that context's roots in its normal forward precedence: the selected package's directory, explicit `-I` roots in command order, then `WW_SRCLIB` or the - selected toolchain source root. Recursive package-test requests privately - insert their symlink-resolved discovery root before user `-I` roots, so - descendants retain their complete relative identity. + selected toolchain source root. A request pattern never becomes an import + root and therefore cannot shorten, replace, or donate package identity. 2. Canonicalize each candidate root and require the package directory to be a strict descendant. Every relative path component must be a non-keyword WW identifier. Convert separators to dots, then resolve that relative spelling @@ -3019,11 +3023,13 @@ its derived `_test` compiler path. The derivation and diagnostics are implemented symmetrically in `cmd/ww/main.c` and `selfhost/cmd/ww/main.ww`. The package coordinator in -`internal/wwpackage/package.ww` supplies the canonical recursive discovery root, -preserves an explicitly resolved logical request identity, and keys a persistent -request workdir only by the canonical discovery directory. `w6c` and `wcc` -continue to consume and validate the finalized dotted identity; neither tool -performs directory lookup or introduces a package registry. +`internal/wwpackage/package.ww` supplies canonical selected directories and +variant descriptors, preserves an explicitly resolved identity only for one +direct request, and forwards a caller's `-w` semantic-action store unchanged. +It never derives identity or persistent layout from a pattern traversal prefix +and does not add that prefix to import search. `w6c` and `wcc` continue to +consume and validate the finalized dotted identity; neither tool performs +directory lookup or introduces a package registry. Artifact publication follows the semantic action instead of product order: production uses the full finalized ordinary identity, internal appends @@ -3368,7 +3374,7 @@ discovery each compile the shared production action once; recursive `a/foo` and `b/foo` directories declaring the same `package foo` publish distinct `a.foo` and `b.foo` variants; and two outside-root `package foo` directories coexist in one command under distinct reversible local identities. Equivalent -recursive spellings reuse the same persistent request directory without new +recursive spellings reuse the same persistent semantic-action store without new compilation, while source imports of the reserved local namespace reject before tool invocation. The exact-argv command root declares `package main` but keeps its non-`main` canonical identity in the unit, export, archive, compiler argv, @@ -3594,23 +3600,18 @@ existing complete-path owner check before tools. This preserves collision checking without serializing machine-specific canonical directories into units or exports and without a sidecar, registry, or new metadata protocol. -The package coordinator similarly preserves the reversible existing -`d_` component while it fits. A longer -canonical request directory uses: - -```text -d_ -``` - -This key selects only the driver workdir container; it is never a package -identity or action address. Equivalent logical, relative, absolute, `.` and -explicit root-symlink requests canonicalize before this derivation. Inside the -container the driver independently derives every action locator from its full -semantic tuple and validates the committed semantic owner, so even a -coordinator-key collision cannot silently reuse an unrelated package action or -invoke a tool with a shortened identity. Explicit root symlinks are followed -and canonicalized; source-file symlinks and symlinked recursive children retain -their existing rejection/skipping rules. +The package coordinator derives no persistent container from a request +directory or pattern. `-w DIR` names the driver's semantic-action store itself, +and the driver independently derives every action locator in that store from +the full semantic tuple above and validates each committed semantic owner. +Equivalent direct, logical, relative, absolute, dotted, recursive, duplicate, +and explicit-root-symlink requests that select the same canonical actions can +therefore reuse the same slots; request shape cannot split or alias persistent +state. For a delegated request only, a missing caller `-w` directory is created +after graph, identity, visibility, cycle, closure, and output preflight. An +explicit root symlink is followed and canonicalized; a source entry symlink to +a regular file is followed under the entry name, a source-shaped symlink to a +directory is ignored, and symlinked recursive children are not traversed. The old `SEP_IMPORT_PATH_MAX` and all corresponding 255-byte WWstage import, variant, local-identity, and generated-main checks are removed. Package names, @@ -3778,7 +3779,7 @@ actions and its exact linker line still contains the root followed by the full `p000` through `p299` transitive archive chain and runtime archive, proving that closure construction—not the wide root's direct imports—crosses the old boundary. The observer compares every unit, export, assembly, object, archive, and binary -across stages. A second equivalent persistent request invokes no compiler or +across stages. A second equivalent request against the same persistent store invokes no compiler or assembler. Changing `p257`'s export recompiles exactly `p257`, direct importer `p256`, and the wide root, and stops before `p255` after `p256` regenerates a byte-identical export. Closing the chain at `p299 -> p000` produces the complete @@ -3801,7 +3802,7 @@ The same observer then exercises the public `ww test /...` coordinator path. The generated tree has 257 real directories: the 52 test-bearing directories above plus 205 production-only directories. The coordinator forms and passes 309 products and 258 contexts (including support) in one driver -request. Reusing the direct request's persistent workdir preserves all 270 +request. Reusing the direct request's persistent semantic-action store preserves all 270 existing actions and adds exactly 410 production/generated-main actions, for 680 distinct actions. Both stages run every product, produce byte-identical ordered coordinator output and all 680 action artifacts, and perform no compilation on a @@ -3810,8 +3811,8 @@ and all 205 no-test result labels, while a builder-boundary observer records exactly one invocation containing all 309 descriptors for each cold and warm public request. Existing focused observers continue to prove dependency-first/root-first canonical reuse, root-only/combined artifact -identity, exact reordered-product trace bytes, and persistent request-directory -stability. +identity, exact reordered-product trace bytes, and request-shape-independent +persistent action reuse. This representation follows the semantic separation and scalable action construction in the pinned official Go 1.26.5 source, identified by @@ -4141,9 +4142,10 @@ Generated-main-to-variant, generated-main-to-support, and coordinator product edges remain synthetic and receive no map or retroactive source legality. `cmd/wwtest` remains a dispatcher. `internal/wwpackage` still discovers and classifies source groups and submits one command-global union; it performs no -vendor resolution. Its only related state change is transactional cleanup: if -it created a cold hashed request workdir and the driver rejects before a tool -stamp exists, it removes that empty request directory. +vendor resolution. It creates only its removable command-temporary coordination +tree. The private driver creates a missing delegated `-w` directory and a +requested output directory only after all semantic graph and output preflight +succeeds; a rejected request therefore leaves neither directory behind. Resolution, identity collision checks, contextual legality, dependency-failure propagation, cycles, command kind, publication paths, and action closures all @@ -4208,6 +4210,253 @@ direct exports/import maps, archive-only link closures, runtime output, stage-equal normalized argv/artifacts/binaries, warm no-op production, and cold and warm rejection-state preservation. +### 11.17 Implemented manifest-free recursive local package-pattern selection + +Build and test now share one local request selector. A positional spelling with +no `...` is one explicit directory root. In a spelling containing `...`, each +occurrence in a valid UTF-8 spelling has Go's regular-expression wildcard +semantics; an invalid UTF-8 pattern matches nothing, and a final `/...` also +matches the directory before that suffix. WW applies the local matcher to its +existing manifest-free `DIR/...` interface as well as `./...`, `../...`, and +absolute spellings. It does not interpret the non-filesystem portion as a +module or registry path. Multiple direct roots and patterns may be mixed in one +command. +For build, `--` before the first positional ends option parsing and every +following argument is a package selector, including a spelling that begins +with `-`. Once the first positional has already ended flag parsing, a later +`--` is itself another package argument. + +Pattern expansion is request processing only. For each pattern, traversal starts +at the directory prefix before the first `...` and is bounded to that physical +tree. The selector never obtains a traversal or source-root boundary from +another product. It produces canonical physical directory roots; only imports +parsed from their real source files add dependency edges. The request spelling, +wildcard prefix, match membership, output name, request order, and discovery +order never become a package import identity or action key and never enter an +import search path. + +The recursive eligibility rules are: + +- Directory entries are read completely and byte-sorted before processing. + Every recursively encountered directory whose basename begins `.` or `_`, or + equals `testdata`, is pruned with its subtree. An explicit literal selection + bypasses these traversal exclusions, so those directories remain legal direct + roots. +- A directory owns only its immediate `.ww` directory entries. Basenames + beginning `.` or `_` are ignored. As in `go/build`, a source symlink whose + target is a regular file is read under the symlink entry's byte-sorted name; + a source-shaped symlink to a directory is ignored. A subdirectory never + donates sources to its parent. +- A directory with at least one production or `*_test.ww` source is eligible. + A recursively encountered source-empty directory is silently skipped. A + direct source-empty root is an error. A malformed source-bearing directory is + retained as a root and fails during ordinary package-clause or driver loading; + malformed files in excluded or source-empty trees do not poison the request. +- The traversal does not prune `vendor`. Instead a wildcard cannot consume a + non-terminal exact path component named `vendor`. Thus `DIR/...` may select a + code-bearing terminal `DIR/vendor` but not `DIR/vendor/x`. + `DIR/vendor/...` explicitly selects that vendor root and its descendants until + another non-terminal `vendor` becomes a barrier. `vendorx` is ordinary. + +The explicit traversal root is opened after following a directory symlink, as +in Go. It may therefore name a target outside the lexical spelling, but the +target becomes the canonical traversal boundary. Directory symlinks encountered +below that root are never followed, so they cannot escape, create cycles, or +change selection. A cyclic explicit root is rejected while canonicalizing it. +Relative, absolute, dotted, and explicit-root-symlink spellings that reach one +physical package collapse to one canonical root. This physical interning is +WW's stronger command-global identity rule; it deliberately avoids Go's few +GOPATH cases in which different lexical import paths can retain distinct package +objects. + +Raw requested spellings are byte-sorted first and each is then lexically cleaned +before traversal. Matched source paths are canonicalized, sorted by canonical +directory and filename, and deduplicated. Products are then byte-sorted by +canonical directory and variant. Duplicate patterns, overlapping patterns, and +canonical aliases therefore select one root/product and reuse one action. Go +itself processes patterns in argv order and suppresses later package objects; WW +performs the stronger final canonical sort required by its +request-order-independent command universe. Reversing request or product order +does not change roots, diagnostics, normalized tool arguments, artifacts, or +runtime output. + +An unmatched pattern emits: + +```text +ww: warning: "PATTERN" matched no packages +``` + +`PATTERN` is quoted with the pinned `strconv.Quote` rules, including +deterministic escapes for quotes, backslashes, controls, non-printing Unicode, +and malformed UTF-8 bytes. Warnings are emitted in the +sorted request order. With no remaining roots, +ordinary build without `-o` succeeds as an empty build, while test reports +`ww test: no packages to test`. A build with a non-directory `-o` reports no +packages to build; a directory `-o` reports no main packages to build. A +non-directory `-o` still requires exactly one production root. An existing +directory or spelling ending in `/` receives each selected command under its +canonical directory basename; non-main selected roots receive no named output. +Two commands with the same destination basename are rejected rather than +overwriting one another. All selection, canonicalization, package-clause, +duplicate-output, and unusable-request diagnostics precede producer execution. +Raw `-o` and `-w` spellings and every derived directory-fan-out output, cold +scratch name, persistent tool-record name, and package artifact are bounded and +validated symmetrically before tools; a raw spelling that fits but whose suffix +or command basename does not fit is rejected with the same Cstage/WWstage +diagnostic and no filesystem publication. +For a delegated multi-root or recursive `-S` build, `-w` is required so the +assembly outputs have caller-owned persistent destinations instead of vanishing +with the coordinator's temporary plan. + +Build and test begin with the same eligible canonical directory set. Recursive +build removes a source-bearing root that has only test files; an explicitly +selected test-only directory remains an unusable build root and fails. Build +creates one production product per remaining directory. Test retains test-only +directories and, after selection, constructs the already specified isolated +production/no-test, internal production-plus-test, external `_test`, support, +and generated-main actions. Pattern expansion does not create those variants +and cannot make one variant visible to another. + +Vendor selection remains distinct from vendor import resolution. Selecting a +directory below `vendor`, literally or through an explicitly vendor-rooted +pattern, keeps its complete canonical local identity; it is never shortened to +the suffix after `vendor`. A generic recursive pattern does not expose vendored +descendants as ordinary short command roots. Independently, an allowed real +source import still searches nearest-first below local `vendor`, creates the +expanded identity described in section 11.16, supplies the required +`--import-map`, and performs spelling and visibility checks for that importer +even when the canonical action already exists. + +The ownership split is exact: + +- `cmd/ww/main.c` and `selfhost/cmd/ww/main.ww` recognize recursive and + multi-root build/test requests symmetrically and delegate them. Their private + build route consumes production product descriptors in one command-global + package universe. It records library-root completion without linking and + links each command product independently. Recursive build forwards `-S`, + `-L`, and `-l` through the same private product route; assembly-only products + receive completion markers only after their producer pass succeeds. Both + stages allocate the same bounded delegation argv and inherit the existing + environment directly, so delegation adds no WWstage-only environment-copy + allocation or failure point. +- `internal/wwpackage/package.ww` owns pattern cleaning and matching, bounded + directory traversal, source-bearing eligibility, canonical root/product + sorting and deduplication, build-versus-test product classification, output + coordination, and deterministic reporting. It does not resolve a source + import and does not prepend a pattern root to `-I`. +- `cmd/wwtest` remains a dispatcher. The Cstage and WWstage drivers own package + enumeration, canonical identity, contextual import resolution, graph loading, + variants, compilation, archive construction, linking, publication, and + persistent reuse. + +Consequently the existing action and tool contracts remain unchanged after +root selection. Every action unit contains only the owner's byte-sorted source +files. A compiler receives exactly the sorted, deduplicated `.wwi` exports of +direct source dependencies and any required vendor import-map binding. Each +executable link receives its root archive and complete reachable archive +closure, never a `.wwi`. Canonical duplicate roots reuse the same action; the +pattern text and the product receiving an output do not affect semantic or +persistent identity. + +Selection and package-clause validation finish before the coordinator creates +its removable temporary plan, and the coordinator never creates persistent +state. Driver graph, identity, visibility, cycle, closure, and output validation +finish before a missing delegated work or output directory is created and +before scratch, voucher, status, tool-state, publication, or producer mutation. +If later setup of another requested directory fails, both drivers remove every +empty path prefix created by that setup while preserving all pre-existing caller +directories. A cold rejected pattern leaves no partial request state. The same +rejection against an existing caller work root leaves its marker and every +committed artifact, voucher, and tool record byte-unchanged and invokes no +producer. A valid equivalent direct or recursive warm request remains a +package-production no-op; changed exports continue to propagate through direct +dependencies only. + +This slice follows only official Go 1.26.5 source at commit +`c19862e5f8415b4f24b189d065ed739517c548ba`: + +- local literal versus wildcard handling, the prefix before the first `...`, + explicit root-symlink following, recursive exclusions, source-empty omission, + and malformed-directory retention are in + [`cmd/go/internal/search/search.go`, lines 276–418](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/search/search.go#L276-L418), + and its quoted unmatched-pattern warning is at + [lines 424–429](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/search/search.go#L424-L429); +- wildcard syntax, the empty match for trailing `/...`, and the non-terminal + `vendor` barrier are in + [`cmd/internal/pkgpattern/pkgpattern.go`, lines 32–106](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/internal/pkgpattern/pkgpattern.go#L32-L106), + including invalid-UTF-8 rejection at lines 75–76, + with non-terminal vendor-element replacement implemented at + [lines 125–137](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/internal/pkgpattern/pkgpattern.go#L125-L137); +- manifest-free request expansion, canonical package loading, first-occurrence + deduplication, and pattern membership are in + [`cmd/go/internal/load/pkg.go`, lines 2922–2965](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/load/pkg.go#L2922-L2965), + while source imports alone recurse through package loading at + [lines 2024–2047](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/load/pkg.go#L2024-L2047); +- build uses that common matcher, implements single versus directory `-o`, and + omits wildcard-selected test-only roots at + [`cmd/go/internal/work/build.go`, lines 459–559](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/work/build.go#L459-L559) + and + [lines 731–745](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/work/build.go#L731-L745); +- test uses the same package request set, reports an empty set, and constructs + isolated test variants only afterward at + [`cmd/go/internal/test/test.go`, lines 684–719](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/test/test.go#L684-L719) + and + [lines 1133–1226](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/test/test.go#L1133-L1226); +- immediate directory ownership and hidden/underscore source-name exclusion + are in + [`go/build/build.go`, lines 859–914](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/go/build/build.go#L859-L914) + (including regular-file source symlink following and symlink-to-directory + omission at lines 886–900), + parse-error retention and production/test classification are in + [lines 931–1036](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/go/build/build.go#L931-L1036), + and `NoGoError` eligibility is in + [lines 1076–1082](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/go/build/build.go#L1076-L1082); and +- child symlinks are skipped by `Lstat`-based directory walking and entries are + traversed in byte order at + [`cmd/go/internal/fsys/walk.go`, lines 14–59](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/fsys/walk.go#L14-L59) + and + [`os/dir.go`, lines 109–125](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/os/dir.go#L109-L125). + +Build subcommand option termination is delegated by the Go command at +[`cmd/go/main.go`, lines 312–321](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/main.go#L312-L321), +with `--` termination implemented by +[`flag/flag.go`, lines 1074–1089](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/flag/flag.go#L1074-L1089), +and the parse loop stops at the first positional at +[lines 1153–1176](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/flag/flag.go#L1153-L1176). + +Unmatched-pattern quoting uses the pinned `strconv.Quote` decoder and escape +rules in +[`strconv/quote.go`, lines 28–123](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/strconv/quote.go#L28-L123), +including the exported `Quote` entry at lines 117–123, and the `IsPrint` +algorithm at +[`strconv/quote.go`, lines 515–559](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/strconv/quote.go#L515-L559), +with its generated tables in +[`strconv/isprint.go`, lines 8–733](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/strconv/isprint.go#L8-L733). + +No module cutoff, `go.mod`, module vendor mode, `vendor/modules.txt`, importcfg, +build ID, registry, database, CAS, or network behavior is copied. + +The native observer `recursive_tree_discovery` generates every tree +temporarily and runs Cstage and WWstage from independent cold roots. It covers +ordinary and test-only roots, hidden/underscore/testdata exclusions and direct +exceptions, terminal and explicit vendor patterns, an imported expanded vendor +dependency at both package-local and ancestor vendor directories, +overlapping/reversed/duplicate patterns independently in each stage, root, +child-directory, regular-file, and directory-target source symlinks, +canonical aliases, source-empty and malformed directories, build/test variant +selection with an asserted common production-root set, middle-position and +valid-UTF-8 wildcard edge cases, byte-sorted quoted unmatched diagnostics, +multi-command directory output and duplicate-destination rejection, recursive +assembly/link-flag forwarding, build `--` termination, raw-versus-derived +output/scratch/work-path boundaries with zero producer calls, owner-only units, +exact direct exports and import maps, archive-only links, normalized stage-equal tool argv and +artifacts/binaries/output, warm no-op package production, and cold/warm pre-tool +rejection-state preservation. The existing +`vendor_directory_import_resolution` observer supplies the exact compiler +`--import-map` argv proof for imported vendor dependencies, and the existing +diamond and long-closure observers independently prove sorted/deduplicated +direct `.wwi` cardinality and archive-only reachable link closures. + ## 12. Candidate architectures and hard-gate decision Five candidates were developed as coherent systems, not as feature bins. diff --git a/test/package/package_test.ww b/test/package/package_test.ww index c5679ec9..da70bfa6 100644 --- a/test/package/package_test.ww +++ b/test/package/package_test.ww @@ -114,6 +114,26 @@ fn runcommand(root: str, name: str, argv: []str, out.stderr = readfile(c.stderrpath); }; +fn runcommanddir(root: str, name: str, dir: str, argv: []str, + lifetime: time.duration, out: *commandout) void = { + let c: exec.command; + c.path = argv[0]; + c.argv = argv; + c.env = os.getenvs(); + c.dir = dir; + 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 runcommandenv(root: str, name: str, argv: []str, env: []str, lifetime: time.duration, out: *commandout) void = { let c: exec.command; @@ -372,16 +392,6 @@ fn packagestoragekey(path: str, canon: str, variant: i32, role: i32) str = { return strings.frombytes(out); }; -fn requestworkkey(dir: str) str = { - let escaped: str = strings.concat("d_", workescape(dir)); - if (escaped.len <= 255) { return escaped; }; - let state: sha256.state = sha256.sha256(); - let h: *hash.hash = (&state): *hash.hash; - hash.write(h, strings.toutf8("ww-request-workdir-v1:")); - hash.write(h, strings.toutf8(dir)); - return strings.concat("d_", digesthex(h)); -}; - fn importrelative(path: str) str = { let out: []u8 = alloc([], (path.len + 1): u64)!; let i: i32 = 0; @@ -741,7 +751,7 @@ fn hexbytes(value: str) str = { let coldtrace: str = readfile(trace); assert(coldtrace.len > 0); assert(has(coldtrace, strings.concat("<--import><", actionid, "><"))); - let key: str = strings.concat(work, "/", requestworkkey(real)); + let key: str = work; assert(os.exists(key)); let suffixes: []str = [".unit.ww", ".wwi", ".a"]; let xi: i32 = 0; @@ -981,10 +991,10 @@ fn hexbytes(value: str) str = { writefile(strings.concat(api, "/api_test.ww"), strings.concat( "package api;\n", "TEST_DEPENDENCY_MUST_NOT_COMPILE\n")); - writefile(strings.concat(testonly, "/__same.ww"), strings.concat( + writefile(strings.concat(testonly, "/source.ww"), strings.concat( "package __same;\n", "export fn value() i32 = { return 1; };\n")); - writefile(strings.concat(externalonly, "/__external.ww"), strings.concat( + writefile(strings.concat(externalonly, "/source.ww"), strings.concat( "package __external;\n", "export fn value() i32 = { return 1; };\n")); writefile(strings.concat(pkg, "/a.ww"), strings.concat( @@ -1327,7 +1337,7 @@ fn hexbytes(value: str) str = { let warmstdout: str = strings.dup(out.stdout); let warmstderr: str = strings.dup(out.stderr); let warmtrace: str = readfile(compilertrace); - let warmwork: str = strings.concat(warmroot, "/d_", workescape(pkg), "/"); + let warmwork: str = strings.concat(warmroot, "/"); assert(has(readfile(strings.concat(warmwork, "__same.unit.ww")), "package __same;")); assert(has(readfile(strings.concat(warmwork, @@ -1533,19 +1543,19 @@ fn hexbytes(value: str) str = { writefile(strings.concat(common, "/common_test.ww"), strings.concat( "package common;\n", "DEPENDENCY_TEST_FILE_MUST_NOT_COMPILE\n")); - writefile(strings.concat(alphasame, "/_alpha_same.ww"), + writefile(strings.concat(alphasame, "/source.ww"), "package _alpha_same;\nexport fn value() i32 = { return 1; };\n"); - writefile(strings.concat(alphaexternal, "/_alpha_external.ww"), + writefile(strings.concat(alphaexternal, "/source.ww"), "package _alpha_external;\nexport fn value() i32 = { return 1; };\n"); - writefile(strings.concat(betasame, "/_beta_same.ww"), + writefile(strings.concat(betasame, "/source.ww"), "package _beta_same;\nexport fn value() i32 = { return 2; };\n"); - writefile(strings.concat(betaexternal, "/_beta_external.ww"), + writefile(strings.concat(betaexternal, "/source.ww"), "package _beta_external;\nexport fn value() i32 = { return 2; };\n"); // The explicit -I root contains deliberately wrong shadows. Each selected // directory must retain its own first lookup root inside the union plan. - writefile(strings.concat(alphashadow, "/_alpha_same.ww"), + writefile(strings.concat(alphashadow, "/source.ww"), "package _alpha_same;\nexport fn value() i32 = { return 99; };\n"); - writefile(strings.concat(betashadow, "/_beta_same.ww"), + writefile(strings.concat(betashadow, "/source.ww"), "package _beta_same;\nexport fn value() i32 = { return 99; };\n"); writefile(strings.concat(alpha, "/a.ww"), strings.concat( "package alpha;\nimport common;\n", @@ -1992,7 +2002,7 @@ fn hexbytes(value: str) str = { expectexit(&out, 0); assert(same(warmstderr, out.stderr)); assert(same(warmtrace, readfile(compilertrace))); - let warmkey: str = strings.concat("/d_", workescape(suite), "/"); + let warmkey: str = "/"; let warmwork: str = strings.concat(warmroot, warmkey); assert(os.exists(strings.concat(warmwork, "alpha-internal-test.unit.ww"))); @@ -2351,7 +2361,7 @@ fn hexbytes(value: str) str = { let si: i32 = 0; for (si < stages.len) { let av: []str = [driver(stages[si]), "test", "-c", "-j", "1", - "-I", root, strings.concat(suite, "/...")]; + "-I", suite, strings.concat(suite, "/...")]; if (si == 0) { runcommandenv(root, "mixed-actions-c", av, linkenv, (120i64 * (time.second: i64)): time.duration, &out); @@ -2911,8 +2921,7 @@ fn hexbytes(value: str) str = { let linkers: []str = ["w6l", "w6l_ww"]; let workroots: []str = [strings.concat(root, "/universe-c-workroot"), strings.concat(root, "/universe-ww-workroot")]; - let works: []str = [strings.concat(workroots[0], "/", requestworkkey(suite)), - strings.concat(workroots[1], "/", requestworkkey(suite))]; + let works: []str = [workroots[0], workroots[1]]; let compilertraces: []str = [strings.concat(root, "/universe-c-compiler"), strings.concat(root, "/universe-ww-compiler")]; let linkertraces: []str = [strings.concat(root, "/universe-c-linker"), @@ -2925,7 +2934,6 @@ fn hexbytes(value: str) str = { let si: i32 = 0; for (si < stages.len) { assert(os.mkdir(workroots[si], 448i32) == 0); - assert(os.mkdir(works[si], 448i32) == 0); writefile(compilertraces[si], ""); writefile(linkertraces[si], ""); writefile(buildertraces[si], ""); @@ -4066,8 +4074,7 @@ fn hexbytes(value: str) str = { assert(same(referencetestout, out.stdout)); assert(same(referencetesterr, out.stderr)); }; - let testwork: str = strings.concat(testworkroot, "/", - requestworkkey(testclient), "/"); + let testwork: str = strings.concat(testworkroot, "/"); let testactions: []str = ["domain.testclient-internal-test", "domain.testclient_test-external-test", "domain.testclient-internal-test-main", @@ -4759,8 +4766,7 @@ fn hexbytes(value: str) str = { for (si < stages.len) { let workroot: str = strings.concat(root, "/command-work-", stages[si]); assert(os.mkdir(workroot, 448i32) == 0); - let work: str = strings.concat(workroot, "/", requestworkkey(target), - "/"); + let work: str = strings.concat(workroot, "/"); let trace: str = strings.concat(root, "/", stages[si], ".trace"); writefile(trace, ""); let env: []str = alloc([], (baseenv.len + 3): u64)!; @@ -5070,10 +5076,10 @@ fn hexbytes(value: str) str = { clean(root); }; -// Go's ./... parity: a trailing "..." element walks the tree, one package -// run per test-bearing directory, dot- and underscore-prefixed directory -// names excluded. The excluded sentinels hold failing tests so a wrong -// descent turns the whole run red rather than passing silently. +// Go's local-pattern parity: "..." walks below the prefix preceding its first +// occurrence. Dot-, underscore-, and testdata-prefixed trees are excluded; +// vendor is traversed but a wildcard cannot consume a non-terminal vendor. +// Excluded sentinels fail so a wrong descent turns the whole run red. @test fn recursive_tree_discovery() void = { let root: str = fresh(); let tree: str = strings.concat(root, "/tree"); @@ -5082,8 +5088,24 @@ fn hexbytes(value: str) str = { assert(os.mkdir(strings.concat(tree, "/beta"), 448i32) == 0); assert(os.mkdir(strings.concat(tree, "/beta/inner"), 448i32) == 0); assert(os.mkdir(strings.concat(tree, "/gamma"), 448i32) == 0); + assert(os.mkdir(strings.concat(tree, "/app"), 448i32) == 0); + assert(os.mkdir(strings.concat(tree, "/app/vendor"), 448i32) == 0); + assert(os.mkdir(strings.concat(tree, "/app/vendor/lib"), 448i32) == 0); + assert(os.mkdir(strings.concat(tree, "/app/vendor/lib/math"), 448i32) == 0); + assert(os.mkdir(strings.concat(tree, "/domain"), 448i32) == 0); + assert(os.mkdir(strings.concat(tree, "/domain/app"), 448i32) == 0); + assert(os.mkdir(strings.concat(tree, "/domain/vendor"), 448i32) == 0); + assert(os.mkdir(strings.concat(tree, "/domain/vendor/dep"), 448i32) == 0); assert(os.mkdir(strings.concat(tree, "/.hidden"), 448i32) == 0); + assert(os.mkdir(strings.concat(tree, "/.hidden/deep"), 448i32) == 0); assert(os.mkdir(strings.concat(tree, "/_skip"), 448i32) == 0); + assert(os.mkdir(strings.concat(tree, "/testdata"), 448i32) == 0); + assert(os.mkdir(strings.concat(tree, "/testdata/bad"), 448i32) == 0); + assert(os.mkdir(strings.concat(tree, "/vendor"), 448i32) == 0); + assert(os.mkdir(strings.concat(tree, "/vendor/bad"), 448i32) == 0); + assert(os.mkdir(strings.concat(tree, "/vendor/bad/vendor"), 448i32) == 0); + assert(os.mkdir(strings.concat(tree, "/vendor/bad/vendor/deep"), 448i32) == 0); + assert(os.mkdir(strings.concat(tree, "/vendorx"), 448i32) == 0); writefile(strings.concat(tree, "/a.ww"), "package rootpkg;\nfn first() i32 = { return 1; };\n"); writefile(strings.concat(tree, "/root_test.ww"), strings.concat( @@ -5094,21 +5116,91 @@ fn hexbytes(value: str) str = { writefile(strings.concat(tree, "/alpha/alpha.ww"), "package alpha;\nexport fn value() int = { return 7; };\n"); writefile(strings.concat(tree, "/alpha/alpha_test.ww"), - "package alpha_test;\nimport alpha;\n@test fn treealpha() void = { assert(alpha.value() == 7); };\n"); + strings.concat("package alpha_test;\nimport alpha;\n", + "@test fn treealpha() void = { assert(alpha.value() == 7);", + " assert(alpha.linked() == 11); };\n")); writefile(strings.concat(tree, "/beta/inner/inner_test.ww"), "package inner;\n@test fn treeinner() void = { assert(1 == 1); };\n"); writefile(strings.concat(tree, "/gamma/gamma.ww"), "package gamma;\nexport fn g() int = { return 1; };\n"); + writefile(strings.concat(tree, "/app/app.ww"), strings.concat( + "package app;\nimport lib.math;\n", + "export fn value() int = { return math.value(); };\n")); + writefile(strings.concat(tree, "/app/app_test.ww"), strings.concat( + "package app;\n@test fn vendored_dependency() void = {", + " assert(value() == 17); };\n")); + writefile(strings.concat(tree, "/app/vendor/lib/math/math.ww"), + "package math;\nexport fn value() int = { return 17; };\n"); + writefile(strings.concat(tree, "/domain/app/app.ww"), strings.concat( + "package app;\nimport dep;\n", + "fn value() int = { return dep.value(); };\n")); + writefile(strings.concat(tree, "/domain/app/app_test.ww"), strings.concat( + "package app;\n@test fn ancestor_vendor() void = {", + " assert(value() == 29); };\n")); + writefile(strings.concat(tree, "/domain/vendor/dep/dep.ww"), + "package dep;\nexport fn value() int = { return 29; };\n"); + writefile(strings.concat(tree, "/gamma/.ignored.ww"), + "this hidden source must not make package loading fail\n"); + writefile(strings.concat(tree, "/gamma/_ignored.ww"), + "this underscore source must not make package loading fail\n"); writefile(strings.concat(tree, "/.hidden/hid_test.ww"), - "package hid;\n@test fn hidden() void = { assert(false); };\n"); + "package hid;\n@test fn hidden_direct() void = { assert(true); };\n"); + writefile(strings.concat(tree, "/.hidden/deep/bad.ww"), + "this excluded malformed directory must not be selected\n"); writefile(strings.concat(tree, "/_skip/skip_test.ww"), - "package skip;\n@test fn skipped() void = { assert(false); };\n"); + "package _skip;\n@test fn underscore_direct() void = { assert(true); };\n"); + writefile(strings.concat(tree, "/testdata/bad/bad_test.ww"), + "package bad;\n@test fn testdata_direct() void = { assert(true); };\n"); + writefile(strings.concat(tree, "/vendor/vendor.ww"), + "package vendor;\nexport fn value() int = { return 1; };\n"); + writefile(strings.concat(tree, "/vendor/bad/bad.ww"), + "package bad;\nexport fn value() int = { return 2; };\n"); + writefile(strings.concat(tree, "/vendor/bad/bad_test.ww"), + "package bad;\n@test fn explicit_vendor() void = { assert(value() == 2); };\n"); + writefile(strings.concat(tree, "/vendor/bad/vendor/deep/deep_test.ww"), + "package deep;\n@test fn nested_vendor_must_not_run() void = { assert(false); };\n"); + writefile(strings.concat(tree, "/vendorx/vendorx.ww"), + "package vendorx;\nexport fn value() int = { return 3; };\n"); + let outside: str = strings.concat(root, "/outside"); + let alias: str = strings.concat(root, "/tree-alias"); + assert(os.mkdir(outside, 448i32) == 0); + writefile(strings.concat(outside, "/outside.ww"), + "package outside;\nexport fn value() int = { return 23; };\n"); + writefile(strings.concat(outside, "/outside_test.ww"), strings.concat( + "package outside;\n@test fn explicit_root_symlink() void = {", + " assert(value() == 23); };\n")); + let linkout: commandout; + let linkedsource: str = strings.concat(root, "/alpha-linked-source"); + writefile(linkedsource, + "package alpha;\nexport fn linked() int = { return 11; };\n"); + let linkedav: []str = ["/bin/ln", "-s", linkedsource, + strings.concat(tree, "/alpha/linked.ww")]; + runcommand(root, "recursive-source-link", linkedav, time.second, &linkout); + expectexit(&linkout, 0); + let linkeddirectory: str = strings.concat(root, "/source-directory"); + assert(os.mkdir(linkeddirectory, 448i32) == 0); + let linkeddirav: []str = ["/bin/ln", "-s", linkeddirectory, + strings.concat(tree, "/alpha/directory.ww")]; + runcommand(root, "recursive-source-directory-link", linkeddirav, + time.second, &linkout); + expectexit(&linkout, 0); + let aliasav: []str = ["/bin/ln", "-s", tree, alias]; + runcommand(root, "recursive-root-alias", aliasav, time.second, &linkout); + expectexit(&linkout, 0); + let escapeav: []str = ["/bin/ln", "-s", outside, + strings.concat(tree, "/escape")]; + runcommand(root, "recursive-child-escape", escapeav, time.second, &linkout); + expectexit(&linkout, 0); + let cycleav: []str = ["/bin/ln", "-s", tree, + strings.concat(tree, "/cycle")]; + runcommand(root, "recursive-child-cycle", cycleav, time.second, &linkout); + expectexit(&linkout, 0); let spec: str = strings.concat(tree, "/..."); let outc: commandout; let outw: commandout; - let treec: []str = [driver("ww"), "test", spec]; - let treew: []str = [driver("ww_ww"), "test", spec]; + let treec: []str = [driver("ww"), "test", "-I", tree, spec]; + let treew: []str = [driver("ww_ww"), "test", "-I", tree, spec]; runcommand(root, "tree-c", treec, (30i64 * (time.second: i64)): time.duration, &outc); runcommand(root, "tree-ww", treew, @@ -5117,31 +5209,700 @@ fn hexbytes(value: str) str = { expectexit(&outw, 0); assert(same(outc.stdout, outw.stdout)); assert(same(outc.stderr, outw.stderr)); - assert(has(outc.stdout, strings.concat("ok ", tree, + let seq: str = strings.dup(outc.stdout); + + // A directory -o publishes every selected command by directory basename. + // Non-command production roots remain graph roots but have no named output. + let commands: str = strings.concat(root, "/commands"); + let commandalpha: str = strings.concat(commands, "/cmd-alpha"); + let commandzeta: str = strings.concat(commands, "/cmd-zeta"); + let commandlib: str = strings.concat(commands, "/library"); + assert(os.mkdir(commands, 448i32) == 0); + assert(os.mkdir(commandalpha, 448i32) == 0); + assert(os.mkdir(commandzeta, 448i32) == 0); + assert(os.mkdir(commandlib, 448i32) == 0); + writefile(strings.concat(commandalpha, "/main.ww"), strings.concat( + "package main;\nimport library;\n", + "fn main() i32 = { return library.value(); };\n")); + writefile(strings.concat(commandzeta, "/main.ww"), + "package main;\nfn main() i32 = { return 0; };\n"); + writefile(strings.concat(commandlib, "/library.ww"), + "package library;\nexport fn value() i32 = { return 0; };\n"); + let commandalias: str = strings.concat(root, "/commands-alias"); + let commandaliasav: []str = ["/bin/ln", "-s", commands, commandalias]; + runcommand(root, "recursive-command-alias", commandaliasav, time.second, + &linkout); + expectexit(&linkout, 0); + let dashselector: str = "-dash-command"; + let dashselectorpath: str = strings.concat(root, "/", dashselector); + let dashselectorav: []str = ["/bin/ln", "-s", commandalpha, + dashselectorpath]; + runcommand(root, "recursive-dash-selector", dashselectorav, time.second, + &linkout); + expectexit(&linkout, 0); + let commandalphaid: str = localidentity(commandalpha, "main"); + let commandzetaid: str = localidentity(commandzeta, "main"); + let binc: str = strings.concat(root, "/bin-c"); + let binw: str = strings.concat(root, "/bin-ww"); + let reversebinc: str = strings.concat(root, "/reverse-bin-c"); + let reversebinw: str = strings.concat(root, "/reverse-bin-ww"); + assert(os.mkdir(binc, 448i32) == 0); + assert(os.mkdir(binw, 448i32) == 0); + let commandspec: str = strings.concat(commands, "/..."); + let commandaliasspec: str = strings.concat(commandalias, "/..."); + let commandworks: []str = [strings.concat(root, "/command-work-c"), + strings.concat(root, "/command-work-ww")]; + let commandreverseworks: []str = [strings.concat(root, + "/command-reverse-work-c"), strings.concat(root, + "/command-reverse-work-ww")]; + let commandbins: []str = [binc, binw]; + let commandreversebins: []str = [reversebinc, reversebinw]; + let commandreverseoutputs: []str = [strings.concat(reversebinc, "/"), + strings.concat(reversebinw, "/")]; + let commandcompilertraces: []str = [strings.concat(root, + "/command-compiler-c"), strings.concat(root, "/command-compiler-ww")]; + let commandassemblertraces: []str = [strings.concat(root, + "/command-assembler-c"), strings.concat(root, "/command-assembler-ww")]; + let commandlinkertraces: []str = [strings.concat(root, + "/command-linker-c"), strings.concat(root, "/command-linker-ww")]; + let commandasmworks: []str = [strings.concat(root, "/asm-work-c"), + strings.concat(root, "/asm-work-ww")]; + let commandtools: str = strings.concat(root, "/command-tools"); + assert(os.mkdir(commandtools, 448i32) == 0); + let commandcompilerwrapper: str = strings.concat(commandtools, "/w6c.sh"); + let commandassemblerwrapper: str = strings.concat(commandtools, "/w6a.sh"); + let commandlinkerwrapper: str = strings.concat(commandtools, "/w6l.sh"); + writeexecutable(commandcompilerwrapper, strings.concat( + "#!/bin/sh\nprintf 'BEGIN' >> \"$WW_PATTERN_COMPILER_TRACE\"\n", + "for arg in \"$@\"; do printf '<%s>' \"$arg\" >> ", + "\"$WW_PATTERN_COMPILER_TRACE\"; done\n", + "printf '\\n' >> \"$WW_PATTERN_COMPILER_TRACE\"\n", + "exec \"$WW_PATTERN_REAL_COMPILER\" \"$@\"\n")); + writeexecutable(commandassemblerwrapper, strings.concat( + "#!/bin/sh\nprintf 'BEGIN' >> \"$WW_PATTERN_ASSEMBLER_TRACE\"\n", + "for arg in \"$@\"; do printf '<%s>' \"$arg\" >> ", + "\"$WW_PATTERN_ASSEMBLER_TRACE\"; done\n", + "printf '\\n' >> \"$WW_PATTERN_ASSEMBLER_TRACE\"\n", + "exec \"$WW_PATTERN_REAL_ASSEMBLER\" \"$@\"\n")); + writeexecutable(commandlinkerwrapper, strings.concat( + "#!/bin/sh\nprintf 'BEGIN' >> \"$WW_PATTERN_LINKER_TRACE\"\n", + "for arg in \"$@\"; do printf '<%s>' \"$arg\" >> ", + "\"$WW_PATTERN_LINKER_TRACE\"; done\n", + "printf '\\n' >> \"$WW_PATTERN_LINKER_TRACE\"\n", + "exec \"$WW_PATTERN_REAL_LINKER\" \"$@\"\n")); + + let collisions: str = strings.concat(root, "/collisions"); + let collisionleft: str = strings.concat(collisions, "/left"); + let collisionright: str = strings.concat(collisions, "/right"); + let collisionleftcmd: str = strings.concat(collisionleft, "/same"); + let collisionrightcmd: str = strings.concat(collisionright, "/same"); + assert(os.mkdir(collisions, 448i32) == 0); + assert(os.mkdir(collisionleft, 448i32) == 0); + assert(os.mkdir(collisionright, 448i32) == 0); + assert(os.mkdir(collisionleftcmd, 448i32) == 0); + assert(os.mkdir(collisionrightcmd, 448i32) == 0); + writefile(strings.concat(collisionleftcmd, "/main.ww"), + "package main;\nfn main() i32 = { return 0; };\n"); + writefile(strings.concat(collisionrightcmd, "/main.ww"), + "package main;\nfn main() i32 = { return 0; };\n"); + let collisionspec: str = strings.concat(collisions, "/..."); + let collisionout: str = strings.concat(root, "/collision-bin"); + assert(os.mkdir(collisionout, 448i32) == 0); + let collisionworks: []str = [strings.concat(root, "/collision-work-c"), + strings.concat(root, "/collision-work-ww")]; + + let commandstages: []str = ["ww", "ww_ww"]; + let commandcompilers: []str = ["w6c", "w6c_ww"]; + let commandassemblers: []str = ["w6a", "w6a_ww"]; + let commandlinkers: []str = ["w6l", "w6l_ww"]; + let commandbaseenv: []str = os.getenvs(); + let referencecommandcompiler: str = ""; + let referencecommandassembler: str = ""; + let referencecommandlinker: str = ""; + let referenceasmcompiler: str = ""; + let referenceasmrejectdiag: str = ""; + let referencecollisiondiag: str = ""; + let referencefanoutpathdiag: str = ""; + let referencescratchpathdiag: str = ""; + let referenceworkpathdiag: str = ""; + // Raw -o/-w spellings still fit the public PATH_MAX contract. The + // directory fan-out, cold scratch, and persistent tool-record spellings + // derived from them do not, and must be rejected before any producer. + let longoutputbytes: []u8 = alloc([], os.PATH_MAX: u64)!; + let longpathi: i32 = 0; + for (longpathi < root.len) { + append(longoutputbytes, root[longpathi]); longpathi += 1; + }; + append(longoutputbytes, '/': u8); + for (longoutputbytes.len < os.PATH_MAX - 2) { + append(longoutputbytes, 'o': u8); + }; + append(longoutputbytes, '/': u8); + assert(longoutputbytes.len == os.PATH_MAX - 1); + let longoutputdir: str = strings.frombytes(longoutputbytes); + let longscratchbytes: []u8 = alloc([], os.PATH_MAX: u64)!; + longpathi = 0; + for (longpathi < root.len) { + append(longscratchbytes, root[longpathi]); longpathi += 1; + }; + append(longscratchbytes, '/': u8); + for (longscratchbytes.len < os.PATH_MAX - 1) { + append(longscratchbytes, 's': u8); + }; + assert(longscratchbytes.len == os.PATH_MAX - 1); + let longscratchout: str = strings.frombytes(longscratchbytes); + let longworkbytes: []u8 = alloc([], os.PATH_MAX: u64)!; + longpathi = 0; + for (longpathi < root.len) { + append(longworkbytes, root[longpathi]); longpathi += 1; + }; + for (longworkbytes.len < os.PATH_MAX - 8) { + append(longworkbytes, '/': u8); + let segmentbytes: i32 = 0; + for (segmentbytes < 120 && longworkbytes.len < os.PATH_MAX - 8) { + append(longworkbytes, 'w': u8); segmentbytes += 1; + }; + }; + let longworkdir: str = strings.frombytes(longworkbytes); + mkdirall(longworkdir); + let commandworkpaths: []str = ["", ""]; + let commandstage: i32 = 0; + for (commandstage < commandstages.len) { + writefile(commandcompilertraces[commandstage], ""); + writefile(commandassemblertraces[commandstage], ""); + writefile(commandlinkertraces[commandstage], ""); + let commandenv: []str = alloc([], (commandbaseenv.len + 9): u64)!; + let commandei: i32 = 0; + for (commandei < commandbaseenv.len) { + if (!strings.hasprefix(commandbaseenv[commandei], "WW_W6C=") + && !strings.hasprefix(commandbaseenv[commandei], "WW_W6A=") + && !strings.hasprefix(commandbaseenv[commandei], "WW_W6L=") + && !strings.hasprefix(commandbaseenv[commandei], + "WW_PATTERN_COMPILER_TRACE=") + && !strings.hasprefix(commandbaseenv[commandei], + "WW_PATTERN_ASSEMBLER_TRACE=") + && !strings.hasprefix(commandbaseenv[commandei], + "WW_PATTERN_LINKER_TRACE=") + && !strings.hasprefix(commandbaseenv[commandei], + "WW_PATTERN_REAL_COMPILER=") + && !strings.hasprefix(commandbaseenv[commandei], + "WW_PATTERN_REAL_ASSEMBLER=") + && !strings.hasprefix(commandbaseenv[commandei], + "WW_PATTERN_REAL_LINKER=")) { + append(commandenv, commandbaseenv[commandei]); + }; + commandei += 1; + }; + append(commandenv, strings.concat("WW_W6C=", commandcompilerwrapper)); + append(commandenv, strings.concat("WW_W6A=", commandassemblerwrapper)); + append(commandenv, strings.concat("WW_W6L=", commandlinkerwrapper)); + append(commandenv, strings.concat("WW_PATTERN_COMPILER_TRACE=", + commandcompilertraces[commandstage])); + append(commandenv, strings.concat("WW_PATTERN_ASSEMBLER_TRACE=", + commandassemblertraces[commandstage])); + append(commandenv, strings.concat("WW_PATTERN_LINKER_TRACE=", + commandlinkertraces[commandstage])); + append(commandenv, strings.concat("WW_PATTERN_REAL_COMPILER=", + driver(commandcompilers[commandstage]))); + append(commandenv, strings.concat("WW_PATTERN_REAL_ASSEMBLER=", + driver(commandassemblers[commandstage]))); + append(commandenv, strings.concat("WW_PATTERN_REAL_LINKER=", + driver(commandlinkers[commandstage]))); + + let commandforward: []str = [driver(commandstages[commandstage]), "build", + "-w", commandworks[commandstage], "-I", commands, + "-L", strings.concat(repo(), "/out/lib"), "-lwcc", "-o", + commandbins[commandstage], commandspec, commandaliasspec, + commandspec, commandalpha]; + let commandreverse: []str = [driver(commandstages[commandstage]), "build", + "-w", commandreverseworks[commandstage], "-I", commands, + "-L", strings.concat(repo(), "/out/lib"), "-lwcc", "-o", + commandreverseoutputs[commandstage], commandalpha, commandspec, + commandaliasspec, commandspec]; + if (commandstage == 0) { + runcommandenv(root, "tree-command-output-c", commandforward, commandenv, + (60i64 * (time.second: i64)): time.duration, &outc); + expectexit(&outc, 0); + } else { + runcommandenv(root, "tree-command-output-ww", commandforward, commandenv, + (60i64 * (time.second: i64)): time.duration, &outw); + expectexit(&outw, 0); + }; + let commandstdout: str; + let commandstderr: str; + if (commandstage == 0) { + commandstdout = strings.dup(outc.stdout); + commandstderr = strings.dup(outc.stderr); + } else { + commandstdout = strings.dup(outw.stdout); + commandstderr = strings.dup(outw.stderr); + }; + let commandalphabin: str = strings.dup(readfile(strings.concat( + commandbins[commandstage], "/cmd-alpha"))); + let commandzetabin: str = strings.dup(readfile(strings.concat( + commandbins[commandstage], "/cmd-zeta"))); + let commandwork: str = strings.concat(commandworks[commandstage], "/"); + commandworkpaths[commandstage] = strings.dup(commandwork); + let commandctrace: str = readfile(commandcompilertraces[commandstage]); + let commandatrace: str = readfile(commandassemblertraces[commandstage]); + let commandltrace: str = readfile(commandlinkertraces[commandstage]); + assert(occurrences(commandctrace, "\n") == 3); + assert(occurrences(commandatrace, "\n") == 3); + assert(occurrences(commandltrace, "\n") == 2); + let commandalphacompile: str = linecontaining(commandctrace, + strings.concat(commandalphaid, ".unit.new")); + assert(occurrences(commandalphacompile, strings.concat( + "<--import><", commandwork, "library.wwi>")) == 1); + assert(!has(commandalphacompile, "cmd-zeta.wwi")); + let commandalphalink: str = linecontaining(commandltrace, + strings.concat("<-o><", commandbins[commandstage], "/cmd-alpha>")); + let commandzetalink: str = linecontaining(commandltrace, + strings.concat("<-o><", commandbins[commandstage], "/cmd-zeta>")); + assert(occurrences(commandalphalink, strings.concat("<", commandwork, + commandalphaid, ".a>")) == 1); + assert(occurrences(commandalphalink, strings.concat("<", commandwork, + "library.a>")) == 1); + assert(!has(commandalphalink, strings.concat(commandzetaid, ".a"))); + assert(!has(commandalphalink, ".wwi>")); + assert(has(commandalphalink, strings.concat("<-L><", repo(), + "/out/lib><-l>"))); + assert(has(commandzetalink, strings.concat("<", commandwork, + commandzetaid, ".a>"))); + assert(!has(commandzetalink, "library.a")); + assert(!has(commandzetalink, ".wwi>")); + let normalizedcommandcompiler: str = normalizedtrace(commandctrace, + commandwork, commandbins[commandstage]); + let normalizedcommandassembler: str = normalizedtrace(commandatrace, + commandwork, commandbins[commandstage]); + let normalizedcommandlinker: str = normalizedtrace(commandltrace, + commandwork, commandbins[commandstage]); + if (commandstage == 0) { + referencecommandcompiler = strings.dup(normalizedcommandcompiler); + referencecommandassembler = strings.dup(normalizedcommandassembler); + referencecommandlinker = strings.dup(normalizedcommandlinker); + } else { + assert(same(referencecommandcompiler, normalizedcommandcompiler)); + assert(same(referencecommandassembler, normalizedcommandassembler)); + assert(same(referencecommandlinker, normalizedcommandlinker)); + }; + // Reverse the identical request multiset into an independent cold + // work/output root inside this stage. Canonical product ordering leaves + // diagnostics, normalized tool argv, artifacts, and binaries fixed. + rewritefile(commandcompilertraces[commandstage], ""); + rewritefile(commandassemblertraces[commandstage], ""); + rewritefile(commandlinkertraces[commandstage], ""); + if (commandstage == 0) { + runcommandenv(root, "tree-command-reverse-c", commandreverse, commandenv, + (60i64 * (time.second: i64)): time.duration, &outc); + expectexit(&outc, 0); + assert(same(commandstdout, outc.stdout)); + assert(same(commandstderr, outc.stderr)); + } else { + runcommandenv(root, "tree-command-reverse-ww", commandreverse, commandenv, + (60i64 * (time.second: i64)): time.duration, &outw); + expectexit(&outw, 0); + assert(same(commandstdout, outw.stdout)); + assert(same(commandstderr, outw.stderr)); + }; + let reversework: str = strings.concat( + commandreverseworks[commandstage], "/"); + let reversectrace: str = readfile(commandcompilertraces[commandstage]); + let reverseatrace: str = readfile(commandassemblertraces[commandstage]); + let reverseltrace: str = readfile(commandlinkertraces[commandstage]); + assert(same(normalizedcommandcompiler, normalizedtrace(reversectrace, + reversework, commandreversebins[commandstage]))); + assert(same(normalizedcommandassembler, normalizedtrace(reverseatrace, + reversework, commandreversebins[commandstage]))); + assert(same(normalizedcommandlinker, normalizedtrace(reverseltrace, + reversework, commandreversebins[commandstage]))); + let reverseactions: []str = [commandalphaid, commandzetaid, "library"]; + let reversesuffixes: []str = [".unit.ww", ".wwi", ".s", ".o", ".a"]; + let rai: i32 = 0; + for (rai < reverseactions.len) { + let rsi: i32 = 0; + for (rsi < reversesuffixes.len) { + assert(same(readfile(strings.concat(commandwork, + reverseactions[rai], reversesuffixes[rsi])), + readfile(strings.concat(reversework, reverseactions[rai], + reversesuffixes[rsi])))); + rsi += 1; + }; + rai += 1; + }; + assert(same(commandalphabin, readfile(strings.concat( + commandreversebins[commandstage], "/cmd-alpha")))); + assert(same(commandzetabin, readfile(strings.concat( + commandreversebins[commandstage], "/cmd-zeta")))); + + // Return to the forward store: a warm equivalent request invokes no + // compiler or assembler before the direct-form reuse check below. + rewritefile(commandcompilertraces[commandstage], ""); + rewritefile(commandassemblertraces[commandstage], ""); + rewritefile(commandlinkertraces[commandstage], ""); + if (commandstage == 0) { + runcommandenv(root, "tree-command-warm-c", commandforward, commandenv, + (60i64 * (time.second: i64)): time.duration, &outc); + expectexit(&outc, 0); + } else { + runcommandenv(root, "tree-command-warm-ww", commandforward, commandenv, + (60i64 * (time.second: i64)): time.duration, &outw); + expectexit(&outw, 0); + }; + assert(readfile(commandcompilertraces[commandstage]).len == 0); + assert(readfile(commandassemblertraces[commandstage]).len == 0); + let directbin: str = strings.concat(root, "/direct-", + commandstages[commandstage]); + let directav: []str = [driver(commandstages[commandstage]), "build", + "-w", commandworks[commandstage], "-I", commands, + "-L", strings.concat(repo(), "/out/lib"), "-lwcc", + "-o", directbin, commandalpha]; + if (commandstage == 0) { + runcommandenv(root, "tree-command-direct-c", directav, commandenv, + (60i64 * (time.second: i64)): time.duration, &outc); + expectexit(&outc, 0); + } else { + runcommandenv(root, "tree-command-direct-ww", directav, commandenv, + (60i64 * (time.second: i64)): time.duration, &outw); + expectexit(&outw, 0); + }; + assert(readfile(commandcompilertraces[commandstage]).len == 0); + assert(readfile(commandassemblertraces[commandstage]).len == 0); + assert(same(commandalphabin, readfile(directbin))); + + rewritefile(commandcompilertraces[commandstage], ""); + rewritefile(commandassemblertraces[commandstage], ""); + rewritefile(commandlinkertraces[commandstage], ""); + let commandasm: []str = [driver(commandstages[commandstage]), "build", + "-S", "-w", commandasmworks[commandstage], "-I", commands, + commandspec, commandaliasspec, commandalpha]; + if (commandstage == 0) { + runcommandenv(root, "tree-command-asm-c", commandasm, commandenv, + (60i64 * (time.second: i64)): time.duration, &outc); + expectexit(&outc, 0); + } else { + runcommandenv(root, "tree-command-asm-ww", commandasm, commandenv, + (60i64 * (time.second: i64)): time.duration, &outw); + expectexit(&outw, 0); + }; + let commandasmwork: str = strings.concat(commandasmworks[commandstage], + "/"); + let commandasmtrace: str = readfile(commandcompilertraces[commandstage]); + assert(occurrences(commandasmtrace, "\n") == 3); + assert(readfile(commandassemblertraces[commandstage]).len == 0); + assert(readfile(commandlinkertraces[commandstage]).len == 0); + assert(os.exists(strings.concat(commandasmwork, commandalphaid, ".s"))); + assert(os.exists(strings.concat(commandasmwork, commandzetaid, ".s"))); + assert(os.exists(strings.concat(commandasmwork, "library.s"))); + let normalizedasmcompiler: str = normalizedtrace(commandasmtrace, + commandasmwork, commandbins[commandstage]); + if (commandstage == 0) { + referenceasmcompiler = strings.dup(normalizedasmcompiler); + } else { assert(same(referenceasmcompiler, normalizedasmcompiler)); }; + if (commandstage == 0) { + runcommandenv(root, "tree-command-asm-warm-c", commandasm, commandenv, + (60i64 * (time.second: i64)): time.duration, &outc); + expectexit(&outc, 0); + } else { + runcommandenv(root, "tree-command-asm-warm-ww", commandasm, commandenv, + (60i64 * (time.second: i64)): time.duration, &outw); + expectexit(&outw, 0); + }; + assert(same(commandasmtrace, + readfile(commandcompilertraces[commandstage]))); + rewritefile(commandcompilertraces[commandstage], ""); + rewritefile(commandassemblertraces[commandstage], ""); + rewritefile(commandlinkertraces[commandstage], ""); + let commandasmnowork: []str = [driver(commandstages[commandstage]), + "build", "-S", "-I", commands, commandspec]; + if (commandstage == 0) { + runcommandenv(root, "tree-command-asm-no-work-c", commandasmnowork, + commandenv, time.second, &outc); + expectexit(&outc, 2); + referenceasmrejectdiag = strings.dup(outc.stderr); + } else { + runcommandenv(root, "tree-command-asm-no-work-ww", commandasmnowork, + commandenv, time.second, &outw); + expectexit(&outw, 2); + assert(same(referenceasmrejectdiag, outw.stderr)); + }; + assert(has(referenceasmrejectdiag, + "wwtest package: recursive -S needs -w\n")); + assert(readfile(commandcompilertraces[commandstage]).len == 0); + assert(readfile(commandassemblertraces[commandstage]).len == 0); + assert(readfile(commandlinkertraces[commandstage]).len == 0); + + rewritefile(commandcompilertraces[commandstage], ""); + rewritefile(commandassemblertraces[commandstage], ""); + rewritefile(commandlinkertraces[commandstage], ""); + let collisionav: []str = [driver(commandstages[commandstage]), "build", + "-w", collisionworks[commandstage], "-o", collisionout, + collisionspec]; + if (commandstage == 0) { + runcommandenv(root, "tree-collision-c", collisionav, commandenv, + time.second, &outc); + expectexit(&outc, 1); + referencecollisiondiag = strings.dup(outc.stderr); + } else { + runcommandenv(root, "tree-collision-ww", collisionav, commandenv, + time.second, &outw); + expectexit(&outw, 1); + assert(same(referencecollisiondiag, outw.stderr)); + }; + assert(readfile(commandcompilertraces[commandstage]).len == 0); + assert(readfile(commandassemblertraces[commandstage]).len == 0); + assert(readfile(commandlinkertraces[commandstage]).len == 0); + assert(!os.exists(collisionworks[commandstage])); + + rewritefile(commandcompilertraces[commandstage], ""); + rewritefile(commandassemblertraces[commandstage], ""); + rewritefile(commandlinkertraces[commandstage], ""); + let fanoutpathav: []str = [driver(commandstages[commandstage]), + "build", "-o", longoutputdir, "-I", commands, commandspec]; + if (commandstage == 0) { + runcommandenv(root, "tree-fanout-path-c", fanoutpathav, commandenv, + time.second, &outc); + expectexit(&outc, 1); + referencefanoutpathdiag = strings.dup(outc.stderr); + } else { + runcommandenv(root, "tree-fanout-path-ww", fanoutpathav, commandenv, + time.second, &outw); + expectexit(&outw, 1); + assert(same(referencefanoutpathdiag, outw.stderr)); + }; + assert(has(referencefanoutpathdiag, + "ww: command output path is too long\n")); + assert(readfile(commandcompilertraces[commandstage]).len == 0); + assert(readfile(commandassemblertraces[commandstage]).len == 0); + assert(readfile(commandlinkertraces[commandstage]).len == 0); + + let scratchpathav: []str = [driver(commandstages[commandstage]), + "build", "-o", longscratchout, "-I", commands, commandalpha]; + if (commandstage == 0) { + runcommandenv(root, "tree-scratch-path-c", scratchpathav, commandenv, + time.second, &outc); + expectexit(&outc, 1); + referencescratchpathdiag = strings.dup(outc.stderr); + } else { + runcommandenv(root, "tree-scratch-path-ww", scratchpathav, commandenv, + time.second, &outw); + expectexit(&outw, 1); + assert(same(referencescratchpathdiag, outw.stderr)); + }; + assert(has(referencescratchpathdiag, + "ww: scratch path is too long\n")); + assert(readfile(commandcompilertraces[commandstage]).len == 0); + assert(readfile(commandassemblertraces[commandstage]).len == 0); + assert(readfile(commandlinkertraces[commandstage]).len == 0); + + let workpathav: []str = [driver(commandstages[commandstage]), + "build", "-w", longworkdir, "-I", commands, commandalpha]; + if (commandstage == 0) { + runcommandenv(root, "tree-work-path-c", workpathav, commandenv, + time.second, &outc); + expectexit(&outc, 1); + referenceworkpathdiag = strings.dup(outc.stderr); + } else { + runcommandenv(root, "tree-work-path-ww", workpathav, commandenv, + time.second, &outw); + expectexit(&outw, 1); + assert(same(referenceworkpathdiag, outw.stderr)); + }; + assert(has(referenceworkpathdiag, + "ww: workdir path is too long\n")); + assert(readfile(commandcompilertraces[commandstage]).len == 0); + assert(readfile(commandassemblertraces[commandstage]).len == 0); + assert(readfile(commandlinkertraces[commandstage]).len == 0); + commandstage += 1; + }; + assert(has(referencecollisiondiag, + "ww: multiple commands produce output basename \"same\"")); + assert(!os.exists(strings.concat(collisionout, "/same"))); + let commandactions: []str = [commandalphaid, commandzetaid, "library"]; + let commandsuffixes: []str = [".unit.ww", ".wwi", ".s", ".o", ".a"]; + let commandaction: i32 = 0; + for (commandaction < commandactions.len) { + let commandsuffix: i32 = 0; + for (commandsuffix < commandsuffixes.len) { + assert(same(readfile(strings.concat(commandworkpaths[0], + commandactions[commandaction], commandsuffixes[commandsuffix])), + readfile(strings.concat(commandworkpaths[1], + commandactions[commandaction], commandsuffixes[commandsuffix])))); + commandsuffix += 1; + }; + commandaction += 1; + }; + assert(os.exists(strings.concat(binc, "/cmd-alpha"))); + assert(os.exists(strings.concat(binc, "/cmd-zeta"))); + assert(os.exists(strings.concat(binw, "/cmd-alpha"))); + assert(os.exists(strings.concat(binw, "/cmd-zeta"))); + assert(!os.exists(strings.concat(binc, "/library"))); + assert(!os.exists(strings.concat(binw, "/library"))); + assert(same(readfile(strings.concat(binc, "/cmd-alpha")), + readfile(strings.concat(binw, "/cmd-alpha")))); + assert(same(readfile(strings.concat(binc, "/cmd-zeta")), + readfile(strings.concat(binw, "/cmd-zeta")))); + // `--` ends build option parsing and every following argument remains a + // package selector. The coordinator receives and builds that direct root. + let dashworks: []str = [strings.concat(root, "/dash-work-c"), + strings.concat(root, "/dash-work-ww")]; + let dashbins: []str = [strings.concat(root, "/dash-bin-c"), + strings.concat(root, "/dash-bin-ww")]; + let dashtrailworks: []str = [strings.concat(root, "/dash-trail-work-c"), + strings.concat(root, "/dash-trail-work-ww")]; + let dashtrailbins: []str = [strings.concat(root, "/dash-trail-bin-c"), + strings.concat(root, "/dash-trail-bin-ww")]; + let referencedashstdout: str = ""; + let referencedashstderr: str = ""; + let dashstage: i32 = 0; + for (dashstage < commandstages.len) { + let dashav: []str = [driver(commandstages[dashstage]), "build", + "-w", dashworks[dashstage], "-I", commands, "-o", + dashbins[dashstage], "--", dashselector]; + if (dashstage == 0) { + runcommanddir(root, "tree-dash-c", root, dashav, + (30i64 * (time.second: i64)): time.duration, &outc); + expectexit(&outc, 0); + referencedashstdout = strings.dup(outc.stdout); + referencedashstderr = strings.dup(outc.stderr); + } else { + runcommanddir(root, "tree-dash-ww", root, dashav, + (30i64 * (time.second: i64)): time.duration, &outw); + expectexit(&outw, 0); + assert(same(referencedashstdout, outw.stdout)); + assert(same(referencedashstderr, outw.stderr)); + }; + assert(os.exists(strings.concat(dashworks[dashstage], "/", + commandalphaid, ".unit.ww"))); + // Once the first positional has stopped flag parsing, a later `--` is + // another package argument, not a second terminator. + let dashtrailav: []str = [driver(commandstages[dashstage]), "build", + "-w", dashtrailworks[dashstage], "-I", commands, "-o", + dashtrailbins[dashstage], commandalpha, "--"]; + if (dashstage == 0) { + runcommand(root, "tree-dash-trailing-c", dashtrailav, + time.second, &outc); + expectexit(&outc, 1); + assert(has(outc.stderr, + "--: cannot canonicalize package directory\n")); + } else { + runcommand(root, "tree-dash-trailing-ww", dashtrailav, + time.second, &outw); + expectexit(&outw, 1); + assert(same(outc.stdout, outw.stdout)); + assert(same(outc.stderr, outw.stderr)); + assert(has(outw.stderr, + "--: cannot canonicalize package directory\n")); + }; + assert(!os.exists(dashtrailworks[dashstage])); + assert(!os.exists(dashtrailbins[dashstage])); + dashstage += 1; + }; + assert(same(readfile(dashbins[0]), readfile(dashbins[1]))); + let commandrun: []str = [strings.concat(binc, "/cmd-alpha")]; + runcommand(root, "tree-command-run", commandrun, time.second, &outc); + expectexit(&outc, 0); + let nomainpattern: str = strings.concat(commandlib, "/..."); + let nomainc: []str = [driver("ww"), "build", "-o", binc, nomainpattern]; + let nomainw: []str = [driver("ww_ww"), "build", "-o", binw, nomainpattern]; + runcommand(root, "tree-no-main-c", nomainc, time.second, &outc); + runcommand(root, "tree-no-main-ww", nomainw, time.second, &outw); + expectexit(&outc, 1); + expectexit(&outw, 1); + assert(same(outc.stderr, outw.stderr)); + assert(has(outc.stderr, "ww: no main packages to build\n")); + assert(has(seq, strings.concat("ok ", tree, " [rootpkg, same-package]\n"))); - assert(has(outc.stdout, strings.concat("ok ", tree, + assert(has(seq, strings.concat("ok ", tree, "/alpha [alpha_test, external]\n"))); - assert(has(outc.stdout, strings.concat("ok ", tree, + assert(has(seq, strings.concat("ok ", tree, + "/app [app, same-package]\n"))); + assert(has(seq, strings.concat("ok ", tree, "/beta/inner [inner, same-package]\n"))); - assert(has(outc.stdout, strings.concat("? ", tree, + assert(has(seq, strings.concat("ok ", tree, + "/domain/app [app, same-package]\n"))); + assert(has(seq, strings.concat("? ", tree, "/gamma [no tests]\n"))); - let rootreport: i32 = pos(outc.stdout, strings.concat("ok ", tree, + assert(has(seq, strings.concat("? ", tree, + "/vendor [no tests]\n"))); + assert(has(seq, strings.concat("? ", tree, + "/vendorx [no tests]\n"))); + let rootreport: i32 = pos(seq, strings.concat("ok ", tree, " [rootpkg, same-package]\n")); - let alphareport: i32 = pos(outc.stdout, strings.concat("ok ", tree, + let alphareport: i32 = pos(seq, strings.concat("ok ", tree, "/alpha [alpha_test, external]\n")); - let innerreport: i32 = pos(outc.stdout, strings.concat("ok ", tree, + let appreport: i32 = pos(seq, strings.concat("ok ", tree, + "/app [app, same-package]\n")); + let innerreport: i32 = pos(seq, strings.concat("ok ", tree, "/beta/inner [inner, same-package]\n")); - let gammareport: i32 = pos(outc.stdout, strings.concat("? ", tree, + let domainreport: i32 = pos(seq, strings.concat("ok ", tree, + "/domain/app [app, same-package]\n")); + let gammareport: i32 = pos(seq, strings.concat("? ", tree, "/gamma [no tests]\n")); + let vendorreport: i32 = pos(seq, strings.concat("? ", tree, + "/vendor [no tests]\n")); + let vendorxreport: i32 = pos(seq, strings.concat("? ", tree, + "/vendorx [no tests]\n")); assert(rootreport >= 0 && rootreport < alphareport - && alphareport < innerreport && innerreport < gammareport); - assert(occurrences(outc.stdout, strings.concat(tree, " [")) == 1); - assert(!has(outc.stdout, ".hidden")); - assert(!has(outc.stdout, "_skip")); + && alphareport < appreport && appreport < innerreport + && innerreport < domainreport && domainreport < gammareport + && gammareport < vendorreport && vendorreport < vendorxreport); + assert(occurrences(seq, strings.concat(tree, " [")) == 1); + assert(occurrences(seq, strings.concat("ok ", tree, + "/alpha [alpha_test, external]\n")) == 1); + assert(occurrences(seq, strings.concat("ok ", tree, + "/app [app, same-package]\n")) == 1); + assert(occurrences(seq, strings.concat("ok ", tree, + "/beta/inner [inner, same-package]\n")) == 1); + assert(occurrences(seq, strings.concat("ok ", tree, + "/domain/app [app, same-package]\n")) == 1); + assert(occurrences(seq, strings.concat("? ", tree, + "/gamma [no tests]\n")) == 1); + assert(occurrences(seq, strings.concat("? ", tree, + "/vendor [no tests]\n")) == 1); + assert(occurrences(seq, strings.concat("? ", tree, + "/vendorx [no tests]\n")) == 1); + assert(!has(seq, ".hidden")); + assert(!has(seq, "_skip")); + assert(!has(seq, "/testdata")); + assert(!has(seq, "/vendor/bad")); + assert(!has(seq, "/app/vendor/lib/math")); + assert(!has(seq, outside)); + + let buildworkc: str = strings.concat(root, "/build-work-c"); + let buildworkw: str = strings.concat(root, "/build-work-ww"); + let buildc: []str = [driver("ww"), "build", "-w", buildworkc, + "-I", tree, spec]; + let buildw: []str = [driver("ww_ww"), "build", "-w", buildworkw, + "-I", tree, spec]; + runcommand(root, "tree-build-c", buildc, + (30i64 * (time.second: i64)): time.duration, &outc); + runcommand(root, "tree-build-ww", buildw, + (30i64 * (time.second: i64)): time.duration, &outw); + expectexit(&outc, 0); + expectexit(&outw, 0); + assert(same(outc.stdout, outw.stdout)); + assert(same(outc.stderr, outw.stderr)); + let buildactions: []str = [localidentity(tree, "rootpkg"), "alpha", + "app", "domain.app", "gamma", "vendor", "vendorx"]; + let bai: i32 = 0; + for (bai < buildactions.len) { + let cunit: str = readfile(strings.concat(buildworkc, "/", + buildactions[bai], ".unit.ww")); + assert(same(cunit, readfile(strings.concat(buildworkw, "/", + buildactions[bai], ".unit.ww")))); + bai += 1; + }; + assert(!os.exists(strings.concat(buildworkc, + "/beta.inner.unit.ww"))); + assert(!os.exists(strings.concat(buildworkw, + "/beta.inner.unit.ww"))); + assert(!os.exists(strings.concat(buildworkc, + "/alpha-internal-test.unit.ww"))); + assert(!os.exists(strings.concat(buildworkw, + "/alpha-internal-test.unit.ww"))); // Concurrent scheduling must not reorder the emitted byte stream. - let seq: str = strings.dup(outc.stdout); - let j4: []str = [driver("ww"), "test", "-j", "4", spec]; + let j4: []str = [driver("ww"), "test", "-j", "4", "-I", tree, spec]; runcommand(root, "tree-j4", j4, (30i64 * (time.second: i64)): time.duration, &outc); expectexit(&outc, 0); @@ -5152,14 +5913,138 @@ fn hexbytes(value: str) str = { expectexit(&outc, 2); assert(has(outc.stderr, "usage: wwtest package")); - let patc: []str = [driver("ww"), "test", spec, "glob*"]; - let patw: []str = [driver("ww_ww"), "test", spec, "glob*"]; - runcommand(root, "tree-pattern-c", patc, time.second, &outc); - runcommand(root, "tree-pattern-ww", patw, time.second, &outw); - expectexit(&outc, 2); - expectexit(&outw, 2); + let overlapc: []str = [driver("ww"), "test", "-I", tree, + strings.concat(tree, "/alpha"), spec, spec, + strings.concat(alias, "/...")]; + let overlapcreverse: []str = [driver("ww"), "test", "-I", tree, + strings.concat(alias, "/..."), spec, spec, + strings.concat(tree, "/alpha")]; + let overlapw: []str = [driver("ww_ww"), "test", "-I", tree, + strings.concat(alias, "/..."), spec, spec, + strings.concat(tree, "/alpha")]; + let overlapwreverse: []str = [driver("ww_ww"), "test", "-I", tree, + strings.concat(tree, "/alpha"), spec, spec, + strings.concat(alias, "/...")]; + runcommand(root, "tree-overlap-c", overlapc, + (30i64 * (time.second: i64)): time.duration, &outc); + runcommand(root, "tree-overlap-ww", overlapw, + (30i64 * (time.second: i64)): time.duration, &outw); + expectexit(&outc, 0); + expectexit(&outw, 0); + assert(same(outc.stdout, seq)); + assert(same(outw.stdout, seq)); assert(same(outc.stderr, outw.stderr)); - assert(has(outc.stderr, "ww test: pattern needs a single test file\n")); + let overlapstderr: str = strings.dup(outc.stderr); + runcommand(root, "tree-overlap-reverse-c", overlapcreverse, + (30i64 * (time.second: i64)): time.duration, &outc); + runcommand(root, "tree-overlap-reverse-ww", overlapwreverse, + (30i64 * (time.second: i64)): time.duration, &outw); + expectexit(&outc, 0); + expectexit(&outw, 0); + assert(same(outc.stdout, seq)); + assert(same(outw.stdout, seq)); + assert(same(outc.stderr, overlapstderr)); + assert(same(outw.stderr, overlapstderr)); + + // The first ellipsis need not be a path element. With cwd as the implicit + // traversal root, matching uses cleaned relative candidates rather than an + // accidental "./" prefix. + let middlec: []str = [driver("ww"), "test", "-I", tree, "tr.../alpha"]; + let middlew: []str = [driver("ww_ww"), "test", "-I", tree, + "tr.../alpha"]; + runcommanddir(root, "tree-middle-pattern-c", root, middlec, + (30i64 * (time.second: i64)): time.duration, &outc); + runcommanddir(root, "tree-middle-pattern-ww", root, middlew, + (30i64 * (time.second: i64)): time.duration, &outw); + expectexit(&outc, 0); + expectexit(&outw, 0); + assert(same(outc.stdout, outw.stdout)); + assert(same(outc.stderr, outw.stderr)); + assert(has(outc.stdout, strings.concat("ok ", tree, + "/alpha [alpha_test, external]\n"))); + assert(!has(outc.stdout, strings.concat("ok ", tree, + " [rootpkg, same-package]\n"))); + + // The recursive exclusions are traversal rules, not bans on direct roots. + let directc: []str = [driver("ww"), "test", "-I", tree, + strings.concat(tree, "/.hidden"), strings.concat(tree, "/_skip"), + strings.concat(tree, "/testdata/bad")]; + let directw: []str = [driver("ww_ww"), "test", "-I", tree, + strings.concat(tree, "/testdata/bad"), strings.concat(tree, "/_skip"), + strings.concat(tree, "/.hidden")]; + runcommand(root, "tree-direct-exclusions-c", directc, + (30i64 * (time.second: i64)): time.duration, &outc); + runcommand(root, "tree-direct-exclusions-ww", directw, + (30i64 * (time.second: i64)): time.duration, &outw); + expectexit(&outc, 0); + expectexit(&outw, 0); + assert(same(outc.stdout, outw.stdout)); + assert(same(outc.stderr, outw.stderr)); + assert(has(outc.stdout, strings.concat("ok ", tree, + "/.hidden [hid, same-package]\n"))); + assert(has(outc.stdout, strings.concat("ok ", tree, + "/_skip [_skip, same-package]\n"))); + assert(has(outc.stdout, strings.concat("ok ", tree, + "/testdata/bad [bad, same-package]\n"))); + + // A generic wildcard selects a code-bearing terminal vendor directory but + // cannot consume it. Rooting the pattern at vendor selects its descendants; + // a second non-terminal vendor remains a barrier. + let vendorspec: str = strings.concat(tree, "/vendor/..."); + let vendorc: []str = [driver("ww"), "test", "-I", tree, vendorspec]; + let vendorw: []str = [driver("ww_ww"), "test", "-I", tree, vendorspec]; + runcommand(root, "tree-explicit-vendor-c", vendorc, + (30i64 * (time.second: i64)): time.duration, &outc); + runcommand(root, "tree-explicit-vendor-ww", vendorw, + (30i64 * (time.second: i64)): time.duration, &outw); + expectexit(&outc, 0); + expectexit(&outw, 0); + assert(same(outc.stdout, outw.stdout)); + assert(same(outc.stderr, outw.stderr)); + assert(has(outc.stdout, strings.concat("? ", tree, + "/vendor [no tests]\n"))); + assert(has(outc.stdout, strings.concat("ok ", tree, + "/vendor/bad [bad, same-package]\n"))); + assert(!has(outc.stdout, "/vendor/bad/vendor/deep")); + + let vendordirectc: []str = [driver("ww"), "test", "-I", tree, + strings.concat(tree, "/app/vendor/lib/math")]; + let vendordirectw: []str = [driver("ww_ww"), "test", "-I", tree, + strings.concat(tree, "/app/vendor/lib/math")]; + runcommand(root, "tree-direct-below-vendor-c", vendordirectc, + (30i64 * (time.second: i64)): time.duration, &outc); + runcommand(root, "tree-direct-below-vendor-ww", vendordirectw, + (30i64 * (time.second: i64)): time.duration, &outw); + expectexit(&outc, 0); + expectexit(&outw, 0); + assert(same(outc.stdout, outw.stdout)); + assert(same(outc.stderr, outw.stderr)); + assert(has(outc.stdout, strings.concat("? ", tree, + "/app/vendor/lib/math [no tests]\n"))); + let vendorroots: []str = [driver("ww"), "test", "-I", tree, + strings.concat(tree, "/app/vendor/...")]; + runcommand(root, "tree-pattern-below-vendor", vendorroots, + (30i64 * (time.second: i64)): time.duration, &outc); + expectexit(&outc, 0); + assert(has(outc.stdout, strings.concat("? ", tree, + "/app/vendor/lib/math [no tests]\n"))); + + // A child symlink is never traversed. The same symlink is followed when it + // is itself the explicit traversal root, then canonical output names it once. + let escapec: []str = [driver("ww"), "test", "-I", root, + strings.concat(tree, "/escape/...")]; + let escapew: []str = [driver("ww_ww"), "test", "-I", root, + strings.concat(tree, "/escape/...")]; + runcommand(root, "tree-explicit-symlink-root-c", escapec, + (30i64 * (time.second: i64)): time.duration, &outc); + runcommand(root, "tree-explicit-symlink-root-ww", escapew, + (30i64 * (time.second: i64)): time.duration, &outw); + expectexit(&outc, 0); + expectexit(&outw, 0); + assert(same(outc.stdout, outw.stdout)); + assert(same(outc.stderr, outw.stderr)); + assert(has(outc.stdout, strings.concat("ok ", outside, + " [outside, same-package]\n"))); let oc: []str = [driver("ww"), "test", "-o", strings.concat(root, "/stem"), spec]; @@ -5179,10 +6064,35 @@ fn hexbytes(value: str) str = { // byte stream must match the plain run, cold and warm. let wdroot: str = strings.concat(root, "/wd"); assert(os.mkdir(wdroot, 448i32) == 0); - let wdc: []str = [driver("ww"), "test", "-w", wdroot, spec]; - let wdw: []str = [driver("ww_ww"), "test", "-w", wdroot, spec]; + let wdc: []str = [driver("ww"), "test", "-w", wdroot, + "-I", tree, spec]; + let wdw: []str = [driver("ww_ww"), "test", "-w", wdroot, + "-I", tree, spec]; runcommand(root, "tree-w-c", wdc, (30i64 * (time.second: i64)): time.duration, &outc); + let wdkey: str = wdroot; + let expandedmath: str = "app.vendor.lib.math"; + let expandedancestor: str = "domain.vendor.dep"; + assert(os.exists(strings.concat(wdkey, "/", expandedmath, ".unit.ww"))); + assert(os.exists(strings.concat(wdkey, "/", expandedancestor, ".unit.ww"))); + assert(!os.exists(strings.concat(wdkey, "/math.unit.ww"))); + let appunit: str = readfile(strings.concat(wdkey, + "/app-internal-test.unit.ww")); + assert(has(appunit, strings.concat("//ww:import-map lib.math ", + expandedmath, " "))); + assert(has(readfile(strings.concat(wdkey, "/", expandedmath, + ".unit.ww")), strings.concat("//ww:module-reset ", + expandedmath, "\n"))); + let ancestorunit: str = readfile(strings.concat(wdkey, + "/domain.app-internal-test.unit.ww")); + assert(has(ancestorunit, strings.concat("//ww:import-map dep ", + expandedancestor, " "))); + assert(has(readfile(strings.concat(wdkey, "/", expandedancestor, + ".unit.ww")), strings.concat("//ww:module-reset ", + expandedancestor, "\n"))); + let alphaunit: str = readfile(strings.concat(wdkey, "/alpha.unit.ww")); + assert(pos(alphaunit, "export fn value()") + < pos(alphaunit, "export fn linked()")); runcommand(root, "tree-w-ww", wdw, (30i64 * (time.second: i64)): time.duration, &outw); expectexit(&outc, 0); @@ -5195,8 +6105,10 @@ fn hexbytes(value: str) str = { assert(same(outc.stdout, seq)); // -c publishes caller-owned artifacts; a persistent workdir is a // different ownership contract — the coordinator rejects the mix. - let wcc: []str = [driver("ww"), "test", "-c", "-w", wdroot, spec]; - let wcw: []str = [driver("ww_ww"), "test", "-c", "-w", wdroot, spec]; + let wcc: []str = [driver("ww"), "test", "-c", "-w", wdroot, + "-I", tree, spec]; + let wcw: []str = [driver("ww_ww"), "test", "-c", "-w", wdroot, + "-I", tree, spec]; runcommand(root, "tree-wc-c", wcc, time.second, &outc); runcommand(root, "tree-wc-ww", wcw, time.second, &outw); expectexit(&outc, 2); @@ -5205,8 +6117,10 @@ fn hexbytes(value: str) str = { assert(has(outc.stderr, "wwtest package: -w conflicts with -c\n")); let bare: str = strings.concat(root, "/bare"); + let barez: str = strings.concat(root, "/zz-bare"); assert(os.mkdir(bare, 448i32) == 0); assert(os.mkdir(strings.concat(bare, "/sub"), 448i32) == 0); + assert(os.mkdir(barez, 448i32) == 0); let barec: []str = [driver("ww"), "test", strings.concat(bare, "/...")]; let barew: []str = [driver("ww_ww"), "test", @@ -5218,7 +6132,357 @@ fn hexbytes(value: str) str = { expectexit(&outc, 1); expectexit(&outw, 1); assert(same(outc.stderr, outw.stderr)); + assert(has(outc.stderr, strings.concat("ww: warning: \"", bare, + "/...\" matched no packages\n"))); + assert(has(outc.stderr, "ww test: no packages to test\n")); + let baredirectc: []str = [driver("ww"), "test", bare]; + let baredirectw: []str = [driver("ww_ww"), "test", bare]; + runcommand(root, "tree-bare-direct-c", baredirectc, time.second, &outc); + runcommand(root, "tree-bare-direct-ww", baredirectw, time.second, &outw); + expectexit(&outc, 1); + expectexit(&outw, 1); + assert(same(outc.stderr, outw.stderr)); assert(has(outc.stderr, "directory contains no WW package sources\n")); + let quotedpattern: str = strings.concat(root, "/no\"..."); + let quotedc: []str = [driver("ww"), "test", quotedpattern]; + let quotedw: []str = [driver("ww_ww"), "test", quotedpattern]; + runcommand(root, "tree-quoted-unmatched-c", quotedc, time.second, &outc); + runcommand(root, "tree-quoted-unmatched-ww", quotedw, time.second, &outw); + expectexit(&outc, 1); + expectexit(&outw, 1); + assert(same(outc.stderr, outw.stderr)); + assert(has(outc.stderr, strings.concat("ww: warning: \"", root, + "/no\\\"...\" matched no packages\n"))); + // strconv.Quote escapes valid but non-printing Unicode, not only malformed + // bytes and ASCII controls. U+2028 is a stable discriminator. + let nonprintingbytes: []u8 = alloc([], (root.len + 8): u64)!; + let qi: i32 = 0; + for (qi < root.len) { append(nonprintingbytes, root[qi]); qi += 1; }; + append(nonprintingbytes, '/': u8); append(nonprintingbytes, 'n': u8); + append(nonprintingbytes, 0xe2u8); append(nonprintingbytes, 0x80u8); + append(nonprintingbytes, 0xa8u8); append(nonprintingbytes, '.': u8); + append(nonprintingbytes, '.': u8); append(nonprintingbytes, '.': u8); + let nonprintingpattern: str = strings.frombytes(nonprintingbytes); + let nonprintingc: []str = [driver("ww"), "test", nonprintingpattern]; + let nonprintingw: []str = [driver("ww_ww"), "test", nonprintingpattern]; + runcommand(root, "tree-nonprinting-unmatched-c", nonprintingc, + time.second, &outc); + runcommand(root, "tree-nonprinting-unmatched-ww", nonprintingw, + time.second, &outw); + expectexit(&outc, 1); + expectexit(&outw, 1); + assert(same(outc.stderr, outw.stderr)); + assert(has(outc.stderr, "\\u2028...\" matched no packages\n")); + let mixedc: []str = [driver("ww"), "test", "-I", tree, + strings.concat(barez, "/..."), spec, strings.concat(bare, "/...")]; + let mixedw: []str = [driver("ww_ww"), "test", "-I", tree, + strings.concat(bare, "/..."), spec, strings.concat(barez, "/...")]; + runcommand(root, "tree-unmatched-order-c", mixedc, + (30i64 * (time.second: i64)): time.duration, &outc); + runcommand(root, "tree-unmatched-order-ww", mixedw, + (30i64 * (time.second: i64)): time.duration, &outw); + expectexit(&outc, 0); + expectexit(&outw, 0); + assert(same(outc.stdout, seq)); + assert(same(outw.stdout, seq)); + assert(same(outc.stderr, outw.stderr)); + assert(has(outc.stderr, strings.concat("ww: warning: \"", bare, + "/...\" matched no packages\n"))); + let barewarning: str = strings.concat("ww: warning: \"", bare, + "/...\" matched no packages\n"); + let barezwarning: str = strings.concat("ww: warning: \"", barez, + "/...\" matched no packages\n"); + assert(pos(outc.stderr, barewarning) < pos(outc.stderr, barezwarning)); + + // Pinned pkgpattern rejects invalid UTF-8 patterns before regexp + // construction, so they match nothing and receive the ordinary quoted + // warning. Candidate names still use regexp's DecodeRune behavior. + let invalidpatternbytes: []u8 = alloc([], (root.len + 6): u64)!; + let mpi: i32 = 0; + for (mpi < root.len) { + append(invalidpatternbytes, root[mpi]); mpi += 1; + }; + append(invalidpatternbytes, '/': u8); append(invalidpatternbytes, '.': u8); + append(invalidpatternbytes, '.': u8); append(invalidpatternbytes, '.': u8); + append(invalidpatternbytes, 0xffu8); + let invalidpattern: str = strings.frombytes(invalidpatternbytes); + let invalidpatternc: []str = [driver("ww"), "test", invalidpattern]; + let invalidpatternw: []str = [driver("ww_ww"), "test", invalidpattern]; + runcommand(root, "tree-invalid-pattern-c", invalidpatternc, + time.second, &outc); + runcommand(root, "tree-invalid-pattern-ww", invalidpatternw, + time.second, &outw); + expectexit(&outc, 1); + expectexit(&outw, 1); + assert(same(outc.stderr, outw.stderr)); + assert(has(outc.stderr, "...\\xff\" matched no packages\n")); + + // U+FFFD in a valid pattern matches a malformed candidate filename byte as + // regexp does, while the wildcard below does not consume a newline. + let utfbase: str = strings.concat(root, "/utf-names"); + assert(os.mkdir(utfbase, 448i32) == 0); + let invalidnamebytes: []u8 = alloc([], (utfbase.len + 2): u64)!; + mpi = 0; + for (mpi < utfbase.len) { + append(invalidnamebytes, utfbase[mpi]); mpi += 1; + }; + append(invalidnamebytes, '/': u8); append(invalidnamebytes, 0xffu8); + let invalidname: str = strings.frombytes(invalidnamebytes); + assert(os.mkdir(invalidname, 448i32) == 0); + writefile(strings.concat(invalidname, "/case_test.ww"), strings.concat( + "package invalidname;\n@test fn replacement_match() void = {", + " assert(true); };\n")); + let replacementbytes: []u8 = alloc([], (utfbase.len + 8): u64)!; + mpi = 0; + for (mpi < utfbase.len) { + append(replacementbytes, utfbase[mpi]); mpi += 1; + }; + append(replacementbytes, '/': u8); append(replacementbytes, 0xefu8); + append(replacementbytes, 0xbfu8); append(replacementbytes, 0xbdu8); + append(replacementbytes, '.': u8); append(replacementbytes, '.': u8); + append(replacementbytes, '.': u8); + let replacementpattern: str = strings.frombytes(replacementbytes); + let replacementc: []str = [driver("ww"), "test", replacementpattern]; + let replacementw: []str = [driver("ww_ww"), "test", replacementpattern]; + runcommand(root, "tree-replacement-pattern-c", replacementc, + (30i64 * (time.second: i64)): time.duration, &outc); + runcommand(root, "tree-replacement-pattern-ww", replacementw, + (30i64 * (time.second: i64)): time.duration, &outw); + expectexit(&outc, 0); + expectexit(&outw, 0); + assert(same(outc.stdout, outw.stdout)); + assert(same(outc.stderr, outw.stderr)); + assert(has(outc.stdout, "invalidname.replacement_match ... ok\n")); + + let newlinebase: str = strings.concat(root, "/newline-names"); + assert(os.mkdir(newlinebase, 448i32) == 0); + let newlinedir: str = strings.concat(newlinebase, "/line\nbreak"); + assert(os.mkdir(newlinedir, 448i32) == 0); + writefile(strings.concat(newlinedir, "/case_test.ww"), + "package newline;\n@test fn must_not_match() void = { assert(false); };\n"); + let newlinepattern: str = strings.concat(newlinebase, "/line...break"); + let newlinec: []str = [driver("ww"), "test", newlinepattern]; + let newlinew: []str = [driver("ww_ww"), "test", newlinepattern]; + runcommand(root, "tree-newline-pattern-c", newlinec, time.second, &outc); + runcommand(root, "tree-newline-pattern-ww", newlinew, time.second, &outw); + expectexit(&outc, 1); + expectexit(&outw, 1); + assert(same(outc.stderr, outw.stderr)); + assert(has(outc.stderr, "line...break\" matched no packages\n")); + let barebuildc: []str = [driver("ww"), "build", + strings.concat(bare, "/...")]; + let barebuildw: []str = [driver("ww_ww"), "build", + strings.concat(bare, "/...")]; + runcommand(root, "tree-empty-build-c", barebuildc, time.second, &outc); + runcommand(root, "tree-empty-build-ww", barebuildw, time.second, &outw); + expectexit(&outc, 0); + expectexit(&outw, 0); + assert(same(outc.stdout, outw.stdout)); + assert(same(outc.stderr, outw.stderr)); + assert(has(outc.stderr, "matched no packages\n")); + + // A wildcard recognizes a test-only directory, so it is not unmatched; + // build then omits it while test keeps its isolated variant. Direct build + // remains an explicit unusable-package error. + let testonly: str = strings.concat(tree, "/beta/..."); + let testonlyc: []str = [driver("ww"), "build", "-I", tree, testonly]; + let testonlyw: []str = [driver("ww_ww"), "build", "-I", tree, testonly]; + runcommand(root, "tree-test-only-build-c", testonlyc, time.second, &outc); + runcommand(root, "tree-test-only-build-ww", testonlyw, time.second, &outw); + expectexit(&outc, 0); + expectexit(&outw, 0); + assert(outc.stdout.len == 0 && outc.stderr.len == 0); + assert(same(outc.stdout, outw.stdout)); + assert(same(outc.stderr, outw.stderr)); + let testdirectc: []str = [driver("ww"), "build", + strings.concat(tree, "/beta/inner")]; + let testdirectw: []str = [driver("ww_ww"), "build", + strings.concat(tree, "/beta/inner")]; + runcommand(root, "tree-test-only-direct-c", testdirectc, time.second, &outc); + runcommand(root, "tree-test-only-direct-ww", testdirectw, time.second, &outw); + expectexit(&outc, 1); + expectexit(&outw, 1); + assert(same(outc.stderr, outw.stderr)); + assert(has(outc.stderr, "directory contains no WW package sources\n")); + + let malformed: str = strings.concat(root, "/malformed"); + let malformedgood: str = strings.concat(malformed, "/good"); + let malformedbad: str = strings.concat(malformed, "/bad"); + assert(os.mkdir(malformed, 448i32) == 0); + assert(os.mkdir(malformedgood, 448i32) == 0); + assert(os.mkdir(malformedbad, 448i32) == 0); + writefile(strings.concat(malformedgood, "/good.ww"), + "package good;\nexport fn value() int = { return 1; };\n"); + let loop: str = strings.concat(root, "/loop"); + let loopav: []str = ["/bin/ln", "-s", loop, loop]; + runcommand(root, "recursive-root-loop", loopav, time.second, &linkout); + expectexit(&linkout, 0); + let rejecttrace: str = strings.concat(root, "/pattern-reject.trace"); + let rejectcompiler: str = strings.concat(root, "/pattern-reject-w6c.sh"); + writefile(rejecttrace, ""); + writeexecutable(rejectcompiler, strings.concat( + "#!/bin/sh\nprintf 'compiler\\n' >> \"$WW_PATTERN_REJECT_TRACE\"\n", + "exit 99\n")); + let baseenv: []str = os.getenvs(); + let rejectenv: []str = alloc([], (baseenv.len + 2): u64)!; + let ei: i32 = 0; + for (ei < baseenv.len) { + if (!strings.hasprefix(baseenv[ei], "WW_W6C=") + && !strings.hasprefix(baseenv[ei], "WW_PATTERN_REJECT_TRACE=")) { + append(rejectenv, baseenv[ei]); + }; + ei += 1; + }; + append(rejectenv, strings.concat("WW_W6C=", rejectcompiler)); + append(rejectenv, strings.concat("WW_PATTERN_REJECT_TRACE=", rejecttrace)); + let rejectspec: str = strings.concat(malformed, "/..."); + let warmworkc: str = strings.concat(root, "/warm-reject-work-c"); + let warmworkw: str = strings.concat(root, "/warm-reject-work-ww"); + let warmc: []str = [driver("ww"), "build", "-w", warmworkc, + "-I", malformed, rejectspec]; + let warmw: []str = [driver("ww_ww"), "build", "-w", warmworkw, + "-I", malformed, rejectspec]; + // Seed genuine committed package, voucher, and tool state before making a + // second selected directory malformed. + runcommand(root, "tree-malformed-seed-c", warmc, + (30i64 * (time.second: i64)): time.duration, &outc); + runcommand(root, "tree-malformed-seed-ww", warmw, + (30i64 * (time.second: i64)): time.duration, &outw); + expectexit(&outc, 0); + expectexit(&outw, 0); + assert(same(outc.stdout, outw.stdout)); + assert(same(outc.stderr, outw.stderr)); + let warmsuffixes: []str = ["/good.unit.ww", "/good.wwi", "/good.s", + "/good.o", "/good.a", "/.wwtool.stamp", "/.wwtool.ww", + "/.wwtool.w6c", "/.wwtool.w6a"]; + let warmcstate: []str = alloc([], warmsuffixes.len: u64)!; + let warmwstate: []str = alloc([], warmsuffixes.len: u64)!; + let wi: i32 = 0; + for (wi < warmsuffixes.len) { + append(warmcstate, readfile(strings.concat(warmworkc, + warmsuffixes[wi]))); + append(warmwstate, readfile(strings.concat(warmworkw, + warmsuffixes[wi]))); + wi += 1; + }; + writefile(strings.concat(warmworkc, "/marker"), "c-warm\n"); + writefile(strings.concat(warmworkw, "/marker"), "ww-warm\n"); + writefile(strings.concat(malformedbad, "/bad.ww"), + "this selected source has no package clause\n"); + + let rejectworkc: str = strings.concat(root, "/cold-reject-work-c"); + let rejectworkw: str = strings.concat(root, "/cold-reject-work-ww"); + let rejectc: []str = [driver("ww"), "build", "-w", rejectworkc, + "-I", malformed, rejectspec]; + let rejectw: []str = [driver("ww_ww"), "build", "-w", rejectworkw, + "-I", malformed, rejectspec]; + runcommandenv(root, "tree-malformed-c", rejectc, rejectenv, + time.second, &outc); + runcommandenv(root, "tree-malformed-ww", rejectw, rejectenv, + time.second, &outw); + expectexit(&outc, 1); + expectexit(&outw, 1); + assert(same(outc.stderr, outw.stderr)); + assert(has(outc.stderr, "invalid or missing package clause\n")); + assert(readfile(rejecttrace).len == 0); + assert(!os.exists(rejectworkc) && !os.exists(rejectworkw)); + // Warm rejection preserves all previously committed package, voucher, + // tool, and caller-owned marker bytes while still reaching no producer. + runcommandenv(root, "tree-malformed-warm-c", warmc, rejectenv, + time.second, &outc); + runcommandenv(root, "tree-malformed-warm-ww", warmw, rejectenv, + time.second, &outw); + expectexit(&outc, 1); + expectexit(&outw, 1); + assert(same(outc.stderr, outw.stderr)); + assert(has(outc.stderr, "invalid or missing package clause\n")); + assert(readfile(rejecttrace).len == 0); + assert(same(readfile(strings.concat(warmworkc, "/marker")), "c-warm\n")); + assert(same(readfile(strings.concat(warmworkw, "/marker")), "ww-warm\n")); + wi = 0; + for (wi < warmsuffixes.len) { + assert(same(warmcstate[wi], readfile(strings.concat(warmworkc, + warmsuffixes[wi])))); + assert(same(warmwstate[wi], readfile(strings.concat(warmworkw, + warmsuffixes[wi])))); + wi += 1; + }; + + // A valid package clause that fails during source-import graph loading is + // also rejected before private work/output directories are materialized. + let graphbad: str = strings.concat(root, "/graph-bad"); + let graphapp: str = strings.concat(graphbad, "/app"); + assert(os.mkdir(graphbad, 448i32) == 0); + assert(os.mkdir(graphapp, 448i32) == 0); + writefile(strings.concat(graphapp, "/main.ww"), + "package main;\nimport nowhere;\nfn main() void = { };\n"); + let graphspec: str = strings.concat(graphbad, "/..."); + let graphworkc: str = strings.concat(root, "/graph-work-c"); + let graphworkw: str = strings.concat(root, "/graph-work-ww"); + let graphoutc: str = strings.concat(root, "/graph-out-c/"); + let graphoutw: str = strings.concat(root, "/graph-out-ww/"); + let graphc: []str = [driver("ww"), "build", "-w", graphworkc, + "-o", graphoutc, "-I", graphbad, graphspec]; + let graphw: []str = [driver("ww_ww"), "build", "-w", graphworkw, + "-o", graphoutw, "-I", graphbad, graphspec]; + runcommandenv(root, "tree-graph-reject-c", graphc, rejectenv, + time.second, &outc); + runcommandenv(root, "tree-graph-reject-ww", graphw, rejectenv, + time.second, &outw); + expectexit(&outc, 1); + expectexit(&outw, 1); + assert(same(outc.stderr, outw.stderr)); + assert(has(outc.stderr, "cannot find package nowhere\n")); + assert(readfile(rejecttrace).len == 0); + assert(!os.exists(graphworkc) && !os.exists(graphworkw)); + assert(!os.exists(graphoutc) && !os.exists(graphoutw)); + // If output creation succeeds but a later workdir prefix is unusable, the + // driver reclaims only the output prefixes it just minted. Both stages make + // the same pre-producer failure and leave the caller tree unchanged. + let setupblock: str = strings.concat(root, "/setup-block"); + let setupwork: str = strings.concat(setupblock, "/child"); + let setupoutroot: str = strings.concat(root, "/setup-output"); + let setuppreserved: str = strings.concat(root, "/setup-preserved"); + assert(os.mkdir(setuppreserved, 448i32) == 0); + let setupout: str = strings.concat(setupoutroot, + "/../setup-preserved/nested/"); + writefile(setupblock, "not a directory\n"); + let setupc: []str = [driver("ww"), "build", "-w", setupwork, + "-o", setupout, "-I", commands, commandspec]; + let setupw: []str = [driver("ww_ww"), "build", "-w", setupwork, + "-o", setupout, "-I", commands, commandspec]; + runcommandenv(root, "tree-setup-rollback-c", setupc, rejectenv, + time.second, &outc); + expectexit(&outc, 1); + assert(has(outc.stderr, strings.concat("ww: workdir ", setupwork, + " is not a directory\n"))); + assert(readfile(rejecttrace).len == 0); + assert(!os.exists(setupoutroot)); + assert(os.exists(setuppreserved)); + assert(!os.exists(strings.concat(setuppreserved, "/nested"))); + assert(!os.exists(setupwork)); + assert(same(readfile(setupblock), "not a directory\n")); + runcommandenv(root, "tree-setup-rollback-ww", setupw, rejectenv, + time.second, &outw); + expectexit(&outw, 1); + assert(same(outc.stderr, outw.stderr)); + assert(readfile(rejecttrace).len == 0); + assert(!os.exists(setupoutroot)); + assert(os.exists(setuppreserved)); + assert(!os.exists(strings.concat(setuppreserved, "/nested"))); + assert(!os.exists(setupwork)); + assert(same(readfile(setupblock), "not a directory\n")); + let loopc: []str = [driver("ww"), "test", + strings.concat(loop, "/...")]; + let loopw: []str = [driver("ww_ww"), "test", + strings.concat(loop, "/...")]; + runcommandenv(root, "tree-loop-c", loopc, rejectenv, time.second, &outc); + runcommandenv(root, "tree-loop-ww", loopw, rejectenv, time.second, &outw); + expectexit(&outc, 1); + expectexit(&outw, 1); + assert(same(outc.stderr, outw.stderr)); + assert(has(outc.stderr, "cannot canonicalize package directory\n")); + assert(readfile(rejecttrace).len == 0); clean(root); }; @@ -5248,6 +6512,7 @@ fn hexbytes(value: str) str = { writefile(strings.concat(b, "/foo_test.ww"), "package foo;\n@test fn second() void = { assert(value() == 2); };\n"); let av: []str = [driver("ww"), "test", "-j", "2", "-w", workc, + "-I", tree, strings.concat(tree, "/...")]; let out: commandout; runcommand(root, "workkey-cold", av, @@ -5258,9 +6523,8 @@ fn hexbytes(value: str) str = { " [foo, same-package]\n"))); assert(has(cold, strings.concat("ok ", b, " [foo, same-package]\n"))); - let ckey: str = strings.concat(workc, "/", requestworkkey(tree)); + let ckey: str = workc; assert(os.exists(ckey)); - assert(requestworkkey(tree).len == 66); let aaction: str = "a.foo-internal-test"; let baction: str = "b.foo-internal-test"; assert(os.exists(strings.concat(ckey, "/", aaction, ".unit.ww"))); @@ -5278,12 +6542,13 @@ fn hexbytes(value: str) str = { expectexit(&out, 0); assert(same(cold, out.stdout)); let wwav: []str = [driver("ww_ww"), "test", "-j", "1", "-w", workww, + "-I", tree, strings.concat(tree, "/...")]; runcommand(root, "workkey-ww", wwav, (60i64 * (time.second: i64)): time.duration, &out); expectexit(&out, 0); assert(same(cold, out.stdout)); - let wwkey: str = strings.concat(workww, "/", requestworkkey(tree)); + let wwkey: str = workww; assert(same(aunit, readfile(strings.concat(wwkey, "/", aaction, ".unit.ww")))); assert(same(bunit, readfile(strings.concat(wwkey, "/", baction, @@ -5292,17 +6557,17 @@ fn hexbytes(value: str) str = { assert(same(bwwi, readfile(strings.concat(wwkey, "/", baction, ".wwi")))); assert(same(aa, readfile(strings.concat(wwkey, "/", aaction, ".a")))); assert(same(ba, readfile(strings.concat(wwkey, "/", baction, ".a")))); - // The canonical coordinator key and warm action set are unchanged by an + // The shared semantic-action store and warm action set are unchanged by an // equivalent recursive-root spelling. let equivalentav: []str = [driver("ww_ww"), "test", "-j", "2", "-w", - workww, strings.concat(tree, "/./...")]; + workww, "-I", tree, strings.concat(tree, "/./...")]; runcommand(root, "workkey-equivalent", equivalentav, (60i64 * (time.second: i64)): time.duration, &out); expectexit(&out, 0); - assert(has(out.stdout, strings.concat("ok ", tree, - "/./a/foo [foo, same-package]\n"))); - assert(has(out.stdout, strings.concat("ok ", tree, - "/./b/foo [foo, same-package]\n"))); + assert(has(out.stdout, strings.concat("ok ", a, + " [foo, same-package]\n"))); + assert(has(out.stdout, strings.concat("ok ", b, + " [foo, same-package]\n"))); assert(os.exists(wwkey)); assert(same(aunit, readfile(strings.concat(wwkey, "/", aaction, ".unit.ww")))); @@ -6416,8 +7681,7 @@ fn runtimepath(relative: str) str = { assert(same(referencetestout, out.stdout)); assert(same(referencetesterr, out.stderr)); }; - let testwork: str = strings.concat(testworkroot, "/", - requestworkkey(testclient), "/"); + let testwork: str = strings.concat(testworkroot, "/"); let testactions: []str = ["domain.app.testclient-internal-test", "domain.app.testclient_test-external-test"]; let testsuffixes: []str = [".unit.ww", ".wwi", ".a"]; @@ -6495,8 +7759,8 @@ fn runtimepath(relative: str) str = { assert(has(readfile(compilertraces[si]), "literal.owner.vendor.lib.math.vendor.lib.math.unit.ww")); - // A cold forbidden public test request is rejected before the - // coordinator leaves its hashed persistent request directory behind. + // A cold forbidden public test request is rejected before any + // persistent tool or package state is written. let testbadworkroot: str = strings.concat(root, "/vendor-test-bad-work-", stages[si]); assert(os.mkdir(testbadworkroot, 448i32) == 0); @@ -6513,8 +7777,7 @@ fn runtimepath(relative: str) str = { assert(has(out.stderr, testbaddiag)); if (si == 0) { referencetestbaddiag = strings.dup(out.stderr); } else { assert(same(referencetestbaddiag, out.stderr)); }; - assert(!os.exists(strings.concat(testbadworkroot, "/", - requestworkkey(testbad)))); + assert(!os.exists(strings.concat(testbadworkroot, "/.wwtool.stamp"))); assert(readfile(compilertraces[si]).len == 0); assert(readfile(assemblertraces[si]).len == 0); assert(readfile(linkertraces[si]).len == 0); @@ -6541,8 +7804,7 @@ fn runtimepath(relative: str) str = { assert(out.stderr.len == 0); if (si == 0) { referencesupportout = strings.dup(out.stdout); } else { assert(same(referencesupportout, out.stdout)); }; - let supportwork: str = strings.concat(supportworkroot, "/", - requestworkkey(supportcase), "/"); + let supportwork: str = strings.concat(supportworkroot, "/"); let supportunit: str = readfile(strings.concat(supportwork, "test.unit.ww")); assert(has(supportunit, strings.concat("//ww:import-map fnmatch ",