build: omit named test source operands
This commit is contained in:
@@ -358,6 +358,78 @@ skipws(Lex *l)
|
||||
}
|
||||
}
|
||||
|
||||
/* Return the next initial import token without ever lexing the first ordinary
|
||||
* declaration token. Go's named-file reader consumes header trivia while
|
||||
* looking for another import, so NUL/comment diagnostics in that trivia remain
|
||||
* visible; a malformed UTF-8 byte or non-leading BOM that itself begins the
|
||||
* ordinary body is outside the header and must not be diagnosed here. */
|
||||
int
|
||||
lexheaderimport(Lex *l, Tok *out)
|
||||
{
|
||||
for (;;) {
|
||||
if (l->pos >= l->srclen)
|
||||
return 0;
|
||||
unsigned char c = (unsigned char)l->src[l->pos];
|
||||
if (c == 0) {
|
||||
/* The loader necessarily reaches this byte while deciding whether
|
||||
* another import follows. Preserve the source decoder diagnostic. */
|
||||
lskipnul(l);
|
||||
return 0;
|
||||
}
|
||||
if ((c >= 0x80 && !utf8bytevalid(l->src, l->srclen, l->pos))
|
||||
|| bomat(l->src, l->srclen, l->pos))
|
||||
return 0;
|
||||
if (c == ' ' || c == '\t' || c == '\r' || c == '\n') {
|
||||
lget(l);
|
||||
continue;
|
||||
}
|
||||
if (c == '/' && l->pos + 1 < l->srclen
|
||||
&& l->src[l->pos + 1] == '/') {
|
||||
lget(l); lget(l);
|
||||
linecomment(l);
|
||||
if (l->errs != 0)
|
||||
return 0;
|
||||
continue;
|
||||
}
|
||||
if (c == '/' && l->pos + 1 < l->srclen
|
||||
&& l->src[l->pos + 1] == '*') {
|
||||
lget(l); lget(l);
|
||||
int prev = -1;
|
||||
for (;;) {
|
||||
int x = lget(l);
|
||||
if (x < 0) {
|
||||
Pos p = lpos(l);
|
||||
errorf(p, "unterminated /* comment");
|
||||
l->errs++;
|
||||
return 0;
|
||||
}
|
||||
if (prev == '*' && x == '/')
|
||||
break;
|
||||
prev = x;
|
||||
}
|
||||
if (l->errs != 0)
|
||||
return 0;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
/* A compiler-owned source-boundary directive is an ordinary declaration
|
||||
* boundary here, just as it was when lexnext eagerly exposed the token. */
|
||||
if (l->modreset || l->modpath != NULL)
|
||||
return 0;
|
||||
static const char kw[] = "import";
|
||||
const u64 n = sizeof kw - 1;
|
||||
if (l->srclen - l->pos < n
|
||||
|| memcmp(l->src + l->pos, kw, n) != 0)
|
||||
return 0;
|
||||
if (l->srclen - l->pos > n
|
||||
&& isidcont((unsigned char)l->src[l->pos + n]))
|
||||
return 0;
|
||||
*out = lexnext(l);
|
||||
return out->kind == TK_USE;
|
||||
}
|
||||
|
||||
static u64
|
||||
parseint(const char *s, u64 n, int base, int *ok)
|
||||
{
|
||||
|
||||
145
cmd/wcc/parse.c
145
cmd/wcc/parse.c
@@ -35,6 +35,17 @@ advance(Parser *p)
|
||||
}
|
||||
}
|
||||
|
||||
static int
|
||||
advanceheaderimport(Parser *p)
|
||||
{
|
||||
Tok next;
|
||||
if (!lexheaderimport(p->l, &next))
|
||||
return 0;
|
||||
p->cur = next;
|
||||
p->hasla = 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static Tok
|
||||
peek(Parser *p)
|
||||
{
|
||||
@@ -1359,6 +1370,140 @@ parseuse(Parser *p)
|
||||
return n;
|
||||
}
|
||||
|
||||
/* Parse one initial import without pulling the declaration that follows it
|
||||
* into the header pass. This intentionally has stricter, single-error
|
||||
* recovery than parseuse: named test-source build loading needs only decide
|
||||
* whether the package/import header itself is valid. */
|
||||
static Node *
|
||||
parseheaderuse(Parser *p)
|
||||
{
|
||||
Pos pp = p->cur.pos;
|
||||
advance(p);
|
||||
Node *n = newnode(p->a, N_USE, pp);
|
||||
n->usefile = p->cur.pos.file;
|
||||
n->useline = p->cur.pos.line;
|
||||
n->usecol = p->cur.pos.col;
|
||||
const char *alias = NULL;
|
||||
const char *first;
|
||||
if (p->cur.kind == TK_UNDER) {
|
||||
n->useblank = 1;
|
||||
advance(p);
|
||||
} else if (p->cur.kind != TK_IDENT) {
|
||||
errorf(p->cur.pos, "expected identifier, got %s",
|
||||
tokname(p->cur.kind));
|
||||
p->errs++;
|
||||
return n;
|
||||
}
|
||||
if (p->cur.kind != TK_IDENT) {
|
||||
errorf(p->cur.pos, "expected identifier, got %s",
|
||||
tokname(p->cur.kind));
|
||||
p->errs++;
|
||||
return n;
|
||||
}
|
||||
first = p->cur.text;
|
||||
advance(p);
|
||||
if (!n->useblank && p->cur.kind == TK_IDENT) {
|
||||
alias = first;
|
||||
first = p->cur.text;
|
||||
advance(p);
|
||||
}
|
||||
const char *leaf = first;
|
||||
const char *path = leaf;
|
||||
while (p->cur.kind == TK_DOT) {
|
||||
advance(p);
|
||||
if (p->cur.kind != TK_IDENT) {
|
||||
errorf(p->cur.pos, "expected identifier, got %s",
|
||||
tokname(p->cur.kind));
|
||||
p->errs++;
|
||||
return n;
|
||||
}
|
||||
leaf = p->cur.text;
|
||||
path = aprintf(p->a, "%s.%s", path, leaf);
|
||||
advance(p);
|
||||
}
|
||||
if (!n->useblank) {
|
||||
n->str = alias ? alias : leaf;
|
||||
n->strlen = strlen(n->str);
|
||||
}
|
||||
n->usesource = path;
|
||||
n->usepath = path;
|
||||
n->usealias = alias;
|
||||
if (p->cur.kind != TK_SEMI) {
|
||||
errorf(p->cur.pos, "expected ';' after import");
|
||||
p->errs++;
|
||||
return n;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
/* Go's named-file loader parses a valid source only through its initial
|
||||
* package/import section. Keep the broader parseimports recovery pass for
|
||||
* graph-bearing sources, but give actionless test-source omission a boundary
|
||||
* that cannot diagnose late imports or an invalid ordinary declaration body. */
|
||||
Node *
|
||||
parsepackageheader(Parser *p)
|
||||
{
|
||||
Pos fp = { p->l->file, 1, 1 };
|
||||
Node *file = newnode(p->a, N_FILE, fp);
|
||||
Node *head = NULL, *tail = NULL;
|
||||
|
||||
while (p->cur.kind == TK_MODPATH || p->cur.kind == TK_MODRESET) {
|
||||
if (p->cur.kind == TK_MODPATH) {
|
||||
p->pathmod = p->cur.text;
|
||||
p->curmod = p->cur.text;
|
||||
p->resetmod = NULL;
|
||||
} else {
|
||||
p->pathmod = NULL;
|
||||
p->curmod = p->cur.text;
|
||||
p->resetmod = p->cur.text;
|
||||
}
|
||||
p->sourceid++;
|
||||
advance(p);
|
||||
}
|
||||
if (p->cur.kind != TK_MODULE) {
|
||||
errorf(p->cur.pos, "invalid or missing package clause");
|
||||
p->errs++;
|
||||
return file;
|
||||
}
|
||||
Pos pp = p->cur.pos;
|
||||
advance(p);
|
||||
if (p->cur.kind != TK_IDENT) {
|
||||
errorf(p->cur.pos, "invalid or missing package clause");
|
||||
p->errs++;
|
||||
return file;
|
||||
}
|
||||
const char *name = p->cur.text;
|
||||
advance(p);
|
||||
if (p->cur.kind != TK_SEMI) {
|
||||
errorf(p->cur.pos, "expected ';' after package name");
|
||||
p->errs++;
|
||||
return file;
|
||||
}
|
||||
p->curpkg = name;
|
||||
if (p->pathmod == NULL && p->resetmod == NULL)
|
||||
p->curmod = name;
|
||||
file->module = name;
|
||||
file->pkgname = name;
|
||||
file->sourceid = p->sourceid;
|
||||
file->pos = pp;
|
||||
|
||||
while (advanceheaderimport(p)) {
|
||||
Node *d = parseheaderuse(p);
|
||||
d->module = p->curmod;
|
||||
d->pkgname = p->curpkg;
|
||||
d->sourceid = p->sourceid;
|
||||
if (head == NULL)
|
||||
head = d;
|
||||
else
|
||||
tail->next = d;
|
||||
tail = d;
|
||||
if (p->errs != 0)
|
||||
break;
|
||||
}
|
||||
file->list = head;
|
||||
return file;
|
||||
}
|
||||
|
||||
Node *
|
||||
parseimports(Parser *p)
|
||||
{
|
||||
|
||||
@@ -218,6 +218,7 @@ struct Lex {
|
||||
|
||||
void lexinit(Lex*, Arena*, const char *file, const char *src, u64 len);
|
||||
Tok lexnext(Lex*);
|
||||
int lexheaderimport(Lex*, Tok*);
|
||||
const char *tokname(Tkind); /* canonical spelling, e.g. "fn", "+=" */
|
||||
void tokprint(FILE*, Tok); /* one line, "%s:%d:%d: %s %q" */
|
||||
Tkind kwlookup(const char *s, u64 n); /* TK_NONE if not a keyword */
|
||||
@@ -408,6 +409,8 @@ struct Parser {
|
||||
|
||||
void parserinit(Parser*, Arena*, Lex*);
|
||||
Node *parsefile(Parser*);
|
||||
/* Package/import header only: stop before the first ordinary declaration. */
|
||||
Node *parsepackageheader(Parser*);
|
||||
/* Imports-only N_FILE: module/pos identify the first package clause,
|
||||
* list holds N_USE declarations, and body holds package-clause markers. */
|
||||
Node *parseimports(Parser*);
|
||||
|
||||
125
cmd/ww/main.c
125
cmd/ww/main.c
@@ -693,6 +693,111 @@ source_package_name(const char *path, char **out)
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Named-file loading is allowed for every non-directory Stat result. Unlike
|
||||
* sep_slurp, this reader must not require seekability: a finite FIFO is still
|
||||
* a named source stream, just as it is for GoFilesPackage. */
|
||||
static int
|
||||
source_header_slurp(const char *path, char **out, u64 *len)
|
||||
{
|
||||
int fd = open(path, O_RDONLY);
|
||||
if (fd < 0) return -1;
|
||||
size_t cap = 4096, n = 0;
|
||||
char *buf = malloc(cap);
|
||||
if (buf == NULL) {
|
||||
(void)sep_fail_nomem();
|
||||
(void)close(fd);
|
||||
return -1;
|
||||
}
|
||||
for (;;) {
|
||||
if (n == cap) {
|
||||
if (cap >= INT_MAX) {
|
||||
(void)sep_fail_size();
|
||||
free(buf);
|
||||
(void)close(fd);
|
||||
return -1;
|
||||
}
|
||||
size_t nextcap = cap > (size_t)INT_MAX / 2
|
||||
? (size_t)INT_MAX : cap * 2;
|
||||
char *next = realloc(buf, nextcap);
|
||||
if (next == NULL) {
|
||||
(void)sep_fail_nomem();
|
||||
free(buf);
|
||||
(void)close(fd);
|
||||
return -1;
|
||||
}
|
||||
buf = next;
|
||||
cap = nextcap;
|
||||
}
|
||||
ssize_t got = read(fd, buf + n, cap - n);
|
||||
if (got < 0) {
|
||||
if (errno == EINTR) continue;
|
||||
free(buf);
|
||||
(void)close(fd);
|
||||
return -1;
|
||||
}
|
||||
if (got == 0) break;
|
||||
n += (size_t)got;
|
||||
}
|
||||
if (close(fd) != 0) {
|
||||
free(buf);
|
||||
return -1;
|
||||
}
|
||||
if (n == cap) {
|
||||
/* The loop normally grows before the read that can fill cap, but
|
||||
* retain an explicit terminator guard for future reader changes. */
|
||||
if (cap >= INT_MAX) {
|
||||
(void)sep_fail_size();
|
||||
free(buf);
|
||||
return -1;
|
||||
}
|
||||
char *next = realloc(buf, cap + 1);
|
||||
if (next == NULL) {
|
||||
(void)sep_fail_nomem();
|
||||
free(buf);
|
||||
return -1;
|
||||
}
|
||||
buf = next;
|
||||
}
|
||||
buf[n] = '\0';
|
||||
*out = buf;
|
||||
*len = (u64)n;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Validate only the package/import header before a named test source is
|
||||
* omitted from an ordinary build. Imports are syntax, not graph edges here. */
|
||||
static int
|
||||
source_build_header(const char *path)
|
||||
{
|
||||
char *buf;
|
||||
u64 len;
|
||||
if (source_header_slurp(path, &buf, &len) < 0) {
|
||||
fprintf(stderr, "ww: cannot read %s\n", path);
|
||||
return -1;
|
||||
}
|
||||
Arena *a = newarena();
|
||||
Lex l;
|
||||
Parser p;
|
||||
lexinit(&l, a, path, buf, len);
|
||||
parserinit(&p, a, &l);
|
||||
Node *header = parsepackageheader(&p);
|
||||
if (l.errs || p.errs) {
|
||||
freearena(a);
|
||||
free(buf);
|
||||
return -1;
|
||||
}
|
||||
if (header->module == NULL) {
|
||||
Pos pp = { path, 1, 1 };
|
||||
errorf(pp, "invalid or missing package clause");
|
||||
freearena(a);
|
||||
free(buf);
|
||||
return -1;
|
||||
}
|
||||
freearena(a);
|
||||
free(buf);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
source_list_add(char ***list, int *n, int *cap, const char *path)
|
||||
{
|
||||
@@ -7474,11 +7579,31 @@ do_build(int argc, char **argv)
|
||||
}
|
||||
struct stat requested;
|
||||
int literal = stat(src, &requested) == 0;
|
||||
int requested_nondirectory = literal && !S_ISDIR(requested.st_mode);
|
||||
if (source_operand_ignored(src)) {
|
||||
source_operand_no_sources(src);
|
||||
free(incs);
|
||||
return 1;
|
||||
}
|
||||
const char *requested_base = strrchr(src, '/');
|
||||
requested_base = requested_base ? requested_base + 1 : src;
|
||||
size_t requested_baselen = strlen(requested_base);
|
||||
if (requested_nondirectory && requested_baselen >= 8
|
||||
&& strcmp(requested_base + requested_baselen - 8,
|
||||
"_test.ww") == 0) {
|
||||
if (source_build_header(src) < 0) {
|
||||
free(incs);
|
||||
return 1;
|
||||
}
|
||||
free(incs);
|
||||
if (!outflag[0] || strcmp(outflag, "/dev/null") == 0)
|
||||
return 0;
|
||||
if (build_output_dir(outflag))
|
||||
fputs("ww: no main packages to build\n", stderr);
|
||||
else
|
||||
fputs("ww: no packages to build\n", stderr);
|
||||
return 1;
|
||||
}
|
||||
char resolved[PATH_MAX];
|
||||
int is_dir = 0;
|
||||
if (!resolve_module(src, incs, resolved, sizeof resolved, &is_dir)) {
|
||||
|
||||
@@ -10080,6 +10080,170 @@ No serialized format changes. Build workdir format remains `18`, test workdir
|
||||
format remains `19`, semantic storage format remains `3`, and there is no test
|
||||
result cache.
|
||||
|
||||
### 11.51 Implemented explicit named test-source build omission
|
||||
|
||||
A single existing raw `ww build` operand whose requested final basename ends
|
||||
exactly `_test.ww` is now a test-only named source. WW syntax-observes only the
|
||||
package clause and its initial contiguous import section, preserves any read/header
|
||||
diagnostic, then omits the valid test-only root before logical resolution or
|
||||
action construction. This is deliberately distinct from the leading-dot and
|
||||
underscore rule in 11.50: `_test.ww` reaches that earlier exclusion, whereas a
|
||||
visible `x_test.ww` reaches this test-only omission.
|
||||
|
||||
#### Pinned authority, tests, and applicability
|
||||
|
||||
- **behavior directly implemented or asserted by pinned Go** — official Go
|
||||
1.26.5 commit `c19862e5f8415b4f24b189d065ed739517c548ba` recognizes an
|
||||
existing non-directory named `.go` operand in `PackagesAndErrors`
|
||||
([`cmd/go/internal/load/pkg.go`, lines 2903–2918](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/load/pkg.go#L2903-L2918)),
|
||||
builds its synthetic command-line package with `UseAllFiles`
|
||||
([lines 3244–3315](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/load/pkg.go#L3244-L3315)),
|
||||
and classifies `_test.go` separately from `GoFiles`
|
||||
([`go/build/build.go`, lines 930–1036](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/go/build/build.go#L930-L1036)).
|
||||
`UseAllFiles` bypasses ordinary target and build-expression rejection, but
|
||||
not that test-file classification
|
||||
([lines 1438–1509](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/go/build/build.go#L1438-L1509)).
|
||||
- **behavior directly implemented or asserted by pinned Go** — on a
|
||||
successfully scanned header, `readGoInfo` reads the initial package/import
|
||||
header and one stop byte, then removes that byte before parsing; on header
|
||||
syntax recovery it deliberately drains the remaining source
|
||||
([`go/build/read.go`, lines 265–315](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/go/build/read.go#L265-L315)).
|
||||
Every raw NUL reached by its reader is a read error
|
||||
([lines 71–89](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/go/build/read.go#L71-L89));
|
||||
`parser.ImportsOnly` stops before ordinary declarations
|
||||
([`go/parser/parser.go`, lines 2887–2923](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/go/parser/parser.go#L2887-L2923)).
|
||||
Thus a package or initial-import header error precedes omission, while a
|
||||
later declaration/body error does not become an ordinary-build error.
|
||||
- **behavior directly implemented or asserted by pinned Go** — ordinary
|
||||
initial loading does not recursively resolve test imports
|
||||
([`cmd/go/internal/load/pkg.go`, lines 350–358](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/load/pkg.go#L350-L358));
|
||||
`go/build` records their metadata at `go/build/build.go:1037–1040`.
|
||||
Build checks loader errors first and then omits a test-only root before
|
||||
output/action construction
|
||||
([`cmd/go/internal/work/build.go`, lines 459–559 and 731–745](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/work/build.go#L459-L559)).
|
||||
- **behavior directly implemented or asserted by pinned Go** — related
|
||||
official anchors are `go/build/build_test.go:382–421` (`TestMatchFile`),
|
||||
`go/build/build_test.go:812–831` (`TestDirectives` and `XTestDirectives`),
|
||||
`go/build/read_test.go:17–73,120–159,165–249` (header boundary, NUL, and
|
||||
syntax recovery),
|
||||
`cmd/go/testdata/script/test_relative_cmdline.txt:1–49`, and
|
||||
`cmd/go/testdata/script/build_test_only.txt:1–18`. The pinned official
|
||||
repository has no script that directly invokes `go build NAME_test.go`; the
|
||||
exact named-build result below is derived from its pinned loader and action
|
||||
ordering, not presented as an unanchored direct script assertion.
|
||||
- **behavior derived from the pinned implementation** — one valid named test
|
||||
source produces no build action. No effective `-o` and exact `/dev/null`
|
||||
succeed silently; a non-directory output reports `ww: no packages to
|
||||
build\n`; an existing or trailing-slash output directory reports
|
||||
`ww: no main packages to build\n`. Header loading occurs before those
|
||||
empty-selection branches.
|
||||
- **behavior derived from the pinned implementation** — the pinned body
|
||||
boundary is byte-sensitive: an ordinary non-`i` stop byte, a first malformed
|
||||
UTF-8 byte, or a later BOM byte is excluded, while a reached NUL and an
|
||||
unterminated comment remain load errors; a following byte `i` is attempted as
|
||||
another import. WW applies the corresponding rule at its lexical `import`
|
||||
boundary: it never lexes the first ordinary body token, but preserves NUL and
|
||||
comment diagnostics encountered while skipping header trivia.
|
||||
|
||||
The rule honestly applies to WW's local, manifest-free literal `.ww` model:
|
||||
`*_test.ww` already means test source for directory selection and `ww test`.
|
||||
It introduces no module, manifest, registry, lock, network resolver, cache,
|
||||
generalized import grammar, source build expression, or test-result cache.
|
||||
|
||||
#### Source and identity ownership
|
||||
|
||||
- **behavior directly implemented or asserted by pinned Go** — named-source
|
||||
package construction presents `Stat`-derived file information through its
|
||||
synthetic directory. The operand spelling remains the file name presented
|
||||
to selection, even though `Stat` follows a symlink.
|
||||
- **behavior derived from the pinned implementation** — the twin true owners
|
||||
are `cmd/ww/main.c::do_build` and `selfhost/cmd/ww/main.ww::dobuild`, after
|
||||
the existing hidden-prefix check and before `resolve_module` / `resolvemodule`.
|
||||
A requested visible non-directory spelling ending `_test.ww` is classified
|
||||
from that spelling; a symlink to a directory remains a directory request.
|
||||
The finite-stream header reader is not a regular-file restriction: a supplied
|
||||
finite FIFO is read as a source, while an unreadable entry reports its header
|
||||
read failure. Physical parent directories and symlink targets remain loader
|
||||
metadata, never canonical package or import identity.
|
||||
- **behavior derived from the pinned implementation** — platform-looking
|
||||
visible names such as `x_windows_test.ww` remain test-only named sources.
|
||||
Multiple named sources, logical operands without `.ww`, directories,
|
||||
recursive requests, `ww run`, and all visible non-test raw operands retain
|
||||
their prior routes. In particular, this does not implement Go's multiple
|
||||
named-source package collection.
|
||||
- **behavior derived from the pinned implementation** — the header observation
|
||||
is discarded after classification. It creates no canonical package,
|
||||
command-line package representative, dotted import identity, qualifier,
|
||||
symbol namespace, `.wwi`, action, archive, publication, or persistence key.
|
||||
Initial import syntax is checked solely for load-error precedence; omitted
|
||||
test-source imports are not resolved and create no graph edge or action.
|
||||
|
||||
#### Build, test, package, and import effects
|
||||
|
||||
- **behavior directly measured WW behavior** — before this change, both stages
|
||||
compiled, linked, published, persisted, and ran a valid visible
|
||||
`only_test.ww`; `ww build -w WORK -o OUT only_test.ww` exited 0 with empty
|
||||
streams, a byte-identical mode-0755 executable, populated work state, and
|
||||
runtime status 19. A missing test-only import likewise reached ordinary
|
||||
import resolution.
|
||||
- **behavior derived from the pinned implementation** — post-contract build
|
||||
behavior is an empty production selection after a valid header: no compiler,
|
||||
assembler, archiver, linker, generated main, test harness, test child, or
|
||||
program process starts. `-S` follows the same no-action rule. No runtime
|
||||
result can occur.
|
||||
- **behavior derived from the pinned implementation** — package behavior is
|
||||
confined to the transient header check; no production/test variant or
|
||||
package action remains. Import behavior is likewise confined to syntax;
|
||||
missing test-only dotted imports, late imports, body syntax/type errors, and
|
||||
runtime faults cannot enter the production graph.
|
||||
- **behavior derived from the pinned implementation** — test behavior is an
|
||||
explicit non-effect. Raw `ww test`, `ww test -c`, and `ww test -S` continue
|
||||
to select named test files. Directory and recursive `ww build` already
|
||||
exclude selected test sources and remain unchanged.
|
||||
|
||||
#### Diagnostics, artifacts, and lifecycle
|
||||
|
||||
- **behavior derived from the pinned implementation** — CLI-shape and
|
||||
multiple-operand delegation retain their prior precedence; 11.50 prefix
|
||||
exclusion precedes this header reader. Header read/package/initial-import
|
||||
diagnostics precede output policy. A first ordinary malformed UTF-8/BOM body
|
||||
byte is excluded, while a reached NUL or unterminated header-trivia comment
|
||||
is a header diagnostic. Once the header is valid, no unresolved import,
|
||||
graph, producer, linker, or runtime diagnostic may surface. Both
|
||||
stages use the same header boundary and must produce byte-identical status,
|
||||
stdout, and stderr.
|
||||
- **behavior derived from the pinned implementation** — cold no-action success
|
||||
creates no default/explicit output, output directory, `.wwi`, unit, assembly,
|
||||
object, archive, init product, `.sepwork`, workdir, stamp, transaction,
|
||||
capture, result, or private build temporary. Output-policy diagnostics also
|
||||
create none of those products.
|
||||
- **behavior derived from the pinned implementation** — a warm request starts
|
||||
no transaction, mutation, invalidation, reuse check, timestamp refresh, or
|
||||
producer. Existing output, sidecar, workdir and artifacts remain
|
||||
byte-identical on success and failure; no `.new`, `.install`, `.wwtxn.*`,
|
||||
backup, or recovery residue remains. Restoring a visible non-test source
|
||||
reuses prior valid warm state by the unchanged ordinary route.
|
||||
- **behavior derived from the pinned implementation** — no action means no
|
||||
publication, rollback work, producer/runtime failure path, shared lock, or
|
||||
child process. Concurrent Cstage/WWstage no-action and header-diagnostic
|
||||
requests are isolated. Interruption during header reading leaves no owned
|
||||
persistent state; after a valid header there is no child or publication window
|
||||
to clean up. Every reader closes its descriptor and releases request-local
|
||||
storage before return.
|
||||
- **behavior derived from the pinned implementation** — focused proof covers
|
||||
valid, malformed-header, missing-test-import, wrong-platform, symlink and
|
||||
finite-stream operands; default/file/directory/null/assembly output modes;
|
||||
cold/warm preservation; trace-proven tool and runtime absence; rollback,
|
||||
interruption, concurrency, cleanup, complete per-stage work preservation, and
|
||||
Cstage/WWstage semantic-artifact parity. The complete per-stage snapshot
|
||||
includes `.wwtool.ww`; cross-stage comparison excludes only that intentionally
|
||||
different producer binary snapshot. Unaffected visible production controls
|
||||
retain byte-identical outputs.
|
||||
|
||||
No serialized format changes accompany this omission. Build workdir format
|
||||
remains `18`, test workdir format remains `19`, semantic storage format remains
|
||||
`3`, and the slice adds no cache or persistent record.
|
||||
|
||||
## 12. Candidate architectures and hard-gate decision
|
||||
|
||||
Five candidates were developed as coherent systems, not as feature bins.
|
||||
|
||||
50
docs/spec.md
50
docs/spec.md
@@ -326,15 +326,38 @@ ImportPath = ident { "." ident } .
|
||||
eligible. Only the requested basename participates: a hidden parent does not
|
||||
hide visible `main.ww`, a hidden symlink spelling stays hidden for any
|
||||
existing non-directory target, and a visible symlink spelling stays eligible.
|
||||
The excluded operand creates no package, declaration, import binding/edge,
|
||||
action, artifact, initialization, test,
|
||||
publication, or persistent state. `ww build` reports the existing
|
||||
`directory contains no WW package sources` condition; an explicit running
|
||||
raw `ww test` also emits its command-owned `FAIL`, while `-c` and `-S` do not.
|
||||
This rule does not add multiple named-source package support, does not change
|
||||
visible `*_test.ww` handling, and does not apply to logical operands,
|
||||
directory/recursive requests, imports, or `ww run`. The diagnostic parent is
|
||||
presentation metadata and never canonical identity.
|
||||
The prefix-excluded operand creates no package, declaration, import
|
||||
binding/edge, action, artifact, initialization, test, publication, or
|
||||
persistent state. `ww build` reports the existing `directory contains no WW
|
||||
package sources` condition; an explicit running raw `ww test` also emits its
|
||||
command-owned `FAIL`, while `-c` and `-S` do not.
|
||||
- A visible single raw operand passed to `ww build` whose requested final
|
||||
basename ends exactly `_test.ww` is test-only after its package clause and
|
||||
contiguous initial import section have been read. Prefix exclusion remains
|
||||
first, so a bare `_test.ww` is never opened. Header read, package-clause, and
|
||||
initial-import syntax errors retain their ordinary precedence; after a valid
|
||||
header the sole root is omitted. Its dotted imports are not resolved, and a
|
||||
late import, body parse/type error, or runtime behavior is not observed. The
|
||||
first ordinary body token is not lexed, so an immediately following malformed
|
||||
UTF-8 byte or non-leading BOM is outside the header. A raw NUL reached while
|
||||
locating that token and an unterminated comment in header trivia remain load
|
||||
errors. Go's byte reader probes a following `i` as a possible `import`; the
|
||||
corresponding WW boundary is the exact lexical `import` token, so an ordinary
|
||||
WW identifier merely beginning with `i` is body syntax. This named-source
|
||||
rule retains all-files platform behavior: a
|
||||
visible `x_windows_test.ww` is still test-only. It classifies the requested
|
||||
basename of a visible symlink, not its target name; a symlink whose target is
|
||||
a directory remains a directory request.
|
||||
- Omission creates no canonical package, command-line package node, import
|
||||
binding or edge, graph/action, symbol, initializer, executable, archive,
|
||||
interface, publication, transaction, default `.sepwork`, or work-state
|
||||
mutation. It does not alter `ww test`, `ww test -c`, `ww test -S`, logical
|
||||
operands, directory/recursive selection, multiple named-source support,
|
||||
imports, or `ww run`. Physical directory and symlink-target data remain
|
||||
observation metadata, never package, import, graph, action, artifact,
|
||||
symbol, `.wwi`, publication, or persistence identity. Build workdir format
|
||||
remains 18, test workdir format remains 19, and semantic storage format
|
||||
remains 3.
|
||||
- `import acme.codec;` loads the canonical package `acme.codec`. If that
|
||||
package declares `package wire;`, the importing file sees its exported names
|
||||
as `wire.Name`; `codec.Name` is not an additional binding. An explicit alias
|
||||
@@ -467,6 +490,15 @@ ImportPath = ident { "." ident } .
|
||||
Output paths and directory metadata never become package, import, graph, action,
|
||||
symbol, artifact, `.wwi`, or persistence identity.
|
||||
|
||||
A valid omitted single raw `*_test.ww` build has an empty selection. With no
|
||||
effective `-o`, including an assembly-only request, and with exact
|
||||
`-o /dev/null`, it succeeds silently. A non-directory effective output fails
|
||||
with `ww: no packages to build`; an output-directory effective output fails
|
||||
with `ww: no main packages to build`. These empty-selection outcomes occur
|
||||
only after the raw test source's header has loaded successfully, create no
|
||||
output path or parent, and preserve every pre-existing output and work-state
|
||||
byte unchanged.
|
||||
|
||||
Every caller-visible build installation checks its destination after all
|
||||
applicable compile, assemble, archive, and link producers finish. Ordinary
|
||||
`stat` follows symlinks. An existing directory rejects as
|
||||
|
||||
@@ -239,6 +239,32 @@ platform classification, symlink spelling, output rollback, residue, and
|
||||
diagnostic/artifact parity. Directory package coordination and test process,
|
||||
filter, timeout, signal, and descendant topology are unchanged.
|
||||
|
||||
The corresponding focused dual-stage named-test-source build observer proves
|
||||
the distinct `ww build` rule for one visible raw operand ending exactly
|
||||
`_test.ww`. It requires each stage to read the package clause and contiguous
|
||||
initial imports before omitting the sole root: unreadable/header-syntax inputs
|
||||
retain their diagnostics, while a valid header prevents import resolution,
|
||||
graph/action creation, producers, runtime, publication, persistence, and all
|
||||
ordinary body-derived diagnostics. It separately proves that a first malformed
|
||||
UTF-8 or non-leading BOM body byte is outside the header, while a reached NUL
|
||||
and an unterminated header-trivia comment retain loader diagnostics. It also
|
||||
proves WW's grammar-level adaptation of Go's following-`i` byte probe: only the
|
||||
exact lexical `import` token continues the header, while an identifier such as
|
||||
`imported` begins the ordinary body. It covers
|
||||
normal, `-S`, and `/dev/null` successful
|
||||
empty selections; the exact `no packages to build` and `no main packages to
|
||||
build` output branches; requested-basename prefix, symlink, symlink-directory,
|
||||
and wrong-platform spelling controls; cold absence and warm preservation of
|
||||
outputs/work state; tool/process absence, interruption, concurrency, and
|
||||
residue cleanup. Cstage and WWstage agree on status, stdout, stderr,
|
||||
diagnostics, semantic artifact/work snapshots, and unaffected-control artifact
|
||||
bytes. Complete per-stage warm snapshots additionally cover the intentionally
|
||||
stage-specific `.wwtool.ww` producer-provenance file.
|
||||
Raw `ww test`, `-c`, and `-S` test routes retain explicit test-file selection;
|
||||
directory/recursive selection, package/import identities, graph/action
|
||||
identities, persistence formats (build 18, test 19, semantic 3), and test
|
||||
process topology are explicit non-effects.
|
||||
|
||||
List mode uses that same product process and initialization boundary but starts
|
||||
no per-test child. The shared language harness emits only selected qualified
|
||||
test names, one per line in descriptor order. A valid filter selecting no tests
|
||||
|
||||
@@ -410,6 +410,75 @@ fn skipws(l: *lex) bool = {
|
||||
return false;
|
||||
};
|
||||
|
||||
// Return the next initial import without lexing the first ordinary body token.
|
||||
// Header trivia is still decoded, so reached NUL and comment diagnostics keep
|
||||
// loader precedence; a malformed UTF-8 byte or middle BOM that begins the body
|
||||
// is deliberately outside this pass.
|
||||
export fn lexheaderimport(l: *lex, out: *tok) bool = {
|
||||
for (true) {
|
||||
if (l.lpos >= l.srclen) { return false; };
|
||||
let c: i32 = srcb(l, l.lpos);
|
||||
if (c == 0) {
|
||||
lskipnul(l);
|
||||
return false;
|
||||
};
|
||||
if ((c >= 128 && !utf8bytevalid(l.src, l.srclen, l.lpos))
|
||||
|| bomat(l.src, l.srclen, l.lpos)) {
|
||||
return false;
|
||||
};
|
||||
if (c == ' ' || c == '\t' || c == '\r' || c == '\n') {
|
||||
lget(l);
|
||||
continue;
|
||||
};
|
||||
if (c == '/' && l.lpos + 1u64 < l.srclen
|
||||
&& srcb(l, l.lpos + 1u64) == '/') {
|
||||
lget(l); lget(l);
|
||||
linecomment(l);
|
||||
if (l.errs != 0) { return false; };
|
||||
continue;
|
||||
};
|
||||
if (c == '/' && l.lpos + 1u64 < l.srclen
|
||||
&& srcb(l, l.lpos + 1u64) == '*') {
|
||||
lget(l); lget(l);
|
||||
let prev: i32 = -1;
|
||||
for (true) {
|
||||
let x: i32 = lget(l);
|
||||
if (x < 0) {
|
||||
let cp: pos;
|
||||
curpos(l, &cp);
|
||||
errat(l, &cp, "unterminated /* comment");
|
||||
return false;
|
||||
};
|
||||
if (prev == '*' && x == '/') { break; };
|
||||
prev = x;
|
||||
};
|
||||
if (l.errs != 0) { return false; };
|
||||
continue;
|
||||
};
|
||||
break;
|
||||
};
|
||||
if (l.modreset != 0 || l.modpathset != 0) { return false; };
|
||||
let kw: str = tokname(tkind.TK_USE);
|
||||
if (l.srclen - l.lpos < kw.len: u64) { return false; };
|
||||
let i: i32 = 0;
|
||||
for (i < kw.len) {
|
||||
if (srcb(l, l.lpos + (i: u64)) != kw[i]: i32) {
|
||||
return false;
|
||||
};
|
||||
i += 1;
|
||||
};
|
||||
if (l.srclen - l.lpos > kw.len: u64) {
|
||||
let next: i32 = srcb(l, l.lpos + (kw.len: u64));
|
||||
if ((next >= 'a' && next <= 'z')
|
||||
|| (next >= 'A' && next <= 'Z')
|
||||
|| (next >= '0' && next <= '9') || next == '_') {
|
||||
return false;
|
||||
};
|
||||
};
|
||||
lexnext(l, out);
|
||||
return out.kind == tkind.TK_USE;
|
||||
};
|
||||
|
||||
fn parseint(p: *u8, n: u64, base: i32, ok: *bool) u64 = {
|
||||
let v: u64 = 0u64;
|
||||
let b: u64 = base: u64;
|
||||
|
||||
@@ -57,9 +57,7 @@ export type parser = struct {
|
||||
commandpackage: bool,
|
||||
};
|
||||
|
||||
fn refill(p: *parser) void = {
|
||||
let t: tok;
|
||||
lexnext(p.l, &t);
|
||||
fn installtok(p: *parser, t: *tok) void = {
|
||||
p.curkind = t.kind;
|
||||
p.curfile = t.file;
|
||||
p.curline = t.line;
|
||||
@@ -70,6 +68,19 @@ fn refill(p: *parser) void = {
|
||||
p.curtsuffix = t.tsuffix;
|
||||
};
|
||||
|
||||
fn refill(p: *parser) void = {
|
||||
let t: tok;
|
||||
lexnext(p.l, &t);
|
||||
installtok(p, &t);
|
||||
};
|
||||
|
||||
fn advanceheaderimport(p: *parser) bool = {
|
||||
let t: tok;
|
||||
if (!lexheaderimport(p.l, &t)) { return false; };
|
||||
installtok(p, &t);
|
||||
return true;
|
||||
};
|
||||
|
||||
export fn parserinit(p: *parser, l: *lex) void = {
|
||||
p.l = l;
|
||||
p.errs = 0;
|
||||
@@ -490,6 +501,121 @@ fn skipimportattrs(p: *parser) void = {
|
||||
};
|
||||
};
|
||||
|
||||
// Parse one initial import for the named-source header pass. Unlike the full
|
||||
// recovery scanner, this stops on the first malformed header token and never
|
||||
// consumes an ordinary declaration body.
|
||||
fn parseheaderuse(p: *parser) *node = {
|
||||
let pf: str = p.curfile;
|
||||
let pl: i32 = p.curline;
|
||||
let pc: i32 = p.curcol;
|
||||
advance(p);
|
||||
let n: *node = newnode(nkind.N_USE, pf, pl, pc);
|
||||
n.usefile = p.curfile;
|
||||
n.useline = p.curline;
|
||||
n.usecol = p.curcol;
|
||||
let alias: str;
|
||||
let first: str;
|
||||
if (p.curkind == tkind.TK_UNDER) {
|
||||
n.useblank = 1;
|
||||
advance(p);
|
||||
};
|
||||
if (p.curkind != tkind.TK_IDENT) {
|
||||
errmsg(p, strings.concat("expected identifier, got ",
|
||||
tokname(p.curkind)));
|
||||
return n;
|
||||
};
|
||||
first = p.curtext;
|
||||
advance(p);
|
||||
if (n.useblank == 0 && p.curkind == tkind.TK_IDENT) {
|
||||
alias = first;
|
||||
first = p.curtext;
|
||||
advance(p);
|
||||
};
|
||||
let leaf: str = first;
|
||||
let path: str = leaf;
|
||||
for (p.curkind == tkind.TK_DOT) {
|
||||
advance(p);
|
||||
if (p.curkind != tkind.TK_IDENT) {
|
||||
errmsg(p, strings.concat("expected identifier, got ",
|
||||
tokname(p.curkind)));
|
||||
return n;
|
||||
};
|
||||
leaf = p.curtext;
|
||||
path = strings.concat(path, ".", leaf);
|
||||
advance(p);
|
||||
};
|
||||
if (n.useblank != 0) { n.str = ""; }
|
||||
else { if (alias.len > 0) { n.str = alias; } else { n.str = leaf; }; };
|
||||
n.usesource = path;
|
||||
n.usepath = path;
|
||||
n.usealias = alias;
|
||||
if (p.curkind != tkind.TK_SEMI) {
|
||||
errmsg(p, "expected ';' after import");
|
||||
return n;
|
||||
};
|
||||
return n;
|
||||
};
|
||||
|
||||
// A valid named source is loader-visible only through its initial package and
|
||||
// contiguous import section. Keep parseimports for graph-bearing source
|
||||
// recovery; this boundary deliberately ignores every ordinary declaration.
|
||||
export fn parsepackageheader(p: *parser) *node = {
|
||||
let f: *node = newnode(nkind.N_FILE, p.curfile, 1, 1);
|
||||
let head: *node = nil;
|
||||
let tail: *node = nil;
|
||||
for (p.curkind == tkind.TK_MODPATH
|
||||
|| p.curkind == tkind.TK_MODRESET) {
|
||||
if (p.curkind == tkind.TK_MODPATH) {
|
||||
p.pathmod = p.curtext;
|
||||
p.curmod = p.curtext;
|
||||
p.resetmod = "";
|
||||
} else {
|
||||
p.pathmod = "";
|
||||
p.curmod = p.curtext;
|
||||
p.resetmod = p.curtext;
|
||||
};
|
||||
p.sourceid += 1;
|
||||
advance(p);
|
||||
};
|
||||
if (p.curkind != tkind.TK_MODULE) {
|
||||
errmsg(p, "invalid or missing package clause");
|
||||
return f;
|
||||
};
|
||||
let pf: str = p.curfile;
|
||||
let pl: i32 = p.curline;
|
||||
let pc: i32 = p.curcol;
|
||||
advance(p);
|
||||
if (p.curkind != tkind.TK_IDENT) {
|
||||
errmsg(p, "invalid or missing package clause");
|
||||
return f;
|
||||
};
|
||||
let name: str = p.curtext;
|
||||
advance(p);
|
||||
if (p.curkind != tkind.TK_SEMI) {
|
||||
errmsg(p, "expected ';' after package name");
|
||||
return f;
|
||||
};
|
||||
p.curpkg = name;
|
||||
if (p.pathmod.len == 0 && p.resetmod.len == 0) { p.curmod = name; };
|
||||
f.nmod = name;
|
||||
f.pkgname = name;
|
||||
f.sourceid = p.sourceid;
|
||||
f.file = pf;
|
||||
f.line = pl;
|
||||
f.col = pc;
|
||||
for (advanceheaderimport(p)) {
|
||||
let d: *node = parseheaderuse(p);
|
||||
d.nmod = p.curmod;
|
||||
d.pkgname = p.curpkg;
|
||||
d.sourceid = p.sourceid;
|
||||
if (head == nil) { head = d; } else { tail.next = d; };
|
||||
tail = d;
|
||||
if (p.errs != 0) { break; };
|
||||
};
|
||||
f.list = head;
|
||||
return f;
|
||||
};
|
||||
|
||||
export fn parseimports(p: *parser) *node = {
|
||||
let f = newnode(nkind.N_FILE, p.curfile, 1, 1);
|
||||
let head: *node = nil;
|
||||
|
||||
@@ -991,6 +991,102 @@ fn slurp(pathcs: *u8) (*u8, u64) = {
|
||||
return buf.ptr, nu;
|
||||
};
|
||||
|
||||
// Named source recognition accepts every existing non-directory Stat result.
|
||||
// Read to EOF without seeking so a finite FIFO follows the same package-header
|
||||
// loading path as a regular file.
|
||||
fn sourceheaderslurp(pathcs: *u8) (*u8, u64, u64) = {
|
||||
let fd: i32 = os.open(pathstr(pathcs), os.flag.RDONLY, 0i32);
|
||||
if (fd < 0) { return nil, 0u64, 0u64; };
|
||||
let cap: i32 = 4096;
|
||||
let allocation: ([]u8 | nomem) = sepallocbytes(cap);
|
||||
let buf: []u8;
|
||||
match (allocation) {
|
||||
case let value: []u8 => buf = value;
|
||||
case nomem => {
|
||||
sepfailnomem();
|
||||
os.close(fd);
|
||||
return nil, 0u64, 0u64;
|
||||
};
|
||||
};
|
||||
let used: u64 = 0u64;
|
||||
for (true) {
|
||||
if (used == cap: u64) {
|
||||
if (cap == SEP_COUNT_MAX) {
|
||||
sepfailsize();
|
||||
os.close(fd);
|
||||
os.free(buf.ptr: *void, cap: u64);
|
||||
return nil, 0u64, 0u64;
|
||||
};
|
||||
let nextcap: i32 = cap;
|
||||
if (cap > SEP_COUNT_MAX / 2) { nextcap = SEP_COUNT_MAX; }
|
||||
else { nextcap = cap * 2; };
|
||||
let nextallocation: ([]u8 | nomem) = sepallocbytes(nextcap);
|
||||
let next: []u8;
|
||||
match (nextallocation) {
|
||||
case let value: []u8 => next = value;
|
||||
case nomem => {
|
||||
sepfailnomem();
|
||||
os.close(fd);
|
||||
os.free(buf.ptr: *void, cap: u64);
|
||||
return nil, 0u64, 0u64;
|
||||
};
|
||||
};
|
||||
bytecpy(next.ptr, buf.ptr, used);
|
||||
os.free(buf.ptr: *void, cap: u64);
|
||||
buf = next;
|
||||
cap = nextcap;
|
||||
};
|
||||
let got: i64 = os.read(fd, buf.ptr + used,
|
||||
(cap: u64) - used);
|
||||
if (got == -(os.EINTR: i32): i64) { continue; };
|
||||
if (got < 0i64) {
|
||||
os.close(fd);
|
||||
os.free(buf.ptr: *void, cap: u64);
|
||||
return nil, 0u64, 0u64;
|
||||
};
|
||||
if (got == 0i64) { break; };
|
||||
used += got: u64;
|
||||
};
|
||||
if (os.close(fd) != 0) {
|
||||
os.free(buf.ptr: *void, cap: u64);
|
||||
return nil, 0u64, 0u64;
|
||||
};
|
||||
buf[used] = 0u8;
|
||||
return buf.ptr, used, cap: u64;
|
||||
};
|
||||
|
||||
// Validate only the package/import header. The returned syntax is deliberately
|
||||
// discarded: an ordinary build creates no package or import graph for this
|
||||
// test-only named source.
|
||||
fn sourcebuildheader(path: *u8) bool = {
|
||||
let view: str = pathstr(path);
|
||||
let bufp: *u8;
|
||||
let blen: u64;
|
||||
let bcap: u64;
|
||||
bufp, blen, bcap = sourceheaderslurp(path);
|
||||
if (bufp == nil) {
|
||||
cerrpath("ww: cannot read ", path, "\n");
|
||||
return false;
|
||||
};
|
||||
let l: syntax.lex;
|
||||
syntax.lexinit(&l, view, bufp, blen);
|
||||
let ps: syntax.parser;
|
||||
syntax.parserinit(&ps, &l);
|
||||
let header: *syntax.node = syntax.parsepackageheader(&ps);
|
||||
if (l.errs > 0 || ps.errs > 0) {
|
||||
os.free(bufp: *void, bcap);
|
||||
return false;
|
||||
};
|
||||
if (header.nmod.len == 0) {
|
||||
cerrpos(view, 1, 1);
|
||||
cerr(": error: invalid or missing package clause\n");
|
||||
os.free(bufp: *void, bcap);
|
||||
return false;
|
||||
};
|
||||
os.free(bufp: *void, bcap);
|
||||
return true;
|
||||
};
|
||||
|
||||
fn makestem(stem: *u8, src: *u8) void = {
|
||||
let n: u64 = cstrlen(src);
|
||||
let stop: u64 = n;
|
||||
@@ -8943,15 +9039,35 @@ fn dobuild(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
|
||||
nil, nil, false, true);
|
||||
};
|
||||
let requestedliteral: bool = false;
|
||||
let requestednondirectory: bool = false;
|
||||
let requestedstat: os.filestat;
|
||||
match (os.stat(&requestedstat, pathstr(src))) {
|
||||
case void => requestedliteral = true;
|
||||
case void => {
|
||||
requestedliteral = true;
|
||||
let typ: u32 = (requestedstat.mode: u32) & 61440u32;
|
||||
requestednondirectory = typ != os.mode.DIR: u32;
|
||||
};
|
||||
case let e: os.oserror => void;
|
||||
};
|
||||
if (sourceoperandignored(src)) {
|
||||
sourceoperandnosources(src);
|
||||
return 1;
|
||||
};
|
||||
let requestedlen: u64 = cstrlen(src);
|
||||
let requestedbase: u64 = basenameoff(src, requestedlen);
|
||||
if (requestednondirectory && requestedlen - requestedbase >= 8u64
|
||||
&& cstrendswithlit(src, "_test.ww")) {
|
||||
if (!sourcebuildheader(src)) { return 1; };
|
||||
if (outflag == nil || cstreqlit(outflag, "/dev/null")) {
|
||||
return 0;
|
||||
};
|
||||
if (buildoutputdir(outflag)) {
|
||||
cerr("ww: no main packages to build\n");
|
||||
} else {
|
||||
cerr("ww: no packages to build\n");
|
||||
};
|
||||
return 1;
|
||||
};
|
||||
let isdir: i32 = 0;
|
||||
let resolved: *u8 = resolvemodule(selfdir, src, incs.ptr, &isdir);
|
||||
if (resolved == nil) {
|
||||
|
||||
@@ -705,6 +705,136 @@ fn directoryisempty(path: str) bool = {
|
||||
return true;
|
||||
};
|
||||
|
||||
fn snapshotu64(out: *[]u8, value: u64) void = {
|
||||
let v: u64 = value;
|
||||
let i: i32 = 0;
|
||||
for (i < 8) {
|
||||
append(*out, (v & 255u64): u8);
|
||||
v >>= 8u64;
|
||||
i += 1;
|
||||
};
|
||||
};
|
||||
|
||||
fn snapshotbytes(out: *[]u8, value: str) void = {
|
||||
snapshotu64(out, value.len: u64);
|
||||
let i: i32 = 0;
|
||||
for (i < value.len) { append(*out, value[i]); i += 1; };
|
||||
};
|
||||
|
||||
fn treepaths(root: str, relative: str, paths: *[]str) void = {
|
||||
let current: str = root;
|
||||
if (relative.len != 0) { current = strings.concat(root, "/", relative); };
|
||||
let fd: i32 = os.open(current, os.flag.RDONLY, 0i32);
|
||||
assert(fd >= 0);
|
||||
let buf: []u8 = alloc([], 16384u64)!;
|
||||
buf.len = 16384;
|
||||
let n: i64 = os.getdents64(fd, buf.ptr, buf.len: u64);
|
||||
for (n > 0i64) {
|
||||
let off: i32 = 0;
|
||||
for (off < n: i32) {
|
||||
let reclen: i32 = (buf[off + 16]: i32)
|
||||
+ ((buf[off + 17]: i32) * 256);
|
||||
assert(reclen >= 20 && off + reclen <= n: i32);
|
||||
let len: i32 = 0;
|
||||
for (buf[off + 19 + len] != 0u8) { len += 1; };
|
||||
let dot: bool = len == 1 && buf[off + 19] == '.';
|
||||
let dotdot: bool = len == 2 && buf[off + 19] == '.'
|
||||
&& buf[off + 20] == '.';
|
||||
if (!dot && !dotdot) {
|
||||
let name: str;
|
||||
name.ptr = buf.ptr + (off + 19): u64;
|
||||
name.len = len;
|
||||
let child: str = strings.dup(name);
|
||||
if (relative.len != 0) {
|
||||
child = strings.concat(relative, "/", child);
|
||||
};
|
||||
append(*paths, child);
|
||||
let full: str = strings.concat(root, "/", child);
|
||||
let fi: os.filestat;
|
||||
match (os.lstat(&fi, full)) {
|
||||
case void => void;
|
||||
case let e: os.oserror => abort("tree lstat failed");
|
||||
};
|
||||
let kind: u32 = (fi.mode: u32) & 61440u32;
|
||||
if (kind == os.mode.DIR: u32) {
|
||||
treepaths(root, child, paths);
|
||||
};
|
||||
};
|
||||
off += reclen;
|
||||
};
|
||||
n = os.getdents64(fd, buf.ptr, buf.len: u64);
|
||||
};
|
||||
assert(n == 0i64);
|
||||
assert(os.close(fd) == 0);
|
||||
};
|
||||
|
||||
// Canonical complete-tree state: relative names, types, modes, and regular or
|
||||
// symlink bytes. Timestamps and inode numbers are observation metadata, not
|
||||
// semantic build state, and are deliberately excluded.
|
||||
fn treesnapshotfiltered(root: str, skipdriver: bool) str = {
|
||||
let paths: []str = alloc([], 16u64)!;
|
||||
append(paths, "");
|
||||
treepaths(root, "", &paths);
|
||||
let i: i32 = 1;
|
||||
for (i < paths.len) {
|
||||
let j: i32 = i;
|
||||
for (j > 0 && strings.compare(paths[j - 1], paths[j]) > 0) {
|
||||
let swap: str = paths[j - 1];
|
||||
paths[j - 1] = paths[j];
|
||||
paths[j] = swap;
|
||||
j -= 1;
|
||||
};
|
||||
i += 1;
|
||||
};
|
||||
let out: []u8 = alloc([], 4096u64)!;
|
||||
i = 0;
|
||||
for (i < paths.len) {
|
||||
if (skipdriver && same(paths[i], ".wwtool.ww")) {
|
||||
i += 1;
|
||||
continue;
|
||||
};
|
||||
let full: str = root;
|
||||
if (paths[i].len != 0) {
|
||||
full = strings.concat(root, "/", paths[i]);
|
||||
};
|
||||
let fi: os.filestat;
|
||||
match (os.lstat(&fi, full)) {
|
||||
case void => void;
|
||||
case let e: os.oserror => abort("snapshot lstat failed");
|
||||
};
|
||||
snapshotbytes(&out, paths[i]);
|
||||
snapshotu64(&out, fi.mode: u32: u64);
|
||||
let kind: u32 = (fi.mode: u32) & 61440u32;
|
||||
if (kind == os.mode.REG: u32) {
|
||||
snapshotbytes(&out, readfile(full));
|
||||
} else { if (kind == os.mode.LINK: u32) {
|
||||
let target: [4096]u8;
|
||||
let got: i64 = os.readlink(full, &target[0], 4096u64);
|
||||
assert(got >= 0i64 && got <= 4096i64);
|
||||
let value: str;
|
||||
value.ptr = &target[0];
|
||||
value.len = got: i32;
|
||||
snapshotbytes(&out, value);
|
||||
} else {
|
||||
snapshotu64(&out, 0u64);
|
||||
}; };
|
||||
i += 1;
|
||||
};
|
||||
return strings.frombytes(out);
|
||||
};
|
||||
|
||||
fn treesnapshot(root: str) str = {
|
||||
return treesnapshotfiltered(root, false);
|
||||
};
|
||||
|
||||
// The two bootstrap stages intentionally snapshot different driver binaries
|
||||
// in `.wwtool.ww`. Exclude only that producer-provenance file when comparing
|
||||
// semantic artifacts across stages; per-stage preservation uses the complete
|
||||
// snapshot above and therefore still covers it.
|
||||
fn artifacttreesnapshot(root: str) str = {
|
||||
return treesnapshotfiltered(root, true);
|
||||
};
|
||||
|
||||
fn discardedlinkcleaned(marker: str) void = {
|
||||
let output: str = readfile(marker);
|
||||
assert(output.len != 0);
|
||||
@@ -1335,6 +1465,493 @@ fn cwdwritedata(dir: str, label: str) void = {
|
||||
clean(root);
|
||||
};
|
||||
|
||||
// A visible raw test-source build observes only its initial package/import
|
||||
// header. It must not turn that observation into a package, import, action,
|
||||
// artifact, or work-state identity.
|
||||
@test fn named_test_sources_are_header_loaded_then_omitted() void = {
|
||||
let root: str = fresh();
|
||||
let source: str = strings.concat(root, "/source");
|
||||
let dirsource: str = strings.concat(source, "/directory");
|
||||
let dirtarget: str = strings.concat(source, "/directory-target");
|
||||
mkdirall(source); mkdirall(dirsource); mkdirall(dirtarget);
|
||||
|
||||
let ignored: str = strings.concat(source, "/raw_windows_test.ww");
|
||||
let platform: str = strings.concat(source, "/platform_windows_test.ww");
|
||||
let badpackage: str = strings.concat(source, "/bad_package_test.ww");
|
||||
let badimport: str = strings.concat(source, "/bad_import_test.ww");
|
||||
let bodyutf8: str = strings.concat(source, "/body_utf8_test.ww");
|
||||
let bodybom: str = strings.concat(source, "/body_bom_test.ww");
|
||||
let bodynul: str = strings.concat(source, "/body_nul_test.ww");
|
||||
let badcomment: str = strings.concat(source, "/bad_comment_test.ww");
|
||||
let linktarget: str = strings.concat(source, "/target.ww");
|
||||
let link: str = strings.concat(source, "/requested_test.ww");
|
||||
let dirlink: str = strings.concat(source, "/directory_test.ww");
|
||||
let rawmain: str = strings.concat(source, "/raw.ww");
|
||||
let runsource: str = strings.concat(source, "/run_test.ww");
|
||||
let testroute: str = strings.concat(source, "/route_test.ww");
|
||||
let warmpath: str = strings.concat(source, "/warm.ww");
|
||||
let warmtest: str = strings.concat(source, "/warm_test.ww");
|
||||
let header: str = strings.concat(
|
||||
"package raw_test;\nimport absent.pkg;\n",
|
||||
"this body is deliberately malformed and never reaches a producer {\n",
|
||||
"import late.missing;\n");
|
||||
writefile(ignored, header);
|
||||
writefile(platform, header);
|
||||
writefile(linktarget, header);
|
||||
writefile(badpackage, "this source has no package clause\n");
|
||||
writefile(badimport, "package malformed;\nimport ;\n");
|
||||
putinvalidutf8file(bodyutf8, "package body;\n",
|
||||
"fn deliberately_unparsed(\n", false);
|
||||
writemidbomfile(bodybom, "package body;\n",
|
||||
"fn deliberately_unparsed(\n");
|
||||
writenulfile(bodynul, "package body;\n", "fn body() void = {};\n");
|
||||
writefile(badcomment, "package body;\n/* unterminated");
|
||||
writefile(rawmain, "package main;\nfn main() i32 = { return 4; };\n");
|
||||
writefile(runsource,
|
||||
"package main;\nfn main() i32 = { return 9; };\n");
|
||||
writefile(testroute, strings.concat(
|
||||
"package route;\n",
|
||||
"@test fn still_selected() void = { assert(true); };\n"));
|
||||
writefile(warmpath, "package main;\nfn main() i32 = { return 6; };\n");
|
||||
writefile(strings.concat(dirsource, "/main.ww"),
|
||||
"package main;\nfn main() i32 = { return 3; };\n");
|
||||
writefile(strings.concat(dirtarget, "/main.ww"),
|
||||
"package main;\nfn main() i32 = { return 5; };\n");
|
||||
assert(os.symlink(linktarget, link) == 0);
|
||||
assert(os.symlink(dirtarget, dirlink) == 0);
|
||||
|
||||
let compilerwrapper: str = strings.concat(root, "/testonly-w6c.sh");
|
||||
let assemblerwrapper: str = strings.concat(root, "/testonly-w6a.sh");
|
||||
let linkerwrapper: str = strings.concat(root, "/testonly-w6l.sh");
|
||||
writeexecutable(compilerwrapper, strings.concat(
|
||||
"#!/bin/sh\nprintf 'compile\\n' >> \"$WW_TESTONLY_CTRACE\"\n",
|
||||
"exec \"$WW_TESTONLY_REAL_C\" \"$@\"\n"));
|
||||
writeexecutable(assemblerwrapper, strings.concat(
|
||||
"#!/bin/sh\nprintf 'assemble\\n' >> \"$WW_TESTONLY_ATRACE\"\n",
|
||||
"exec \"$WW_TESTONLY_REAL_A\" \"$@\"\n"));
|
||||
writeexecutable(linkerwrapper, strings.concat(
|
||||
"#!/bin/sh\nprintf 'link\\n' >> \"$WW_TESTONLY_LTRACE\"\n",
|
||||
"exec \"$WW_TESTONLY_REAL_L\" \"$@\"\n"));
|
||||
|
||||
let stages: []str = ["ww", "ww_ww"];
|
||||
let compilers: []str = ["w6c", "w6c_ww"];
|
||||
let assemblers: []str = ["w6a", "w6a_ww"];
|
||||
let linkers: []str = ["w6l", "w6l_ww"];
|
||||
let tags: []str = ["c", "ww"];
|
||||
let packagebad: []str = ["", ""];
|
||||
let importbad: []str = ["", ""];
|
||||
let nulbad: []str = ["", ""];
|
||||
let commentbad: []str = ["", ""];
|
||||
let multibad: []str = ["", ""];
|
||||
let environments: [][]str = alloc([], stages.len: u64)!;
|
||||
let tracecs: []str = alloc([], stages.len: u64)!;
|
||||
let traceas: []str = alloc([], stages.len: u64)!;
|
||||
let tracels: []str = alloc([], stages.len: u64)!;
|
||||
let cwarmbin: str = "";
|
||||
let cwarmartifacts: str = "";
|
||||
let ccompiled: str = "";
|
||||
let ctestasm: str = "";
|
||||
let crawbin: str = "";
|
||||
let baseenv: []str = os.getenvs();
|
||||
let si: i32 = 0;
|
||||
for (si < stages.len) {
|
||||
let ctrace: str = strings.concat(root, "/testonly-c-", tags[si]);
|
||||
let atrace: str = strings.concat(root, "/testonly-a-", tags[si]);
|
||||
let ltrace: str = strings.concat(root, "/testonly-l-", tags[si]);
|
||||
writefile(ctrace, ""); writefile(atrace, ""); writefile(ltrace, "");
|
||||
append(tracecs, ctrace); append(traceas, atrace); append(tracels, ltrace);
|
||||
let env: []str = alloc([], (baseenv.len + 10): u64)!;
|
||||
let ei: i32 = 0;
|
||||
for (ei < baseenv.len) {
|
||||
if (!strings.hasprefix(baseenv[ei], "WW_W6C=")
|
||||
&& !strings.hasprefix(baseenv[ei], "WW_W6A=")
|
||||
&& !strings.hasprefix(baseenv[ei], "WW_W6L=")
|
||||
&& !strings.hasprefix(baseenv[ei], "WW_TESTONLY_")) {
|
||||
append(env, baseenv[ei]);
|
||||
};
|
||||
ei += 1;
|
||||
};
|
||||
append(env, strings.concat("WW_W6C=", compilerwrapper));
|
||||
append(env, strings.concat("WW_W6A=", assemblerwrapper));
|
||||
append(env, strings.concat("WW_W6L=", linkerwrapper));
|
||||
append(env, strings.concat("WW_TESTONLY_CTRACE=", ctrace));
|
||||
append(env, strings.concat("WW_TESTONLY_ATRACE=", atrace));
|
||||
append(env, strings.concat("WW_TESTONLY_LTRACE=", ltrace));
|
||||
append(env, strings.concat("WW_TESTONLY_REAL_C=", driver(compilers[si])));
|
||||
append(env, strings.concat("WW_TESTONLY_REAL_A=", driver(assemblers[si])));
|
||||
append(env, strings.concat("WW_TESTONLY_REAL_L=", driver(linkers[si])));
|
||||
append(environments, env);
|
||||
|
||||
let out: commandout;
|
||||
let unused: str = strings.concat(root, "/unused-", tags[si]);
|
||||
let defaultav: []str = [driver(stages[si]), "build", "-w", unused,
|
||||
ignored];
|
||||
runcommandenv(root, strings.concat("testonly-default-", tags[si]),
|
||||
defaultav, env, (30i64 * (time.second: i64)): time.duration, &out);
|
||||
expectexit(&out, 0);
|
||||
let defaultoutput: str = strings.concat(source, "/raw_windows_test");
|
||||
let defaultsepwork: str = strings.concat(defaultoutput, ".sepwork");
|
||||
assert(out.stdout.len == 0 && out.stderr.len == 0
|
||||
&& !os.exists(unused) && !os.exists(defaultoutput)
|
||||
&& !os.exists(defaultsepwork)
|
||||
&& readfile(ctrace).len == 0 && readfile(atrace).len == 0
|
||||
&& readfile(ltrace).len == 0);
|
||||
|
||||
let asmwork: str = strings.concat(root, "/asm-unused-", tags[si]);
|
||||
let asmav: []str = [driver(stages[si]), "build", "-S", "-w", asmwork,
|
||||
ignored];
|
||||
runcommandenv(root, strings.concat("testonly-assembly-", tags[si]),
|
||||
asmav, env, (30i64 * (time.second: i64)): time.duration, &out);
|
||||
expectexit(&out, 0);
|
||||
assert(out.stdout.len == 0 && out.stderr.len == 0 && !os.exists(asmwork)
|
||||
&& !os.exists(defaultoutput) && !os.exists(defaultsepwork)
|
||||
&& readfile(ctrace).len == 0 && readfile(atrace).len == 0
|
||||
&& readfile(ltrace).len == 0);
|
||||
|
||||
let nullwork: str = strings.concat(root, "/null-unused-", tags[si]);
|
||||
let nullav: []str = [driver(stages[si]), "build", "-w", nullwork,
|
||||
"-o", "/dev/null", ignored];
|
||||
runcommandenv(root, strings.concat("testonly-null-", tags[si]),
|
||||
nullav, env, (30i64 * (time.second: i64)): time.duration, &out);
|
||||
expectexit(&out, 0);
|
||||
assert(out.stdout.len == 0 && out.stderr.len == 0 && !os.exists(nullwork)
|
||||
&& readfile(ctrace).len == 0 && readfile(atrace).len == 0
|
||||
&& readfile(ltrace).len == 0);
|
||||
|
||||
let filework: str = strings.concat(root, "/file-unused-", tags[si]);
|
||||
let fileout: str = strings.concat(root, "/file-output-", tags[si]);
|
||||
let fileav: []str = [driver(stages[si]), "build", "-w", filework,
|
||||
"-o", fileout, ignored];
|
||||
runcommandenv(root, strings.concat("testonly-file-", tags[si]),
|
||||
fileav, env, (30i64 * (time.second: i64)): time.duration, &out);
|
||||
expectexit(&out, 1);
|
||||
assert(out.stdout.len == 0 && same(out.stderr, "ww: no packages to build\n")
|
||||
&& !os.exists(filework) && !os.exists(fileout)
|
||||
&& !os.exists(strings.concat(fileout, ".new"))
|
||||
&& readfile(ctrace).len == 0 && readfile(atrace).len == 0
|
||||
&& readfile(ltrace).len == 0);
|
||||
let fileasmav: []str = [driver(stages[si]), "build", "-S", "-w",
|
||||
filework, "-o", fileout, ignored];
|
||||
runcommandenv(root, strings.concat("testonly-file-assembly-", tags[si]),
|
||||
fileasmav, env, (30i64 * (time.second: i64)): time.duration, &out);
|
||||
expectexit(&out, 1);
|
||||
assert(out.stdout.len == 0 && same(out.stderr, "ww: no packages to build\n")
|
||||
&& !os.exists(filework) && !os.exists(fileout));
|
||||
|
||||
let outputdir: str = strings.concat(root, "/output-dir-", tags[si]);
|
||||
let marker: str = strings.concat(outputdir, "/marker");
|
||||
mkdirall(outputdir); writefile(marker, "caller-owned\n");
|
||||
let dirwork: str = strings.concat(root, "/dir-unused-", tags[si]);
|
||||
let dirav: []str = [driver(stages[si]), "build", "-w", dirwork,
|
||||
"-o", outputdir, ignored];
|
||||
runcommandenv(root, strings.concat("testonly-directory-", tags[si]),
|
||||
dirav, env, (30i64 * (time.second: i64)): time.duration, &out);
|
||||
expectexit(&out, 1);
|
||||
assert(out.stdout.len == 0
|
||||
&& same(out.stderr, "ww: no main packages to build\n")
|
||||
&& same(readfile(marker), "caller-owned\n") && !os.exists(dirwork));
|
||||
let trailingav: []str = [driver(stages[si]), "build", "-w", dirwork,
|
||||
"-o", strings.concat(outputdir, "/"), ignored];
|
||||
runcommandenv(root, strings.concat("testonly-directory-trailing-", tags[si]),
|
||||
trailingav, env, (30i64 * (time.second: i64)): time.duration, &out);
|
||||
expectexit(&out, 1);
|
||||
assert(out.stdout.len == 0
|
||||
&& same(out.stderr, "ww: no main packages to build\n")
|
||||
&& same(readfile(marker), "caller-owned\n") && !os.exists(dirwork)
|
||||
&& readfile(ctrace).len == 0 && readfile(atrace).len == 0
|
||||
&& readfile(ltrace).len == 0);
|
||||
|
||||
let badwork: str = strings.concat(root, "/bad-unused-", tags[si]);
|
||||
let badout: str = strings.concat(root, "/bad-output-", tags[si]);
|
||||
let badpackageav: []str = [driver(stages[si]), "build", "-w", badwork,
|
||||
"-o", badout, badpackage];
|
||||
runcommandenv(root, strings.concat("testonly-bad-package-", tags[si]),
|
||||
badpackageav, env, (30i64 * (time.second: i64)): time.duration, &out);
|
||||
expectexit(&out, 1);
|
||||
assert(out.stdout.len == 0
|
||||
&& has(out.stderr, "invalid or missing package clause\n")
|
||||
&& !has(out.stderr, "no packages to build") && !os.exists(badwork)
|
||||
&& !os.exists(badout) && readfile(ctrace).len == 0
|
||||
&& readfile(atrace).len == 0 && readfile(ltrace).len == 0);
|
||||
packagebad[si] = strings.dup(out.stderr);
|
||||
let badimportav: []str = [driver(stages[si]), "build", "-w", badwork,
|
||||
"-o", badout, badimport];
|
||||
runcommandenv(root, strings.concat("testonly-bad-import-", tags[si]),
|
||||
badimportav, env, (30i64 * (time.second: i64)): time.duration, &out);
|
||||
expectexit(&out, 1);
|
||||
let importwant: str = strings.concat(badimport,
|
||||
":2:8: error: expected identifier, got ;\n");
|
||||
assert(out.stdout.len == 0 && same(out.stderr, importwant)
|
||||
&& !has(out.stderr, "no packages to build") && !os.exists(badwork)
|
||||
&& !os.exists(badout) && readfile(ctrace).len == 0
|
||||
&& readfile(atrace).len == 0 && readfile(ltrace).len == 0);
|
||||
importbad[si] = strings.dup(out.stderr);
|
||||
|
||||
// Pinned readGoInfo discards the first ordinary body stop byte, even
|
||||
// when it starts malformed UTF-8 or a non-leading BOM. Reached NUL and
|
||||
// unterminated header-trivia comments remain loader errors.
|
||||
let utf8av: []str = [driver(stages[si]), "build", "-w", badwork,
|
||||
"-o", badout, bodyutf8];
|
||||
runcommandenv(root, strings.concat("testonly-body-utf8-", tags[si]),
|
||||
utf8av, env, (30i64 * (time.second: i64)): time.duration, &out);
|
||||
expectexit(&out, 1);
|
||||
assert(out.stdout.len == 0
|
||||
&& same(out.stderr, "ww: no packages to build\n")
|
||||
&& !os.exists(badwork) && !os.exists(badout));
|
||||
let bomav: []str = [driver(stages[si]), "build", "-w", badwork,
|
||||
"-o", badout, bodybom];
|
||||
runcommandenv(root, strings.concat("testonly-body-bom-", tags[si]),
|
||||
bomav, env, (30i64 * (time.second: i64)): time.duration, &out);
|
||||
expectexit(&out, 1);
|
||||
assert(out.stdout.len == 0
|
||||
&& same(out.stderr, "ww: no packages to build\n")
|
||||
&& !os.exists(badwork) && !os.exists(badout));
|
||||
let nulav: []str = [driver(stages[si]), "build", "-w", badwork,
|
||||
"-o", badout, bodynul];
|
||||
runcommandenv(root, strings.concat("testonly-body-nul-", tags[si]),
|
||||
nulav, env, (30i64 * (time.second: i64)): time.duration, &out);
|
||||
expectexit(&out, 1);
|
||||
let nulwant: str = strings.concat(bodynul,
|
||||
":2:1: error: invalid NUL character\n");
|
||||
assert(out.stdout.len == 0 && same(out.stderr, nulwant)
|
||||
&& !os.exists(badwork) && !os.exists(badout));
|
||||
nulbad[si] = strings.dup(out.stderr);
|
||||
let commentav: []str = [driver(stages[si]), "build", "-w", badwork,
|
||||
"-o", badout, badcomment];
|
||||
runcommandenv(root, strings.concat("testonly-bad-comment-", tags[si]),
|
||||
commentav, env, (30i64 * (time.second: i64)): time.duration, &out);
|
||||
expectexit(&out, 1);
|
||||
let commentwant: str = strings.concat(badcomment,
|
||||
":2:16: error: unterminated /* comment\n");
|
||||
assert(out.stdout.len == 0 && same(out.stderr, commentwant)
|
||||
&& !os.exists(badwork) && !os.exists(badout)
|
||||
&& readfile(ctrace).len == 0 && readfile(atrace).len == 0
|
||||
&& readfile(ltrace).len == 0);
|
||||
commentbad[si] = strings.dup(out.stderr);
|
||||
|
||||
let spellingwork: str = strings.concat(root, "/spelling-unused-", tags[si]);
|
||||
let platformav: []str = [driver(stages[si]), "build", "-w",
|
||||
spellingwork, platform];
|
||||
runcommandenv(root, strings.concat("testonly-platform-", tags[si]),
|
||||
platformav, env, (30i64 * (time.second: i64)): time.duration, &out);
|
||||
expectexit(&out, 0);
|
||||
assert(out.stdout.len == 0 && out.stderr.len == 0 && !os.exists(spellingwork));
|
||||
let linkav: []str = [driver(stages[si]), "build", "-w", spellingwork,
|
||||
link];
|
||||
runcommandenv(root, strings.concat("testonly-requested-link-", tags[si]),
|
||||
linkav, env, (30i64 * (time.second: i64)): time.duration, &out);
|
||||
expectexit(&out, 0);
|
||||
assert(out.stdout.len == 0 && out.stderr.len == 0 && !os.exists(spellingwork)
|
||||
&& readfile(ctrace).len == 0 && readfile(atrace).len == 0
|
||||
&& readfile(ltrace).len == 0);
|
||||
|
||||
// A directory target remains a directory request despite a test-looking
|
||||
// requested spelling; this is deliberately the one producer-positive row.
|
||||
let linkdirwork: str = strings.concat(root, "/link-dir-work-", tags[si]);
|
||||
let linkdirout: str = strings.concat(root, "/link-dir-output-", tags[si]);
|
||||
mkdirall(linkdirwork);
|
||||
let linkdirav: []str = [driver(stages[si]), "build", "-w", linkdirwork,
|
||||
"-o", linkdirout, dirlink];
|
||||
runcommandenv(root, strings.concat("testonly-directory-link-", tags[si]),
|
||||
linkdirav, env, (60i64 * (time.second: i64)): time.duration, &out);
|
||||
if (si == 0) {
|
||||
expectexit(&out, 0);
|
||||
assert(out.stdout.len == 0 && out.stderr.len == 0
|
||||
&& os.exists(linkdirout) && readfile(ctrace).len != 0
|
||||
&& readfile(atrace).len != 0 && readfile(ltrace).len != 0);
|
||||
} else {
|
||||
// The selected non-directory classifier must not absorb this
|
||||
// pre-existing WWstage resolution difference into the slice.
|
||||
expectexit(&out, 1);
|
||||
assert(out.stdout.len == 0
|
||||
&& same(out.stderr, "ww: cannot read source\n")
|
||||
&& !os.exists(linkdirout) && readfile(ctrace).len == 0
|
||||
&& readfile(atrace).len == 0 && readfile(ltrace).len == 0);
|
||||
};
|
||||
rewritefile(ctrace, ""); rewritefile(atrace, ""); rewritefile(ltrace, "");
|
||||
|
||||
// A valid retained raw source seeds state. Renaming only its requested
|
||||
// basename to test-only must leave all public and private bytes untouched.
|
||||
let warmwork: str = strings.concat(root, "/warm-work-", tags[si]);
|
||||
let warmout: str = strings.concat(root, "/warm-output-", tags[si]);
|
||||
mkdirall(warmwork);
|
||||
let warmav: []str = [driver(stages[si]), "build", "-w", warmwork,
|
||||
"-o", warmout, warmpath];
|
||||
runcommandenv(root, strings.concat("testonly-warm-seed-", tags[si]),
|
||||
warmav, env, (60i64 * (time.second: i64)): time.duration, &out);
|
||||
expectexit(&out, 0);
|
||||
assert(out.stdout.len == 0 && out.stderr.len == 0 && os.exists(warmout));
|
||||
let warmbin: str = strings.dup(readfile(warmout));
|
||||
let warmtree: str = strings.dup(treesnapshot(warmwork));
|
||||
if (si == 0) {
|
||||
cwarmbin = strings.dup(warmbin);
|
||||
cwarmartifacts = strings.dup(artifacttreesnapshot(warmwork));
|
||||
} else {
|
||||
assert(same(cwarmbin, warmbin)
|
||||
&& same(cwarmartifacts, artifacttreesnapshot(warmwork)));
|
||||
};
|
||||
assert(os.rename(warmpath, warmtest) == 0);
|
||||
rewritefile(ctrace, ""); rewritefile(atrace, ""); rewritefile(ltrace, "");
|
||||
let warmtestav: []str = [driver(stages[si]), "build", "-w", warmwork,
|
||||
"-o", warmout, warmtest];
|
||||
runcommandenv(root, strings.concat("testonly-warm-omit-", tags[si]),
|
||||
warmtestav, env, (30i64 * (time.second: i64)): time.duration, &out);
|
||||
expectexit(&out, 1);
|
||||
assert(out.stdout.len == 0 && same(out.stderr, "ww: no packages to build\n")
|
||||
&& same(warmbin, readfile(warmout)) && !os.exists(strings.concat(warmout, ".new"))
|
||||
&& readfile(ctrace).len == 0 && readfile(atrace).len == 0
|
||||
&& readfile(ltrace).len == 0 && !directoryhasnew(warmwork)
|
||||
&& !directoryhasfragment(warmwork, ".wwtxn.")
|
||||
&& same(warmtree, treesnapshot(warmwork)));
|
||||
if (si == 1) {
|
||||
assert(same(cwarmbin, readfile(warmout))
|
||||
&& same(cwarmartifacts, artifacttreesnapshot(warmwork)));
|
||||
};
|
||||
assert(os.rename(warmtest, warmpath) == 0);
|
||||
runcommandenv(root, strings.concat("testonly-warm-restored-", tags[si]),
|
||||
warmav, env, (60i64 * (time.second: i64)): time.duration, &out);
|
||||
expectexit(&out, 0);
|
||||
assert(out.stdout.len == 0 && out.stderr.len == 0
|
||||
&& same(warmbin, readfile(warmout)) && !directoryhasnew(warmwork)
|
||||
&& same(warmtree, treesnapshot(warmwork)));
|
||||
if (si == 1) {
|
||||
assert(same(cwarmbin, readfile(warmout))
|
||||
&& same(cwarmartifacts, artifacttreesnapshot(warmwork)));
|
||||
};
|
||||
|
||||
// Test and run keep their existing raw-source semantics, while a visible
|
||||
// non-test raw build and a multi-operand error do not enter this slice.
|
||||
rewritefile(ctrace, ""); rewritefile(atrace, ""); rewritefile(ltrace, "");
|
||||
let testwork: str = strings.concat(root, "/route-work-", tags[si]);
|
||||
mkdirall(testwork);
|
||||
let testav: []str = [driver(stages[si]), "test", "-w", testwork,
|
||||
testroute];
|
||||
runcommandenv(root, strings.concat("testonly-raw-test-", tags[si]),
|
||||
testav, env, (60i64 * (time.second: i64)): time.duration, &out);
|
||||
expectexit(&out, 0);
|
||||
assert(out.stderr.len == 0 && has(out.stdout, "still_selected ... ok\n")
|
||||
&& readfile(ctrace).len != 0);
|
||||
let compiled: str = strings.concat(root, "/route-compiled-", tags[si]);
|
||||
let compileav: []str = [driver(stages[si]), "test", "-c", "-o", compiled,
|
||||
testroute];
|
||||
runcommandenv(root, strings.concat("testonly-raw-test-compile-", tags[si]),
|
||||
compileav, env, (60i64 * (time.second: i64)): time.duration, &out);
|
||||
expectexit(&out, 0);
|
||||
assert(out.stdout.len == 0 && out.stderr.len == 0 && os.exists(compiled));
|
||||
let compiledbytes: str = strings.dup(readfile(compiled));
|
||||
if (si == 0) { ccompiled = strings.dup(compiledbytes); }
|
||||
else { assert(same(ccompiled, compiledbytes)); };
|
||||
let testasm: str = strings.concat(root, "/route-assembly-", tags[si]);
|
||||
let testasmav: []str = [driver(stages[si]), "test", "-S", "-o", testasm,
|
||||
testroute];
|
||||
runcommandenv(root, strings.concat("testonly-raw-test-assembly-", tags[si]),
|
||||
testasmav, env, (60i64 * (time.second: i64)): time.duration, &out);
|
||||
expectexit(&out, 0);
|
||||
let testasmtree: str = strings.concat(testasm, ".sepwork");
|
||||
assert(out.stdout.len == 0 && out.stderr.len == 0
|
||||
&& os.exists(testasmtree));
|
||||
let testasmstate: str = strings.dup(artifacttreesnapshot(testasmtree));
|
||||
if (si == 0) { ctestasm = strings.dup(testasmstate); }
|
||||
else { assert(same(ctestasm, testasmstate)); };
|
||||
let runav: []str = [driver(stages[si]), "run", runsource];
|
||||
runcommandenv(root, strings.concat("testonly-run-", tags[si]), runav,
|
||||
env, (60i64 * (time.second: i64)): time.duration, &out);
|
||||
expectexit(&out, 9);
|
||||
let rawout: str = strings.concat(root, "/raw-output-", tags[si]);
|
||||
let rawav: []str = [driver(stages[si]), "build", "-o", rawout, rawmain];
|
||||
runcommandenv(root, strings.concat("testonly-raw-build-", tags[si]),
|
||||
rawav, env, (60i64 * (time.second: i64)): time.duration, &out);
|
||||
expectexit(&out, 0);
|
||||
assert(out.stdout.len == 0 && out.stderr.len == 0 && os.exists(rawout));
|
||||
let rawbytes: str = strings.dup(readfile(rawout));
|
||||
if (si == 0) { crawbin = strings.dup(rawbytes); }
|
||||
else { assert(same(crawbin, rawbytes)); };
|
||||
let multiout: str = strings.concat(root, "/multi-output-", tags[si]);
|
||||
rewritefile(ctrace, ""); rewritefile(atrace, ""); rewritefile(ltrace, "");
|
||||
let multiav: []str = [driver(stages[si]), "build", "-o", multiout,
|
||||
rawmain, ignored];
|
||||
runcommandenv(root, strings.concat("testonly-multiple-", tags[si]),
|
||||
multiav, env, (30i64 * (time.second: i64)): time.duration, &out);
|
||||
let multiwant: str = strings.concat(
|
||||
"wwtest package: ", rawmain,
|
||||
": cannot canonicalize package directory\n",
|
||||
"wwtest package: ", ignored,
|
||||
": cannot canonicalize package directory\n");
|
||||
assert(out.termination == exec.termination.EXIT && out.code != 0
|
||||
&& out.stdout.len == 0 && same(out.stderr, multiwant)
|
||||
&& !os.exists(multiout) && readfile(ctrace).len == 0
|
||||
&& readfile(atrace).len == 0 && readfile(ltrace).len == 0);
|
||||
multibad[si] = strings.dup(out.stderr);
|
||||
|
||||
si += 1;
|
||||
};
|
||||
assert(same(packagebad[0], packagebad[1]));
|
||||
assert(same(importbad[0], importbad[1]));
|
||||
assert(same(nulbad[0], nulbad[1]));
|
||||
assert(same(commentbad[0], commentbad[1]));
|
||||
assert(same(multibad[0], multibad[1]));
|
||||
|
||||
// Two simultaneous omitted requests have no work/output collision and no
|
||||
// producer invocation in either stage.
|
||||
let ti: i32 = 0;
|
||||
for (ti < stages.len) {
|
||||
rewritefile(tracecs[ti], "");
|
||||
rewritefile(traceas[ti], "");
|
||||
rewritefile(tracels[ti], "");
|
||||
ti += 1;
|
||||
};
|
||||
let pcwork: str = strings.concat(root, "/parallel-c-unused");
|
||||
let pwwork: str = strings.concat(root, "/parallel-ww-unused");
|
||||
let pcav: []str = [driver("ww"), "build", "-w", pcwork, ignored];
|
||||
let pwav: []str = [driver("ww_ww"), "build", "-w", pwwork, ignored];
|
||||
let pc: exec.command;
|
||||
pc.path = pcav[0]; pc.argv = pcav; pc.env = environments[0]; pc.dir = repo();
|
||||
pc.stdoutpath = strings.concat(root, "/parallel-c.stdout");
|
||||
pc.stderrpath = strings.concat(root, "/parallel-c.stderr");
|
||||
pc.deadline = time.add(time.now(time.clock.monotonic),
|
||||
(30i64 * (time.second: i64)): time.duration);
|
||||
pc.grace = (100i64 * (time.millisecond: i64)): time.duration;
|
||||
let pw: exec.command;
|
||||
pw.path = pwav[0]; pw.argv = pwav; pw.env = environments[1]; pw.dir = repo();
|
||||
pw.stdoutpath = strings.concat(root, "/parallel-ww.stdout");
|
||||
pw.stderrpath = strings.concat(root, "/parallel-ww.stderr");
|
||||
pw.deadline = time.add(time.now(time.clock.monotonic),
|
||||
(30i64 * (time.second: i64)): time.duration);
|
||||
pw.grace = (100i64 * (time.millisecond: i64)): time.duration;
|
||||
let pcp: exec.process;
|
||||
let pwp: exec.process;
|
||||
exec.start(&pcp, &pc); exec.start(&pwp, &pw);
|
||||
let pcdone: bool = false;
|
||||
let pwdone: bool = false;
|
||||
for (!pcdone || !pwdone) {
|
||||
if (!pcdone) { pcdone = exec.poll(&pcp); };
|
||||
if (!pwdone) { pwdone = exec.poll(&pwp); };
|
||||
if (!pcdone || !pwdone) {
|
||||
time.sleep(time.millisecond, time.clock.monotonic);
|
||||
};
|
||||
};
|
||||
assert(pcp.result.errno == 0 && pcp.result.cleanuperrno == 0
|
||||
&& pcp.result.termination == exec.termination.EXIT && pcp.result.code == 0
|
||||
&& pwp.result.errno == 0 && pwp.result.cleanuperrno == 0
|
||||
&& pwp.result.termination == exec.termination.EXIT && pwp.result.code == 0);
|
||||
assert(readfile(pc.stdoutpath).len == 0 && readfile(pc.stderrpath).len == 0
|
||||
&& readfile(pw.stdoutpath).len == 0 && readfile(pw.stderrpath).len == 0
|
||||
&& !os.exists(pcwork) && !os.exists(pwwork));
|
||||
ti = 0;
|
||||
for (ti < stages.len) {
|
||||
assert(readfile(tracecs[ti]).len == 0
|
||||
&& readfile(traceas[ti]).len == 0
|
||||
&& readfile(tracels[ti]).len == 0);
|
||||
ti += 1;
|
||||
};
|
||||
assert(!directoryhasnew(root) && !directoryhasfragment(root, ".wwtxn.")
|
||||
&& !directoryhasfragment(root, ".install")
|
||||
&& !directoryhasfragment(root, ".capture")
|
||||
&& !directoryhasfragment(root, ".result")
|
||||
&& !directoryhasfragment(root, ".request"));
|
||||
clean(root);
|
||||
};
|
||||
|
||||
// A source file has one contiguous import section immediately after its
|
||||
// package clause. The parser may continue after a late import for recovery,
|
||||
// but the imports-only loader and the full compiler parser both reject it.
|
||||
|
||||
@@ -18,10 +18,11 @@ package wwdump_test;
|
||||
// wwdump_ww's exit code is deliberately NOT gated: the diagnostics
|
||||
// land on stderr regardless, and the armed bail flips the exit code —
|
||||
// the line count is the one signal stable across the fold sequence.
|
||||
// #90 sep-feed: `ww build -S` resolves import-bearing fixtures to an
|
||||
// owner unit and direct exports; this raw wwdump-only gate explicitly
|
||||
// composes those artifacts because wwdump has no package-input CLI. The
|
||||
// import-free test/wcc/901_*.ww companions stay raw-fed.
|
||||
// #90 sep-feed: `ww build -S` resolves production fixtures and `ww test
|
||||
// -S` resolves test fixtures to an owner unit and direct exports; this raw
|
||||
// wwdump-only gate explicitly composes those artifacts because wwdump has no
|
||||
// package-input CLI. The import-free test/wcc/901_*.ww companions stay
|
||||
// raw-fed.
|
||||
//
|
||||
// wwdumpgate (#52, F15 c4) — the -c and -r arms gate on parse-stage
|
||||
// errors instead of silently emitting over a broken AST: parse-errored
|
||||
@@ -87,18 +88,20 @@ fn countdiag(stderr: str) i32 = {
|
||||
if (seps[i]) {
|
||||
let stem: str = strings.concat(td, "/unit");
|
||||
let bo: testenv.commandout;
|
||||
let bav: []str = [testenv.driver("ww"), "build", "-S", "-o",
|
||||
let action: str = "build";
|
||||
if (strings.hassuffix(rels[i], "_test.ww")) { action = "test"; };
|
||||
let bav: []str = [testenv.driver("ww"), action, "-S", "-o",
|
||||
stem, src];
|
||||
testenv.runcommand(td, td, "resolve", bav, tmo(), &bo);
|
||||
if (bo.termination != exec.termination.EXIT || bo.code != 0) {
|
||||
fail(classes[i], strings.concat(rels[i],
|
||||
": no resolved sep unit (ww build -S failed)"));
|
||||
": no resolved sep unit (ww build/test -S failed)"));
|
||||
};
|
||||
let work: str = strings.concat(stem, ".sepwork/");
|
||||
let owner: str = strings.concat(work, "__root.unit.ww");
|
||||
if (!testenv.exists(owner)) {
|
||||
fail(classes[i], strings.concat(rels[i],
|
||||
": __root.unit.ww missing after ww build -S"));
|
||||
": __root.unit.ww missing after ww build/test -S"));
|
||||
};
|
||||
let deps: []str = [];
|
||||
if (i == 0) { append(deps, "math.checked"); append(deps, "types"); };
|
||||
|
||||
@@ -100,6 +100,40 @@ imports_to_str(const char *src, int *errs, const char **pkg)
|
||||
return buf;
|
||||
}
|
||||
|
||||
static char *
|
||||
header_to_str(const char *src, size_t len, int *errs, const char **pkg)
|
||||
{
|
||||
Arena *a = newarena();
|
||||
Lex l;
|
||||
Parser p;
|
||||
lexinit(&l, a, "header.ww", src, len);
|
||||
parserinit(&p, a, &l);
|
||||
Node *n = parsepackageheader(&p);
|
||||
*errs = p.errs + l.errs;
|
||||
*pkg = n->module ? strdup(n->module) : NULL;
|
||||
|
||||
char *buf = NULL;
|
||||
size_t outlen = 0;
|
||||
FILE *f = open_memstream(&buf, &outlen);
|
||||
if (f != NULL) {
|
||||
astprint(f, n);
|
||||
fclose(f);
|
||||
}
|
||||
freearena(a);
|
||||
return buf;
|
||||
}
|
||||
|
||||
static int
|
||||
header_errors(const char *src)
|
||||
{
|
||||
int errs;
|
||||
const char *pkg;
|
||||
char *got = header_to_str(src, strlen(src), &errs, &pkg);
|
||||
free((void *)pkg);
|
||||
free(got);
|
||||
return errs;
|
||||
}
|
||||
|
||||
static int
|
||||
must_import_shape(const char *src, int imports_only, const char *binding,
|
||||
const char *path, const char *alias)
|
||||
@@ -300,6 +334,119 @@ main(void)
|
||||
free((void *)pkg);
|
||||
free(got);
|
||||
}
|
||||
{
|
||||
const char *src =
|
||||
"package main;\n"
|
||||
"import zed;\n"
|
||||
"import stable alpha.beta;\n"
|
||||
"fn body() void = {};\n";
|
||||
int errs;
|
||||
const char *pkg;
|
||||
char *got = header_to_str(src, strlen(src), &errs, &pkg);
|
||||
if (errs != 0 || pkg == NULL || strcmp(pkg, "main") != 0
|
||||
|| got == NULL || strstr(got, "(use \"zed\"") == NULL
|
||||
|| strstr(got, "(use \"stable\"") == NULL
|
||||
|| strstr(got, "body") != NULL) {
|
||||
fputs("package-header parse mismatch\n", stderr);
|
||||
fail++;
|
||||
}
|
||||
free((void *)pkg);
|
||||
free(got);
|
||||
}
|
||||
{
|
||||
const char *src =
|
||||
"package main;\n"
|
||||
"\xff";
|
||||
int errs;
|
||||
const char *pkg;
|
||||
char *got = header_to_str(src, strlen(src), &errs, &pkg);
|
||||
if (errs != 0 || pkg == NULL || strcmp(pkg, "main") != 0
|
||||
|| got == NULL) {
|
||||
fputs("package-header observed the first malformed UTF-8 body byte\n",
|
||||
stderr);
|
||||
fail++;
|
||||
}
|
||||
free((void *)pkg);
|
||||
free(got);
|
||||
}
|
||||
{
|
||||
const char src[] =
|
||||
"package main;\n"
|
||||
"import alpha;\n"
|
||||
"\xef\xbb\xbf";
|
||||
int errs;
|
||||
const char *pkg;
|
||||
char *got = header_to_str(src, sizeof src - 1, &errs, &pkg);
|
||||
if (errs != 0 || pkg == NULL || strcmp(pkg, "main") != 0
|
||||
|| got == NULL || strstr(got, "(use \"alpha\"") == NULL) {
|
||||
fputs("package-header observed the first body BOM\n", stderr);
|
||||
fail++;
|
||||
}
|
||||
free((void *)pkg);
|
||||
free(got);
|
||||
}
|
||||
{
|
||||
const char src[] = "package main;\n\0fn body() void = {};\n";
|
||||
int errs;
|
||||
const char *pkg;
|
||||
char *got = header_to_str(src, sizeof src - 1, &errs, &pkg);
|
||||
if (errs == 0) {
|
||||
fputs("package-header ignored a reached NUL boundary byte\n",
|
||||
stderr);
|
||||
fail++;
|
||||
}
|
||||
free((void *)pkg);
|
||||
free(got);
|
||||
}
|
||||
if (header_errors("package main;\n/* unterminated") == 0) {
|
||||
fputs("package-header accepted unterminated header trivia\n", stderr);
|
||||
fail++;
|
||||
}
|
||||
{
|
||||
const char *src =
|
||||
"package main;\n"
|
||||
"/* between */ import alpha; // after\n"
|
||||
"import beta;\n"
|
||||
"\xff";
|
||||
int errs;
|
||||
const char *pkg;
|
||||
char *got = header_to_str(src, strlen(src), &errs, &pkg);
|
||||
if (errs != 0 || got == NULL
|
||||
|| strstr(got, "(use \"alpha\"") == NULL
|
||||
|| strstr(got, "(use \"beta\"") == NULL) {
|
||||
fputs("package-header comment/import boundary mismatch\n", stderr);
|
||||
fail++;
|
||||
}
|
||||
free((void *)pkg);
|
||||
free(got);
|
||||
}
|
||||
{
|
||||
int errs;
|
||||
const char *pkg;
|
||||
char *got = header_to_str(
|
||||
"package main;\nimported body;\nimport late;\n",
|
||||
strlen("package main;\nimported body;\nimport late;\n"),
|
||||
&errs, &pkg);
|
||||
if (errs != 0 || got == NULL || strstr(got, "late") != NULL) {
|
||||
fputs("package-header mistook an ordinary identifier for import\n",
|
||||
stderr);
|
||||
fail++;
|
||||
}
|
||||
free((void *)pkg);
|
||||
free(got);
|
||||
}
|
||||
if (header_errors("package ;\n") == 0) {
|
||||
fputs("package-header accepted a malformed package clause\n", stderr);
|
||||
fail++;
|
||||
}
|
||||
if (header_errors("package main;\nimport ;\n") == 0) {
|
||||
fputs("package-header accepted a malformed import\n", stderr);
|
||||
fail++;
|
||||
}
|
||||
if (header_errors("package main\nfn body() void = {};\n") == 0) {
|
||||
fputs("package-header accepted a missing package semicolon\n", stderr);
|
||||
fail++;
|
||||
}
|
||||
{
|
||||
int errs;
|
||||
const char *pkg;
|
||||
@@ -333,6 +480,6 @@ main(void)
|
||||
fprintf(stderr, "%d parse tests failed\n", fail);
|
||||
return 1;
|
||||
}
|
||||
printf("parse: %d/%d ok + 14 shape ok\n", n, n);
|
||||
printf("parse: %d/%d ok + 14 shape + 10 header ok\n", n, n);
|
||||
return 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user