ww: package-level -c -o names the single package's test artifact

The last Open-driver-work bullet. `-c -o <name>` replaces the fixed
<package>.test stem for exactly one package; the coordinator rejects a
multi-package fan-out ("cannot use -o with multiple packages", Go's
`go test -o` rule) and -o without -c is rejected at the driver
("needs -c for a package target" — a plain run executes from the temp
root, so a caller-owned name has nothing to name). Both driver stages
byte-identical wording; -S keeps its single-file-only reject.

package_test gains the contract row (naming, fixed-stem absence,
artifact runs, both rejects, both drivers); the tree-mode -o row's
pinned wording follows the contract.
This commit is contained in:
2026-08-08 17:30:36 +09:00
parent cf90c50c38
commit 192ddd4c66
5 changed files with 150 additions and 22 deletions

View File

@@ -1696,9 +1696,16 @@ do_test(int argc, char **argv)
size_t tlen = strlen(target);
if (strcmp(target, "...") == 0 ||
(tlen >= 4 && strcmp(target + tlen - 4, "/...") == 0)) {
if (outstem[0]) {
if (emit_asm) {
fprintf(stderr,
"ww test: -c/-S/-o need a single test file\n");
"ww test: -S needs a single test file\n");
return 2;
}
/* -c -o forwards: the coordinator names the single
* package's artifact and rejects a multi-package fan-out. */
if (outstem[0] && !compileonly) {
fprintf(stderr,
"ww test: -o needs -c for a package target\n");
return 2;
}
if (workdir[0]) {
@@ -1725,9 +1732,14 @@ do_test(int argc, char **argv)
return 1;
}
if (is_dir) {
if (outstem[0]) {
if (emit_asm) {
fprintf(stderr,
"ww test: -c/-S/-o need a single test file\n");
"ww test: -S needs a single test file\n");
return 2;
}
if (outstem[0] && !compileonly) {
fprintf(stderr,
"ww test: -o needs -c for a package target\n");
return 2;
}
if (workdir[0]) {
@@ -1862,8 +1874,12 @@ do_test(int argc, char **argv)
fprintf(stderr, "ww test: %s is neither file nor directory\n", target);
return 1;
}
if (outstem[0]) {
fprintf(stderr, "ww test: -c/-S/-o need a single test file\n");
if (emit_asm) {
fprintf(stderr, "ww test: -S needs a single test file\n");
return 2;
}
if (outstem[0] && !compileonly) {
fprintf(stderr, "ww test: -o needs -c for a package target\n");
return 2;
}
if (workdir[0]) {

View File

@@ -207,9 +207,14 @@ at every `-j` level, and `-j 1` — the default — matches the former
sequential loop exactly. Measured on the 31-package `lib/...` walk:
7.0s sequential, 2.4s at `-j 4`. With `-c`, it publishes each exact
`<package>.test` binary and adjacent `<package>.test.sepwork` tree in the
package directory; those become caller-owned artifacts. Without `-c`, it
removes the temporary binary and scratch with its workspace. The language
runtime owns individual `@test` functions.
package directory; those become caller-owned artifacts. `-c -o <name>`
names that artifact instead of the fixed stem, for exactly one package:
the coordinator rejects a multi-package fan-out ("cannot use -o with
multiple packages", Go's `go test -o` rule), and `-o` without `-c` is
rejected at the driver ("needs -c for a package target") because a plain
run always executes from the temp root. Without `-c`, it removes the
temporary binary and scratch with its workspace. The language runtime
owns individual `@test` functions.
Separate compilation is the only driver build path; no compatibility mode
switch remains.
@@ -320,11 +325,8 @@ timeout policy in this architecture.
## Open driver work
Carried over from the dissolved project plan; each is deliberate scope, not
drift:
- A package-level `-o` contract (single-file `ww test -o` exists; directory
packages publish fixed `<package>.test` stems under `-c`).
None; the package-level `-o` contract (the last carried bullet) landed as
`-c -o <name>` for exactly one package.
## Validation policy

View File

@@ -506,13 +506,17 @@ fn pkgcombined(g: *pkggroup, srcs: []pkgsource) bool = {
};
fn pkgsetpaths(g: *pkggroup, root: str, index: i32,
compileonly: bool) bool = {
compileonly: bool, outname: str) bool = {
let num: str = strconv.i32tos(index, strconv.base.DEC);
g.root = strings.concat(root, "/group-", num);
if (!pkgmakedir(g.root)) { return false; };
g.combined = strings.concat(g.root, "/package.ww");
if (compileonly) {
if (outname.len != 0) {
g.bin = outname;
} else {
g.bin = strings.concat(g.dir, "/", g.pkg, ".test");
};
} else {
g.bin = strings.concat(g.root, "/package.test");
};
@@ -669,6 +673,7 @@ export fn packagecommand(args: []str) int = {
let filters: []str = alloc([], (args.len + 1): u64)!;
let includes: []str = alloc([], (args.len + 1): u64)!;
let timeoutarg: str = "";
let outname: str = "";
let builder: str = pkgdefaultbuilder();
let i: i32 = 0;
for (i < args.len) {
@@ -688,6 +693,17 @@ export fn packagecommand(args: []str) int = {
i += 1;
continue;
};
if (strings.compare(a, "-o") == 0) {
if (i + 1 >= args.len) { pkgusage(); return 2; };
outname = args[i + 1];
i += 2;
continue;
};
if (strings.hasprefix(a, "-o") && a.len > 2) {
outname = a[2:a.len];
i += 1;
continue;
};
if (strings.hasprefix(a, "-timeout-ms=")) {
if (timeoutarg.len != 0 || a.len == 12
|| pkgparsedec(a[12:a.len], 3600000i64) <= 0i64) {
@@ -735,6 +751,12 @@ export fn packagecommand(args: []str) int = {
pkgusage();
return 2;
};
// -o names the -c artifact; a plain run always executes from the
// temp root, so a caller-owned name has nothing to name.
if (outname.len != 0 && !compileonly) {
pkgputln(os.STDERR_FILENO, "wwtest package: -o needs -c");
return 2;
};
// Go's ./... form: a trailing "..." path element walks the tree
// rooted at the prefix instead of one explicit directory.
@@ -870,6 +892,13 @@ export fn packagecommand(args: []str) int = {
};
if (groups.len == 0) { return 0; };
pkgsortgroups(groups);
// One caller-owned name cannot fan out (Go: `go test -o` with
// multiple packages is an error).
if (outname.len != 0 && groups.len > 1) {
pkgputln(os.STDERR_FILENO,
"wwtest package: cannot use -o with multiple packages");
return 2;
};
let borrowed: str = temp.dir();
let tmproot: str = strings.dup(borrowed);
@@ -894,7 +923,8 @@ export fn packagecommand(args: []str) int = {
for (emitted < groups.len) {
for (!stopped && launched < groups.len && active < jobs) {
let g: *pkggroup = &groups[launched];
if (!pkgsetpaths(g, tmproot, launched, compileonly)) {
if (!pkgsetpaths(g, tmproot, launched, compileonly,
outname)) {
g.fail = PKGFAILSETUP;
g.state = PKGDONE;
stopped = true;

View File

@@ -2684,8 +2684,14 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
&& target[tlen - 1u64] == '.';
};
if (istree) {
if (outstem != nil) {
cerr("ww test: -c/-S/-o need a single test file\n");
if (emitasm != 0) {
cerr("ww test: -S needs a single test file\n");
return 2;
};
// -c -o forwards: the coordinator names the single
// package's artifact and rejects a multi-package fan-out.
if (outstem != nil && compileonly == 0) {
cerr("ww test: -o needs -c for a package target\n");
return 2;
};
if (workdir != nil) {
@@ -2728,8 +2734,12 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
emitasm, outstem, workdir, patarg);
};
if (outstem != nil) {
cerr("ww test: -c/-S/-o need a single test file\n");
if (emitasm != 0) {
cerr("ww test: -S needs a single test file\n");
return 2;
};
if (outstem != nil && compileonly == 0) {
cerr("ww test: -o needs -c for a package target\n");
return 2;
};
if (workdir != nil) {

View File

@@ -569,7 +569,8 @@ fn packagepath(relative: str) str = {
expectexit(&outc, 2);
expectexit(&outw, 2);
assert(same(outc.stderr, outw.stderr));
assert(has(outc.stderr, "ww test: -c/-S/-o need a single test file\n"));
assert(has(outc.stderr,
"ww test: -o needs -c for a package target\n"));
let wdc: []str = [driver("ww"), "test", "-w", root, spec];
let wdw: []str = [driver("ww_ww"), "test", "-w", root, spec];
@@ -775,3 +776,72 @@ fn runtimepath(relative: str) str = {
"escaped_writer_does_not_hold_runner ... ok\n"));
clean(root);
};
// The package-level -o contract: -c -o names the single package's
// artifact in place of the fixed <package>.test stem; -o without -c
// has nothing to name (runs execute from the temp root); one name
// cannot fan out over multiple packages (Go's `go test -o` rule).
@test fn compile_artifact_naming() void = {
let root: str = fresh();
let pdir: str = strings.concat(root, "/pkg");
assert(os.mkdir(pdir, 493i32) == 0);
writefile(strings.concat(pdir, "/pkg.ww"),
"package pkg;\nexport fn v() i32 = { return 7; };\n");
writefile(strings.concat(pdir, "/pkg_test.ww"), strings.concat(
"package pkg_test;\nimport pkg;\n",
"@test fn seven() void = { assert(pkg.v() == 7); };\n"));
let adir: str = strings.concat(root, "/a");
assert(os.mkdir(adir, 493i32) == 0);
// '_'-prefixed: the tree walk skips it, so the artifacts and
// their .sepwork trees never pollute the multi-package discovery.
let outdir: str = strings.concat(root, "/_out");
assert(os.mkdir(outdir, 493i32) == 0);
writefile(strings.concat(adir, "/a.ww"),
"package a;\nexport fn v() i32 = { return 1; };\n");
writefile(strings.concat(adir, "/a_test.ww"), strings.concat(
"package a_test;\nimport a;\n",
"@test fn one() void = { assert(a.v() == 1); };\n"));
let out: commandout;
let drvs: []str = ["ww", "ww_ww"];
let i: i32 = 0;
for (i < 2) {
let named: str = strings.concat(outdir, "/out_", drvs[i],
".bin");
let coav: []str = [driver(drvs[i]), "test", "-c", "-o",
named, pdir];
runcommand(root, strings.concat("nameco_", drvs[i]), coav,
(30i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
let fi: os.filestat;
match (os.stat(&fi, named)) {
case void => void;
case let e: os.oserror => abort("-c -o artifact missing");
};
match (os.stat(&fi, strings.concat(pdir, "/pkg.test"))) {
case void => abort("-c -o still published the fixed stem");
case let e: os.oserror => void;
};
let runav: []str = [named];
runcommand(root, strings.concat("namerun_", drvs[i]), runav,
(30i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
assert(has(out.stdout, "seven ... ok"));
let nocav: []str = [driver(drvs[i]), "test", "-o", named,
pdir];
runcommand(root, strings.concat("noc_", drvs[i]), nocav,
(30i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 2);
assert(has(out.stderr, "-o needs -c for a package target"));
let mulav: []str = [driver(drvs[i]), "test", "-c", "-o",
named, strings.concat(root, "/...")];
runcommand(root, strings.concat("multi_", drvs[i]), mulav,
(30i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 2);
assert(has(out.stderr,
"cannot use -o with multiple packages"));
i += 1;
};
clean(root);
};