Files
ww/test/wcc/910_at_test.c
Hojun-Cho 906e17b128 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).
2026-05-12 03:14:20 +09:00

172 lines
3.9 KiB
C

/*
* 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;
}