wcc: @test marker attributes + runner
Hare-style `@test fn check_foo() void = { ... }` now parses. The
attribute is recognised by making the args list optional in
parseattrs: `@symbol("rt_syscall")` still requires the parens;
`@test` doesn't. Same change mirrored in lib/ww/parse/decl.ww.
The runner (test/wcc/910_at_test.c) scans a fixture for
`@test fn IDENT(`, synthesises a wrapper `main()` that calls each
test fn, builds it via `ww run`, and asserts exit 0. A failing
@test would either explicitly call abort or trip a runtime trap
(div-by-zero, etc.) and the whole driver exits non-zero.
The 910_at_test target sits alongside the existing C-side test
binaries; `make test` now runs 20 tests instead of 19.
Fixture: test/wcc/data/attest_pass.ww exercises two passing tests
(simple arithmetic and a match-with-yield).
This commit is contained in:
5
Makefile
5
Makefile
@@ -212,6 +212,7 @@ $(BIN) $(LIB) $(OBJ)/wcc $(OBJ)/ww $(OBJ)/wwdump $(OBJ)/w6c $(OBJ)/w6a $(OBJ)/w6
|
|||||||
TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \
|
TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \
|
||||||
$(BIN)/test_w6c $(BIN)/test_w6a $(BIN)/test_w6l $(BIN)/test_arch \
|
$(BIN)/test_w6c $(BIN)/test_w6a $(BIN)/test_w6l $(BIN)/test_arch \
|
||||||
$(BIN)/test_e2e $(BIN)/test_ffi $(BIN)/test_dyn $(BIN)/test_stdlib \
|
$(BIN)/test_e2e $(BIN)/test_ffi $(BIN)/test_dyn $(BIN)/test_stdlib \
|
||||||
|
$(BIN)/test_at_test \
|
||||||
$(BIN)/test_selfhost $(BIN)/test_w6a_ww $(BIN)/test_w6l_ww \
|
$(BIN)/test_selfhost $(BIN)/test_w6a_ww $(BIN)/test_w6l_ww \
|
||||||
$(BIN)/test_w6c_ww $(BIN)/test_ww_ww $(BIN)/test_self_rebuild \
|
$(BIN)/test_w6c_ww $(BIN)/test_ww_ww $(BIN)/test_self_rebuild \
|
||||||
$(BIN)/test_dyn_ww
|
$(BIN)/test_dyn_ww
|
||||||
@@ -255,6 +256,10 @@ $(BIN)/test_dyn: test/wcc/810_dyn.c $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \
|
|||||||
$(BIN)/test_stdlib: test/wcc/900_stdlib.c $(BIN)/w6c | $(BIN)
|
$(BIN)/test_stdlib: test/wcc/900_stdlib.c $(BIN)/w6c | $(BIN)
|
||||||
$(CC) $(CFLAGS) -o $@ $<
|
$(CC) $(CFLAGS) -o $@ $<
|
||||||
|
|
||||||
|
$(BIN)/test_at_test: test/wcc/910_at_test.c $(BIN)/ww $(BIN)/w6c $(BIN)/w6a \
|
||||||
|
$(BIN)/w6l $(LIB)/libwwrt.a | $(BIN)
|
||||||
|
$(CC) $(CFLAGS) -o $@ $<
|
||||||
|
|
||||||
$(BIN)/test_selfhost: test/wcc/990_selfhost.c $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \
|
$(BIN)/test_selfhost: test/wcc/990_selfhost.c $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \
|
||||||
$(BIN)/ww $(BIN)/wwdump $(BIN)/wwdump_ww $(LIB)/libwwrt.a | $(BIN)
|
$(BIN)/ww $(BIN)/wwdump $(BIN)/wwdump_ww $(LIB)/libwwrt.a | $(BIN)
|
||||||
$(CC) $(CFLAGS) -o $@ $<
|
$(CC) $(CFLAGS) -o $@ $<
|
||||||
|
|||||||
@@ -1241,9 +1241,13 @@ parseattrs(Parser *p)
|
|||||||
advance(p);
|
advance(p);
|
||||||
Node *a = newnode(p->a, N_ATTR, pp);
|
Node *a = newnode(p->a, N_ATTR, pp);
|
||||||
a->str = expectident(p);
|
a->str = expectident(p);
|
||||||
expect(p, TK_LPAREN);
|
/* `@name(args...)` for FFI-style attributes;
|
||||||
|
* `@name` (no parens) for marker-only attributes like
|
||||||
|
* `@test`. */
|
||||||
|
if (accept(p, TK_LPAREN)) {
|
||||||
a->list = parsearglist(p, TK_RPAREN);
|
a->list = parsearglist(p, TK_RPAREN);
|
||||||
expect(p, TK_RPAREN);
|
expect(p, TK_RPAREN);
|
||||||
|
}
|
||||||
if (head == NULL) head = a;
|
if (head == NULL) head = a;
|
||||||
else tail->next = a;
|
else tail->next = a;
|
||||||
tail = a;
|
tail = a;
|
||||||
|
|||||||
@@ -74,11 +74,14 @@ fn parseattrs(p: *parser) *node = {
|
|||||||
let id: str;
|
let id: str;
|
||||||
expectident(p, &id);
|
expectident(p, &id);
|
||||||
a.str = id;
|
a.str = id;
|
||||||
expecttok(p, TK_LPAREN, "expected '(' after attribute name");
|
// `@name(args...)` for FFI-style attrs; `@name` for marker-
|
||||||
|
// only attrs like @test (no parens).
|
||||||
|
if (accepttok(p, TK_LPAREN)) {
|
||||||
let arghead: *node = nil;
|
let arghead: *node = nil;
|
||||||
parsearglist(p, TK_RPAREN, &arghead);
|
parsearglist(p, TK_RPAREN, &arghead);
|
||||||
a.list = arghead;
|
a.list = arghead;
|
||||||
expecttok(p, TK_RPAREN, "expected ')' after attribute args");
|
expecttok(p, TK_RPAREN, "expected ')' after attribute args");
|
||||||
|
};
|
||||||
if (head == nil) { head = a; tail = a; }
|
if (head == nil) { head = a; tail = a; }
|
||||||
else { tail.next = a; tail = a; };
|
else { tail.next = a; tail = a; };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2793,11 +2793,14 @@ fn parseattrs(p: *parser) *node = {
|
|||||||
let id: str;
|
let id: str;
|
||||||
expectident(p, &id);
|
expectident(p, &id);
|
||||||
a.str = id;
|
a.str = id;
|
||||||
expecttok(p, TK_LPAREN, "expected '(' after attribute name");
|
// `@name(args...)` for FFI-style attrs; `@name` for marker-
|
||||||
|
// only attrs like @test (no parens).
|
||||||
|
if (accepttok(p, TK_LPAREN)) {
|
||||||
let arghead: *node = nil;
|
let arghead: *node = nil;
|
||||||
parsearglist(p, TK_RPAREN, &arghead);
|
parsearglist(p, TK_RPAREN, &arghead);
|
||||||
a.list = arghead;
|
a.list = arghead;
|
||||||
expecttok(p, TK_RPAREN, "expected ')' after attribute args");
|
expecttok(p, TK_RPAREN, "expected ')' after attribute args");
|
||||||
|
};
|
||||||
if (head == nil) { head = a; tail = a; }
|
if (head == nil) { head = a; tail = a; }
|
||||||
else { tail.next = a; tail = a; };
|
else { tail.next = a; tail = a; };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2793,11 +2793,14 @@ fn parseattrs(p: *parser) *node = {
|
|||||||
let id: str;
|
let id: str;
|
||||||
expectident(p, &id);
|
expectident(p, &id);
|
||||||
a.str = id;
|
a.str = id;
|
||||||
expecttok(p, TK_LPAREN, "expected '(' after attribute name");
|
// `@name(args...)` for FFI-style attrs; `@name` for marker-
|
||||||
|
// only attrs like @test (no parens).
|
||||||
|
if (accepttok(p, TK_LPAREN)) {
|
||||||
let arghead: *node = nil;
|
let arghead: *node = nil;
|
||||||
parsearglist(p, TK_RPAREN, &arghead);
|
parsearglist(p, TK_RPAREN, &arghead);
|
||||||
a.list = arghead;
|
a.list = arghead;
|
||||||
expecttok(p, TK_RPAREN, "expected ')' after attribute args");
|
expecttok(p, TK_RPAREN, "expected ')' after attribute args");
|
||||||
|
};
|
||||||
if (head == nil) { head = a; tail = a; }
|
if (head == nil) { head = a; tail = a; }
|
||||||
else { tail.next = a; tail = a; };
|
else { tail.next = a; tail = a; };
|
||||||
};
|
};
|
||||||
|
|||||||
171
test/wcc/910_at_test.c
Normal file
171
test/wcc/910_at_test.c
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
/*
|
||||||
|
* 910_at_test — runs @test fn blocks in a source file by
|
||||||
|
* generating a synthetic main() that calls each one. A test
|
||||||
|
* passes iff the generated binary exits 0; if any @test calls
|
||||||
|
* abort/exit(1), the run fails.
|
||||||
|
*
|
||||||
|
* Scanning is regex-free, matching the literal sequence
|
||||||
|
* `\n@test\s+fn\s+(IDENT)`. Same parser shape as ww accepts.
|
||||||
|
*/
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <ctype.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
|
||||||
|
static int
|
||||||
|
runwait(const char *cmd)
|
||||||
|
{
|
||||||
|
int rc = system(cmd);
|
||||||
|
if (rc == -1) return -1;
|
||||||
|
if (WIFEXITED(rc)) return WEXITSTATUS(rc);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
isws(int c)
|
||||||
|
{
|
||||||
|
return c == ' ' || c == '\t' || c == '\n' || c == '\r';
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
isident(int c)
|
||||||
|
{
|
||||||
|
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
|
||||||
|
(c >= '0' && c <= '9') || c == '_';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* find_attest — scan src for `@test\s+fn\s+(ident)`; on a match,
|
||||||
|
* write the identifier into `out` (NUL-terminated) and return the
|
||||||
|
* scan position past the matched ident. Returns -1 when no more. */
|
||||||
|
static long
|
||||||
|
find_attest(const char *src, long pos, char *out, size_t outsz)
|
||||||
|
{
|
||||||
|
long n = (long)strlen(src);
|
||||||
|
while (pos < n) {
|
||||||
|
const char *q = strstr(src + pos, "@test");
|
||||||
|
if (q == NULL) return -1;
|
||||||
|
long off = q - src;
|
||||||
|
/* must be a top-level marker: previous non-space char is
|
||||||
|
* '\n' or we're at the file start. */
|
||||||
|
long pre = off - 1;
|
||||||
|
while (pre >= 0 && (src[pre] == ' ' || src[pre] == '\t')) pre--;
|
||||||
|
if (pre >= 0 && src[pre] != '\n') {
|
||||||
|
pos = off + 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
long c = off + 5; /* past "@test" */
|
||||||
|
while (c < n && isws(src[c])) c++;
|
||||||
|
if (c + 2 > n || strncmp(src + c, "fn", 2) != 0) {
|
||||||
|
pos = off + 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
c += 2;
|
||||||
|
if (c < n && isident(src[c])) {
|
||||||
|
pos = off + 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
while (c < n && isws(src[c])) c++;
|
||||||
|
long ids = c;
|
||||||
|
while (c < n && isident(src[c])) c++;
|
||||||
|
if (c == ids) {
|
||||||
|
pos = off + 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
size_t len = (size_t)(c - ids);
|
||||||
|
if (len + 1 > outsz) return -1;
|
||||||
|
memcpy(out, src + ids, len);
|
||||||
|
out[len] = '\0';
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static char *
|
||||||
|
slurp(const char *path)
|
||||||
|
{
|
||||||
|
FILE *f = fopen(path, "rb");
|
||||||
|
if (!f) return NULL;
|
||||||
|
fseek(f, 0, SEEK_END);
|
||||||
|
long n = ftell(f);
|
||||||
|
fseek(f, 0, SEEK_SET);
|
||||||
|
char *b = malloc((size_t)n + 1);
|
||||||
|
if (!b) { fclose(f); return NULL; }
|
||||||
|
if (fread(b, 1, (size_t)n, f) != (size_t)n) {
|
||||||
|
free(b); fclose(f); return NULL;
|
||||||
|
}
|
||||||
|
b[n] = '\0';
|
||||||
|
fclose(f);
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
runtests(const char *path, const char *bin)
|
||||||
|
{
|
||||||
|
char *src = slurp(path);
|
||||||
|
if (!src) {
|
||||||
|
fprintf(stderr, "910 FAIL: cannot read %s\n", path);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Build a synthetic test driver: original source + a main()
|
||||||
|
* that calls each @test fn. Each test that runs to completion
|
||||||
|
* (no abort/exit) counts as a pass. */
|
||||||
|
char tmpsrc[256];
|
||||||
|
snprintf(tmpsrc, sizeof tmpsrc, "/tmp/wwd_attest_%d.ww", getpid());
|
||||||
|
FILE *out = fopen(tmpsrc, "wb");
|
||||||
|
if (!out) { free(src); return -1; }
|
||||||
|
fputs(src, out);
|
||||||
|
fputs("\nexport fn main() i32 = {\n", out);
|
||||||
|
|
||||||
|
long pos = 0;
|
||||||
|
int count = 0;
|
||||||
|
for (;;) {
|
||||||
|
char name[128];
|
||||||
|
long next = find_attest(src, pos, name, sizeof name);
|
||||||
|
if (next < 0) break;
|
||||||
|
fprintf(out, "\t%s();\n", name);
|
||||||
|
pos = next;
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
fputs("\treturn 0;\n};\n", out);
|
||||||
|
fclose(out);
|
||||||
|
free(src);
|
||||||
|
|
||||||
|
if (count == 0) {
|
||||||
|
fprintf(stderr, "910 FAIL: no @test fns found in %s\n", path);
|
||||||
|
unlink(tmpsrc);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
char cmd[2048];
|
||||||
|
snprintf(cmd, sizeof cmd, "%s/ww run %s 2>/dev/null", bin, tmpsrc);
|
||||||
|
int rc = runwait(cmd);
|
||||||
|
unlink(tmpsrc);
|
||||||
|
if (rc != 0) {
|
||||||
|
fprintf(stderr, "910 FAIL: %s: %d @test fn(s), driver exited %d\n",
|
||||||
|
path, count, rc);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
main(void)
|
||||||
|
{
|
||||||
|
const char *bin = getenv("BIN");
|
||||||
|
if (!bin) bin = "out/bin";
|
||||||
|
const char *files[] = {
|
||||||
|
"test/wcc/data/attest_pass.ww",
|
||||||
|
NULL,
|
||||||
|
};
|
||||||
|
int total = 0;
|
||||||
|
for (int i = 0; files[i]; i++) {
|
||||||
|
int n = runtests(files[i], bin);
|
||||||
|
if (n < 0) return 1;
|
||||||
|
total += n;
|
||||||
|
}
|
||||||
|
printf("@test: %d test(s) ran ok\n", total);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
21
test/wcc/data/attest_pass.ww
Normal file
21
test/wcc/data/attest_pass.ww
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
// @test fixture: every test passes (exits without aborting).
|
||||||
|
|
||||||
|
@test fn check_add() void = {
|
||||||
|
let a: i32 = 2;
|
||||||
|
let b: i32 = 3;
|
||||||
|
let c: i32 = a + b;
|
||||||
|
if (c != 5) {
|
||||||
|
let _: i32 = 1 / 0; // abort via div-by-zero would also work
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
@test fn check_match() void = {
|
||||||
|
let r: (i32 | str) = 7;
|
||||||
|
let v: i32 = match (r) {
|
||||||
|
case let n: i32 => yield n;
|
||||||
|
case let s: str => yield 0;
|
||||||
|
};
|
||||||
|
if (v != 7) {
|
||||||
|
let _: i32 = 1 / 0;
|
||||||
|
};
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user