diff --git a/cmd/ww/main.c b/cmd/ww/main.c index a04b70b4..16bb2a79 100644 --- a/cmd/ww/main.c +++ b/cmd/ww/main.c @@ -7706,6 +7706,19 @@ do_run(int argc, char **argv) if (src == NULL) src = "."; struct stat requested; int literal = stat(src, &requested) == 0; + if (literal && S_ISDIR(requested.st_mode)) { + size_t n = strlen(src); + if (n >= 3 && strcmp(src + n - 3, ".ww") == 0) { + if (n >= 8 && strcmp(src + n - 8, "_test.ww") == 0) + fprintf(stderr, + "ww: cannot run *_test.ww files (%s)\n", src); + else + fprintf(stderr, + "%s is a directory, should be a WW file\n", src); + free(incs); + return 1; + } + } char resolved[PATH_MAX]; int is_dir = 0; if (!resolve_module(src, incs, resolved, sizeof resolved, &is_dir)) { diff --git a/docs/build-system.md b/docs/build-system.md index 41941c1a..ff187f7f 100644 --- a/docs/build-system.md +++ b/docs/build-system.md @@ -10376,6 +10376,141 @@ test/run command boundaries. No serialized representation changes: build workdir format remains `18`, test workdir format remains `19`, semantic storage format remains `3`, and no cache or result record is added. +### 11.53 Implemented `.ww`-spelled directory run rejection + +An existing target selected by `ww run` whose exact requested spelling ends +`.ww` is a named-source request when ordinary symlink-following `stat` reports +a directory. Run rejects that directory before package resolution or private +work creation. This is deliberately different from section 11.52's build/test +directory route: the command-specific front door, not the shared resolver, +owns the distinction. + +#### Pinned authority and applicability + +- **behavior directly implemented or asserted by pinned Go** — official Go + 1.26.5 commit `c19862e5f8415b4f24b189d065ed739517c548ba` + scans leading `.go` operands at + `cmd/go/internal/run/run.go:73–112`, rejecting an `_test.go` spelling at + 103–109 before passing the named set to `GoFilesPackage`. + `cmd/go/internal/load/pkg.go:3244–3289`, especially 3274–3281, follows + `Stat` and fatals when a named operand is a directory. Later checking, + action construction, linking, and execution at `run.go:141–173` are not + reached. +- **behavior directly implemented or asserted by pinned Go** — the exact Go + status, header-free diagnostic, and terminating newline follow + `cmd/go/internal/base/base.go:137–145,175–183`, + `cmd/go/main.go:98–100`, and `log/log.go:200–245`. Official + `cmd/go/testdata/script/run_hello.txt:1–10`, `run_dirs.txt:1–20`, and + `run_set_executable_name.txt:4–17` anchor the named-file run front and its + distinction from package-form run. Indirect official directory-kind anchors + are `list_ambiguous_path.txt:4–15,29–36`, + `mod_get_go_file.txt:47–58`, and `mod_symlink_dotgo.txt:4–9`. No official + end-to-end `go run` test asserts this directory diagnostic; the result is + implemented directly by the cited source. +- **behavior derived from the pinned implementation** — WW's `.ww` + named-source spelling and existing directory-package form meet at the same + practical boundary. The applicable adaptation uses `WW file` and + `*_test.ww` while preserving requested operand bytes. It requires no module, + manifest, registry, network lookup, generalized import grammar, cache, + database, CAS, or build expression. + +#### Ownership, selection, and diagnostics + +- **directly measured WW behavior** — before this change, Cstage followed a + direct `.ww` directory or visible `.ww`/`_test.ww` symlink into the ordinary + directory graph, invoked compiler, assemblers, linker, and user program, and + returned that program's status. WWstage instead entered raw-source loading, + returned status 1 with exact `ww: cannot read source\n`, and ran no producer + or program. Both removed the private work they had unnecessarily created. +- **behavior derived from the pinned implementation** — the true WW owners + are only `cmd/ww/main.c::do_run` and + `selfhost/cmd/ww/main.ww::dorun`, after their existing option/target parse + and requested `stat`, but before `resolve_module`/`resolvemodule` and run + scratch creation. Changing the shared resolver would wrongly change the + distinct build, test, logical, and dotted-package fronts. +- **directly measured WW behavior** — after the run-local classification, + both stages return status 1 with empty stdout. A requested spelling ending + `_test.ww` emits exactly + `ww: cannot run *_test.ww files (OPERAND)\n`; every other selected directory + ending `.ww` emits exactly + `OPERAND is a directory, should be a WW file\n`. The requested operand is + reproduced unchanged. `_test.ww` precedence applies before ordinary + directory rejection, including through a symlink. +- **directly measured WW behavior** — the rule includes direct and symlinked + `.ww` directories and hidden directory spellings `.hidden.ww` and + `_hidden.ww`. It also precedes malformed package clauses and missing imports + inside the directory because no member is selected or opened. Existing run + option errors and target determination retain their earlier precedence. + A trailing separator does not end `.ww`; non-`.ww` directories and dotted + logical requests retain the established package route. + +#### Build, test, package, and import effects + +- **directly measured WW behavior** — package/source selection is the primary + axis. The rejected request creates no raw or directory package, canonical + identity, declared-name instance, test variant, initializer, graph node, or + action. Requested spelling and followed target are diagnostic observations + only, never canonical package, action, symbol, artifact, `.wwi`, + publication, or persistence identity. +- **directly measured WW behavior** — build retains section 11.52's stat-first + rule for the same direct and symlink spellings. It still enumerates the + directory, discovers its dotted dependencies, constructs ordinary package + actions, and applies the established default, explicit, directory, + `/dev/null`, `-S`, publication, persistence, invalidation, and rollback + policies. Unaffected build controls retain stage-byte-identical public and + semantic artifacts. +- **directly measured WW behavior** — raw `ww test`, `ww test -c`, and + directory `ww test -S` retain their stat-first directory selection and + established diagnostics/artifact bytes. Test package variants, generated + main, process topology, filtering, capture, result ordering, retention, and + result non-caching are unchanged. +- **directly measured WW behavior** — imports are not scanned on the selected + run rejection and create no qualifier or edge. Dotted and non-`.ww` package + controls retain exact canonical import identity, dependency discovery, and + runtime behavior. No physical directory becomes canonical identity through + this rule. + +#### Actions, lifecycle, parity, and scope + +- **directly measured WW behavior** — selected rejection invokes no compiler, + assembler, archiver, linker, initializer, runtime, or owned child/process + group. A fixture-preoccupied exact `/tmp/ww_run_` plus sentinel remains + untouched, proving that the driver does not acquire or mutate its run-scratch + path; final cleanup leaves that path absent. It also creates no output, + `.sepwork`, unit, `.wwi`, assembly, object, archive, executable, capture, + result, transaction, stage, backup, or persistent record. Thus + producer/runtime failure, publication, reuse, invalidation, and rollback are + inapplicable on this preflight route; all prior caller and committed bytes + remain untouched. +- **directly measured WW behavior** — classification is request-local, + read-only, and isolated under concurrency. An interruption in this preflight + owns no child or filesystem resource. The general direct-driver interruption + gap from section 11.52 remains open: a different request interrupted after + transaction staging can still leave fixed-name `.new` files and poison + later persistent reuse. This slice neither reaches nor fixes that machinery. +- **directly measured WW behavior** — Cstage and WWstage match exactly on + status, stdout, stderr, diagnostic bytes, empty producer traces, artifact + absence, exact PID-path non-acquisition and final absence, concurrent + isolation, and cleanup for every selected row. Unaffected controls retain + diagnostic and public or semantic artifact-byte parity. +- **behavior derived from the pinned implementation** — this narrow + file-kind slice does not implement the complete suffix-first run front. + Regular or missing `_test.ww`, missing or logical `.ww`, multiple named + sources, finite FIFOs, and hidden regular sources retain their prior routes + and remain separately open where different. Recursive requests, ordinary + directory requests, and generalized imports are not changed. + +The focused WW-native observer covers direct, symlinked, hidden, ordinary, and +`_test.ww` directory spellings; malformed-package and missing-import +precedence; exact requested diagnostics; empty tool/runtime traces; exact +PID-path non-acquisition and final absence; concurrency and cleanup; +trailing-separator, +non-`.ww`, regular-source, and dotted logical run controls; and unchanged +same-spelling build and raw/compile-only/assembly-only test boundaries. No +serialized representation changes: build workdir format remains `18`, test +workdir format remains `19`, semantic storage format remains `3`, and no cache +or result record is added. + ## 12. Candidate architectures and hard-gate decision Five candidates were developed as coherent systems, not as feature bins. diff --git a/docs/spec.md b/docs/spec.md index 64eec652..e46db098 100644 --- a/docs/spec.md +++ b/docs/spec.md @@ -370,10 +370,31 @@ ImportPath = ident { "." ident } . no new package, dotted-import, graph, action, symbol, artifact, `.wwi`, publication, or persistence identity. Raw `ww test`, `ww test -c`, and `ww test -S` already use the same directory classification and are unchanged. - `ww run` retains its separate named-operand front door. Multiple operands, - recursive and logical requests, regular and non-regular source targets, and - dangling symlinks are not changed. Build workdir format remains 18, test - workdir format remains 19, and semantic storage format remains 3. + `ww run` retains its separate named-operand front door, with its + `.ww`-spelled directory rule specified next. Multiple operands, recursive + and logical requests, regular and non-regular source targets, and dangling + symlinks are not changed. Build workdir format remains 18, test workdir + format remains 19, and semantic storage format remains 3. +- For `ww run`, an existing selected target whose exact requested spelling + ends `.ww` is a named-source request when symlink-following `stat` reports a + directory. The command rejects it before shared resolution, directory + enumeration, source/import loading, graph/action construction, private run + scratch, producers, or runtime. A requested suffix `_test.ww` has the earlier + exact diagnostic + `ww: cannot run *_test.ww files (OPERAND)\n`; every other such directory has + exact diagnostic `OPERAND is a directory, should be a WW file\n`. Both forms + return status 1 with empty stdout and reproduce the requested operand bytes + unchanged. The rule follows a terminal symlink, includes `.hidden.ww` and + `_hidden.ww` directory spellings, and precedes any malformed package or + missing import inside the directory. A trailing separator does not end + `.ww`; ordinary non-`.ww` directories, dotted logical requests, regular and + missing operands, multiple named operands, and non-directory non-regular + targets retain their established routes. The rejected spelling and followed + target are diagnostic metadata only and create no package/import identity, + graph/action, symbol, artifact, `.wwi`, publication, transaction, + persistence, process, or filesystem owner. Build and all test forms retain + their distinct stat-first directory behavior. Build workdir format remains + 18, test workdir format remains 19, and semantic storage format remains 3. - `import acme.codec;` loads the canonical package `acme.codec`. If that package declares `package wire;`, the importing file sees its exported names as `wire.Name`; `codec.Name` is not an additional binding. An explicit alias diff --git a/docs/test-system-v2.md b/docs/test-system-v2.md index c2d4c8d9..84b2ebd0 100644 --- a/docs/test-system-v2.md +++ b/docs/test-system-v2.md @@ -278,12 +278,29 @@ parity, direct and symlinked `.ww` directories, a dotted dependency, output modes, cold/warm transaction behavior, compiler and signaled-linker failure preservation, selected-route concurrency, cleanup, and the exact test-command behavior. Direct driver interruption remains owned by the unchanged directory -machinery and is not claimed fixed here. The shared resolver and `ww run` are -deliberately unchanged—and the observer -retains their pre-existing stage difference—because pinned Go's run command -has a distinct named-file front door. -No test product, result, package/import identity, or serialized format changes; -build remains 18, test remains 19, and semantic storage remains 3. +machinery and is not claimed fixed here. + +Pinned Go's run command has a distinct suffix-first named-file front door. +Accordingly, an existing `ww run` target whose exact requested spelling ends +`.ww` and whose symlink-following `stat` result is a directory is rejected in +the twin run fronts before the shared resolver and private run scratch. An +`_test.ww` spelling emits exactly +`ww: cannot run *_test.ww files (OPERAND)\n`; every other selected `.ww` +directory emits exactly `OPERAND is a directory, should be a WW file\n`. +Both stages return status 1 with empty stdout, open no directory member, scan +no import, construct no package/graph/action, invoke no producer or runtime, +and create no run scratch or artifact. This includes direct and symlinked +directories and hidden directory spellings; directory contents cannot displace +the classifier diagnostic. A trailing separator and non-`.ww` or dotted +package requests retain the directory package route. The same-spelling build, +raw test, `test -c`, and directory `test -S` routes remain stat-first and keep +their existing diagnostics and artifact bytes. Thus this run change creates no +test product, result, package/import identity, test-process/filter change, or +serialized-format change; build remains 18, test remains 19, and semantic +storage remains 3. Regular or missing `_test.ww`, missing/logical `.ww`, +multiple named sources, finite FIFOs, and hidden regular sources are not +claimed by this existing-directory slice. The general direct-driver `.new` +interruption residue also remains open. List mode uses that same product process and initialization boundary but starts no per-test child. The shared language harness emits only selected qualified diff --git a/selfhost/cmd/ww/main.ww b/selfhost/cmd/ww/main.ww index 3fc3a9b7..3db700b8 100644 --- a/selfhost/cmd/ww/main.ww +++ b/selfhost/cmd/ww/main.ww @@ -9321,6 +9321,16 @@ fn dorun(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { case void => requestedliteral = true; case let e: os.oserror => void; }; + if (requestedliteral + && ((requestedstat.mode: u32) & 61440u32) == os.mode.DIR: u32 + && cstrendswithlit(src, ".ww")) { + if (cstrendswithlit(src, "_test.ww")) { + cerrpath("ww: cannot run *_test.ww files (", src, ")\n"); + } else { + cerrpath("", src, " is a directory, should be a WW file\n"); + }; + return 1; + }; let isdir: i32 = 0; let resolved: *u8 = resolvemodule(selfdir, src, incs.ptr, &isdir); if (resolved == nil) { diff --git a/test/package/package_test.ww b/test/package/package_test.ww index 0f1bd096..5ff62011 100644 --- a/test/package/package_test.ww +++ b/test/package/package_test.ww @@ -1466,8 +1466,8 @@ fn cwdwritedata(dir: str, label: str) void = { }; // A visible raw test-source build observes only its initial package/import -// header and then omits it. A .ww-spelled directory, including a symlink with -// a _test.ww spelling, instead enters the ordinary directory-package route. +// header and then omits it. Build and test retain their directory-package +// front door, while run's named-source front rejects a .ww-spelled directory. // Neither classification may redefine package or dotted-import identity. @test fn named_test_sources_are_header_loaded_then_omitted() void = { let root: str = fresh(); @@ -1593,6 +1593,8 @@ fn cwdwritedata(dir: str, label: str) void = { let linkersignaldiag: str = ""; let directorytestrunout: str = ""; let directorytestrunerr: str = ""; + let directoryrunout: str = ""; + let directoryrunerr: str = ""; let directorytestrunartifacts: str = ""; let directorytestbin: str = ""; let directorytestcompileartifacts: str = ""; @@ -2202,21 +2204,25 @@ fn cwdwritedata(dir: str, label: str) void = { if (si == 0) { directorytestasm = strings.dup(out.stderr); } else { assert(same(directorytestasm, out.stderr)); }; - // Run was not changed by the build-only dispatcher fix. Cstage follows - // the selected directory and executes it; WWstage retains its exact - // pre-existing raw-source failure for the same requested symlink. + // Run's named-source front rejects the requested _test.ww spelling + // before directory loading, tool production, or runtime execution. let directoryrunav: []str = [driver(stages[si]), "run", "-I", include, dirlink]; runcommandenv(root, strings.concat("testonly-directory-run-", tags[si]), directoryrunav, env, (60i64 * (time.second: i64)): time.duration, &out); + let directoryrundiag: str = strings.concat( + "ww: cannot run *_test.ww files (", dirlink, ")\n"); + expectexit(&out, 1); + assert(out.stdout.len == 0 && same(out.stderr, directoryrundiag) + && readfile(ctrace).len == 0 && readfile(atrace).len == 0 + && readfile(ltrace).len == 0); if (si == 0) { - expectexit(&out, 5); - assert(out.stdout.len == 0 && out.stderr.len == 0); + directoryrunout = strings.dup(out.stdout); + directoryrunerr = strings.dup(out.stderr); } else { - expectexit(&out, 1); - assert(out.stdout.len == 0 - && same(out.stderr, "ww: cannot read source\n")); + assert(same(directoryrunout, out.stdout) + && same(directoryrunerr, out.stderr)); }; rewritefile(dirtargettest, dirtargettestexcluded); @@ -2420,6 +2426,396 @@ fn cwdwritedata(dir: str, label: str) void = { clean(root); }; +fn expectrundirectoryrejection(root: str, name: str, av: []str, env: []str, + pidrecord: str, ctrace: str, atrace: str, ltrace: str, want: str) void = { + rewritefile(pidrecord, ""); + rewritefile(ctrace, ""); + rewritefile(atrace, ""); + rewritefile(ltrace, ""); + let out: commandout; + runcommandenv(root, name, av, env, + (60i64 * (time.second: i64)): time.duration, &out); + let ownedrun: str = readfile(pidrecord); + let ownedmarker: str = strings.concat(ownedrun, "/observer-marker"); + let ownedvalid: bool = ownedrun.len > 12 + && strings.hasprefix(ownedrun, "/tmp/ww_run_"); + let observed: bool = out.termination == exec.termination.EXIT + && out.code == 1 && out.stdout.len == 0 && same(out.stderr, want) + && ownedvalid && os.exists(ownedrun) + && same(readfile(ownedmarker), "observer-owned\n") + && readfile(ctrace).len == 0 && readfile(atrace).len == 0 + && readfile(ltrace).len == 0; + let markerclean: i32 = -1; + let dirclean: i32 = -1; + if (ownedvalid) { + markerclean = os.remove(ownedmarker); + dirclean = os.rmdir(ownedrun); + }; + assert(observed && markerclean == 0 && dirclean == 0 + && !os.exists(ownedrun)); +}; + +// Run's named-source spelling owns the directory rejection. The physical +// directory remains only loader metadata for the distinct build and test +// fronts, and rejected source contents cannot create package or import state. +@test fn run_dot_ww_directories_are_named_sources() void = { + let root: str = fresh(); + let source: str = strings.concat(root, "/source"); + let include: str = strings.concat(root, "/include"); + let dependency: str = strings.concat(include, "/domain/value"); + let logical: str = strings.concat(include, "/domain/logical"); + let ordinary: str = strings.concat(source, "/ordinary.ww"); + let ordinarylink: str = strings.concat(source, "/requested.ww"); + let testdirectory: str = strings.concat(source, "/direct_test.ww"); + let testlink: str = strings.concat(source, "/requested_test.ww"); + let hidden: str = strings.concat(source, "/.hidden.ww"); + let private: str = strings.concat(source, "/_hidden.ww"); + let malformed: str = strings.concat(source, "/malformed.ww"); + let missingimport: str = strings.concat(source, "/missing-import.ww"); + let nonsuffix: str = strings.concat(source, "/directory-control"); + let nonsuffixlink: str = strings.concat(source, "/directory-link"); + let regular: str = strings.concat(source, "/regular.ww"); + let rawtest: str = strings.concat(source, "/raw_control_test.ww"); + let prior: str = strings.concat(root, "/prior-public-output"); + mkdirall(dependency); mkdirall(logical); mkdirall(ordinary); + mkdirall(testdirectory); mkdirall(hidden); mkdirall(private); + mkdirall(malformed); mkdirall(missingimport); mkdirall(nonsuffix); + writefile(strings.concat(dependency, "/value.ww"), strings.concat( + "package value;\n", + "export fn number() i32 = { return 37; };\n")); + writefile(strings.concat(ordinary, "/main.ww"), strings.concat( + "package main;\nimport domain.value;\n", + "fn main() i32 = { return value.number(); };\n")); + writefile(strings.concat(testdirectory, "/main.ww"), + "package main;\nfn main() i32 = { return 47; };\n"); + writefile(strings.concat(hidden, "/main.ww"), + "package main;\nfn main() i32 = { return 48; };\n"); + writefile(strings.concat(private, "/main.ww"), + "package main;\nfn main() i32 = { return 49; };\n"); + writefile(strings.concat(malformed, "/main.ww"), + "this directory has no package clause\n"); + writefile(strings.concat(missingimport, "/main.ww"), strings.concat( + "package main;\nimport absent.before.run.classification;\n", + "fn main() i32 = { return 50; };\n")); + writefile(strings.concat(nonsuffix, "/main.ww"), + "package main;\nfn main() i32 = { return 43; };\n"); + writefile(regular, + "package main;\nfn main() i32 = { return 39; };\n"); + writefile(strings.concat(logical, "/main.ww"), + "package main;\nfn main() i32 = { return 41; };\n"); + writefile(rawtest, strings.concat( + "package runroute;\n", + "@test fn raw_control() void = { assert(true); };\n")); + writefile(prior, "prior-public-bytes\n"); + assert(os.symlink(ordinary, ordinarylink) == 0); + assert(os.symlink(testdirectory, testlink) == 0); + assert(os.symlink(nonsuffix, nonsuffixlink) == 0); + + let driverwrapper: str = strings.concat(root, "/driver-wrapper.sh"); + let compilerwrapper: str = strings.concat(root, "/compiler-wrapper.sh"); + let assemblerwrapper: str = strings.concat(root, "/assembler-wrapper.sh"); + let linkerwrapper: str = strings.concat(root, "/linker-wrapper.sh"); + writeexecutable(driverwrapper, strings.concat( + "#!/bin/sh\nowned=/tmp/ww_run_$$\n", + "printf '%s' \"$owned\" > \"$WW_RUN_PID_RECORD\"\n", + "if test \"$WW_RUN_PREOCCUPY\" = 1; then\n", + " mkdir \"$owned\" || exit 124\n", + " printf 'observer-owned\\n' > \"$owned/observer-marker\"\n", + "fi\n", + "exec \"$WW_RUN_REAL_DRIVER\" \"$@\"\n")); + writeexecutable(compilerwrapper, strings.concat( + "#!/bin/sh\nprintf 'compile\\n' >> \"$WW_RUN_CTRACE\"\n", + "exec \"$WW_RUN_REAL_C\" \"$@\"\n")); + writeexecutable(assemblerwrapper, strings.concat( + "#!/bin/sh\nprintf 'assemble\\n' >> \"$WW_RUN_ATRACE\"\n", + "exec \"$WW_RUN_REAL_A\" \"$@\"\n")); + writeexecutable(linkerwrapper, strings.concat( + "#!/bin/sh\nprintf 'link\\n' >> \"$WW_RUN_LTRACE\"\n", + "exec \"$WW_RUN_REAL_L\" \"$@\"\n")); + + let stages: []str = ["ww", "ww_ww"]; + let compilers: []str = ["w6c", "w6c_ww"]; + let assemblers: []str = ["w6a", "w6a_ww"]; + let linkers: []str = ["w6l", "w6l_ww"]; + let tags: []str = ["c", "ww"]; + let environments: [][]str = alloc([], 2u64)!; + let rejectenvironments: [][]str = alloc([], 2u64)!; + let pidrecords: []str = alloc([], 2u64)!; + let ctraces: []str = alloc([], 2u64)!; + let atraces: []str = alloc([], 2u64)!; + let ltraces: []str = alloc([], 2u64)!; + let inherited: []str = os.getenvs(); + let si: i32 = 0; + for (si < stages.len) { + let pidrecord: str = strings.concat(root, "/driver-pid-", tags[si]); + let ctrace: str = strings.concat(root, "/compiler-trace-", tags[si]); + let atrace: str = strings.concat(root, "/assembler-trace-", tags[si]); + let ltrace: str = strings.concat(root, "/linker-trace-", tags[si]); + writefile(pidrecord, ""); writefile(ctrace, ""); + writefile(atrace, ""); writefile(ltrace, ""); + append(pidrecords, pidrecord); append(ctraces, ctrace); + append(atraces, atrace); append(ltraces, ltrace); + let env: []str = alloc([], (inherited.len + 11): u64)!; + let ei: i32 = 0; + for (ei < inherited.len) { + if (!strings.hasprefix(inherited[ei], "WW_W6C=") + && !strings.hasprefix(inherited[ei], "WW_W6A=") + && !strings.hasprefix(inherited[ei], "WW_W6L=") + && !strings.hasprefix(inherited[ei], "WW_RUN_")) { + append(env, inherited[ei]); + }; + ei += 1; + }; + append(env, strings.concat("WW_RUN_REAL_DRIVER=", driver(stages[si]))); + append(env, strings.concat("WW_RUN_PID_RECORD=", pidrecord)); + append(env, strings.concat("WW_W6C=", compilerwrapper)); + append(env, strings.concat("WW_W6A=", assemblerwrapper)); + append(env, strings.concat("WW_W6L=", linkerwrapper)); + append(env, strings.concat("WW_RUN_CTRACE=", ctrace)); + append(env, strings.concat("WW_RUN_ATRACE=", atrace)); + append(env, strings.concat("WW_RUN_LTRACE=", ltrace)); + append(env, strings.concat("WW_RUN_REAL_C=", driver(compilers[si]))); + append(env, strings.concat("WW_RUN_REAL_A=", driver(assemblers[si]))); + append(env, strings.concat("WW_RUN_REAL_L=", driver(linkers[si]))); + append(environments, env); + let rejectenv: []str = alloc([], (env.len + 1): u64)!; + let ri: i32 = 0; + for (ri < env.len) { append(rejectenv, env[ri]); ri += 1; }; + append(rejectenv, "WW_RUN_PREOCCUPY=1"); + append(rejectenvironments, rejectenv); + si += 1; + }; + + let selected: []str = [ordinary, ordinarylink, testdirectory, testlink, + hidden, private, malformed, missingimport]; + let selectedlabels: []str = ["direct", "symlink", "direct-test", + "symlink-test", "dot-hidden", "underscore-hidden", "malformed", + "missing-import"]; + let selectedtest: []bool = [false, false, true, true, false, false, + false, false]; + let sourcebefore: str = strings.dup(treesnapshot(source)); + si = 0; + for (si < stages.len) { + let ri: i32 = 0; + for (ri < selected.len) { + let want: str; + if (selectedtest[ri]) { + want = strings.concat("ww: cannot run *_test.ww files (", + selected[ri], ")\n"); + } else { + want = strings.concat(selected[ri], + " is a directory, should be a WW file\n"); + }; + let av: []str = [driverwrapper, "run", "-I", include, + selected[ri]]; + expectrundirectoryrejection(root, strings.concat("run-reject-", + tags[si], "-", selectedlabels[ri]), av, rejectenvironments[si], + pidrecords[si], ctraces[si], atraces[si], ltraces[si], want); + ri += 1; + }; + let boundaryav: []str = [driverwrapper, "run", "-I", include, + ordinarylink, "-I", "program-argument"]; + let boundarywant: str = strings.concat(ordinarylink, + " is a directory, should be a WW file\n"); + expectrundirectoryrejection(root, strings.concat("run-boundary-", + tags[si]), boundaryav, rejectenvironments[si], pidrecords[si], + ctraces[si], atraces[si], ltraces[si], boundarywant); + si += 1; + }; + assert(same(sourcebefore, treesnapshot(source)) + && same(readfile(prior), "prior-public-bytes\n") + && !directoryhasfragment(root, ".sepwork") + && !directoryhasfragment(root, ".wwtxn.") + && !directoryhasfragment(root, ".capture") + && !directoryhasfragment(root, ".result") + && !directoryhasfragment(root, ".request")); + + // Concurrent preflight rejections leave independently preoccupied exact + // run paths and diagnostics untouched. + let pi: i32 = 0; + for (pi < stages.len) { + rewritefile(pidrecords[pi], ""); rewritefile(ctraces[pi], ""); + rewritefile(atraces[pi], ""); rewritefile(ltraces[pi], ""); + pi += 1; + }; + let cav: []str = [driverwrapper, "run", ordinary]; + let wav: []str = [driverwrapper, "run", ordinarylink]; + let cc: exec.command; + cc.path = cav[0]; cc.argv = cav; + cc.env = rejectenvironments[0]; cc.dir = repo(); + cc.stdoutpath = strings.concat(root, "/parallel-c.stdout"); + cc.stderrpath = strings.concat(root, "/parallel-c.stderr"); + cc.deadline = time.add(time.now(time.clock.monotonic), + (60i64 * (time.second: i64)): time.duration); + cc.grace = (100i64 * (time.millisecond: i64)): time.duration; + let wc: exec.command; + wc.path = wav[0]; wc.argv = wav; + wc.env = rejectenvironments[1]; wc.dir = repo(); + wc.stdoutpath = strings.concat(root, "/parallel-ww.stdout"); + wc.stderrpath = strings.concat(root, "/parallel-ww.stderr"); + wc.deadline = time.add(time.now(time.clock.monotonic), + (60i64 * (time.second: i64)): time.duration); + wc.grace = (100i64 * (time.millisecond: i64)): time.duration; + let cp: exec.process; + let wp: exec.process; + exec.start(&cp, &cc); exec.start(&wp, &wc); + let cdone: bool = false; + let wdone: bool = false; + for (!cdone || !wdone) { + if (!cdone) { cdone = exec.poll(&cp); }; + if (!wdone) { wdone = exec.poll(&wp); }; + if (!cdone || !wdone) { + time.sleep(time.millisecond, time.clock.monotonic); + }; + }; + let cownedrun: str = readfile(pidrecords[0]); + let wownedrun: str = readfile(pidrecords[1]); + let cownedmarker: str = strings.concat(cownedrun, "/observer-marker"); + let wownedmarker: str = strings.concat(wownedrun, "/observer-marker"); + let cownedvalid: bool = cownedrun.len > 12 + && strings.hasprefix(cownedrun, "/tmp/ww_run_"); + let wownedvalid: bool = wownedrun.len > 12 + && strings.hasprefix(wownedrun, "/tmp/ww_run_"); + let parallelobserved: bool = cp.result.errno == 0 + && cp.result.cleanuperrno == 0 + && cp.result.termination == exec.termination.EXIT && cp.result.code == 1 + && wp.result.errno == 0 && wp.result.cleanuperrno == 0 + && wp.result.termination == exec.termination.EXIT && wp.result.code == 1 + && readfile(cc.stdoutpath).len == 0 && readfile(wc.stdoutpath).len == 0 + && same(readfile(cc.stderrpath), strings.concat(ordinary, + " is a directory, should be a WW file\n")) + && same(readfile(wc.stderrpath), strings.concat(ordinarylink, + " is a directory, should be a WW file\n")) + && cownedvalid && wownedvalid + && os.exists(cownedrun) && os.exists(wownedrun) + && same(readfile(cownedmarker), "observer-owned\n") + && same(readfile(wownedmarker), "observer-owned\n") + && readfile(ctraces[0]).len == 0 && readfile(atraces[0]).len == 0 + && readfile(ltraces[0]).len == 0 && readfile(ctraces[1]).len == 0 + && readfile(atraces[1]).len == 0 && readfile(ltraces[1]).len == 0 + && same(sourcebefore, treesnapshot(source)) + && same(readfile(prior), "prior-public-bytes\n"); + let cmarkerclean: i32 = -1; + let cdirclean: i32 = -1; + let wmarkerclean: i32 = -1; + let wdirclean: i32 = -1; + if (cownedvalid) { + cmarkerclean = os.remove(cownedmarker); + cdirclean = os.rmdir(cownedrun); + }; + if (wownedvalid) { + wmarkerclean = os.remove(wownedmarker); + wdirclean = os.rmdir(wownedrun); + }; + assert(parallelobserved && cmarkerclean == 0 && cdirclean == 0 + && wmarkerclean == 0 && wdirclean == 0 + && !os.exists(cownedrun) && !os.exists(wownedrun)); + + let cbuildbytes: str = ""; + let cbuildartifacts: str = ""; + let cteststdout: str = ""; + let ctestbinary: str = ""; + si = 0; + for (si < stages.len) { + let out: commandout; + let trailing: str = strings.concat(ordinary, "/"); + let trailingav: []str = [driverwrapper, "run", "-I", include, trailing]; + runcommandenv(root, strings.concat("run-trailing-", tags[si]), trailingav, + environments[si], (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 37); + assert(out.stdout.len == 0 && out.stderr.len == 0 + && !os.exists(readfile(pidrecords[si]))); + let nonwwav: []str = [driverwrapper, "run", nonsuffixlink]; + runcommandenv(root, strings.concat("run-non-ww-", tags[si]), nonwwav, + environments[si], (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 43); + assert(out.stdout.len == 0 && out.stderr.len == 0 + && !os.exists(readfile(pidrecords[si]))); + let regularav: []str = [driverwrapper, "run", regular]; + runcommandenv(root, strings.concat("run-regular-", tags[si]), regularav, + environments[si], (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 39); + assert(out.stdout.len == 0 && out.stderr.len == 0 + && !os.exists(readfile(pidrecords[si]))); + let logicalav: []str = [driverwrapper, "run", "-I", include, + "domain.logical"]; + runcommandenv(root, strings.concat("run-logical-", tags[si]), logicalav, + environments[si], (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 41); + assert(out.stdout.len == 0 && out.stderr.len == 0 + && !os.exists(readfile(pidrecords[si]))); + + let buildwork: str = strings.concat(root, "/build-work-", tags[si]); + let buildout: str = strings.concat(root, "/build-output-", tags[si]); + mkdirall(buildwork); + let buildav: []str = [driverwrapper, "build", "-w", buildwork, + "-I", include, "-o", buildout, ordinarylink]; + runcommandenv(root, strings.concat("build-same-spelling-", tags[si]), + buildav, environments[si], + (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stdout.len == 0 && out.stderr.len == 0 && os.exists(buildout)); + let buildbytes: str = strings.dup(readfile(buildout)); + let buildartifacts: str = strings.dup(artifacttreesnapshot(buildwork)); + if (si == 0) { + cbuildbytes = strings.dup(buildbytes); + cbuildartifacts = strings.dup(buildartifacts); + } else { + assert(same(cbuildbytes, buildbytes) + && same(cbuildartifacts, buildartifacts)); + }; + let builtav: []str = [buildout]; + runcommand(root, strings.concat("built-control-", tags[si]), builtav, + time.second, &out); + expectexit(&out, 37); + + let testwork: str = strings.concat(root, "/test-work-", tags[si]); + mkdirall(testwork); + let testav: []str = [driverwrapper, "test", "-w", testwork, rawtest]; + runcommandenv(root, strings.concat("raw-test-control-", tags[si]), testav, + environments[si], (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stderr.len == 0 && has(out.stdout, "raw_control ... ok\n")); + if (si == 0) { cteststdout = strings.dup(out.stdout); } + else { assert(same(cteststdout, out.stdout)); }; + let testbinary: str = strings.concat(root, "/test-binary-", tags[si]); + let compileav: []str = [driverwrapper, "test", "-c", "-o", + testbinary, rawtest]; + runcommandenv(root, strings.concat("raw-test-compile-", tags[si]), + compileav, environments[si], + (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stdout.len == 0 && out.stderr.len == 0 && os.exists(testbinary)); + if (si == 0) { ctestbinary = strings.dup(readfile(testbinary)); } + else { assert(same(ctestbinary, readfile(testbinary))); }; + + let assemblywork: str = strings.concat(root, "/assembly-work-", tags[si]); + let assemblyout: str = strings.concat(root, "/assembly-output-", tags[si]); + mkdirall(assemblywork); + rewritefile(ctraces[si], ""); rewritefile(atraces[si], ""); + rewritefile(ltraces[si], ""); + let assemblyav: []str = [driverwrapper, "test", "-S", "-w", + assemblywork, "-o", assemblyout, ordinarylink]; + runcommandenv(root, strings.concat("directory-test-assembly-", tags[si]), + assemblyav, environments[si], + (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 2); + assert(out.stdout.len == 0 + && same(out.stderr, "ww test: -S needs a single test file\n") + && !os.exists(assemblyout) && directoryisempty(assemblywork) + && readfile(ctraces[si]).len == 0 + && readfile(atraces[si]).len == 0 + && readfile(ltraces[si]).len == 0); + si += 1; + }; + assert(!directoryhasnew(root) && !directoryhasfragment(root, ".wwtxn.") + && !directoryhasfragment(root, ".install") + && !directoryhasfragment(root, ".capture") + && !directoryhasfragment(root, ".result") + && !directoryhasfragment(root, ".request")); + clean(root); +}; + // A source file has one contiguous import section immediately after its // package clause. The parser may continue after a late import for recovery, // but the imports-only loader and the full compiler parser both reject it.