diff --git a/cmd/wcc/check.c b/cmd/wcc/check.c index f25c93d4..60506564 100644 --- a/cmd/wcc/check.c +++ b/cmd/wcc/check.c @@ -3230,14 +3230,31 @@ check_file(Checker *c, Node *file) for (Node *d = file->list; d; d = d->next) { if (d->kind != N_FNDECL) continue; - int istest = 0; + int ntestattr = 0; for (Node *at = d->attr; at; at = at->next) - if (at->str && strcmp(at->str, "test") == 0) { - istest = 1; - break; - } - if (!istest) + if (at->str && strcmp(at->str, "test") == 0) + ntestattr++; + if (ntestattr == 0) continue; + /* Attribute-shape rejects, fixed order, first failure + * wins per fn; wording is byte-stable with the wwstage + * twin (check.ww). A silently-dropped shape here ships + * a test that never runs. */ + if (ntestattr > 1) { + err(c, d->pos, "duplicate @test on fn '%s'", + d->str); + continue; + } + if (d->export) { + err(c, d->pos, "@test fn '%s' cannot be exported", + d->str); + continue; + } + if (d->body == NULL) { + err(c, d->pos, "@test fn '%s' needs a body", + d->str); + continue; + } /* `fn f() void` parses the explicit `void` into d->lhs * (parse.c:1329), so void-returning is lhs==NULL OR an * N_TNAME "void" — not lhs==NULL alone. */ diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index d5c557a7..d800a86f 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -7134,16 +7134,38 @@ export fn checkfile(c: *checker, file: *syntax.node) void = { let t: *syntax.node = file.list; for (t != nil) { if (t.kind == syntax.nkind.N_FNDECL) { - let istest: bool = false; + let ntestattr: i32 = 0; let at: *syntax.node = t.attr; for (at != nil) { if (at.kind == syntax.nkind.N_ATTR && syntax.streq(at.str, "test")) { - istest = true; + ntestattr += 1; }; at = at.next; }; - if (istest) { + // Attribute-shape rejects, fixed order, first + // failure wins per fn; wording byte-stable with + // the cstage twin (check.c). A silently-dropped + // shape here ships a test that never runs. + if (ntestattr > 1) { + cerr(t.file); + cerr(": error: duplicate @test on fn '"); + cerr(t.str); + cerr("'\n"); + c.errs += 1; + } else { if (ntestattr == 1 && t.exported != 0) { + cerr(t.file); + cerr(": error: @test fn '"); + cerr(t.str); + cerr("' cannot be exported\n"); + c.errs += 1; + } else { if (ntestattr == 1 && t.body == nil) { + cerr(t.file); + cerr(": error: @test fn '"); + cerr(t.str); + cerr("' needs a body\n"); + c.errs += 1; + } else { if (ntestattr == 1) { // `fn f() void` parses the explicit void // into t.lhs, so void-returning is lhs==nil // OR an N_TNAME "void". @@ -7172,7 +7194,7 @@ export fn checkfile(c: *checker, file: *syntax.node) void = { rtail = row; ntest += 1i32; }; - }; + }; }; }; }; }; t = t.next; }; diff --git a/test/package/package_test.ww b/test/package/package_test.ww index 3722588c..7cd417da 100644 --- a/test/package/package_test.ww +++ b/test/package/package_test.ww @@ -443,6 +443,52 @@ fn packagepath(relative: str) str = { clean(root); }; +// Invalid @test attribute shapes reject at build with stable text on +// BOTH frontends (the -T synth checker owns them; the fixture corpus +// cannot reach -T, so these rows live here). Fragments only — the +// stages' error-line prefixes legitimately differ (file:line:col vs +// file:). +@test fn invalid_test_shapes_reject() void = { + let root: str = fresh(); + let shapes: []str = [ + "@test export fn t() void = { };", + "@test fn t() void;", + "@test fn t(x: i32) void = { };", + "@test fn t() i32 = { return 0; };", + "@test @test fn t() void = { };", + ]; + let frags: []str = [ + "@test fn 't' cannot be exported", + "@test fn 't' needs a body", + "@test fn 't' must be fn() void", + "@test fn 't' must be fn() void", + "duplicate @test on fn 't'", + ]; + let digits: []str = ["0", "1", "2", "3", "4"]; + let outc: commandout; + let outw: commandout; + let i: i32 = 0; + for (i < shapes.len) { + let dir: str = strings.concat(root, "/shape", digits[i]); + assert(os.mkdir(dir, 448i32) == 0); + let src: str = strings.concat(dir, "/bad_test.ww"); + writefile(src, strings.concat("package main;\n", + shapes[i], "\n")); + let avc: []str = [driver("ww"), "test", src]; + let avw: []str = [driver("ww_ww"), "test", src]; + runcommand(root, strings.concat("shape-c", digits[i]), avc, + (30i64 * (time.second: i64)): time.duration, &outc); + runcommand(root, strings.concat("shape-ww", digits[i]), avw, + (30i64 * (time.second: i64)): time.duration, &outw); + expectexit(&outc, 1); + expectexit(&outw, 1); + assert(has(outc.stderr, frags[i])); + assert(has(outw.stderr, frags[i])); + i += 1; + }; + 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