ww: test sources are *_test.ww only (Go contract)
Go compiles only _test.go files as tests; discovery now keys on the
_test.ww suffix alone. The line-leading-@test compatibility allowance
(noncanonical filenames admitted as test sources) is removed from both
driver stages and the coordinator. An @test declaration outside a
*_test.ww file is rejected loudly ("@test declaration outside
*_test.ww", wording byte-identical cs/ww) instead of silently running
under compose or silently dropping in a non-T build (#6). Tree audit
found zero real carriers; the two allowance fixtures flip canonical
(dep_test.ww, widget_test.ww). New pins: direnum attest-noncanon
reject row (both-stage stderr parity) and the coordinator
noncanonical_attest_rejected package row.
This commit is contained in:
@@ -196,7 +196,7 @@ locate_import_in(const char *dir, const char *path_form, char *out,
|
||||
*
|
||||
* #98: "a module IS the directory" — a directory-package on ANY entry
|
||||
* wins over a same-named sibling FILE on an EARLIER entry. The driver
|
||||
* builds the searchpath srcd-first; a co-located `lib/<mod>/<mod>test.ww`
|
||||
* builds the searchpath srcd-first; a co-located `lib/<mod>/<x>_test.ww`
|
||||
* entry makes srcd = lib/<mod>, so a self-named `import <mod>` would
|
||||
* else file-hit the sibling lib/<mod>/<mod>.ww and fold it
|
||||
* inline under the wrong module-reset → "package <mod> does not match
|
||||
@@ -243,9 +243,9 @@ strs_cmp(const void *a, const void *b)
|
||||
return strcmp(sa, sb);
|
||||
}
|
||||
|
||||
/* A temporary migration rule keeps the pre-underscore corpus working without
|
||||
* reserving every filename ending in "test.ww": only a real line-leading
|
||||
* @test declaration makes a noncanonical source test-only. */
|
||||
/* Go's contract: only *_test.ww is a test source. A line-leading @test
|
||||
* declaration anywhere else would be silently dropped by a non-T build
|
||||
* (#6, the Hare model), so directory enumeration rejects it loudly. */
|
||||
static int
|
||||
file_has_line_test(const char *path)
|
||||
{
|
||||
@@ -267,9 +267,10 @@ file_has_line_test(const char *path)
|
||||
}
|
||||
|
||||
/* enumerate_dir_ww — collect production *.ww paths in `dirpath`, excluding
|
||||
* canonical *_test.ww plus the explicit line-leading-@test compatibility
|
||||
* sources, then sort byte-wise. This is the sole directory-membership
|
||||
* discovery path; the owning seppkg retains the returned list. */
|
||||
* *_test.ww test sources, then sort byte-wise. A line-leading @test in
|
||||
* any other source is diagnosed here and returns -2. This is the sole
|
||||
* directory-membership discovery path; the owning seppkg retains the
|
||||
* returned list. */
|
||||
static int
|
||||
enumerate_dir_ww(const char *dirpath, char ***out_files)
|
||||
{
|
||||
@@ -285,9 +286,18 @@ enumerate_dir_ww(const char *dirpath, char ***out_files)
|
||||
if (strcmp(nm + nl - 3, ".ww") != 0) continue;
|
||||
char path[2048];
|
||||
snprintf(path, sizeof path, "%s/%s", dirpath, nm);
|
||||
if ((nl >= 8 && strcmp(nm + nl - 8, "_test.ww") == 0)
|
||||
|| file_has_line_test(path))
|
||||
if (nl >= 8 && strcmp(nm + nl - 8, "_test.ww") == 0)
|
||||
continue;
|
||||
if (file_has_line_test(path)) {
|
||||
fprintf(stderr,
|
||||
"ww: %s: @test declaration outside *_test.ww\n",
|
||||
path);
|
||||
for (int i = 0; i < n; i++) free(arr[i]);
|
||||
free(arr);
|
||||
closedir(d);
|
||||
*out_files = NULL;
|
||||
return -2;
|
||||
}
|
||||
if (n + 1 > cap) {
|
||||
cap = cap ? cap * 2 : 8;
|
||||
arr = realloc(arr, cap * sizeof *arr);
|
||||
@@ -634,7 +644,9 @@ sep_load_pkg(struct sepgraph *g, int pi, const char *searchpath)
|
||||
if (g->pkg[pi].is_dir) {
|
||||
g->pkg[pi].nsources = enumerate_dir_ww(g->pkg[pi].entry,
|
||||
&g->pkg[pi].sources);
|
||||
if (g->pkg[pi].nsources < 0) {
|
||||
if (g->pkg[pi].nsources == -2) {
|
||||
rc = -1; /* diagnosed in enumerate_dir_ww */
|
||||
} else if (g->pkg[pi].nsources < 0) {
|
||||
fprintf(stderr, "ww: cannot read directory %s\n",
|
||||
g->pkg[pi].entry);
|
||||
rc = -1;
|
||||
|
||||
@@ -194,7 +194,10 @@ and nonempty-version-constant assertions.
|
||||
`ww test` delegates directory package requests to the native package
|
||||
coordinator. The coordinator owns discovery, package grouping, same-package and
|
||||
external-package test composition, filtering, result aggregation, and its
|
||||
internal temporary workspace. A trailing `...` path element (`ww test lib/...`,
|
||||
internal temporary workspace. Test sources are exclusively `*_test.ww` (Go's
|
||||
`_test.go` contract): a line-leading `@test` declaration in any other source is
|
||||
rejected loudly — by both driver stages at directory enumeration and by the
|
||||
coordinator at source classification. A trailing `...` path element (`ww test lib/...`,
|
||||
Go's `./...` form) is recognized by both driver stages before path resolution
|
||||
and walks the tree rooted at the prefix: every subdirectory whose name does not
|
||||
begin with `.` or `_` is descended with the same lstat/no-symlink discipline,
|
||||
|
||||
@@ -107,7 +107,7 @@ fn pkgfailpath(path: str, reason: str) void = {
|
||||
fn pkgusage() void = {
|
||||
let s: str = strings.concat(
|
||||
"usage: wwtest package [-c] [-list] [-j N] [-I DIR] [-w DIR] [-run|-filter GLOB] [-timeout-ms=N] [DIR | DIR/...] [-- GLOB ...]\n",
|
||||
" *_test.ww is canonical; noncanonical files require an actual @test declaration\n",
|
||||
" *_test.ww is the sole test-source form; @test elsewhere is rejected\n",
|
||||
" -c retains the compiled package binaries; -j N runs up to N package groups at once\n",
|
||||
" -w DIR keys a persistent per-package-group build workdir under DIR\n");
|
||||
pkgput(os.STDERR_FILENO, s);
|
||||
@@ -846,8 +846,14 @@ export fn packagecommand(args: []str) int = {
|
||||
s.dir = strings.dup(pkgdirname(ds.paths[i]));
|
||||
s.pkg = strings.dup(pn);
|
||||
s.attest = pkgattest(body);
|
||||
s.test = strings.hassuffix(pkgbase(ds.paths[i]), "_test.ww")
|
||||
|| s.attest;
|
||||
s.test = strings.hassuffix(pkgbase(ds.paths[i]), "_test.ww");
|
||||
// Go's contract: only *_test.ww is a test source; @test
|
||||
// anywhere else fails loudly rather than run or drop silently.
|
||||
if (s.attest && !s.test) {
|
||||
pkgfailpath(ds.paths[i],
|
||||
"@test declaration outside *_test.ww");
|
||||
return 1;
|
||||
};
|
||||
append(srcs, s);
|
||||
i += 1;
|
||||
};
|
||||
|
||||
@@ -352,10 +352,10 @@ fn locateimport(dirs: *u8, name: *u8, namelen: u64,
|
||||
return nil;
|
||||
};
|
||||
|
||||
// Temporary compatibility for the repository's pre-underscore test sources:
|
||||
// a noncanonical filename is test-only only when its body contains a real
|
||||
// line-leading @test declaration. This is the wwstage twin of
|
||||
// cmd/ww/main.c:file_has_line_test.
|
||||
// Go's contract: only *_test.ww is a test source. A line-leading @test
|
||||
// declaration anywhere else would be silently dropped by a non-T build
|
||||
// (#6, the Hare model), so directory enumeration rejects it loudly.
|
||||
// This is the wwstage twin of cmd/ww/main.c:file_has_line_test.
|
||||
fn dirfileattest(dirpath: *u8, name: *u8) bool = {
|
||||
let path: *u8 = joinpath(dirpath, name);
|
||||
let fd: i32 = os.open(pathstr(path), os.flag.RDONLY, 0i32);
|
||||
@@ -393,20 +393,20 @@ fn dirfileattest(dirpath: *u8, name: *u8) bool = {
|
||||
return false;
|
||||
};
|
||||
|
||||
// Filter for production enumeration: keep `*.ww` minus canonical
|
||||
// `*_test.ww` and the explicit compatibility sources above. Returns
|
||||
// true to keep.
|
||||
fn dirfilekeep(dirpath: *u8, name: *u8, nlen: u64) bool = {
|
||||
// Classify a directory entry for production enumeration: 1 keep,
|
||||
// 0 skip (non-source or *_test.ww), -1 @test outside *_test.ww
|
||||
// (caller diagnoses and fails).
|
||||
fn dirfileclass(dirpath: *u8, name: *u8, nlen: u64) i32 = {
|
||||
// nlen<=3 guard kept: a bare ".ww" (len 3) is rejected here but
|
||||
// would pass strings.hassuffix(".ww"); preserves cstage parity.
|
||||
if (nlen <= 3u64) { return false; };
|
||||
if (nlen <= 3u64) { return 0; };
|
||||
let s: str;
|
||||
s.ptr = name;
|
||||
s.len = nlen: i32;
|
||||
if (!strings.hassuffix(s, ".ww")) { return false; };
|
||||
if (strings.hassuffix(s, "_test.ww")
|
||||
|| dirfileattest(dirpath, name)) { return false; };
|
||||
return true;
|
||||
if (!strings.hassuffix(s, ".ww")) { return 0; };
|
||||
if (strings.hassuffix(s, "_test.ww")) { return 0; };
|
||||
if (dirfileattest(dirpath, name)) { return -1; };
|
||||
return 1;
|
||||
};
|
||||
|
||||
// Byte-wise memcmp returning < 0, 0, > 0. Rule-10 byte-id requires
|
||||
@@ -429,10 +429,11 @@ fn bytecmp(a: *u8, alen: u64, b: *u8, blen: u64) i32 = {
|
||||
return 0;
|
||||
};
|
||||
|
||||
// enumeratedir — list production *.ww paths of `dirpath` (less canonical
|
||||
// and compatibility test sources), byte-sort. Returns
|
||||
// one exact pointer array of NUL-terminated full paths. This is the sole
|
||||
// directory-membership discovery path; the owning seppkg retains the list.
|
||||
// enumeratedir — list production *.ww paths of `dirpath` (less *_test.ww
|
||||
// test sources), byte-sort. A line-leading @test in any other source is
|
||||
// diagnosed here and returns -2. Returns one exact pointer array of
|
||||
// NUL-terminated full paths. This is the sole directory-membership
|
||||
// discovery path; the owning seppkg retains the list.
|
||||
fn enumeratedir(dirpath: *u8) (**u8, i32) = {
|
||||
let fd: i32 = os.open(pathstr(dirpath), os.flag.RDONLY, 0i32);
|
||||
if (fd < 0) { return nil: **u8, -1; };
|
||||
@@ -456,7 +457,15 @@ fn enumeratedir(dirpath: *u8) (**u8, i32) = {
|
||||
let reclen: u64 = blo + (bhi * 256u64);
|
||||
let nm: *u8 = buf.ptr + off + 19u64;
|
||||
let nl: u64 = cstrlen(nm);
|
||||
if (dirfilekeep(dirpath, nm, nl)) {
|
||||
let cls: i32 = dirfileclass(dirpath, nm, nl);
|
||||
if (cls < 0) {
|
||||
cerr("ww: ");
|
||||
cerr(pathstr(joinpath(dirpath, nm)));
|
||||
cerr(": @test declaration outside *_test.ww\n");
|
||||
os.close(fd);
|
||||
return nil: **u8, -2;
|
||||
};
|
||||
if (cls > 0) {
|
||||
if (n >= cap) {
|
||||
let ncap: i32 = cap * 2;
|
||||
let nn: []*u8 = alloc([], ncap: u64)!;
|
||||
@@ -1000,7 +1009,10 @@ fn seploadpkg(g: *sepgraph, pi: i32, searchpath: *u8) i32 = {
|
||||
sources, nsources = enumeratedir(g.pkg[pi].entry);
|
||||
g.pkg[pi].sources = sources;
|
||||
g.pkg[pi].nsources = nsources;
|
||||
if (g.pkg[pi].nsources < 0) {
|
||||
if (g.pkg[pi].nsources == -2) {
|
||||
// diagnosed in enumeratedir
|
||||
rc = -1;
|
||||
} else { if (g.pkg[pi].nsources < 0) {
|
||||
cerr("ww: cannot read directory ");
|
||||
cerr(pathstr(g.pkg[pi].entry));
|
||||
cerr("\n");
|
||||
@@ -1010,7 +1022,7 @@ fn seploadpkg(g: *sepgraph, pi: i32, searchpath: *u8) i32 = {
|
||||
cerr(pathstr(g.pkg[pi].entry));
|
||||
cerr(": directory contains no WW package sources\n");
|
||||
rc = -1;
|
||||
}; };
|
||||
}; }; };
|
||||
let i: i32 = 0;
|
||||
for (i < g.pkg[pi].nsources) {
|
||||
if (rc == 0) {
|
||||
|
||||
7
test/package/dependency/dep/dep_test.ww
Normal file
7
test/package/dependency/dep/dep_test.ww
Normal file
@@ -0,0 +1,7 @@
|
||||
package dep;
|
||||
|
||||
// Go's contract: a package's *_test.ww sources are not part of the
|
||||
// imported package. Must be excluded when dep is imported by root.
|
||||
@test fn imported_dependency_test_must_not_leak() void = {
|
||||
assert(false);
|
||||
};
|
||||
@@ -1,7 +0,0 @@
|
||||
package dep;
|
||||
|
||||
// Noncanonical migration spelling: the actual line-leading @test makes this
|
||||
// test-only. It must be excluded when dep is imported by the root package.
|
||||
@test fn imported_dependency_test_must_not_leak() void = {
|
||||
assert(false);
|
||||
};
|
||||
@@ -232,6 +232,26 @@ fn packagepath(relative: str) str = {
|
||||
clean(root);
|
||||
};
|
||||
|
||||
// Go's contract: only *_test.ww sources are test sources; a line-leading
|
||||
// @test anywhere else fails classification loudly rather than run or
|
||||
// drop silently.
|
||||
@test fn noncanonical_attest_rejected() void = {
|
||||
let root: str = fresh();
|
||||
let pkgdir: str = strings.concat(root, "/noncanon");
|
||||
assert(os.mkdir(pkgdir, 448i32) == 0);
|
||||
writefile(strings.concat(pkgdir, "/noncanon.ww"), strings.concat(
|
||||
"package noncanon;\n",
|
||||
"@test fn hidden() void = { assert(false); };\n"));
|
||||
let av: []str = [driver("ww"), "test", pkgdir];
|
||||
let out: commandout;
|
||||
runcommand(root, "noncanon", av,
|
||||
(30i64 * (time.second: i64)): time.duration, &out);
|
||||
expectexit(&out, 1);
|
||||
assert(has(out.stderr, "@test declaration outside *_test.ww"));
|
||||
assert(!has(out.stdout, "hidden"));
|
||||
clean(root);
|
||||
};
|
||||
|
||||
@test fn empty_and_invalid_package_classes() void = {
|
||||
let root: str = fresh();
|
||||
let av: []str = [driver("ww"), "test", packagepath("no_tests")];
|
||||
|
||||
@@ -89,7 +89,7 @@ fn cmpsepwork(label: str, csdir: str, wwdir: str) void = {
|
||||
@test fn coloimport() void = {
|
||||
let td: str = testenv.fresh();
|
||||
let inc: str = strings.concat(testenv.repo(), "/test/wcc/data/colo98");
|
||||
let entry: str = strings.concat(inc, "/widget/widgettest.ww");
|
||||
let entry: str = strings.concat(inc, "/widget/widget_test.ww");
|
||||
let drvs: []str = ["ww", "ww_ww"];
|
||||
let tags: []str = ["cs", "ww"];
|
||||
let i: i32 = 0;
|
||||
|
||||
@@ -24,8 +24,9 @@ package direnum_test;
|
||||
// rejects — the authoritative loader-reject table over private trees:
|
||||
// root-conflict ("conflicting package names"), import-leaf ("does
|
||||
// not match import path"), root-missing and root-invalid ("invalid
|
||||
// or missing package clause"); per row both stages fail, carry the
|
||||
// needle, and their captured stderr is byte-identical.
|
||||
// or missing package clause"), attest-noncanon ("@test declaration
|
||||
// outside *_test.ww" — Go's test-file contract); per row both stages
|
||||
// fail, carry the needle, and their captured stderr is byte-identical.
|
||||
//
|
||||
// Dropped C machinery, not assertions: the ww_ww-absent skip gate
|
||||
// (the Make target declares both drivers) and the unlink/rmdir
|
||||
@@ -164,11 +165,13 @@ fn rejectpair(label: str, td: str, target: str, needle: str) void = {
|
||||
let wanted: str = strings.concat(importbad, "/wanted");
|
||||
let missing: str = strings.concat(td, "/missing");
|
||||
let invalid: str = strings.concat(td, "/invalid");
|
||||
let attbad: str = strings.concat(td, "/attbad");
|
||||
assert(os.mkdir(rootbad, 493) == 0);
|
||||
assert(os.mkdir(importbad, 493) == 0);
|
||||
assert(os.mkdir(wanted, 493) == 0);
|
||||
assert(os.mkdir(missing, 493) == 0);
|
||||
assert(os.mkdir(invalid, 493) == 0);
|
||||
assert(os.mkdir(attbad, 493) == 0);
|
||||
testenv.writefile(strings.concat(rootbad, "/a.ww"),
|
||||
"package rootbad;\nfn main() i32 = { return 0; };\n");
|
||||
testenv.writefile(strings.concat(rootbad, "/b.ww"),
|
||||
@@ -182,15 +185,21 @@ fn rejectpair(label: str, td: str, target: str, needle: str) void = {
|
||||
"fn main() i32 = { return 0; };\n");
|
||||
testenv.writefile(strings.concat(invalid, "/a.ww"),
|
||||
"package 7bad;\nfn main() i32 = { return 0; };\n");
|
||||
testenv.writefile(strings.concat(attbad, "/a.ww"),
|
||||
"package attbad;\nfn main() i32 = { return 0; };\n");
|
||||
testenv.writefile(strings.concat(attbad, "/t.ww"), strings.concat(
|
||||
"package attbad;\n",
|
||||
"@test fn hidden() void = { assert(false); };\n"));
|
||||
|
||||
let tags: []str = ["root-conflict", "import-leaf", "root-missing",
|
||||
"root-invalid"];
|
||||
"root-invalid", "attest-noncanon"];
|
||||
let targets: []str = [rootbad, strings.concat(importbad,
|
||||
"/entry.ww"), missing, invalid];
|
||||
"/entry.ww"), missing, invalid, attbad];
|
||||
let needles: []str = ["conflicting package names",
|
||||
"does not match import path",
|
||||
"invalid or missing package clause",
|
||||
"invalid or missing package clause"];
|
||||
"invalid or missing package clause",
|
||||
"@test declaration outside *_test.ww"];
|
||||
let r: i32 = 0;
|
||||
for (r < tags.len) {
|
||||
rejectpair(tags[r], td, targets[r], needles[r]);
|
||||
|
||||
Reference in New Issue
Block a user