From 02574c88f1b6b70923a4a478ad18c46631eea2ed Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Wed, 12 Aug 2026 11:42:29 +0900 Subject: [PATCH] test: cover shared directory test build plans --- docs/build-system.md | 82 ++++++--- test/package/package_test.ww | 316 +++++++++++++++++++++++++++++------ 2 files changed, 326 insertions(+), 72 deletions(-) diff --git a/docs/build-system.md b/docs/build-system.md index f1bee5ea..d82e1166 100644 --- a/docs/build-system.md +++ b/docs/build-system.md @@ -2849,11 +2849,16 @@ has one test variant; the coordinator rejects one output name for a multi-varian or recursive request. Explicit `ww test FILE` retains its compatibility path. The test coordinator still discovers requested directories, enumerates the -test package names, schedules independent binaries, executes them, and emits +test package names, selects variants, executes independent binaries, and emits captured results in byte-sorted package order. It no longer concatenates a -generated production/test root or resolves imports. Instead it asks the Cstage -or WWstage command to build one of two non-importable root variants from the -real directory: +generated production/test root, resolves imports, or starts one package graph +per binary. Instead it sends one ordered build request per directory to the +Cstage or WWstage command. Its semantic selections are only the directory and +the selected variant/package identities; it also carries output destinations, +coordinator-private completion paths, import search roots, and the optional +shared work-directory policy. The command owns source selection, +package loading, compiler inputs, archive construction, and linking for both +non-importable roots: - `same-test` selects the byte-sorted production files followed by the byte-sorted matching `package p` test files. They form one compiler unit, so @@ -2863,32 +2868,55 @@ real directory: directory. That node selects every production file, emits `p.wwi` and `p.a`, and exposes no private declaration to the external root. +The command loads those roots into one command-scoped package universe. The +roots retain distinct artifact keys (`__ww-test-same` and +`__ww-test-external`) that cannot collide with a legal import identity, +while imports of the same canonical production directory intern to one +production node. A deterministic dependency-first traversal of the union +therefore invokes the compiler and archiver once for every reachable canonical +production package, even when both test products need it. Each root is still +compiled once with its own selected sources and linked separately. The shared +plan is deliberately package-test-specific: it is not a generalized scheduler, +action schema, cache, or protocol. + +A production action failure blocks exactly the roots that reach it. A +root-local compile or link failure does not suppress a successfully built +sibling product: the command records completion per product, and the +coordinator can run and report successful siblings while attributing each +missing product as a build failure. Directory builds and completed test +products share the coordinator's existing `-j` process bound; captured output +is still emitted only in byte-sorted directory/package order. + Every non-root dependency is always a production variant, so dependency `*_test.ww` files never enter the graph. Imports that occur only in selected test files add edges only to that test root. The compiler unit for each package contains only its byte-sorted direct dependency `.wwi` artifacts; the final test link still receives the root object and the complete reverse-topological archive closure. The compiler-generated `-T` dispatcher owns the implicit -direct test-runtime support edge and remains embedded in each independent test -root. The command resolves that edge from the selected toolchain source tree, -not the user search path. Normally its graph qualifier is `test`, so an explicit -source `import test` coalesces with the same canonical package. When a real user -package occupies that identity, the command presents the runtime edge to the -compiler under the reserved `__wwtest` qualifier. This keeps a production -package named `test` available to external tests while preserving raw `w6c -T` -compatibility, whose default unresolved qualifier remains `test`. +direct test-runtime support edge and remains embedded in each independently +compiled test root; it is the narrow test-main variant, not a +coordinator-generated graph package. The shared plan compiles the common +runtime production package once. The command resolves that edge from the +selected toolchain source tree, not the user search path. Normally its graph +qualifier is `test`, so an explicit source `import test` coalesces with the same +canonical package. When a real user package occupies that identity, the command +presents the runtime edge to the compiler under the reserved `__wwtest` +qualifier. This keeps a production package named `test` available to external +tests while preserving raw `w6c -T` compatibility, whose default unresolved +qualifier remains `test`. -The selected test root and a production variant reached by its imports or test -runtime closure are the only sanctioned pair of graph nodes that may share a +The selected test roots and a production variant reached by their imports or +test-runtime closure are the only sanctioned graph nodes that may share a physical directory. This also lets a toolchain package's own tests coexist with the production variant required by the test runtime. A same-package root -already defines those production symbols, so the duplicate production archive -is omitted from its final link while that node's dependency archives remain in -the closure. All ordinary logical and physical package-identity collision -checks remain unchanged. Recursive -discovery groups by physical directory before sorting filenames, and persistent -coordinator work directories use an injective escaped `(directory, test -package)` key. Every `*_test.ww` package variant is built even when a file only +already defines those production symbols, so the production archive is omitted +from that product's final link while the production node's dependency archives +remain in its closure. The external product includes the production archive. +Variant-only archives are never linked into the other product. All ordinary +logical and physical package-identity collision checks remain unchanged. +Recursive discovery groups by physical directory before sorting filenames, and +persistent coordinator work directories use one injective escaped directory +plan key. Every `*_test.ww` package variant is built even when a file only declares helpers and contains no `@test`, so its package clause and imports are still checked by the shared loader. Every built variant is run, and a successful compiler-owned dispatcher with empty output is reported as `[no tests]`. @@ -2896,9 +2924,15 @@ compiler-owned dispatcher with empty output is reported as `[no tests]`. This ownership split follows Go 1.26.5's separation of production, same-package-test, external-test, and generated test-main inputs in [`cmd/go/internal/load/test.go`](https://go.googlesource.com/go/+/refs/tags/go1.26.5/src/cmd/go/internal/load/test.go), -while retaining WW's compiler-owned dispatcher. Direct compile dependencies and -the separately expanded link closure follow the boundary in -[`cmd/go/internal/work/action.go`](https://go.googlesource.com/go/+/refs/tags/go1.26.5/src/cmd/go/internal/work/action.go). +while retaining WW's compiler-owned dispatcher. Go's loader returns the same +cached package pointer for repeated imports, and its command-scoped builder +deduplicates compile actions by package identity; WW borrows that one-universe, +multi-root ownership boundary without borrowing Go's cache machinery. Direct +compile dependencies and the separately expanded link closure follow the +boundary in +[`cmd/go/internal/work/action.go`](https://go.googlesource.com/go/+/refs/tags/go1.26.5/src/cmd/go/internal/work/action.go), +with compiler and linker import configurations emitted separately in +[`cmd/go/internal/work/exec.go`](https://go.googlesource.com/go/+/refs/tags/go1.26.5/src/cmd/go/internal/work/exec.go). ## 12. Candidate architectures and hard-gate decision diff --git a/test/package/package_test.ww b/test/package/package_test.ww index d57291f4..c6db9540 100644 --- a/test/package/package_test.ww +++ b/test/package/package_test.ww @@ -71,6 +71,17 @@ fn writefile(path: str, content: str) void = { assert(os.close(fd) == 0); }; +fn writeexecutable(path: str, content: str) void = { + let fd: i32 = os.open(path, + os.flag.WRONLY | os.flag.CREATE | os.flag.EXCL, 448i32); + assert(fd >= 0); + match (os.writeall(fd, content.ptr, content.len: u64)) { + case let n: i64 => assert(n == content.len: i64); + case let e: os.oserror => abort("write failed"); + }; + assert(os.close(fd) == 0); +}; + fn runcommand(root: str, name: str, argv: []str, lifetime: time.duration, out: *commandout) void = { let c: exec.command; @@ -169,6 +180,22 @@ fn occurrences(haystack: str, needle: str) i32 = { return count; }; +fn linecontaining(text: str, needle: str) str = { + let start: i32 = 0; + let i: i32 = 0; + for (i <= text.len) { + if (i == text.len || text[i] == '\n': u8) { + let line: str; + line.ptr = text.ptr + (start: u64); + line.len = i - start; + if (has(line, needle)) { return line; }; + start = i + 1; + }; + i += 1; + }; + abort("missing trace line"); +}; + fn expectexit(out: *commandout, code: i32) void = { assert(out.termination == exec.termination.EXIT); assert(out.code == code); @@ -617,14 +644,13 @@ fn workescape(s: str) str = { assert(has(outc.stdout, strings.concat(" -> ", root, "/route_test.test\n"))); let cwhite: str = readfile(strings.concat(root, - "/route.test.sepwork/__root.s")); + "/route.test.sepwork/__ww-test-same.s")); let cexternal: str = readfile(strings.concat(root, - "/route_test.test.sepwork/__root.s")); + "/route.test.sepwork/__ww-test-external.s")); // The explicit package `-c` outputs are caller-owned artifacts. Release - // the two exact C-stage trees before asking the WW driver to acquire the - // same stems; the driver never deletes a pre-existing `.sepwork` path. + // the shared C-stage tree before asking the WW driver to acquire the same + // stem; the driver never deletes a pre-existing `.sepwork` path. clean(strings.concat(root, "/route.test.sepwork")); - clean(strings.concat(root, "/route_test.test.sepwork")); let wc: []str = [driver("ww_ww"), "test", "-c", root]; runcommand(root, "compile-ww", wc, @@ -633,9 +659,9 @@ fn workescape(s: str) str = { assert(same(outc.stdout, outw.stdout)); assert(same(outc.stderr, outw.stderr)); assert(same(cwhite, readfile(strings.concat(root, - "/route.test.sepwork/__root.s")))); + "/route.test.sepwork/__ww-test-same.s")))); assert(same(cexternal, readfile(strings.concat(root, - "/route_test.test.sepwork/__root.s")))); + "/route.test.sepwork/__ww-test-external.s")))); clean(root); }; @@ -643,8 +669,8 @@ fn workescape(s: str) str = { let root: str = fresh(); let implementation: str = strings.concat(root, "/implementation"); let api: str = strings.concat(root, "/api"); - let testonly: str = strings.concat(root, "/testonly"); - let externalonly: str = strings.concat(root, "/externalonly"); + let testonly: str = strings.concat(root, "/__same"); + let externalonly: str = strings.concat(root, "/__external"); let pkg: str = strings.concat(root, "/pkg"); assert(os.mkdir(implementation, 448i32) == 0); assert(os.mkdir(api, 448i32) == 0); @@ -662,11 +688,11 @@ fn workescape(s: str) str = { writefile(strings.concat(api, "/api_test.ww"), strings.concat( "package api;\n", "TEST_DEPENDENCY_MUST_NOT_COMPILE\n")); - writefile(strings.concat(testonly, "/testonly.ww"), strings.concat( - "package testonly;\n", + writefile(strings.concat(testonly, "/__same.ww"), strings.concat( + "package __same;\n", "export fn value() i32 = { return 1; };\n")); - writefile(strings.concat(externalonly, "/externalonly.ww"), strings.concat( - "package externalonly;\n", + writefile(strings.concat(externalonly, "/__external.ww"), strings.concat( + "package __external;\n", "export fn value() i32 = { return 1; };\n")); writefile(strings.concat(pkg, "/a.ww"), strings.concat( "package pkg;\nimport api;\n", @@ -678,16 +704,16 @@ fn workescape(s: str) str = { "fn secret() i32 = { return 6; };\n", "export fn second() i32 = { return 9; };\n")); writefile(strings.concat(pkg, "/same_test.ww"), strings.concat( - "package pkg;\nimport testonly;\n", + "package pkg;\nimport __same;\n", "// SAME_TEST_SOURCE\n", "@test fn same_graph() void = {\n", - " assert(value() + testonly.value() == 42);\n", + " assert(value() + __same.value() == 42);\n", " assert(secret() == 6);\n};\n")); writefile(strings.concat(pkg, "/external_test.ww"), strings.concat( - "package pkg_test;\nimport externalonly;\nimport pkg;\n", + "package pkg_test;\nimport __external;\nimport pkg;\n", "// EXTERNAL_TEST_SOURCE\n", "@test fn external_graph() void = {\n", - " assert(pkg.value() + externalonly.value() == 42);\n", + " assert(pkg.value() + __external.value() == 42);\n", " assert(pkg.second() == 9);\n};\n")); let prodout: str = strings.concat(root, "/production.a"); @@ -702,19 +728,54 @@ fn workescape(s: str) str = { "pkg.unit.ww")); assert(has(produnit, "//ww:module api\n")); assert(!has(produnit, "//ww:module implementation\n")); - assert(!has(produnit, "testonly")); - assert(!has(produnit, "externalonly")); - assert(!os.exists(strings.concat(prodwork, "testonly.unit.ww"))); - assert(!os.exists(strings.concat(prodwork, "externalonly.unit.ww"))); + assert(!has(produnit, "__same")); + assert(!has(produnit, "__external")); + assert(!os.exists(strings.concat(prodwork, "__same.unit.ww"))); + assert(!os.exists(strings.concat(prodwork, "__external.unit.ww"))); assert(!has(readfile(strings.concat(prodwork, "api.unit.ww")), "TEST_DEPENDENCY_MUST_NOT_COMPILE")); let samebin: str = strings.concat(pkg, "/pkg.test"); let externalbin: str = strings.concat(pkg, "/pkg_test.test"); - let sameworkroot: str = strings.concat(samebin, ".sepwork"); - let externalworkroot: str = strings.concat(externalbin, ".sepwork"); - let samework: str = strings.concat(sameworkroot, "/"); - let externalwork: str = strings.concat(externalworkroot, "/"); + let sharedworkroot: str = strings.concat(samebin, ".sepwork"); + let sharedwork: str = strings.concat(sharedworkroot, "/"); + let compilertrace: str = strings.concat(root, "/compiler.trace"); + let linkertrace: str = strings.concat(root, "/linker.trace"); + let compilerwrapper: str = strings.concat(root, "/trace-w6c.sh"); + let linkerwrapper: str = strings.concat(root, "/trace-w6l.sh"); + writefile(compilertrace, ""); + writefile(linkertrace, ""); + writeexecutable(compilerwrapper, strings.concat( + "#!/bin/sh\n", + "printf '%s\\n' \"$*\" >> \"$WW_PACKAGE_COMPILER_TRACE\"\n", + "exec \"$WW_PACKAGE_W6C\" \"$@\"\n")); + writeexecutable(linkerwrapper, strings.concat( + "#!/bin/sh\n", + "printf '%s\\n' \"$*\" >> \"$WW_PACKAGE_LINKER_TRACE\"\n", + "exec \"$WW_PACKAGE_W6L\" \"$@\"\n")); + let baseenv: []str = os.getenvs(); + let traceenv: []str = alloc([], (baseenv.len + 6): u64)!; + let ei: i32 = 0; + for (ei < baseenv.len) { + if (!strings.hasprefix(baseenv[ei], "WW_W6C=") + && !strings.hasprefix(baseenv[ei], "WW_W6L=") + && !strings.hasprefix(baseenv[ei], + "WW_PACKAGE_COMPILER_TRACE=") + && !strings.hasprefix(baseenv[ei], + "WW_PACKAGE_LINKER_TRACE=") + && !strings.hasprefix(baseenv[ei], "WW_PACKAGE_W6C=") + && !strings.hasprefix(baseenv[ei], "WW_PACKAGE_W6L=")) { + append(traceenv, baseenv[ei]); + }; + ei += 1; + }; + append(traceenv, strings.concat("WW_W6C=", compilerwrapper)); + append(traceenv, strings.concat("WW_W6L=", linkerwrapper)); + append(traceenv, strings.concat("WW_PACKAGE_COMPILER_TRACE=", + compilertrace)); + append(traceenv, strings.concat("WW_PACKAGE_LINKER_TRACE=", linkertrace)); + append(traceenv, strings.concat("WW_PACKAGE_W6C=", driver("w6c"))); + append(traceenv, strings.concat("WW_PACKAGE_W6L=", driver("w6l"))); let drivers: []str = ["ww", "ww", "ww_ww"]; let tags: []str = ["c1", "c2", "ww"]; let referenceout: str = ""; @@ -728,34 +789,41 @@ fn workescape(s: str) str = { for (i < drivers.len) { let av: []str = [driver(drivers[i]), "test", "-c", "-I", root, pkg]; - runcommand(root, strings.concat("variant-build-", tags[i]), av, - (120i64 * (time.second: i64)): time.duration, &out); + if (i == 0) { + runcommandenv(root, strings.concat("variant-build-", tags[i]), av, + traceenv, (120i64 * (time.second: i64)): time.duration, + &out); + } else { + runcommand(root, strings.concat("variant-build-", tags[i]), av, + (120i64 * (time.second: i64)): time.duration, &out); + }; expectexit(&out, 0); let buildstdout: str = strings.dup(out.stdout); let buildstderr: str = strings.dup(out.stderr); - let sameunit: str = readfile(strings.concat(samework, - "__root.unit.ww")); - let externalunit: str = readfile(strings.concat(externalwork, - "__root.unit.ww")); - let externalproduction: str = readfile(strings.concat(externalwork, + let sameunit: str = readfile(strings.concat(sharedwork, + "__ww-test-same.unit.ww")); + let externalunit: str = readfile(strings.concat(sharedwork, + "__ww-test-external.unit.ww")); + let externalproduction: str = readfile(strings.concat(sharedwork, "pkg.unit.ww")); + assert(!os.exists(strings.concat(externalbin, ".sepwork"))); assert(has(sameunit, "//ww:module api\n")); - assert(has(sameunit, "//ww:module testonly\n")); + assert(has(sameunit, "//ww:module __same\n")); assert(has(sameunit, "//ww:module test\n")); assert(!has(sameunit, "//ww:module implementation\n")); - assert(!has(sameunit, "//ww:module externalonly\n")); - assert(!os.exists(strings.concat(samework, - "externalonly.unit.ww"))); + assert(!has(sameunit, "//ww:module __external\n")); + assert(os.exists(strings.concat(sharedwork, + "__external.unit.ww"))); assert(has(sameunit, "PACKAGE_PRODUCTION_A")); assert(has(sameunit, "PACKAGE_PRODUCTION_Z")); assert(has(sameunit, "SAME_TEST_SOURCE")); assert(!has(sameunit, "EXTERNAL_TEST_SOURCE")); assert(has(externalunit, "//ww:module pkg\n")); - assert(has(externalunit, "//ww:module externalonly\n")); + assert(has(externalunit, "//ww:module __external\n")); assert(has(externalunit, "//ww:module test\n")); assert(!has(externalunit, "//ww:module api\n")); assert(!has(externalunit, "//ww:module implementation\n")); - assert(!has(externalunit, "//ww:module testonly\n")); + assert(!has(externalunit, "//ww:module __same\n")); assert(has(externalunit, "EXTERNAL_TEST_SOURCE")); assert(!has(externalunit, "PACKAGE_PRODUCTION_A")); assert(!has(externalunit, "PACKAGE_PRODUCTION_Z")); @@ -765,12 +833,61 @@ fn workescape(s: str) str = { assert(!has(externalproduction, "//ww:module implementation\n")); assert(!has(externalproduction, "SAME_TEST_SOURCE")); assert(!has(externalproduction, "EXTERNAL_TEST_SOURCE")); - assert(!has(readfile(strings.concat(externalwork, "api.unit.ww")), + assert(!has(readfile(strings.concat(sharedwork, "api.unit.ww")), "TEST_DEPENDENCY_MUST_NOT_COMPILE")); - assert(os.exists(strings.concat(samework, "implementation.a"))); - assert(os.exists(strings.concat(externalwork, "implementation.a"))); - assert(os.exists(strings.concat(externalwork, "api.a"))); - assert(os.exists(strings.concat(externalwork, "pkg.a"))); + let artifacts: []str = ["implementation", "api", "pkg", + "__same", "__external", "test"]; + let ai: i32 = 0; + for (ai < artifacts.len) { + assert(os.exists(strings.concat(sharedwork, artifacts[ai], ".wwi"))); + assert(os.exists(strings.concat(sharedwork, artifacts[ai], ".a"))); + ai += 1; + }; + assert(os.exists(strings.concat(sharedwork, "__ww-test-same.o"))); + assert(os.exists(strings.concat(sharedwork, "__ww-test-external.o"))); + assert(!os.exists(strings.concat(sharedwork, "__ww-test-same.wwi"))); + assert(!os.exists(strings.concat(sharedwork, + "__ww-test-external.wwi"))); + assert(!os.exists(strings.concat(sharedwork, "__ww-test-same.a"))); + assert(!os.exists(strings.concat(sharedwork, + "__ww-test-external.a"))); + assert(os.exists(samebin)); + assert(os.exists(externalbin)); + if (i == 0) { + let ctrace: str = readfile(compilertrace); + assert(occurrences(ctrace, "implementation.unit.ww") == 1); + assert(occurrences(ctrace, "api.unit.ww") == 1); + assert(occurrences(ctrace, "pkg.unit.ww") == 1); + assert(occurrences(ctrace, "/__same.unit.ww") == 1); + assert(occurrences(ctrace, "/__external.unit.ww") == 1); + assert(occurrences(ctrace, "/test.unit.ww") == 1); + assert(occurrences(ctrace, "__ww-test-same.unit.ww") == 1); + assert(occurrences(ctrace, + "__ww-test-external.unit.ww") == 1); + assert(occurrences(ctrace, + "-T --test-support-module") == 2); + let ltrace: str = readfile(linkertrace); + assert(occurrences(ltrace, "\n") == 2); + let samelink: str = linecontaining(ltrace, + strings.concat("-o ", samebin, " ")); + let externallink: str = linecontaining(ltrace, + strings.concat("-o ", externalbin, " ")); + assert(has(samelink, strings.concat(sharedwork, "api.a"))); + assert(has(samelink, + strings.concat(sharedwork, "implementation.a"))); + assert(has(samelink, strings.concat(sharedwork, "__same.a"))); + assert(!has(samelink, strings.concat(sharedwork, "pkg.a"))); + assert(!has(samelink, + strings.concat(sharedwork, "__external.a"))); + assert(has(externallink, strings.concat(sharedwork, "pkg.a"))); + assert(has(externallink, strings.concat(sharedwork, "api.a"))); + assert(has(externallink, + strings.concat(sharedwork, "implementation.a"))); + assert(has(externallink, + strings.concat(sharedwork, "__external.a"))); + assert(!has(externallink, + strings.concat(sharedwork, "__same.a"))); + }; let runav: []str = [samebin]; runcommand(root, strings.concat("variant-run-same-", tags[i]), runav, (60i64 * (time.second: i64)): time.duration, &out); @@ -799,11 +916,39 @@ fn workescape(s: str) str = { assert(same(referenceerr, buildstderr)); }; if (i + 1 < drivers.len) { - clean(sameworkroot); clean(externalworkroot); + clean(sharedworkroot); clean(samebin); clean(externalbin); }; i += 1; }; + // The illegal-in-import root keys stay disjoint from the valid packages + // literally named __same and __external, including warm reuse. + let warmroot: str = strings.concat(root, "/warm"); + assert(os.mkdir(warmroot, 448i32) == 0); + let warmav: []str = [driver("ww"), "test", "-w", warmroot, + "-I", root, pkg]; + runcommandenv(root, "variant-warm-cold", warmav, traceenv, + (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + 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), + "_p_pkg/"); + assert(has(readfile(strings.concat(warmwork, "__same.unit.ww")), + "package __same;")); + assert(has(readfile(strings.concat(warmwork, + "__ww-test-same.unit.ww")), "SAME_TEST_SOURCE")); + assert(has(readfile(strings.concat(warmwork, "__external.unit.ww")), + "package __external;")); + assert(has(readfile(strings.concat(warmwork, + "__ww-test-external.unit.ww")), "EXTERNAL_TEST_SOURCE")); + runcommandenv(root, "variant-warm-reuse", warmav, traceenv, + (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(same(warmstdout, out.stdout)); + assert(same(warmstderr, out.stderr)); + assert(same(warmtrace, readfile(compilertrace))); let aliasunit: str = strings.concat(root, "/alias.unit.ww"); writefile(aliasunit, strings.concat( "//ww:module-reset __wwtest\n", @@ -834,6 +979,79 @@ fn workescape(s: str) str = { clean(root); }; +// One failing root is a product failure, not permission for the shared build +// request to suppress an independently compilable sibling root. +@test fn sibling_test_variant_failures_are_isolated() void = { + let root: str = fresh(); + let badexternal: str = strings.concat(root, "/badexternal"); + let badsame: str = strings.concat(root, "/badsame"); + assert(os.mkdir(badexternal, 448i32) == 0); + assert(os.mkdir(badsame, 448i32) == 0); + writefile(strings.concat(badexternal, "/badexternal.ww"), strings.concat( + "package badexternal;\n", + "fn hidden() i32 = { return 41; };\n", + "export fn shown() i32 = { return 41; };\n")); + writefile(strings.concat(badexternal, "/same_test.ww"), strings.concat( + "package badexternal;\n", + "@test fn valid_same_runs() void = { assert(hidden() == 41); };\n")); + writefile(strings.concat(badexternal, "/external_test.ww"), strings.concat( + "package badexternal_test;\nimport badexternal;\n", + "@test fn invalid_external() void = {\n", + " assert(badexternal.hidden() == 41);\n};\n")); + writefile(strings.concat(badsame, "/badsame.ww"), strings.concat( + "package badsame;\n", + "export fn shown() i32 = { return 42; };\n")); + writefile(strings.concat(badsame, "/same_test.ww"), strings.concat( + "package badsame;\n", + "@test fn invalid_same() void = { assert(missing() == 0); };\n")); + writefile(strings.concat(badsame, "/external_test.ww"), strings.concat( + "package badsame_test;\nimport badsame;\n", + "@test fn valid_external_runs() void = {\n", + " assert(badsame.shown() == 42);\n};\n")); + + let stages: []str = ["ww", "ww_ww"]; + let tags: []str = ["c", "ww"]; + let externaldiagnostics: []str = ["", ""]; + let samediagnostics: []str = ["", ""]; + let out: commandout; + let i: i32 = 0; + for (i < stages.len) { + let externalav: []str = [driver(stages[i]), "test", "-I", root, + badexternal]; + runcommand(root, strings.concat("isolated-external-", tags[i]), + externalav, (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(has(out.stdout, "valid_same_runs ... ok\n")); + assert(has(out.stdout, "[badexternal, same-package]\n")); + assert(has(out.stderr, + "has no exported declaration 'hidden'")); + assert(has(out.stderr, + strings.concat("FAIL ", badexternal, + " [badexternal_test] (build exit 1)\n"))); + assert(!has(out.stderr, strings.concat("FAIL ", badexternal, + " [badexternal]"))); + externaldiagnostics[i] = strings.dup(primarydiagnostic(out.stderr)); + + let sameav: []str = [driver(stages[i]), "test", "-I", root, + badsame]; + runcommand(root, strings.concat("isolated-same-", tags[i]), sameav, + (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(has(out.stdout, "valid_external_runs ... ok\n")); + assert(has(out.stdout, "[badsame_test, external]\n")); + assert(has(out.stderr, "undefined: missing")); + assert(has(out.stderr, strings.concat("FAIL ", badsame, + " [badsame] (build exit 1)\n"))); + assert(!has(out.stderr, strings.concat("FAIL ", badsame, + " [badsame_test]"))); + samediagnostics[i] = strings.dup(primarydiagnostic(out.stderr)); + i += 1; + }; + assert(same(externaldiagnostics[0], externaldiagnostics[1])); + assert(same(samediagnostics[0], samediagnostics[1])); + clean(root); +}; + // The compiler-generated runner uses a reserved graph qualifier rather than // claiming the user-visible import path `test`. A real multi-file production // package named test can therefore be the normal direct dependency of its @@ -872,7 +1090,8 @@ fn workescape(s: str) str = { runcommand(root, strings.concat("named-test-build-", stages[i]), av, (120i64 * (time.second: i64)): time.duration, &out); expectexit(&out, 0); - let rootunit: str = readfile(strings.concat(work, "__root.unit.ww")); + let rootunit: str = readfile(strings.concat(work, + "__ww-test-external.unit.ww")); let produnit: str = readfile(strings.concat(work, "test.unit.ww")); let supportunit: str = readfile(strings.concat(work, "__wwtest.unit.ww")); @@ -967,7 +1186,8 @@ fn workescape(s: str) str = { expectexit(&out, 0); assert(occurrences(readfile(trace), "shared.unit.ww") == 1); let work: str = strings.concat(bin, ".sepwork/"); - let unit: str = readfile(strings.concat(work, "__root.unit.ww")); + let unit: str = readfile(strings.concat(work, + "__ww-test-same.unit.ww")); assert(has(unit, "//ww:module left\n")); assert(has(unit, "//ww:module right\n")); assert(!has(unit, "//ww:module shared\n")); @@ -1185,8 +1405,8 @@ fn workescape(s: str) str = { assert(has(outc.stderr, "ww test: -o needs -c for a package target\n")); - // -w on a package target forwards to the coordinator: one - // persistent driver workdir per package group under the root, + // -w on a package target forwards to the coordinator: one shared + // persistent driver workdir per directory plan under the root, // reuse owned by the driver's content-identity contract. The // byte stream must match the plain run, cold and warm. let wdroot: str = strings.concat(root, "/wd");