From 8dad2755b3b0c511f3de738de257aa51ba1ef7cd Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Sat, 15 Aug 2026 01:27:16 +0900 Subject: [PATCH] ww: unify directory package-test products --- cmd/w6c/main.c | 32 +- cmd/wcc/check.c | 20 +- cmd/wcc/ww.h | 4 +- cmd/ww/main.c | 714 +++++++++++++++++++------ internal/wwpackage/package.ww | 210 +++++--- selfhost/cmd/w6c/main.ww | 56 +- selfhost/cmd/wcc/api.ww | 4 +- selfhost/cmd/wcc/check.ww | 25 +- selfhost/cmd/ww/main.ww | 970 ++++++++++++++++++++++++++-------- selfhost/cmd/wwdump/main.ww | 3 +- 10 files changed, 1553 insertions(+), 485 deletions(-) diff --git a/cmd/w6c/main.c b/cmd/w6c/main.c index fbdced9c..63955a11 100644 --- a/cmd/w6c/main.c +++ b/cmd/w6c/main.c @@ -373,7 +373,7 @@ main(int argc, char **argv) const char *out = NULL; const char *wwiout = NULL; /* -I : M2 export-data producer */ const char *testsupport = NULL; - const char *testtarget = NULL; + const char **testtargets = calloc((size_t)argc, sizeof *testtargets); const char *packageinit = NULL; const char *initdispatch = NULL; int testmode = 0; @@ -385,14 +385,16 @@ main(int argc, char **argv) * only; treat `.wwi` deps as external) */ struct importin *imports = calloc((size_t)argc, sizeof *imports); struct importmap *maps = calloc((size_t)argc, sizeof *maps); - if (imports == NULL || maps == NULL) { + if (imports == NULL || maps == NULL || testtargets == NULL) { fputs("w6c: out of memory\n", stderr); + free(testtargets); free(maps); free(imports); return 1; } int nimports = 0; int nmaps = 0; + int ntesttargets = 0; for (int i = 1; i < argc; i++) { const char *a = argv[i]; if (strcmp(a, "-o") == 0 && i + 1 < argc) { @@ -430,7 +432,7 @@ main(int argc, char **argv) fputs("w6c: --test-target-package requires arg\n", stderr); return 2; } - testtarget = argv[++i]; + testtargets[ntesttargets++] = argv[++i]; } else if (strcmp(a, "-c") == 0) { sepmode = 1; } else if (strcmp(a, "--import") == 0) { @@ -535,12 +537,17 @@ main(int argc, char **argv) fputs("w6c: invalid --test-support-module\n", stderr); return 2; } - if (testtarget != NULL && (!sepmode || !testmode - || testtarget[0] == '\0')) { - fputs("w6c: invalid --test-target-package\n", stderr); - return 2; - } - if (testtarget != NULL) { + for (int ti = 0; ti < ntesttargets; ti++) { + const char *testtarget = testtargets[ti]; + if (!sepmode || !testmode || testtarget[0] == '\0') { + fputs("w6c: invalid --test-target-package\n", stderr); + return 2; + } + if (ti > 0 && strcmp(testtargets[ti - 1], testtarget) >= 0) { + fputs("w6c: --test-target-package paths must be sorted and unique\n", + stderr); + return 2; + } int direct = 0; for (int i = 0; i < nimports; i++) if (strcmp(imports[i].path, testtarget) == 0) @@ -621,11 +628,11 @@ main(int argc, char **argv) if (bind_import_names(file->list, imports, nimports, file, testsupport) < 0) return 1; - if (testtarget != NULL) { + for (int ti = 0; ti < ntesttargets; ti++) { int seen = 0; for (Node *u = file->list; u; u = u->next) { if (u->kind != N_USE || u->imported || u->usepath == NULL - || strcmp(u->usepath, testtarget) != 0) + || strcmp(u->usepath, testtargets[ti]) != 0) continue; u->str = u->usepath; u->strlen = strlen(u->usepath); @@ -646,7 +653,8 @@ main(int argc, char **argv) c.is_test = testmode; c.is_test_package = testpackage; if (testsupport != NULL) c.test_module = testsupport; - c.test_target = testtarget; + c.test_targets = testtargets; + c.n_test_targets = ntesttargets; c.sep_mode = sepmode; c.package_init_symbol = packageinit; check_file(&c, file); diff --git a/cmd/wcc/check.c b/cmd/wcc/check.c index 5a7d53b5..7bf03a4f 100644 --- a/cmd/wcc/check.c +++ b/cmd/wcc/check.c @@ -2869,7 +2869,8 @@ check_init(Checker *c, Arena *a) c->is_test = 0; /* #15: caller (w6c main) sets it after init */ c->is_test_package = 0; c->test_module = "test"; - c->test_target = NULL; + c->test_targets = NULL; + c->n_test_targets = 0; typesinit(a); c->top = newscope(a, NULL); c->cur = c->top; @@ -3995,6 +3996,15 @@ check_import_usage_and_collisions(Checker *c, Node *file) } } +static int +check_test_target(const Checker *c, const char *path) +{ + if (path == NULL) return 0; + for (int i = 0; i < c->n_test_targets; i++) + if (strcmp(c->test_targets[i], path) == 0) return 1; + return 0; +} + void check_file(Checker *c, Node *file) { @@ -4018,8 +4028,7 @@ check_file(Checker *c, Node *file) && !u->imported && u->sourceid == file->sourceid && u->usepath && (strcmp(u->usepath, c->test_module) == 0 - || (c->test_target != NULL - && strcmp(u->usepath, c->test_target) == 0))) + || check_test_target(c, u->usepath))) u->used = 1; if (u->kind == N_USE && !u->imported && u->sourceid == file->sourceid @@ -4383,9 +4392,8 @@ check_file(Checker *c, Node *file) * package as `main`: that would collide with its own entry. * This is a compiler-owned binding, never source alias syntax; * use the canonical target path as its private qualifier. */ - if (c->test_target != NULL - && strcmp(d->module, c->test_target) == 0) - alias = c->test_target; + if (check_test_target(c, d->module)) + alias = d->module; id = newnode(c->a, N_DOT, fp); id->lhs = newnode(c->a, N_IDENT, fp); id->lhs->str = alias; diff --git a/cmd/wcc/ww.h b/cmd/wcc/ww.h index a8e12f72..0fd38ec3 100644 --- a/cmd/wcc/ww.h +++ b/cmd/wcc/ww.h @@ -613,8 +613,8 @@ struct Checker { * bodies and export compiler-private metadata, * but do not synthesize an entry. */ const char *test_module; /* generated dispatcher support qualifier */ - const char *test_target; /* canonical target path used only as the - * compiler-owned generated-main qualifier */ + const char **test_targets; /* canonical generated-main target paths */ + int n_test_targets; int sep_mode; /* -c package compilation: imported interfaces are * present, so absent members are hard export errors. */ Node *synth_test_run; /* exact compiler-generated support.run DOT; diff --git a/cmd/ww/main.c b/cmd/ww/main.c index 5ba8db4f..2345f93d 100644 --- a/cmd/ww/main.c +++ b/cmd/ww/main.c @@ -507,6 +507,7 @@ source_has_test_decl(const char *path) #define SEP_VARIANT_SAME_TEST 1 #define SEP_VARIANT_EXTERNAL 2 #define SEP_VARIANT_TEST_MAIN 3 +#define SEP_VARIANT_TEST_COPY 4 #define SEP_ROLE_NORMAL 0 #define SEP_ROLE_TEST_SUPPORT 1 #define SEP_ROLE_GENERATED_MAIN 2 @@ -846,6 +847,7 @@ struct seppkg { int storage_hashed; /* storage is the bounded complete-action locator */ char *name; /* validated declared name; directory packages only */ char *test_package; /* selected test package; root variants only */ + char *for_test; /* directory product owning a recompiled action */ char **sources; /* owned, byte-sorted selected paths; dirs only */ int nsources; int is_dir; @@ -854,7 +856,9 @@ struct seppkg { int root; /* requested usage; never package-action identity */ int link_entry; /* package supplies the executable's bare main */ int generated_main; /* compiler-owned generated test-main package */ - int generated_target; /* target variant used by generated test main */ + int *generated_targets; /* sorted ptest/pxtest target actions */ + int ngenerated_targets; + int generated_targetcap; int failed; /* discovery/compile failure reaches this action */ int test_support; /* compiler-generated -T support package */ int loaded; /* directory membership/name loaded exactly once */ @@ -895,12 +899,20 @@ struct sepproduct { const char *out; const char *identity; /* explicit canonical lookup identity, if any */ const char *test_package; + const char *production_package; + const char *internal_package; + const char *external_package; const char *status; const char *artifact; int variant; + int directory_product; + int no_tests; int context; int root; - int variant_root; /* production-plus-test or external test package */ + int variant_root; /* retained single-unit root outside directory products */ + int production_root; + int ptest; + int pxtest; int support; /* direct generated-main support action, or -1 */ char *stage_out; /* request-private linked/published output */ char *stage_iface; /* request-private published package interface */ @@ -908,6 +920,7 @@ struct sepproduct { }; static void sep_pkg_free_fields(struct seppkg *); +static int sep_topo_visit(struct sepgraph *, int, int *, int *, int *, int); static int sep_reserve_packages(struct sepgraph *g, int need) @@ -1101,7 +1114,7 @@ sep_sha256_sum(struct sepsha256 *s, u8 out[32]) static char * sep_storage_digest(const struct seppkg *p) { - static const char prefix[] = "ww-package-storage-v2:"; + static const char prefix[] = "ww-package-storage-v3:"; static const char hex[] = "0123456789abcdef"; struct sepsha256 s; u8 sum[32]; @@ -1114,6 +1127,9 @@ sep_storage_digest(const struct seppkg *p) sep_sha256_write(&s, p->path, strlen(p->path)); sep_sha256_write(&s, &zero, 1); sep_sha256_write(&s, p->canon, strlen(p->canon)); + sep_sha256_write(&s, &zero, 1); + if (p->for_test != NULL) + sep_sha256_write(&s, p->for_test, strlen(p->for_test)); sep_sha256_sum(&s, sum); char *out = malloc(80); if (out == NULL) { @@ -1676,6 +1692,8 @@ sep_pkg_free_fields(struct seppkg *p) free(p->init_symbol); free(p->name); free(p->test_package); + free(p->for_test); + free(p->generated_targets); memset(p, 0, sizeof *p); } @@ -2216,7 +2234,8 @@ sep_external_production_edge(const struct sepgraph *g, int importer, int dep) const struct seppkg *from = &g->pkg[importer]; const struct seppkg *to = &g->pkg[dep]; return from->variant == SEP_VARIANT_EXTERNAL - && to->variant == SEP_VARIANT_PRODUCTION + && (to->variant == SEP_VARIANT_PRODUCTION + || to->variant == SEP_VARIANT_SAME_TEST) && to->role == SEP_ROLE_NORMAL && strcmp(from->canon, to->canon) == 0; } @@ -2723,11 +2742,26 @@ sep_scan_file(struct sepgraph *g, int pi, const char *file, rc = -1; break; } - /* The external package's self-production import is the ordinary - * canonical production action for this directory. Discovery role and - * the external product's artifact name never create another action. */ - int di = sep_find_or_add(g, resolved.identity, - resolved.entry, 1); + /* Preserve normal resolution and legality, then bind an external + * self-import to the already-created augmented package when present. */ + int di = -1; + if (external_production) + for (int candidate = 0; candidate < g->n; candidate++) { + struct seppkg *q = &g->pkg[candidate]; + if (q->variant == SEP_VARIANT_SAME_TEST + && q->role == SEP_ROLE_NORMAL + && strcmp(q->canon, canon) == 0 + && (q->import_base != NULL + ? strcmp(q->import_base, + resolved.identity) == 0 + : g->pkg[pi].import_base == NULL)) { + di = candidate; + break; + } + } + if (di < 0) + di = sep_find_or_add(g, resolved.identity, + resolved.entry, 1); if (di < 0) { free(canon); sep_resolved_free(&resolved); @@ -2812,7 +2846,12 @@ sep_dep_cmp(const struct sepgraph *g, int a, int b) return g->pkg[a].variant - g->pkg[b].variant; if (g->pkg[a].role != g->pkg[b].role) return g->pkg[a].role - g->pkg[b].role; - return strcmp(g->pkg[a].canon, g->pkg[b].canon); + int rcanon = strcmp(g->pkg[a].canon, g->pkg[b].canon); + if (rcanon != 0) return rcanon; + if (g->pkg[a].for_test == NULL || g->pkg[b].for_test == NULL) + return g->pkg[a].for_test == NULL + ? (g->pkg[b].for_test == NULL ? 0 : -1) : 1; + return strcmp(g->pkg[a].for_test, g->pkg[b].for_test); } static char * @@ -2825,29 +2864,29 @@ sep_package_init_symbol(const struct seppkg *p) p->path, p->variant, p->role); } -/* Add the compiler-owned test main as a real package action. Its identity is a - * pure function of the selected variant (and its one command-global support - * edge), never a product ordinal. Equivalent products therefore reuse it. */ +/* Add the one compiler-owned main for a canonical directory test product. + * The direct dependency set and the compiler target subset are kept + * separately because support is a dependency but never a test target. */ static int sep_add_generated_main(struct sepgraph *g, struct sepproduct *product, int ordinal, int support) { (void)ordinal; - int variant = product->variant_root; - if (variant < 0 || variant >= g->n) return -1; - const char *kind = "production"; - if (g->pkg[variant].variant == SEP_VARIANT_SAME_TEST) - kind = "internal"; - else if (g->pkg[variant].variant == SEP_VARIANT_EXTERNAL) - kind = "external"; - char *path = sep_sprintf("__wwtestmain.%s.%s.main", - g->pkg[variant].path, kind); - const char *variant_artifact = g->pkg[variant].artifact != NULL - ? g->pkg[variant].artifact : g->pkg[variant].path; - char *artifact = sep_sprintf("%s-main", variant_artifact); - char *canon = sep_sprintf("%s#%s-test-main", - g->pkg[variant].canon, kind); - char *entry = strdup(g->pkg[variant].entry); + int targets[2], ntargets = 0; + if (product->ptest >= 0) targets[ntargets++] = product->ptest; + if (product->pxtest >= 0) targets[ntargets++] = product->pxtest; + if (ntargets == 0) return -1; + if (ntargets == 2 && sep_dep_cmp(g, targets[0], targets[1]) > 0) { + int t = targets[0]; targets[0] = targets[1]; targets[1] = t; + } + int owner = targets[0]; + const char *base = g->pkg[owner].import_base; + if (base == NULL || base[0] == '\0') return -1; + char *path = sep_sprintf("__wwtestmain.%s.main", base); + char *artifact = sep_sprintf("%s-test-main", base); + char *canon = sep_sprintf("%s#directory-test-main", + g->pkg[owner].canon); + char *entry = strdup(g->pkg[owner].entry); if (entry == NULL) sep_fail_nomem(); if (path == NULL || artifact == NULL || canon == NULL || entry == NULL) { free(path); free(artifact); free(canon); free(entry); @@ -2856,15 +2895,21 @@ sep_add_generated_main(struct sepgraph *g, struct sepproduct *product, for (int i = 0; i < g->n; i++) { if (strcmp(g->pkg[i].path, path) != 0) continue; if (g->pkg[i].generated_main) { - int wants_support = support >= 0 && support != variant; - int has_variant = 0, has_support = 0; + int support_is_target = 0; + for (int k = 0; k < ntargets; k++) + if (targets[k] == support) support_is_target = 1; + int wants_support = support >= 0 && !support_is_target; + int same_targets = g->pkg[i].ngenerated_targets == ntargets; + for (int k = 0; k < ntargets && same_targets; k++) + if (g->pkg[i].generated_targets[k] != targets[k]) + same_targets = 0; + int has_support = 0; for (int k = 0; k < g->pkg[i].ndeps; k++) { - if (g->pkg[i].deps[k] == variant) has_variant = 1; if (wants_support && g->pkg[i].deps[k] == support) has_support = 1; } - if (has_variant && has_support == wants_support - && g->pkg[i].ndeps == 1 + wants_support) { + if (same_targets && has_support == wants_support + && g->pkg[i].ndeps == ntargets + wants_support) { free(path); free(artifact); free(canon); free(entry); return i; } @@ -2900,14 +2945,23 @@ sep_add_generated_main(struct sepgraph *g, struct sepproduct *product, p->root = 1; p->link_entry = 1; p->generated_main = 1; - p->generated_target = variant; p->loaded = 1; p->emit_context = product->context; - if (sep_set_context_state(p, product->context, 2) < 0 - || sep_add_dep(g, g->n, variant) < 0 - || (support >= 0 && support != variant - && sep_add_dep(g, g->n, support) < 0)) + if (sep_set_context_state(p, product->context, 2) < 0) goto fail; + for (int k = 0; k < ntargets; k++) { + if (sep_reserve((void **)&p->generated_targets, + &p->generated_targetcap, p->ngenerated_targets + 1, + sizeof *p->generated_targets) < 0) + goto fail; + p->generated_targets[p->ngenerated_targets++] = targets[k]; + if (sep_add_dep(g, g->n, targets[k]) < 0) goto fail; + } + int support_is_target = 0; + for (int k = 0; k < ntargets; k++) + if (targets[k] == support) support_is_target = 1; + if (support >= 0 && !support_is_target + && sep_add_dep(g, g->n, support) < 0) goto fail; for (int i = 1; i < p->ndeps; i++) { int v = p->deps[i]; int j = i; @@ -2926,10 +2980,195 @@ fail: free(p->name); free(p->context_state); free(p->deps); + free(p->generated_targets); memset(p, 0, sizeof *p); return -1; } +static int +sep_rewrite_action_deps(struct sepgraph *g, int pi, const int *replacement, + int nreplacement) +{ + struct seppkg *p = &g->pkg[pi]; + int *deps = NULL, ndeps = 0, depcap = 0; + for (int i = 0; i < p->ndeps; i++) { + int dep = p->deps[i]; + if (dep >= 0 && dep < nreplacement) dep = replacement[dep]; + int found = 0; + for (int j = 0; j < ndeps; j++) + if (deps[j] == dep) { found = 1; break; } + if (found) continue; + if (ndeps == INT_MAX + || sep_reserve((void **)&deps, &depcap, ndeps + 1, + sizeof *deps) < 0) { + free(deps); + return -1; + } + deps[ndeps++] = dep; + } + for (int i = 1; i < ndeps; i++) { + int dep = deps[i], j = i; + while (j > 0 && sep_dep_cmp(g, deps[j - 1], dep) > 0) { + deps[j] = deps[j - 1]; + j--; + } + deps[j] = dep; + } + for (int i = 0; i < p->bindings.n; i++) { + int dep = p->bindings.v[i].dep; + if (dep >= 0 && dep < nreplacement) + p->bindings.v[i].dep = replacement[dep]; + } + if (p->bindings.n > 1) + qsort(p->bindings.v, (size_t)p->bindings.n, + sizeof *p->bindings.v, sep_binding_cmp); + free(p->deps); + p->deps = deps; + p->ndeps = ndeps; + p->depcap = depcap; + return 0; +} + +static int +sep_clone_for_test(struct sepgraph *g, int original, const char *owner, + const int *replacement, int nreplacement) +{ + if (g->n == INT_MAX || sep_reserve_packages(g, g->n + 1) < 0) + return -1; + struct seppkg *src = &g->pkg[original]; + int ni = g->n++; + struct seppkg *p = &g->pkg[ni]; + memset(p, 0, sizeof *p); + p->path = strdup(src->path); + p->import_base = src->import_base != NULL + ? strdup(src->import_base) : NULL; + p->entry = strdup(src->entry); + p->canon = strdup(src->canon); + p->name = src->name != NULL ? strdup(src->name) : NULL; + p->test_package = src->test_package != NULL + ? strdup(src->test_package) : NULL; + p->for_test = strdup(owner); + if (p->path == NULL || (src->import_base != NULL && p->import_base == NULL) + || p->entry == NULL || p->canon == NULL + || (src->name != NULL && p->name == NULL) + || (src->test_package != NULL && p->test_package == NULL) + || p->for_test == NULL) { + sep_fail_nomem(); + goto fail; + } + p->is_dir = src->is_dir; + p->variant = SEP_VARIANT_TEST_COPY; + p->role = src->role; + p->loaded = src->loaded; + p->failed = src->failed; + p->test_support = src->test_support; + p->emit_context = src->emit_context; + if (src->context_cap > 0) { + p->context_state = malloc((size_t)src->context_cap); + if (p->context_state == NULL) { + sep_fail_nomem(); + goto fail; + } + memcpy(p->context_state, src->context_state, + (size_t)src->context_cap); + p->context_cap = src->context_cap; + } + int sourcecap = 0; + for (int i = 0; i < src->nsources; i++) { + if (source_list_add(&p->sources, &p->nsources, &sourcecap, + src->sources[i]) < 0) + goto fail; + } + for (int i = 0; i < src->bindings.n; i++) { + struct sepbind *b = &src->bindings.v[i]; + Pos pos = { b->source, b->line, b->col }; + if (sep_binding_add(&p->bindings, b->kind, b->name, + b->dep, pos) < 0) + goto fail; + } + if (src->ndeps > 0) { + p->deps = malloc((size_t)src->ndeps * sizeof *p->deps); + if (p->deps == NULL) { + sep_fail_nomem(); + goto fail; + } + memcpy(p->deps, src->deps, (size_t)src->ndeps * sizeof *p->deps); + p->ndeps = p->depcap = src->ndeps; + } + if (sep_rewrite_action_deps(g, ni, replacement, nreplacement) < 0) + goto fail; + return ni; +fail: + sep_pkg_free_fields(p); + g->n--; + return -1; +} + +/* Go's recompileForTest is copy-on-write over one directory product. Keep + * canonical package paths unchanged while giving every rebuilt action a + * product-scoped storage identity, then rewire both graph edges and original + * source bindings before any compiler action is formed. */ +static int +sep_recompile_for_test(struct sepgraph *g, struct sepproduct *product) +{ + if (product->production_root < 0 || product->ptest < 0 + || product->production_root == product->ptest) + return 0; + int nbase = g->n; + int *order = calloc((size_t)nbase, sizeof *order); + int *stack = calloc((size_t)nbase, sizeof *stack); + int *replacement = malloc((size_t)nbase * sizeof *replacement); + if (order == NULL || stack == NULL || replacement == NULL) { + sep_fail_nomem(); + free(replacement); free(stack); free(order); + return -1; + } + for (int i = 0; i < nbase; i++) replacement[i] = i; + replacement[product->production_root] = product->ptest; + int norder = 0; + for (int i = 0; i < nbase; i++) g->pkg[i].color = 0; + if (sep_topo_visit(g, product->root, order, &norder, stack, 0) < 0) { + free(replacement); free(stack); free(order); + return -1; + } + const struct seppkg *ownerpkg = &g->pkg[product->ptest]; + char *owner = sep_sprintf("%s#%s", ownerpkg->import_base, + ownerpkg->canon); + if (owner == NULL) { + free(replacement); free(stack); free(order); + return -1; + } + for (int oi = 0; oi < norder; oi++) { + int pi = order[oi]; + if (pi == product->production_root) continue; + int changed = 0; + for (int k = 0; k < g->pkg[pi].ndeps; k++) { + int dep = g->pkg[pi].deps[k]; + if (dep >= 0 && dep < nbase && replacement[dep] != dep) { + changed = 1; + break; + } + } + if (!changed) continue; + if (pi == product->ptest || pi == product->pxtest + || pi == product->root) { + if (sep_rewrite_action_deps(g, pi, replacement, nbase) < 0) { + free(owner); free(replacement); free(stack); free(order); + return -1; + } + } else { + int copy = sep_clone_for_test(g, pi, owner, replacement, nbase); + if (copy < 0) { + free(owner); free(replacement); free(stack); free(order); + return -1; + } + replacement[pi] = copy; + } + } + free(owner); free(replacement); free(stack); free(order); + return 0; +} + /* Load one action's owned sources and direct bindings under one context. * Dependency descent is iterative below so a valid deep graph consumes the * growable frame vector rather than the process call stack. */ @@ -3448,37 +3687,6 @@ sep_topo_visit(struct sepgraph *g, int pi, int *order, int *no, return 0; } -/* Internal test variants replace their colocated production action in the - * corresponding test link closure. Canonical package identity has already - * made every remaining compiler qualifier globally unambiguous. */ -static int -sep_internal_replaces_production(const struct sepgraph *g, int a, int b) -{ - const struct seppkg *internal = &g->pkg[a]; - const struct seppkg *production = &g->pkg[b]; - if (internal->variant != SEP_VARIANT_SAME_TEST) { - internal = &g->pkg[b]; - production = &g->pkg[a]; - } - return internal->variant == SEP_VARIANT_SAME_TEST - && production->variant == SEP_VARIANT_PRODUCTION - && production->role != SEP_ROLE_TEST_SUPPORT - && strcmp(internal->canon, production->canon) == 0; -} - -/* A production action is physically omitted from an internal-test product - * because the augmented variant owns those same production sources. Map every - * edge through that replacement before scheduling initialization, exactly as - * the link-closure filter does. */ -static int -sep_init_effective(const struct sepgraph *g, int variant_root, int pi) -{ - if (variant_root >= 0 && variant_root < g->n - && sep_internal_replaces_production(g, variant_root, pi)) - return variant_root; - return pi; -} - static int sep_init_cmp(const struct sepgraph *g, int a, int b) { @@ -3486,15 +3694,21 @@ sep_init_cmp(const struct sepgraph *g, int a, int b) if (r != 0) return r; if (g->pkg[a].variant != g->pkg[b].variant) return g->pkg[a].variant - g->pkg[b].variant; - return g->pkg[a].role - g->pkg[b].role; + if (g->pkg[a].role != g->pkg[b].role) + return g->pkg[a].role - g->pkg[b].role; + int rcanon = strcmp(g->pkg[a].canon, g->pkg[b].canon); + if (rcanon != 0) return rcanon; + if (g->pkg[a].for_test == NULL || g->pkg[b].for_test == NULL) + return g->pkg[a].for_test == NULL + ? (g->pkg[b].for_test == NULL ? 0 : -1) : 1; + return strcmp(g->pkg[a].for_test, g->pkg[b].for_test); } /* Go's linker uses a lexical ready queue over the reachable init-task DAG. * Compute that schedule explicitly: dependencies become ready first; among * otherwise independent actions canonical package identity breaks ties. */ static int -sep_init_order(const struct sepgraph *g, int root, int variant_root, - int **out, int *nout) +sep_init_order(const struct sepgraph *g, int root, int **out, int *nout) { unsigned char *active = calloc((size_t)g->n, 1); unsigned char *done = calloc((size_t)g->n, 1); @@ -3506,14 +3720,12 @@ sep_init_order(const struct sepgraph *g, int root, int variant_root, return -1; } int ntodo = 0; - int effective_root = sep_init_effective(g, variant_root, root); - active[effective_root] = 1; - todo[ntodo++] = effective_root; + active[root] = 1; + todo[ntodo++] = root; while (ntodo > 0) { int pi = todo[--ntodo]; for (int k = 0; k < g->pkg[pi].ndeps; k++) { - int dep = sep_init_effective(g, variant_root, - g->pkg[pi].deps[k]); + int dep = g->pkg[pi].deps[k]; if (!active[dep]) { active[dep] = 1; todo[ntodo++] = dep; @@ -3529,8 +3741,7 @@ sep_init_order(const struct sepgraph *g, int root, int variant_root, if (!active[pi] || done[pi]) continue; int blocked = 0; for (int k = 0; k < g->pkg[pi].ndeps; k++) { - int dep = sep_init_effective(g, variant_root, - g->pkg[pi].deps[k]); + int dep = g->pkg[pi].deps[k]; if (dep != pi && active[dep] && !done[dep]) { blocked = 1; break; @@ -3559,8 +3770,7 @@ sep_compose_init_dispatch(const struct sepgraph *g, const char *asmpath) { int *order = NULL, norder = 0; - if (sep_init_order(g, product->root, product->variant_root, - &order, &norder) < 0) + if (sep_init_order(g, product->root, &order, &norder) < 0) return -1; FILE *unit = fopen(unitpath, "wb"); if (unit == NULL) { @@ -3622,8 +3832,7 @@ sep_validate_module_closure(struct sepgraph *g, const int *order, int n, for (int j = i + 1; j < n; j++) { int b = order[j]; if (!include_root && g->pkg[b].root) continue; - if (strcmp(g->pkg[a].path, g->pkg[b].path) == 0 - && !sep_internal_replaces_production(g, a, b)) { + if (strcmp(g->pkg[a].path, g->pkg[b].path) == 0) { fprintf(stderr, "ww: product closure contains multiple packages named %s\n", g->pkg[a].path); @@ -4156,6 +4365,41 @@ sep_prepare_product_stage(char **slot, const char *dst) return 0; } +static int +sep_product_paths_overlap(const char *a, const char *b) +{ + if (a == NULL || b == NULL) return 0; + if (strcmp(a, b) == 0) return 1; + size_t an = strlen(a), bn = strlen(b); + return (an == bn + sizeof ".new" - 1 + && memcmp(a, b, bn) == 0 && strcmp(a + bn, ".new") == 0) + || (bn == an + sizeof ".new" - 1 + && memcmp(b, a, an) == 0 && strcmp(b + an, ".new") == 0); +} + +static int +sep_validate_product_path_pair(const struct sepproduct *a, + const struct sepproduct *b) +{ + const char *ap[] = { + a->stage_status != NULL ? a->status : NULL, + a->stage_out != NULL ? a->out : NULL, + a->stage_status, a->stage_out, a->stage_iface }; + const char *bp[] = { + b->stage_status != NULL ? b->status : NULL, + b->stage_out != NULL ? b->out : NULL, + b->stage_status, b->stage_out, b->stage_iface }; + for (size_t i = 0; i < nelem(ap); i++) { + if (ap[i] == NULL) continue; + for (size_t j = 0; j < nelem(bp); j++) { + if (!sep_product_paths_overlap(ap[i], bp[j])) continue; + fprintf(stderr, "ww: product path collision: %s\n", bp[j]); + return -1; + } + } + return 0; +} + /* Loader/coordinator-owned staging names are structural request inputs. Check * every one before scratch acquisition or producer execution, and never treat * a dangling symlink as an absent path. */ @@ -4205,7 +4449,8 @@ sep_validate_request_staging(struct sepgraph *g, const char *scratch, int warm, return -1; if (emit_asm) continue; int owns_output = root_package ? publish_package - : is_test || sep_root_is_command(&g->pkg[products[i].root]); + : (is_test && !products[i].no_tests) + || sep_root_is_command(&g->pkg[products[i].root]); if (!owns_output) continue; if (sep_prepare_product_stage(&products[i].stage_out, products[i].out) < 0) @@ -4220,6 +4465,11 @@ sep_validate_request_staging(struct sepgraph *g, const char *scratch, int warm, return -1; } } + for (int i = 0; i < nproducts; i++) + for (int j = 0; j < i; j++) + if (sep_validate_product_path_pair(&products[j], + &products[i]) < 0) + return -1; return 0; } @@ -4258,7 +4508,7 @@ static void workdir_stamp_text(char *buf, size_t bufsz, int is_test, int emit_asm) { snprintf(buf, bufsz, "ww workdir fmt %d mode %s asm %d\n", - is_test ? 17 : 18, is_test ? "test" : "build", emit_asm); + is_test ? 19 : 18, is_test ? "test" : "build", emit_asm); } static int @@ -4536,6 +4786,9 @@ build_one_sep_impl(const char *src, int entry_is_dir, if (graphout) *graphout = g; for (int i = 0; i < nproducts; i++) { products[i].support = -1; + products[i].production_root = -1; + products[i].ptest = -1; + products[i].pxtest = -1; products[i].stage_out = NULL; products[i].stage_iface = NULL; products[i].stage_status = NULL; @@ -4557,8 +4810,6 @@ build_one_sep_impl(const char *src, int entry_is_dir, } contextroot = contextdir; } - const char *selector = products[i].variant == SEP_VARIANT_PRODUCTION - ? NULL : products[i].test_package; const char *requested_path = products[i].identity != NULL ? products[i].identity : ""; products[i].context = sep_context_for(g, contextroot, @@ -4572,21 +4823,91 @@ build_one_sep_impl(const char *src, int entry_is_dir, if (inferred < 0) return 1; if (inferred > 0) rootpath = inferred_path; } - products[i].root = sep_find_or_add_variant(g, rootpath, entry, - entry_is_dir, products[i].variant, selector, - SEP_ROLE_NORMAL, products[i].artifact, 1); + if (products[i].directory_product) { + if (products[i].production_package != NULL) { + products[i].production_root = sep_find_or_add_variant(g, + rootpath, entry, 1, SEP_VARIANT_PRODUCTION, NULL, + SEP_ROLE_NORMAL, NULL, 1); + if (products[i].production_root < 0) { + free(inferred_path); + return 1; + } + } + if (products[i].internal_package != NULL) { + products[i].ptest = sep_find_or_add_variant(g, rootpath, + entry, 1, SEP_VARIANT_SAME_TEST, + products[i].internal_package, SEP_ROLE_NORMAL, NULL, 1); + if (products[i].ptest < 0) { + free(inferred_path); + return 1; + } + } else if (is_test && !products[i].no_tests) { + products[i].ptest = products[i].production_root; + } + if (products[i].external_package != NULL) { + products[i].pxtest = sep_find_or_add_variant(g, rootpath, + entry, 1, SEP_VARIANT_EXTERNAL, + products[i].external_package, SEP_ROLE_NORMAL, NULL, 1); + if (products[i].pxtest < 0) { + free(inferred_path); + return 1; + } + } + products[i].root = !is_test || products[i].no_tests + ? products[i].production_root + : products[i].ptest >= 0 ? products[i].ptest + : products[i].pxtest; + products[i].variant_root = products[i].ptest; + } else { + const char *selector = products[i].variant + == SEP_VARIANT_PRODUCTION ? NULL : products[i].test_package; + products[i].root = sep_find_or_add_variant(g, rootpath, entry, + entry_is_dir, products[i].variant, selector, + SEP_ROLE_NORMAL, products[i].artifact, 1); + products[i].variant_root = products[i].root; + } free(inferred_path); if (products[i].root < 0) return 1; - products[i].variant_root = products[i].root; + } + for (int i = 0; i < nproducts; i++) { + if (!products[i].directory_product) continue; + int a = products[i].production_root >= 0 + ? products[i].production_root + : products[i].ptest >= 0 ? products[i].ptest : products[i].pxtest; + for (int j = 0; j < i; j++) { + if (!products[j].directory_product) continue; + int b = products[j].production_root >= 0 + ? products[j].production_root + : products[j].ptest >= 0 ? products[j].ptest + : products[j].pxtest; + int duplicate = a >= 0 && b >= 0 && a == b; + if (!duplicate && a >= 0 && b >= 0) { + const char *aid = g->pkg[a].import_base; + const char *bid = g->pkg[b].import_base; + if (aid != NULL && bid != NULL) + duplicate = strcmp(aid, bid) == 0; + else if (aid == NULL && bid == NULL) + duplicate = strcmp(g->pkg[a].canon, + g->pkg[b].canon) == 0; + } + if (duplicate) { + fprintf(stderr, + "ww test: duplicate --ww-package-test product for canonical directory\n"); + return 1; + } + } } const char *test_support_module = "test"; + int have_runnable_tests = 0; + for (int i = 0; i < nproducts; i++) + if (is_test && !products[i].no_tests) have_runnable_tests = 1; /* -T generates a dispatcher whose support qualifier is selected by the * command. Represent that compiler-generated requirement as a direct edge * of the generated-main action. It normally coalesces with an explicit * toolchain `import test`; * when user source occupies that identity, the reserved graph alias keeps * it distinct. The linker receives the same support archive closure. */ - if (is_test) { + if (have_runnable_tests) { char tpath[PATH_MAX]; int tdir = 0; if (locate_import(toolsrcdir, "test", tpath, sizeof tpath)) { @@ -4606,8 +4927,11 @@ build_one_sep_impl(const char *src, int entry_is_dir, } int collision = 0; for (int i = 0; i < nproducts; i++) { - int root_is_support = tc != NULL - && strcmp(g->pkg[products[i].root].canon, tc) == 0; + if (products[i].no_tests) continue; + int target = products[i].ptest >= 0 + ? products[i].ptest : products[i].pxtest; + int root_is_support = target >= 0 && tc != NULL + && strcmp(g->pkg[target].canon, tc) == 0; const char *name = products[i].test_package; if (!root_is_support && name != NULL && (strcmp(name, "test") == 0 @@ -4615,6 +4939,7 @@ build_one_sep_impl(const char *src, int entry_is_dir, collision = 1; } for (int i = 0; i < nproducts && !collision; i++) { + if (products[i].no_tests) continue; char userpath[PATH_MAX]; int userdir = 0; if (locate_import(g->context[products[i].context].searchpath, @@ -4636,16 +4961,17 @@ build_one_sep_impl(const char *src, int entry_is_dir, } if (collision) test_support_module = SEP_TEST_SUPPORT_MODULE; for (int i = 0; i < nproducts; i++) { - int root = products[i].root; - int root_is_support = tc != NULL - && strcmp(g->pkg[root].canon, tc) == 0; + if (products[i].no_tests) continue; + int target = products[i].ptest >= 0 + ? products[i].ptest : products[i].pxtest; + int root_is_support = target >= 0 && tc != NULL + && strcmp(g->pkg[target].canon, tc) == 0; /* A same-test build of the runtime package already owns run * and its source imports. An external test still needs the * colocated production node, which is also its support dep. */ if (root_is_support - && strcmp(test_support_module, "test") == 0 - && products[i].variant != SEP_VARIANT_EXTERNAL) { - products[i].support = root; + && strcmp(test_support_module, "test") == 0) { + products[i].support = target; continue; } int ti; @@ -4664,34 +4990,58 @@ build_one_sep_impl(const char *src, int entry_is_dir, } } for (int i = 0; i < nproducts; i++) { - int root = products[i].variant_root; + int roots[3] = { products[i].production_root, + products[i].ptest, products[i].pxtest }; + const char *selectors[3] = { products[i].production_package, + products[i].internal_package, products[i].external_package }; + int nroots = products[i].directory_product ? 3 : 1; + if (!products[i].directory_product) { + roots[0] = products[i].variant_root; + selectors[0] = products[i].variant == SEP_VARIANT_PRODUCTION + ? NULL : products[i].test_package; + } /* Raw single-file test fixtures are the one retained non-directory * exception: keep compiler-owned test-main synthesis in that action. * Its support export is still an exact direct input. */ if (is_test && !entry_is_dir) { int support = products[i].support; - if (support >= 0 && support != root - && sep_add_dep(g, root, support) < 0) + if (support >= 0 && support != roots[0] + && sep_add_dep(g, roots[0], support) < 0) return 1; - g->pkg[root].link_entry = 1; + g->pkg[roots[0]].link_entry = 1; } - int lr = sep_load_pkg(g, root, products[i].context); - if (lr == -2) return 1; - if (lr == SEP_LOAD_INTERNAL || lr == SEP_LOAD_VENDOR) return 1; - if (lr < 0) { - g->pkg[root].failed = 1; - continue; - } - if (products[i].test_package != NULL - && strcmp(g->pkg[root].name, products[i].test_package) != 0) { - fprintf(stderr, - "ww: package-test selector does not match loaded package\n"); - g->pkg[root].failed = 1; + for (int ri = 0; ri < nroots; ri++) { + int root = roots[ri]; + if (root < 0) continue; + if (ri > 0 && products[i].production_root >= 0 + && g->pkg[products[i].production_root].failed) { + g->pkg[root].failed = 1; + continue; + } + int duplicate = 0; + for (int rj = 0; rj < ri; rj++) + if (roots[rj] == root) duplicate = 1; + if (duplicate) continue; + int lr = sep_load_pkg(g, root, products[i].context); + if (lr == -2) return 1; + if (lr == SEP_LOAD_INTERNAL || lr == SEP_LOAD_VENDOR) return 1; + if (lr < 0) { + g->pkg[root].failed = 1; + continue; + } + if (selectors[ri] != NULL + && strcmp(g->pkg[root].name, selectors[ri]) != 0) { + fprintf(stderr, + "ww: package-test selector does not match loaded package\n"); + g->pkg[root].failed = 1; + } } } - if (is_test) { + if (have_runnable_tests) { for (int i = 0; i < nproducts; i++) { - int variant = products[i].variant_root; + if (products[i].no_tests) continue; + int variant = products[i].ptest >= 0 + ? products[i].ptest : products[i].pxtest; int support = products[i].support; if (support >= 0 && support != variant) { int lr = sep_load_pkg(g, support, products[i].context); @@ -4706,7 +5056,7 @@ build_one_sep_impl(const char *src, int entry_is_dir, if (sep_finalize_directory_identities(g) < 0) return 1; if (!is_test) { for (int i = 0; i < nproducts; i++) { - int root = products[i].variant_root; + int root = products[i].root; if (!g->pkg[root].failed) g->pkg[root].link_entry = sep_root_is_command(&g->pkg[root]); @@ -4714,11 +5064,21 @@ build_one_sep_impl(const char *src, int entry_is_dir, } if (is_test && entry_is_dir) { for (int i = 0; i < nproducts; i++) { - int variant = products[i].variant_root; + if (products[i].no_tests) continue; + int variant = products[i].ptest >= 0 + ? products[i].ptest : products[i].pxtest; int support = products[i].support; - if (g->pkg[variant].failed + int failed = g->pkg[variant].failed; + if (products[i].production_root >= 0 + && g->pkg[products[i].production_root].failed) failed = 1; + if (products[i].ptest >= 0 + && g->pkg[products[i].ptest].failed) failed = 1; + if (products[i].pxtest >= 0 + && g->pkg[products[i].pxtest].failed) failed = 1; + if (failed || (support >= 0 && g->pkg[support].failed)) { products[i].root = variant; + g->pkg[variant].failed = 1; continue; } int mainpkg = sep_add_generated_main(g, &products[i], i, @@ -4726,6 +5086,11 @@ build_one_sep_impl(const char *src, int entry_is_dir, if (mainpkg < 0) return 1; products[i].root = mainpkg; } + for (int i = 0; i < nproducts; i++) { + if (products[i].no_tests + || g->pkg[products[i].root].failed) continue; + if (sep_recompile_for_test(g, &products[i]) < 0) return 1; + } } for (int pi = 0; pi < g->n; pi++) { g->pkg[pi].init_symbol = sep_package_init_symbol(&g->pkg[pi]); @@ -4777,8 +5142,7 @@ build_one_sep_impl(const char *src, int entry_is_dir, int root = products[i].root; if (g->pkg[root].failed) continue; int *initcheck = NULL, ninitcheck = 0; - if (sep_init_order(g, root, products[i].variant_root, - &initcheck, &ninitcheck) < 0) + if (sep_init_order(g, root, &initcheck, &ninitcheck) < 0) g->pkg[root].failed = 1; free(initcheck); } @@ -5069,6 +5433,16 @@ build_one_sep_impl(const char *src, int entry_is_dir, nmaps++; } size_t cargvcap = 18; + if ((size_t)g->pkg[pi].ngenerated_targets + > ((size_t)-1 - cargvcap) / 2) { + fprintf(stderr, "ww: package graph is too large\n"); + (void)sep_discard_action_staging(warm, unitnew, wwinew, + asmnew, objnew, anew); + (void)sep_discard_init_staging(warm, initunitnew, + initasmnew, initobjnew); + goto request_fail; + } + cargvcap += 2 * (size_t)g->pkg[pi].ngenerated_targets; if ((size_t)g->pkg[pi].ndeps > ((size_t)-1 - cargvcap) / 3) { fprintf(stderr, "ww: package graph is too large\n"); (void)sep_discard_action_staging(warm, unitnew, wwinew, @@ -5112,8 +5486,11 @@ build_one_sep_impl(const char *src, int entry_is_dir, cargv[cpos++] = "--test-support-module"; cargv[cpos++] = (char *)test_support_module; if (g->pkg[pi].generated_main) { - cargv[cpos++] = "--test-target-package"; - cargv[cpos++] = g->pkg[g->pkg[pi].generated_target].path; + for (int k = 0; k < g->pkg[pi].ngenerated_targets; k++) { + cargv[cpos++] = "--test-target-package"; + cargv[cpos++] = g->pkg[ + g->pkg[pi].generated_targets[k]].path; + } } } else { if (g->pkg[pi].variant == SEP_VARIANT_SAME_TEST @@ -5271,10 +5648,9 @@ build_one_sep_impl(const char *src, int entry_is_dir, goto prepare_transaction; } - /* Each product gets its own reverse-topological archive closure: root `.a` - * first, then every transitively reachable package `.a`, then libwwrt.a. An - * internal test variant already contains production sources, so its - * colocated production archive is omitted without dropping dependencies. */ + /* Each product gets its exact reverse-topological archive closure: root + * `.a` first, then every transitively reachable package `.a`, then + * libwwrt.a. Test substitution is already explicit in the graph. */ char rtpaths[2][PATH_MAX]; int nrt = 1; int rn = snprintf(rtpaths[0], sizeof rtpaths[0], "%s/libwwrt.a", libdir); @@ -5293,7 +5669,13 @@ build_one_sep_impl(const char *src, int entry_is_dir, int nlibs = linkflags ? linkflags->nlibs : 0; for (int i = 0; i < nproducts; i++) { int root = products[i].root; - int variant_root = products[i].variant_root; + if (is_test && products[i].no_tests) { + if (sep_stage_product_status(&products[i]) != 0) { + fprintf(stderr, "ww: cannot stage package-test product\n"); + goto request_fail; + } + continue; + } if (!is_test && !sep_root_is_command(&g->pkg[root])) { if (sep_stage_product_status(&products[i]) != 0) { fprintf(stderr, "ww: cannot stage package-build product\n"); @@ -5345,9 +5727,6 @@ build_one_sep_impl(const char *src, int entry_is_dir, largv[pos++] = products[i].stage_out; for (int oi = nlink - 1; oi >= 0; oi--) { int pi = linkorder[oi]; - if (variant_root >= 0 - && sep_internal_replaces_production(g, variant_root, pi)) - continue; const char *suffix = warm && g->pkg[pi].archive_staged ? ".a.new" : ".a"; sep_fname(g, pi, scratch, suffix, linkpaths[npath], @@ -6049,34 +6428,40 @@ do_test(int argc, char **argv) } package_create_output_dir = argv[++i]; } else if (strcmp(argv[i], "--ww-package-test") == 0) { - if (i + 5 >= argc) { + if (i + 8 >= argc) { fprintf(stderr, - "ww test: --ww-package-test needs kind, package, directory, output, and status\n"); + "ww test: --ww-package-test needs kind, package, production, internal, external, directory, output, and status\n"); return 2; } const char *kind = argv[++i]; const char *name = argv[++i]; + const char *production = argv[++i]; + const char *internal = argv[++i]; + const char *external = argv[++i]; const char *dir = argv[++i]; const char *output = argv[++i]; const char *status = argv[++i]; size_t pn = strlen(name); - int variant = SEP_VARIANT_EXTERNAL; - if (strcmp(kind, "production") == 0) - variant = SEP_VARIANT_PRODUCTION; - else if (strcmp(kind, "same") == 0) - variant = SEP_VARIANT_SAME_TEST; - if ((strcmp(kind, "production") != 0 - && strcmp(kind, "same") != 0 - && strcmp(kind, "external") != 0) + int build_product = strcmp(kind, "build") == 0; + int test_product = strcmp(kind, "test") == 0; + int has_production = strcmp(production, "-") != 0; + int has_internal = strcmp(internal, "-") != 0; + int has_external = strcmp(external, "-") != 0; + if ((!build_product && !test_product) || pn == 0 || dir[0] == '\0' || output[0] == '\0' || status[0] == '\0' - || (strcmp(kind, "external") == 0 - && (pn <= 5 - || strcmp(name + pn - 5, - "_test") != 0))) { + || (has_production && strcmp(production, name) != 0) + || (has_internal && strcmp(internal, name) != 0) + || (has_external && (strlen(external) != pn + 5 + || strncmp(external, name, pn) != 0 + || strcmp(external + pn, "_test") != 0)) + || (build_product && (!has_production + || has_internal || has_external)) + || (test_product && !has_production + && !has_internal && !has_external)) { fprintf(stderr, - "ww test: invalid --ww-package-test variant\n"); + "ww test: invalid --ww-package-test product\n"); return 2; } if (nproducts == INT_MAX) { @@ -6089,11 +6474,24 @@ do_test(int argc, char **argv) products[nproducts].dir = dir; products[nproducts].out = output; products[nproducts].test_package = name; + products[nproducts].production_package = has_production + ? production : NULL; + products[nproducts].internal_package = has_internal + ? internal : NULL; + products[nproducts].external_package = has_external + ? external : NULL; products[nproducts].status = status; products[nproducts].artifact = NULL; - products[nproducts].variant = variant; + products[nproducts].variant = build_product + ? SEP_VARIANT_PRODUCTION : SEP_VARIANT_TEST_MAIN; + products[nproducts].directory_product = 1; + products[nproducts].no_tests = test_product + && !has_internal && !has_external; products[nproducts].root = -1; products[nproducts].variant_root = -1; + products[nproducts].production_root = -1; + products[nproducts].ptest = -1; + products[nproducts].pxtest = -1; products[nproducts].support = -1; nproducts++; } else if (strcmp(argv[i], "-S") == 0) { @@ -6187,19 +6585,16 @@ do_test(int argc, char **argv) for (int i = 1; i < nproducts; i++) { struct sepproduct p = products[i]; int j = i; - while (j > 0 && (strcmp(products[j - 1].dir, p.dir) > 0 - || (strcmp(products[j - 1].dir, p.dir) == 0 - && products[j - 1].variant > p.variant))) { + while (j > 0 && strcmp(products[j - 1].dir, p.dir) > 0) { products[j] = products[j - 1]; j--; } products[j] = p; } for (int i = 0; i < nproducts; i++) { - if (i > 0 && strcmp(products[i - 1].dir, products[i].dir) == 0 - && products[i - 1].variant == products[i].variant) { + if (i > 0 && strcmp(products[i - 1].dir, products[i].dir) == 0) { fprintf(stderr, - "ww test: duplicate --ww-package-test variant for directory\n"); + "ww test: duplicate --ww-package-test product for directory\n"); return 2; } /* Artifact identity is derived from canonical package identity after @@ -6244,6 +6639,15 @@ do_test(int argc, char **argv) return 2; } } + } else { + for (int i = 0; i < nproducts; i++) { + if (products[i].directory_product + && products[i].variant == SEP_VARIANT_PRODUCTION) { + fprintf(stderr, + "ww test: package-test products must use test kind\n"); + return 2; + } + } } if (pattern != NULL) { struct stat first; diff --git a/internal/wwpackage/package.ww b/internal/wwpackage/package.ww index 8fd01440..5f5840e0 100644 --- a/internal/wwpackage/package.ww +++ b/internal/wwpackage/package.ww @@ -24,7 +24,12 @@ type pkgfolder = struct { type pkggroup = struct { dir: str, pkg: str, - external: bool, + prodpkg: str, + samepkg: str, + externalpkg: str, + hassame: bool, + hasexternal: bool, + notests: bool, production: bool, plan: i32, root: str, @@ -1299,8 +1304,6 @@ fn pkglabel(g: *pkggroup) void = { pkgput(os.STDOUT_FILENO, g.dir); pkgput(os.STDOUT_FILENO, " ["); pkgput(os.STDOUT_FILENO, g.pkg); - if (g.external) { pkgput(os.STDOUT_FILENO, ", external"); } - else { pkgput(os.STDOUT_FILENO, ", same-package"); }; pkgput(os.STDOUT_FILENO, "]"); }; @@ -1331,11 +1334,11 @@ fn pkgstartbuild(p: *pkgplan, groups: []pkggroup, builder: str, includes: []str, libdirs: []str, libs: []str, h: *exec.process) bool = { let nproducts: i32 = p.end - p.start; let capacity: i32 = 16; - if (nproducts < 0 || nproducts > (PKG_COUNT_MAX - capacity) / 6) { + if (nproducts < 0 || nproducts > (PKG_COUNT_MAX - capacity) / 9) { pkgputln(os.STDERR_FILENO, "wwtest package: package graph is too large"); return false; }; - capacity += nproducts * 6; + capacity += nproducts * 9; if (includes.len > (PKG_COUNT_MAX - capacity) / 2) { pkgputln(os.STDERR_FILENO, "wwtest package: package graph is too large"); return false; @@ -1379,11 +1382,15 @@ fn pkgstartbuild(p: *pkgplan, groups: []pkggroup, builder: str, includes: []str, for (i < p.end) { let g: *pkggroup = &groups[i]; append(ba, "--ww-package-test"); - if (g.production) { append(ba, "production"); } - else { if (g.external) { append(ba, "external"); } - else { append(ba, "same"); }; - }; + if (p.buildonly) { append(ba, "build"); } + else { append(ba, "test"); }; append(ba, g.pkg); + if (g.prodpkg.len != 0) { append(ba, g.prodpkg); } + else { append(ba, "-"); }; + if (g.hassame) { append(ba, g.samepkg); } + else { append(ba, "-"); }; + if (g.hasexternal) { append(ba, g.externalpkg); } + else { append(ba, "-"); }; append(ba, g.dir); append(ba, g.bin); append(ba, g.buildok); @@ -1469,7 +1476,7 @@ fn pkgstartrun(g: *pkggroup, filters: []str, timeoutarg: str, }; fn pkgproductbuilt(g: *pkggroup, buildonly: bool) bool = { - if (buildonly) { return pkgisreg(g.buildok); }; + if (buildonly || g.notests) { return pkgisreg(g.buildok); }; return pkgisreg(g.buildok) && pkgisreg(g.bin); }; @@ -1480,6 +1487,12 @@ fn pkgrunok(g: *pkggroup) bool = { }; fn pkgemitgroup(g: *pkggroup, compileonly: bool) bool = { + if (g.notests) { + pkgput(os.STDOUT_FILENO, "? "); + pkgput(os.STDOUT_FILENO, g.dir); + pkgputln(os.STDOUT_FILENO, " [no tests]"); + return true; + }; if (compileonly) { pkgput(os.STDOUT_FILENO, "built "); pkglabel(g); @@ -1494,15 +1507,6 @@ fn pkgemitgroup(g: *pkggroup, compileonly: bool) bool = { pkgfailpath(g.root, "cannot read test capture"); return false; }; - // The compiler-owned dispatcher is the authority on whether a variant - // contains runnable tests. With an empty generated table, test.run returns - // zero without output; no coordinator-side source scan is involved. - if (pkgrunok(g) && runstdout.len == 0 && runstderr.len == 0) { - pkgput(os.STDOUT_FILENO, "? "); - pkgput(os.STDOUT_FILENO, g.dir); - pkgputln(os.STDOUT_FILENO, " [no tests]"); - return true; - }; pkgput(os.STDOUT_FILENO, runstdout); pkgput(os.STDERR_FILENO, runstderr); if (!pkgrunok(g)) { @@ -1888,17 +1892,23 @@ export fn packagecommand(args: []str) int = { }; i = 0; for (i < ds.paths.len) { + let s: pkgsource; + s.path = ds.paths[i]; + if (!pkgstring(&s.dir, pkgdirname(ds.paths[i]))) { return 1; }; + s.test = strings.hassuffix(pkgbase(ds.paths[i]), "_test.ww"); + if (buildonly && s.test) { + s.pkg = ""; + append(srcs, s); + i += 1; + continue; + }; let body: str; let pn: str; if (!pkgread(ds.paths[i], &body) || !pkgclause(body, &pn)) { pkgfailpath(ds.paths[i], "invalid or missing package clause"); return 1; }; - let s: pkgsource; - s.path = ds.paths[i]; - if (!pkgstring(&s.dir, pkgdirname(ds.paths[i])) - || !pkgstring(&s.pkg, pn)) { return 1; }; - s.test = strings.hassuffix(pkgbase(ds.paths[i]), "_test.ww"); + if (!pkgstring(&s.pkg, pn)) { return 1; }; append(srcs, s); i += 1; }; @@ -1948,7 +1958,12 @@ export fn packagecommand(args: []str) int = { let g: pkggroup; g.dir = f.path; g.pkg = f.prodpkg; - g.external = false; + g.prodpkg = f.prodpkg; + g.samepkg = ""; + g.externalpkg = ""; + g.hassame = false; + g.hasexternal = false; + g.notests = false; g.production = true; append(groups, g); } else { @@ -1970,64 +1985,110 @@ export fn packagecommand(args: []str) int = { i += 1; continue; }; + let family: str = f.prodpkg; + let samepkg: str = ""; let externalpkg: str = ""; - if (f.prodpkg.len != 0) { - if (!pkgstring(&externalpkg, f.prodpkg, "_test")) { return 1; }; - }; - let standalonepkg: str = ""; + let hassame: bool = false; + let hasexternal: bool = false; let sawtestfile: bool = false; - let j: i32 = f.start; - for (j < f.end) { - if (srcs[j].test) { - sawtestfile = true; - if (f.prodpkg.len != 0 - && strings.compare(srcs[j].pkg, f.prodpkg) != 0 - && strings.compare(srcs[j].pkg, externalpkg) != 0) { - pkgfailpath(srcs[j].path, - "test package must match production package or _test"); - return 1; - }; - if (f.prodpkg.len == 0) { - if (standalonepkg.len == 0) { - standalonepkg = srcs[j].pkg; - } else if (strings.compare(standalonepkg, - srcs[j].pkg) != 0) { - pkgfailpath(f.path, - "test-only directory declares conflicting packages"); + if (f.prodpkg.len != 0) { + let expectedexternal: str; + if (!pkgstring(&expectedexternal, f.prodpkg, "_test")) { + return 1; + }; + let j: i32 = f.start; + for (j < f.end) { + if (srcs[j].test) { + sawtestfile = true; + if (strings.compare(srcs[j].pkg, f.prodpkg) == 0) { + hassame = true; + samepkg = srcs[j].pkg; + } else if (strings.compare(srcs[j].pkg, + expectedexternal) == 0) { + hasexternal = true; + externalpkg = srcs[j].pkg; + } else { + pkgfailpath(srcs[j].path, + "test package must match production package or _test"); return 1; }; }; - let found: bool = false; - let k: i32 = 0; - for (k < groups.len) { - if (strings.compare(groups[k].dir, f.path) == 0 - && strings.compare(groups[k].pkg, srcs[j].pkg) == 0) { - found = true; - break; + j += 1; + }; + } else { + let firstpkg: str = ""; + let secondpkg: str = ""; + let j: i32 = f.start; + for (j < f.end) { + if (srcs[j].test) { + sawtestfile = true; + if (firstpkg.len == 0) { + firstpkg = srcs[j].pkg; + } else if (strings.compare(srcs[j].pkg, firstpkg) != 0 + && secondpkg.len == 0) { + secondpkg = srcs[j].pkg; + } else if (strings.compare(srcs[j].pkg, firstpkg) != 0 + && strings.compare(srcs[j].pkg, secondpkg) != 0) { + pkgfailpath(srcs[j].path, + "test package must match production package or _test"); + return 1; }; - k += 1; }; - if (!found) { - let g: pkggroup; - g.dir = f.path; - g.pkg = srcs[j].pkg; - g.external = f.prodpkg.len != 0 - && strings.compare(f.prodpkg, g.pkg) != 0; - g.production = false; - append(groups, g); + j += 1; + }; + if (secondpkg.len == 0) { + if (strings.hassuffix(firstpkg, "_test")) { + family = firstpkg[0:(firstpkg.len - 5)]; + if (family.len == 0) { + pkgfailpath(f.path, + "external test package has empty base name"); + return 1; + }; + hasexternal = true; + externalpkg = firstpkg; + } else { + family = firstpkg; + hassame = true; + samepkg = firstpkg; + }; + } else { + let firstexternal: str; + let secondexternal: str; + if (!pkgstring(&firstexternal, firstpkg, "_test") + || !pkgstring(&secondexternal, secondpkg, "_test")) { + return 1; + }; + if (strings.compare(secondpkg, firstexternal) == 0) { + family = firstpkg; + hassame = true; + samepkg = firstpkg; + hasexternal = true; + externalpkg = secondpkg; + } else if (strings.compare(firstpkg, secondexternal) == 0) { + family = secondpkg; + hassame = true; + samepkg = secondpkg; + hasexternal = true; + externalpkg = firstpkg; + } else { + pkgfailpath(f.path, + "test package must match production package or _test"); + return 1; }; }; - j += 1; - }; - if (!sawtestfile) { - let g: pkggroup; - g.dir = f.path; - g.pkg = f.prodpkg; - if (g.pkg.len == 0) { g.pkg = srcs[f.start].pkg; }; - g.external = false; - g.production = true; - append(groups, g); }; + let g: pkggroup; + g.dir = f.path; + g.pkg = family; + if (g.pkg.len == 0) { g.pkg = srcs[f.start].pkg; }; + g.prodpkg = f.prodpkg; + g.samepkg = samepkg; + g.externalpkg = externalpkg; + g.hassame = hassame; + g.hasexternal = hasexternal; + g.notests = !sawtestfile; + g.production = f.prodpkg.len != 0; + append(groups, g); i += 1; }; if (groups.len == 0) { @@ -2225,6 +2286,9 @@ export fn packagecommand(args: []str) int = { if (!pkgproductbuilt(g, buildonly)) { g.state = PKGDONE; productcompleted += 1; + } else if (g.notests) { + g.state = PKGDONE; + productcompleted += 1; } else { if (!pkgstartrun(g, filters, timeoutarg, list, &runhandles[gi])) { diff --git a/selfhost/cmd/w6c/main.ww b/selfhost/cmd/w6c/main.ww index 49d6fc64..3efe3924 100644 --- a/selfhost/cmd/w6c/main.ww +++ b/selfhost/cmd/w6c/main.ww @@ -327,6 +327,11 @@ fn allocimportptrs(count: i32) ([]*u8 | nomem) = { return value; }; +fn allocimportstrs(count: i32) ([]str | nomem) = { + let value: []str = alloc([], count: u64)?; + return value; +}; + fn allocnodeptrs(count: i32) ([]*syntax.node | nomem) = { let value: []*syntax.node = alloc([], count: u64)?; return value; @@ -457,7 +462,6 @@ export fn main(argc: i32, argv: **u8) i32 = { let out: *u8 = nil; let wwiout: *u8 = nil; // -I : M2 export-data producer let testsupport: *u8 = nil; - let testtarget: *u8 = nil; let testmode: i32 = 0i32; // #15: `-T` test-mode let testpackage: i32 = 0i32; let commandpackage: i32 = 0i32; @@ -469,8 +473,10 @@ export fn main(argc: i32, argv: **u8) i32 = { // only; treat `.wwi` deps as external) let pathallocation: ([]*u8 | nomem) = allocimportptrs(argc); let fileallocation: ([]*u8 | nomem) = allocimportptrs(argc); + let targetallocation: ([]str | nomem) = allocimportstrs(argc); let importpaths: []*u8; let importfiles: []*u8; + let testtargets: []str; let astallocation: ([]*syntax.node | nomem) = allocnodeptrs(argc); let importasts: []*syntax.node; match (pathallocation) { @@ -491,6 +497,15 @@ export fn main(argc: i32, argv: **u8) i32 = { }; importpaths.len = argc; importfiles.len = argc; + match (targetallocation) { + case let value: []str => testtargets = value; + case nomem => { + let m: str = "w6c: out of memory\n"; + os.write(2, m.ptr, m.len: u64); + return 1; + }; + }; + testtargets.len = argc; match (astallocation) { case let value: []*syntax.node => importasts = value; case nomem => { @@ -501,6 +516,7 @@ export fn main(argc: i32, argv: **u8) i32 = { }; importasts.len = argc; let nimports: i32 = 0; + let ntesttargets: i32 = 0; let mapallocation: ([]importmap | nomem) = allocimportmaps(argc); let importmaps: []importmap; match (mapallocation) { @@ -572,7 +588,8 @@ export fn main(argc: i32, argv: **u8) i32 = { os.write(2, m.ptr, m.len: u64); return 2; }; - testtarget = argv[i]; + testtargets[ntesttargets] = pathstr(argv[i]); + ntesttargets += 1; } else { if (cstreq(a, "-c")) { sepmode = 1i32; } else { if (cstreq(a, "--import")) { @@ -711,17 +728,24 @@ export fn main(argc: i32, argv: **u8) i32 = { os.write(2, m.ptr, m.len: u64); return 2; }; - if (testtarget != nil && (sepmode == 0 || testmode == 0 - || testtarget[0u64] == 0u8)) { - let m: str = "w6c: invalid --test-target-package\n"; - os.write(2, m.ptr, m.len: u64); - return 2; - }; - if (testtarget != nil) { + let targeti: i32 = 0; + for (targeti < ntesttargets) { + let testtarget: str = testtargets[targeti]; + if (sepmode == 0 || testmode == 0 || testtarget.len == 0) { + let m: str = "w6c: invalid --test-target-package\n"; + os.write(2, m.ptr, m.len: u64); + return 2; + }; + if (targeti > 0 + && strings.compare(testtargets[targeti - 1], testtarget) >= 0) { + let m: str = "w6c: --test-target-package paths must be sorted and unique\n"; + os.write(2, m.ptr, m.len: u64); + return 2; + }; let direct: bool = false; importi = 0; for (importi < nimports) { - if (cstreq(importpaths[importi], pathstr(testtarget))) { + if (syntax.streq(pathstr(importpaths[importi]), testtarget)) { direct = true; }; importi += 1; @@ -731,6 +755,7 @@ export fn main(argc: i32, argv: **u8) i32 = { os.write(2, m.ptr, m.len: u64); return 2; }; + targeti += 1; }; let importhead: *syntax.node = nil; @@ -848,13 +873,14 @@ export fn main(argc: i32, argv: **u8) i32 = { testsupport)) { return 1; }; - if (testtarget != nil) { + targeti = 0; + for (targeti < ntesttargets) { let seen: i32 = 0; let targetuse: *syntax.node = f.list; for (targetuse != nil) { if (targetuse.kind == syntax.nkind.N_USE && targetuse.imported == 0 - && syntax.streq(targetuse.usepath, pathstr(testtarget))) { + && syntax.streq(targetuse.usepath, testtargets[targeti])) { targetuse.str = targetuse.usepath; seen += 1; }; @@ -865,6 +891,7 @@ export fn main(argc: i32, argv: **u8) i32 = { os.write(2, m.ptr, m.len: u64); return 2; }; + targeti += 1; }; if (importhead != nil) { importtail.next = f.list; @@ -896,8 +923,7 @@ export fn main(argc: i32, argv: **u8) i32 = { let testmodule: str; if (testsupport != nil) { testmodule = pathstr(testsupport); }; - let testtargetmodule: str; - if (testtarget != nil) { testtargetmodule = pathstr(testtarget); }; + testtargets.len = ntesttargets; let packageinitsymbol: str; if (packageinit != nil) { packageinitsymbol = pathstr(packageinit); }; let initdispatchsymbol: str; @@ -905,7 +931,7 @@ export fn main(argc: i32, argv: **u8) i32 = { let haswwi: i32 = 0; if (wwiout != nil) { haswwi = 1; }; let compilerc: i32 = wcc.compilefile(f, testmode, testpackage, testmodule, - testtargetmodule, sepmode, wwifd, haswwi, entrymode, + testtargets, sepmode, wwifd, haswwi, entrymode, packageinitsymbol, initdispatchsymbol); if (out != nil) { os.close(1i32); }; if (compilerc != 0) { diff --git a/selfhost/cmd/wcc/api.ww b/selfhost/cmd/wcc/api.ww index 401d0583..d744cd9b 100644 --- a/selfhost/cmd/wcc/api.ww +++ b/selfhost/cmd/wcc/api.ww @@ -3,7 +3,7 @@ package wcc; import syntax; export fn compilefile(file: *syntax.node, testmode: i32, testpackage: i32, - testmodule: str, testtarget: str, sepmode: i32, wwifd: i32, + testmodule: str, testtargets: []str, sepmode: i32, wwifd: i32, haswwi: i32, entrymode: i32, packageinitsymbol: str, initdispatchsymbol: str) i32 = { let tc: syntax.tctx; @@ -13,7 +13,7 @@ export fn compilefile(file: *syntax.node, testmode: i32, testpackage: i32, ck.istest = testmode; ck.istestpackage = testpackage; if (testmodule.len > 0) { ck.testmodule = testmodule; }; - ck.testtarget = testtarget; + ck.testtargets = testtargets; ck.sepmode = sepmode; ck.packageinitsymbol = packageinitsymbol; checkfile(&ck, file); diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index 486cac78..dfe0196a 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -23,8 +23,8 @@ type checker = struct { istestpackage: i32, // validate/retain package-owned @test bodies and // export compiler-private metadata; no entry synth testmodule: str, // generated dispatcher support qualifier - testtarget: str, // canonical target path used only as the - // compiler-owned generated-main qualifier + testtargets: []str, // canonical generated-main target paths used only as + // compiler-owned generated-main qualifiers sepmode: i32, // -c package compilation: imported interfaces are // present, so absent members are hard export errors. synthtestrun: *syntax.node, // exact generated support.run DOT; its @@ -8507,8 +8507,8 @@ fn checkinit(c: *checker, tc: *syntax.tctx) void = { c.istest = 0i32; // #15: caller (w6c main) sets it after init c.istestpackage = 0i32; c.testmodule = "test"; - let emptytesttarget: str; - c.testtarget = emptytesttarget; + let emptytesttargets: []str; + c.testtargets = emptytesttargets; c.sepmode = 0i32; // caller (w6c main) sets it from -c c.synthtestrun = nil; c.verbose = 0; @@ -8524,6 +8524,15 @@ fn checkinit(c: *checker, tc: *syntax.tctx) void = { seedprimitives(c); }; +fn checktesttarget(c: *checker, path: str) bool = { + let i: i32 = 0; + for (i < c.testtargets.len) { + if (syntax.streq(c.testtargets[i], path)) { return true; }; + i += 1; + }; + return false; +}; + fn checkfile(c: *checker, file: *syntax.node) void = { if (file == nil) { return; }; if (file.kind != syntax.nkind.N_FILE) { return; }; @@ -8547,8 +8556,7 @@ fn checkfile(c: *checker, file: *syntax.node) void = { if (su.kind == syntax.nkind.N_USE && su.imported == 0 && su.sourceid == file.sourceid && (syntax.streq(su.usepath, c.testmodule) - || (c.testtarget.len > 0 - && syntax.streq(su.usepath, c.testtarget)))) { + || checktesttarget(c, su.usepath))) { su.used = 1i32; }; if (su.kind == syntax.nkind.N_USE && su.imported == 0 @@ -8719,9 +8727,8 @@ fn checkfile(c: *checker, file: *syntax.node) void = { if (t.imported != 0 && t.nmod.len > 0) { let alias: str = t.pkgname; if (alias.len == 0) { alias = t.nmod; }; - if (c.testtarget.len > 0 - && syntax.streq(t.nmod, c.testtarget)) { - alias = c.testtarget; + if (checktesttarget(c, t.nmod)) { + alias = t.nmod; }; id = syntax.newnode(syntax.nkind.N_DOT, pf, pl, pc); id.lhs = syntax.newnode(syntax.nkind.N_IDENT, pf, pl, pc); diff --git a/selfhost/cmd/ww/main.ww b/selfhost/cmd/ww/main.ww index ac93f6ee..adcacc64 100644 --- a/selfhost/cmd/ww/main.ww +++ b/selfhost/cmd/ww/main.ww @@ -554,6 +554,7 @@ def SEP_VARIANT_PRODUCTION: i32 = 0; def SEP_VARIANT_SAME_TEST: i32 = 1; def SEP_VARIANT_EXTERNAL: i32 = 2; def SEP_VARIANT_TEST_MAIN: i32 = 3; +def SEP_VARIANT_TEST_COPY: i32 = 4; def SEP_ROLE_NORMAL: i32 = 0; def SEP_ROLE_TEST_SUPPORT: i32 = 1; def SEP_ROLE_GENERATED_MAIN: i32 = 2; @@ -1062,6 +1063,7 @@ type seppkg = struct { storagehashed: bool, name: *u8, // validated declared name; directory packages only testpackage: *u8, + fortest: *u8, sources: **u8, // owned, byte-sorted selected paths; dirs only nsources: i32, isdir: i32, @@ -1070,7 +1072,8 @@ type seppkg = struct { root: bool, // requested usage; never package-action identity linkentry: bool, generatedmain: bool, - generatedtarget: i32, + generatedtargets: []i32, + ngeneratedtargets: i32, failed: bool, testsupport: bool, loaded: bool, @@ -1107,12 +1110,20 @@ type sepproduct = struct { out: *u8, identity: *u8, testpackage: *u8, + productionpackage: *u8, + internalpackage: *u8, + externalpackage: *u8, status: *u8, artifact: *u8, variant: i32, + directoryproduct: bool, + notests: bool, context: i32, root: i32, variantroot: i32, + productionroot: i32, + ptest: i32, + pxtest: i32, support: i32, stageout: *u8, stageiface: *u8, @@ -2142,6 +2153,7 @@ fn sepfindoraddvariant(g: *sepgraph, path: *u8, entry: *u8, g.pkg[g.n].storagehashed = false; g.pkg[g.n].name = nil; g.pkg[g.n].testpackage = nil; + g.pkg[g.n].fortest = nil; if (testpackage != nil) { g.pkg[g.n].testpackage = sepdupcstr(testpackage, cstrlen(testpackage)); @@ -2155,6 +2167,9 @@ fn sepfindoraddvariant(g: *sepgraph, path: *u8, entry: *u8, g.pkg[g.n].root = root; g.pkg[g.n].linkentry = false; g.pkg[g.n].generatedmain = false; + let emptytargets: []i32; + g.pkg[g.n].generatedtargets = emptytargets; + g.pkg[g.n].ngeneratedtargets = 0; g.pkg[g.n].failed = false; g.pkg[g.n].testsupport = false; g.pkg[g.n].loaded = false; @@ -2211,54 +2226,64 @@ fn sepfindoraddrole(g: *sepgraph, path: *u8, entry: *u8, isdir: i32, SEP_VARIANT_PRODUCTION, nil, role, artifact, false); }; -// Release the package-owned directory-membership lists through one graph -// cleanup function. rt_free is a no-op in today's no-free runtime, but this -// records the same ownership boundary as the C bootstrap twin. +fn seppkgfreeowned(p: *seppkg) void = { + let j: i32 = 0; + for (j < p.nsources) { + os.free(p.sources[j]: *void, os.PATH_MAX: u64); + j += 1; + }; + if (p.sources != nil) { + os.free(p.sources: *void, + (p.nsources: u64) * (size(*u8): u64)); + }; + if (p.name != nil) { + os.free(p.name: *void, cstrlen(p.name) + 1u64); + }; + if (p.initsymbol != nil) { + os.free(p.initsymbol: *void, + cstrlen(p.initsymbol) + 1u64); + }; + if (p.contextstate.ptr != nil) { + os.free(p.contextstate.ptr: *void, + (p.contextstate.cap: u64) * (size(u8): u64)); + }; + if (p.deps.ptr != nil) { + os.free(p.deps.ptr: *void, + (p.deps.cap: u64) * (size(i32): u64)); + }; + if (p.generatedtargets.ptr != nil) { + os.free(p.generatedtargets.ptr: *void, + (p.generatedtargets.cap: u64) * (size(i32): u64)); + }; + if (p.fortest != nil) { + os.free(p.fortest: *void, cstrlen(p.fortest) + 1u64); + }; + let bi: i32 = 0; + for (bi < p.bindings.len) { + if (p.bindings[bi].name.ptr != nil) { + os.free(p.bindings[bi].name.ptr: *void, + p.bindings[bi].name.cap: u64); + }; + if (p.bindings[bi].source.ptr != nil) { + os.free(p.bindings[bi].source.ptr: *void, + p.bindings[bi].source.cap: u64); + }; + bi += 1; + }; + if (p.bindings.ptr != nil) { + os.free(p.bindings.ptr: *void, + (p.bindings.cap: u64) * (size(sepbind): u64)); + }; + +}; + +// Release package-owned action state through one boundary shared by normal +// graph teardown and an unpublished copy-on-write clone failure. fn sepgraphfree(g: *sepgraph) void = { if (g == nil) { return; }; let i: i32 = 0; for (i < g.n) { - let j: i32 = 0; - for (j < g.pkg[i].nsources) { - let p: *u8 = g.pkg[i].sources[j]; - os.free(p: *void, os.PATH_MAX: u64); - j += 1; - }; - if (g.pkg[i].sources != nil) { - os.free(g.pkg[i].sources: *void, - (g.pkg[i].nsources: u64) * (size(*u8): u64)); - }; - if (g.pkg[i].name != nil) { - os.free(g.pkg[i].name: *void, cstrlen(g.pkg[i].name) + 1u64); - }; - if (g.pkg[i].initsymbol != nil) { - os.free(g.pkg[i].initsymbol: *void, - cstrlen(g.pkg[i].initsymbol) + 1u64); - }; - if (g.pkg[i].contextstate.ptr != nil) { - os.free(g.pkg[i].contextstate.ptr: *void, - (g.pkg[i].contextstate.cap: u64) * (size(u8): u64)); - }; - if (g.pkg[i].deps.ptr != nil) { - os.free(g.pkg[i].deps.ptr: *void, - (g.pkg[i].deps.cap: u64) * (size(i32): u64)); - }; - let bi: i32 = 0; - for (bi < g.pkg[i].bindings.len) { - if (g.pkg[i].bindings[bi].name.ptr != nil) { - os.free(g.pkg[i].bindings[bi].name.ptr: *void, - g.pkg[i].bindings[bi].name.cap: u64); - }; - if (g.pkg[i].bindings[bi].source.ptr != nil) { - os.free(g.pkg[i].bindings[bi].source.ptr: *void, - g.pkg[i].bindings[bi].source.cap: u64); - }; - bi += 1; - }; - if (g.pkg[i].bindings.ptr != nil) { - os.free(g.pkg[i].bindings.ptr: *void, - (g.pkg[i].bindings.cap: u64) * (size(sepbind): u64)); - }; + seppkgfreeowned(&g.pkg[i]); i += 1; }; if (g.pkg.ptr != nil) { @@ -2585,7 +2610,7 @@ def SEP_NAME_MAX: u64 = 255u64; fn sepstoragedigest(p: *seppkg) *u8 = { let state: sha256.state = sha256.sha256(); let h: *hash.hash = (&state): *hash.hash; - hash.write(h, strings.toutf8("ww-package-storage-v2:")); + hash.write(h, strings.toutf8("ww-package-storage-v3:")); let tag: [4]u8; tag[0] = ('0': i32 + p.variant): u8; tag[1] = ':': u8; @@ -2597,6 +2622,10 @@ fn sepstoragedigest(p: *seppkg) *u8 = { zero[0] = 0u8; hash.write(h, zero[0:1]); hash.write(h, strings.toutf8(pathstr(p.canon))); + hash.write(h, zero[0:1]); + if (p.fortest != nil) { + hash.write(h, strings.toutf8(pathstr(p.fortest))); + }; let digest: [32]u8; hash.sum(h, digest[0:32]); let need: u64 = "__wwpkg.v".len: u64 + 1u64 + ".r".len: u64 @@ -2746,7 +2775,8 @@ fn sepexternalproductionedge(g: *sepgraph, importer: i32, dep: i32) bool = { let from: *seppkg = &g.pkg[importer]; let to: *seppkg = &g.pkg[dep]; return from.variant == SEP_VARIANT_EXTERNAL - && to.variant == SEP_VARIANT_PRODUCTION + && (to.variant == SEP_VARIANT_PRODUCTION + || to.variant == SEP_VARIANT_SAME_TEST) && to.role == SEP_ROLE_NORMAL && cstreq(from.canon, to.canon); }; @@ -3221,10 +3251,33 @@ fn sepscanfile(g: *sepgraph, pi: i32, file: *u8, context: i32, cerr("' cannot import itself\n"); return -1; }; - // External self-production is the ordinary canonical production - // action. Discovery role and product artifact never create another. - let di: i32 = sepfindoradd(g, resolved.identity, - resolved.entry, 1); + // Normal resolution and legality precede test-graph substitution. + let di: i32 = -1; + if (externalproduction) { + let candidate: i32 = 0; + for (candidate < g.n) { + let q: *seppkg = &g.pkg[candidate]; + let identitymatch: bool = false; + if (q.importbase != nil) { + identitymatch = cstreq(q.importbase, + resolved.identity); + } else { + identitymatch = g.pkg[pi].importbase == nil; + }; + if (q.variant == SEP_VARIANT_SAME_TEST + && q.role == SEP_ROLE_NORMAL + && os.samefile(pathstr(q.entry), + pathstr(resolved.entry)) + && identitymatch) { + di = candidate; + break; + }; + candidate += 1; + }; + }; + if (di < 0) { + di = sepfindoradd(g, resolved.identity, resolved.entry, 1); + }; if (di < 0) { return -1; }; let allowed: i32 = sepinternalimportallowed(&g.pkg[pi], resolved.identity, resolved.entry); @@ -3299,10 +3352,21 @@ fn sepdepcmp(g: *sepgraph, a: i32, b: i32) i32 = { if (g.pkg[a].role < g.pkg[b].role) { return -1; }; if (g.pkg[a].role > g.pkg[b].role) { return 1; }; if (g.pkg[a].canon != nil && g.pkg[b].canon != nil) { - return strings.compare(pathstr(g.pkg[a].canon), + r = strings.compare(pathstr(g.pkg[a].canon), pathstr(g.pkg[b].canon)): i32; + if (r != 0) { return r; }; + } else { + r = strings.compare(pathstr(g.pkg[a].entry), + pathstr(g.pkg[b].entry)): i32; + if (r != 0) { return r; }; }; - return strings.compare(pathstr(g.pkg[a].entry), pathstr(g.pkg[b].entry)): i32; + if (g.pkg[a].fortest == nil || g.pkg[b].fortest == nil) { + if (g.pkg[a].fortest == nil && g.pkg[b].fortest == nil) { return 0; }; + if (g.pkg[a].fortest == nil) { return -1; }; + return 1; + }; + return strings.compare(pathstr(g.pkg[a].fortest), + pathstr(g.pkg[b].fortest)): i32; }; fn seppackageinitsymbol(p: *seppkg) *u8 = { @@ -3338,59 +3402,75 @@ fn seppackageinitsymbol(p: *seppkg) *u8 = { return buf.ptr; }; -fn generatedmainkind(variant: i32) str = { - if (variant == SEP_VARIANT_SAME_TEST) { return "internal"; }; - if (variant == SEP_VARIANT_EXTERNAL) { return "external"; }; - return "production"; -}; - -fn generatedmainpath(pkgpath: *u8, kind: str) *u8 = { +fn generatedmainpath(pkgpath: *u8) *u8 = { let need: u64 = 0u64; if (!sepaddbytes(&need, "__wwtestmain.".len: u64) || !sepaddbytes(&need, cstrlen(pkgpath)) - || !sepaddbytes(&need, 1u64) - || !sepaddbytes(&need, kind.len: u64) || !sepaddbytes(&need, ".main".len: u64) || !sepaddbytes(&need, 1u64)) { return nil; }; let buf: []u8; if (!sepmakebytes(need, &buf)) { return nil; }; let off: u64 = strinto(buf.ptr, 0u64, "__wwtestmain."); off = cstrinto(buf.ptr, off, pkgpath); - off = byteinto(buf.ptr, off, '.': u8); - off = strinto(buf.ptr, off, kind); off = strinto(buf.ptr, off, ".main"); cstrseal(buf.ptr, off); return buf.ptr; }; -// Materialize the generated test dispatcher as a normal package action. It -// owns a generated source unit and imports exactly its test variant/support. -// Its identity is variant-derived, never product-ordinal-derived. +// One canonical directory owns one generated main and an explicit target set. fn sepaddgeneratedmain(g: *sepgraph, product: *sepproduct, ordinal: i32, support: i32) i32 = { - let variant: i32 = product.variantroot; - if (variant < 0 || variant >= g.n) { return -1; }; - let kind: str = generatedmainkind(g.pkg[variant].variant); - let mainpath: *u8 = generatedmainpath(g.pkg[variant].path, kind); + let targets: [2]i32; + let ntargets: i32 = 0; + if (product.ptest >= 0) { + targets[ntargets] = product.ptest; + ntargets += 1; + }; + if (product.pxtest >= 0) { + targets[ntargets] = product.pxtest; + ntargets += 1; + }; + if (ntargets == 0) { return -1; }; + if (ntargets == 2 && sepdepcmp(g, targets[0], targets[1]) > 0) { + let swap: i32 = targets[0]; + targets[0] = targets[1]; + targets[1] = swap; + }; + let owner: i32 = targets[0]; + if (g.pkg[owner].importbase == nil) { return -1; }; + let mainpath: *u8 = generatedmainpath(g.pkg[owner].importbase); if (mainpath == nil) { return -1; }; let pathi: i32 = 0; for (pathi < g.n) { if (cstreq(g.pkg[pathi].path, mainpath)) { if (g.pkg[pathi].generatedmain) { - let wantssupport: bool = support >= 0 && support != variant; - let wanteddeps: i32 = 1; - if (wantssupport) { wanteddeps = 2; }; - let hasvariant: bool = false; + let supportistarget: bool = false; + let tk: i32 = 0; + for (tk < ntargets) { + if (targets[tk] == support) { supportistarget = true; }; + tk += 1; + }; + let wantssupport: bool = support >= 0 && !supportistarget; + let wanteddeps: i32 = ntargets; + if (wantssupport) { wanteddeps += 1; }; + let sametargets: bool = + g.pkg[pathi].ngeneratedtargets == ntargets; + tk = 0; + for (tk < ntargets && sametargets) { + if (g.pkg[pathi].generatedtargets[tk] != targets[tk]) { + sametargets = false; + }; + tk += 1; + }; let hassupport: bool = false; let dk: i32 = 0; for (dk < g.pkg[pathi].ndeps) { - if (g.pkg[pathi].deps[dk] == variant) { hasvariant = true; }; if (wantssupport && g.pkg[pathi].deps[dk] == support) { hassupport = true; }; dk += 1; }; - if (hasvariant && hassupport == wantssupport + if (sametargets && hassupport == wantssupport && g.pkg[pathi].ndeps == wanteddeps) { return pathi; }; @@ -3408,16 +3488,10 @@ fn sepaddgeneratedmain(g: *sepgraph, product: *sepproduct, ordinal: i32, let p: *seppkg = &g.pkg[g.n]; p.path = mainpath; p.importbase = nil; - p.entry = g.pkg[variant].entry; - let canonkind: *u8 = sepappendlit(g.pkg[variant].canon, "#"); - if (canonkind == nil) { return -1; }; - canonkind = sepappendlit(canonkind, kind); - if (canonkind == nil) { return -1; }; - p.canon = sepappendlit(canonkind, "-test-main"); + p.entry = g.pkg[owner].entry; + p.canon = sepappendlit(g.pkg[owner].canon, "#directory-test-main"); if (p.canon == nil) { return -1; }; - let variantartifact: *u8 = g.pkg[variant].artifact; - if (variantartifact == nil) { variantartifact = g.pkg[variant].path; }; - p.artifact = sepappendlit(variantartifact, "-main"); + p.artifact = sepappendlit(g.pkg[owner].importbase, "-test-main"); if (p.artifact == nil) { return -1; }; p.storage = nil; p.initsymbol = nil; @@ -3425,6 +3499,7 @@ fn sepaddgeneratedmain(g: *sepgraph, product: *sepproduct, ordinal: i32, p.name = sepdupcstr("main\0".ptr, 4u64); if (p.name == nil) { return -1; }; p.testpackage = nil; + p.fortest = nil; p.sources = nil; p.nsources = 0; p.isdir = 0; @@ -3433,7 +3508,15 @@ fn sepaddgeneratedmain(g: *sepgraph, product: *sepproduct, ordinal: i32, p.root = true; p.linkentry = true; p.generatedmain = true; - p.generatedtarget = variant; + let generatedtargets: []i32; + if (!sepmakeints(ntargets, &generatedtargets)) { return -1; }; + p.generatedtargets = generatedtargets; + p.ngeneratedtargets = ntargets; + let ti: i32 = 0; + for (ti < ntargets) { + p.generatedtargets[ti] = targets[ti]; + ti += 1; + }; p.failed = false; p.testsupport = false; p.loaded = true; @@ -3449,10 +3532,20 @@ fn sepaddgeneratedmain(g: *sepgraph, product: *sepproduct, ordinal: i32, let emptydeps: []i32; p.deps = emptydeps; p.ndeps = 0; - if (!sepsetcontextstate(p, product.context, 2u8) - || !sepadddep(g, g.n, variant) - || (support >= 0 && support != variant - && !sepadddep(g, g.n, support))) { return -1; }; + if (!sepsetcontextstate(p, product.context, 2u8)) { return -1; }; + ti = 0; + for (ti < ntargets) { + if (!sepadddep(g, g.n, targets[ti])) { return -1; }; + ti += 1; + }; + let supportistarget: bool = false; + ti = 0; + for (ti < ntargets) { + if (targets[ti] == support) { supportistarget = true; }; + ti += 1; + }; + if (support >= 0 && !supportistarget + && !sepadddep(g, g.n, support)) { return -1; }; let i: i32 = 1; for (i < p.ndeps) { let v: i32 = p.deps[i]; @@ -3470,6 +3563,248 @@ fn sepaddgeneratedmain(g: *sepgraph, product: *sepproduct, ordinal: i32, return r; }; +fn seprewriteactiondeps(g: *sepgraph, pi: i32, + replacement: []i32) bool = { + let p: *seppkg = &g.pkg[pi]; + let deps: []i32; + if (!sepmakeints(p.ndeps, &deps)) { return false; }; + let ndeps: i32 = 0; + let i: i32 = 0; + for (i < p.ndeps) { + let dep: i32 = p.deps[i]; + if (dep >= 0 && dep < replacement.len) { dep = replacement[dep]; }; + let found: bool = false; + let j: i32 = 0; + for (j < ndeps) { + if (deps[j] == dep) { found = true; break; }; + j += 1; + }; + if (!found) { deps[ndeps] = dep; ndeps += 1; }; + i += 1; + }; + i = 1; + for (i < ndeps) { + let dep: i32 = deps[i]; + let j: i32 = i; + for (j > 0 && sepdepcmp(g, deps[j - 1], dep) > 0) { + deps[j] = deps[j - 1]; + j -= 1; + }; + deps[j] = dep; + i += 1; + }; + i = 0; + for (i < p.bindings.len) { + let dep: i32 = p.bindings[i].dep; + if (dep >= 0 && dep < replacement.len) { + p.bindings[i].dep = replacement[dep]; + }; + i += 1; + }; + sepbindsort(&p.bindings); + if (p.deps.ptr != nil) { + os.free(p.deps.ptr: *void, + (p.deps.cap: u64) * (size(i32): u64)); + }; + p.deps = deps; + p.ndeps = ndeps; + return true; +}; + +fn sepclonefortest(g: *sepgraph, original: i32, owner: *u8, + replacement: []i32) i32 = { + if (g.n == SEP_COUNT_MAX || !sepreservepackages(g, g.n + 1)) { + return -1; + }; + let src: *seppkg = &g.pkg[original]; + let p: seppkg; + p.path = src.path; + p.importbase = src.importbase; + p.entry = src.entry; + p.canon = src.canon; + p.artifact = nil; + p.storage = nil; + p.initsymbol = nil; + p.storagehashed = false; + p.name = nil; + p.testpackage = src.testpackage; + p.fortest = nil; + p.sources = nil; + p.nsources = 0; + p.isdir = src.isdir; + p.variant = SEP_VARIANT_TEST_COPY; + p.role = src.role; + p.root = false; + p.linkentry = false; + p.generatedmain = false; + let emptytargets: []i32; + p.generatedtargets = emptytargets; + p.ngeneratedtargets = 0; + p.failed = src.failed; + p.testsupport = src.testsupport; + p.loaded = src.loaded; + p.exportchanged = false; + p.sourcestaged = false; + p.initstaged = false; + p.archivestaged = false; + p.emitcontext = src.emitcontext; + let emptycontext: []u8; + p.contextstate = emptycontext; + let emptybindings: []sepbind; + p.bindings = emptybindings; + let emptydeps: []i32; + p.deps = emptydeps; + p.ndeps = 0; + p.color = 0; + + if (src.name != nil) { + p.name = sepdupcstr(src.name, cstrlen(src.name)); + if (p.name == nil) { + seppkgfreeowned(&p); + return -1; + }; + }; + p.fortest = sepdupcstr(owner, cstrlen(owner)); + if (p.fortest == nil) { + seppkgfreeowned(&p); + return -1; + }; + + if (src.nsources > 0) { + let sources: []*u8; + if (!sepmakeptrs(src.nsources, &sources)) { + seppkgfreeowned(&p); + return -1; + }; + let si: i32 = 0; + for (si < src.nsources) { + let bytes: []u8; + if (!sepmakebytes(os.PATH_MAX: u64, &bytes)) { + let sj: i32 = 0; + for (sj < si) { + os.free(sources[sj]: *void, os.PATH_MAX: u64); + sj += 1; + }; + os.free(sources.ptr: *void, + (sources.cap: u64) * (size(*u8): u64)); + seppkgfreeowned(&p); + return -1; + }; + let n: u64 = cstrlen(src.sources[si]); + let sj: u64 = 0u64; + for (sj < n) { + bytes[sj] = src.sources[si][sj]; + sj += 1u64; + }; + bytes[n] = 0u8; + sources[si] = bytes.ptr; + si += 1; + }; + p.sources = sources.ptr; + p.nsources = src.nsources; + }; + + if (src.contextstate.len > 0) { + let contextstate: []u8; + if (!sepmakebytes(src.contextstate.len: u64, &contextstate)) { + seppkgfreeowned(&p); + return -1; + }; + let ci: i32 = 0; + for (ci < src.contextstate.len) { + contextstate[ci] = src.contextstate[ci]; + ci += 1; + }; + p.contextstate = contextstate; + }; + let bi: i32 = 0; + for (bi < src.bindings.len) { + let b: sepbind = src.bindings[bi]; + if (!sepbindadd(&p.bindings, b.kind, b.name, b.dep, + b.source, b.line, b.col)) { + seppkgfreeowned(&p); + return -1; + }; + bi += 1; + }; + if (src.ndeps > 0) { + let deps: []i32; + if (!sepmakeints(src.ndeps, &deps)) { + seppkgfreeowned(&p); + return -1; + }; + let di: i32 = 0; + for (di < src.ndeps) { + deps[di] = src.deps[di]; + di += 1; + }; + p.deps = deps; + p.ndeps = src.ndeps; + }; + + let ni: i32 = g.n; + g.pkg[ni] = p; + g.n += 1; + if (!seprewriteactiondeps(g, ni, replacement)) { + seppkgfreeowned(&g.pkg[ni]); + g.n -= 1; + return -1; + }; + return ni; +}; + +fn seprecompilefortest(g: *sepgraph, product: *sepproduct) i32 = { + if (product.productionroot < 0 || product.ptest < 0 + || product.productionroot == product.ptest) { return 0; }; + let nbase: i32 = g.n; + let order: []i32; + let stack: []i32; + let replacement: []i32; + if (!sepmakeints(nbase, &order) || !sepmakeints(nbase, &stack) + || !sepmakeints(nbase, &replacement)) { return -1; }; + let i: i32 = 0; + for (i < nbase) { + g.pkg[i].color = 0; + replacement[i] = i; + i += 1; + }; + replacement[product.productionroot] = product.ptest; + let norder: i32 = 0; + if (septopovisit(g, product.root, order, &norder, stack, 0) < 0) { + return -1; + }; + let ownerprefix: *u8 = sepappendlit(g.pkg[product.ptest].importbase, "#"); + if (ownerprefix == nil) { return -1; }; + let owner: *u8 = sepappendlit(ownerprefix, pathstr(g.pkg[product.ptest].canon)); + if (owner == nil) { return -1; }; + let oi: i32 = 0; + for (oi < norder) { + let pi: i32 = order[oi]; + if (pi == product.productionroot) { oi += 1; continue; }; + let changed: bool = false; + let k: i32 = 0; + for (k < g.pkg[pi].ndeps && !changed) { + let dep: i32 = g.pkg[pi].deps[k]; + if (dep >= 0 && dep < nbase && replacement[dep] != dep) { + changed = true; + }; + k += 1; + }; + if (changed) { + if (pi == product.ptest || pi == product.pxtest + || pi == product.root) { + if (!seprewriteactiondeps(g, pi, replacement)) { return -1; }; + } else { + let copy: i32 = sepclonefortest(g, pi, owner, replacement); + if (copy < 0) { return -1; }; + replacement[pi] = copy; + }; + }; + oi += 1; + }; + return 0; +}; + // Load one action's owned sources and direct bindings. Dependency descent is // iterative below so a valid deep graph is not limited by the native stack. fn seppreparepkgcontext(g: *sepgraph, pi: i32, context: i32, @@ -4101,30 +4436,6 @@ fn septopovisit(g: *sepgraph, pi: i32, order: []i32, no: *i32, return sepfinishtopoframes(frames, 0); }; -fn sepinternalreplacesproduction(g: *sepgraph, a: i32, b: i32) bool = { - let internal: i32 = a; - let production: i32 = b; - if (g.pkg[internal].variant != SEP_VARIANT_SAME_TEST) { - internal = b; - production = a; - }; - return g.pkg[internal].variant == SEP_VARIANT_SAME_TEST - && g.pkg[production].variant == SEP_VARIANT_PRODUCTION - && g.pkg[production].role != SEP_ROLE_TEST_SUPPORT - && cstreq(g.pkg[internal].canon, g.pkg[production].canon); -}; - -// A production action is omitted from an internal-test product because the -// augmented variant owns those same production sources. Map initialization -// edges through that replacement exactly as the link closure does. -fn sepiniteffective(g: *sepgraph, variantroot: i32, pi: i32) i32 = { - if (variantroot >= 0 && variantroot < g.n - && sepinternalreplacesproduction(g, variantroot, pi)) { - return variantroot; - }; - return pi; -}; - fn sepinitcmp(g: *sepgraph, a: i32, b: i32) i32 = { let r: i32 = strings.compare(pathstr(g.pkg[a].path), pathstr(g.pkg[b].path)): i32; @@ -4133,13 +4444,23 @@ fn sepinitcmp(g: *sepgraph, a: i32, b: i32) i32 = { if (g.pkg[a].variant > g.pkg[b].variant) { return 1; }; if (g.pkg[a].role < g.pkg[b].role) { return -1; }; if (g.pkg[a].role > g.pkg[b].role) { return 1; }; - return 0; + if (g.pkg[a].canon != nil && g.pkg[b].canon != nil) { + r = strings.compare(pathstr(g.pkg[a].canon), + pathstr(g.pkg[b].canon)): i32; + if (r != 0) { return r; }; + }; + if (g.pkg[a].fortest == nil || g.pkg[b].fortest == nil) { + if (g.pkg[a].fortest == nil && g.pkg[b].fortest == nil) { return 0; }; + if (g.pkg[a].fortest == nil) { return -1; }; + return 1; + }; + return strings.compare(pathstr(g.pkg[a].fortest), + pathstr(g.pkg[b].fortest)): i32; }; // Go's linker uses a lexical ready queue over the reachable init-task DAG. // Dependencies become ready first; canonical package identity breaks ties. -fn sepinitorder(g: *sepgraph, root: i32, variantroot: i32, - out: *[]i32, nout: *i32) i32 = { +fn sepinitorder(g: *sepgraph, root: i32, out: *[]i32, nout: *i32) i32 = { let active: []u8; let done: []u8; let todo: []i32; @@ -4157,17 +4478,15 @@ fn sepinitorder(g: *sepgraph, root: i32, variantroot: i32, zi += 1; }; let ntodo: i32 = 0; - let effectiveroot: i32 = sepiniteffective(g, variantroot, root); - active[effectiveroot] = 1u8; - todo[ntodo] = effectiveroot; + active[root] = 1u8; + todo[ntodo] = root; ntodo += 1; for (ntodo > 0) { ntodo -= 1; let pi: i32 = todo[ntodo]; let k: i32 = 0; for (k < g.pkg[pi].ndeps) { - let dep: i32 = sepiniteffective(g, variantroot, - g.pkg[pi].deps[k]); + let dep: i32 = g.pkg[pi].deps[k]; if (active[dep] == 0u8) { active[dep] = 1u8; todo[ntodo] = dep; @@ -4191,8 +4510,7 @@ fn sepinitorder(g: *sepgraph, root: i32, variantroot: i32, let blocked: bool = false; let k: i32 = 0; for (k < g.pkg[pi].ndeps && !blocked) { - let dep: i32 = sepiniteffective(g, variantroot, - g.pkg[pi].deps[k]); + let dep: i32 = g.pkg[pi].deps[k]; if (dep != pi && active[dep] != 0u8 && done[dep] == 0u8) { blocked = true; }; k += 1; @@ -4220,8 +4538,7 @@ fn sepcomposeinitdispatch(g: *sepgraph, product: *sepproduct, unitpath: *u8, asmpath: *u8) i32 = { let order: []i32; let norder: i32 = 0; - if (sepinitorder(g, product.root, product.variantroot, - &order, &norder) < 0) { return -1; }; + if (sepinitorder(g, product.root, &order, &norder) < 0) { return -1; }; let unit: i32 = os.open(pathstr(unitpath), os.flag.WRONLY | os.flag.CREATE | os.flag.TRUNC, 420i32); if (unit < 0) { @@ -4287,8 +4604,7 @@ fn sepvalidatemoduleclosure(g: *sepgraph, order: []i32, n: i32, for (j < n) { let b: i32 = order[j]; if ((includeroot || !g.pkg[b].root) - && cstreq(g.pkg[a].path, g.pkg[b].path) - && !sepinternalreplacesproduction(g, a, b)) { + && cstreq(g.pkg[a].path, g.pkg[b].path)) { cerr("ww: product closure contains multiple packages named "); cerr(pathstr(g.pkg[a].path)); cerr("\n"); return -1; @@ -4783,6 +5099,39 @@ fn sepprepareproductstage(current: *u8, dst: *u8) *u8 = { return stage; }; +fn sepproductpathsoverlap(a: *u8, b: *u8) bool = { + if (a == nil || b == nil) { return false; }; + if (cstreq(a, b)) { return true; }; + let an: u64 = cstrlen(a); + let bn: u64 = cstrlen(b); + return (an == bn + 4u64 && bytecmp(a, bn, ".new".ptr, 4u64) == 0) + || (bn == an + 4u64 && bytecmp(b, an, ".new".ptr, 4u64) == 0); +}; + +fn sepvalidateproductpathpair(a: *sepproduct, b: *sepproduct) i32 = { + let ap: []*u8 = [nil, nil, a.stagestatus, a.stageout, a.stageiface]; + let bp: []*u8 = [nil, nil, b.stagestatus, b.stageout, b.stageiface]; + if (a.stagestatus != nil) { ap[0] = a.status; }; + if (a.stageout != nil) { ap[1] = a.out; }; + if (b.stagestatus != nil) { bp[0] = b.status; }; + if (b.stageout != nil) { bp[1] = b.out; }; + let i: i32 = 0; + for (i < ap.len) { + if (ap[i] != nil) { + let j: i32 = 0; + for (j < bp.len) { + if (sepproductpathsoverlap(ap[i], bp[j])) { + cerrpath("ww: product path collision: ", bp[j], "\n"); + return -1; + }; + j += 1; + }; + }; + i += 1; + }; + return 0; +}; + fn sepwritetextstage(path: *u8, body: str) i32 = { let fd: i32 = os.open(pathstr(path), os.flag.WRONLY | os.flag.CREATE | os.flag.TRUNC, 420i32); @@ -4855,7 +5204,7 @@ fn sepvalidaterequeststaging(g: *sepgraph, scratch: *u8, warm: bool, if (emitasm == 0) { let ownsoutput: bool = false; if (rootpackage) { ownsoutput = publishpackage != 0; } - else { ownsoutput = istest != 0 + else { ownsoutput = (istest != 0 && !products[i].notests) || seprootiscommand(&g.pkg[products[i].root]); }; if (ownsoutput) { products[i].stageout = sepprepareproductstage( @@ -4872,6 +5221,16 @@ fn sepvalidaterequeststaging(g: *sepgraph, scratch: *u8, warm: bool, }; i += 1; }; + i = 0; + for (i < nproducts) { + let j: i32 = 0; + for (j < i) { + if (sepvalidateproductpathpair(&products[j], + &products[i]) < 0) { return -1; }; + j += 1; + }; + i += 1; + }; return 0; }; @@ -5129,9 +5488,9 @@ fn validatecommandoutputpath(out: *u8) i32 = { fn workdirstamptext(istest: i32, emitasm: i32) str = { if (istest != 0) { if (emitasm != 0) { - return "ww workdir fmt 17 mode test asm 1\n"; + return "ww workdir fmt 19 mode test asm 1\n"; }; - return "ww workdir fmt 17 mode test asm 0\n"; + return "ww workdir fmt 19 mode test asm 0\n"; }; if (emitasm != 0) { return "ww workdir fmt 18 mode build asm 1\n"; @@ -5473,7 +5832,14 @@ fn sepfinishrequest(selfdir: *u8, l6: *u8, c6: *u8, a6: *u8, producti = 0; for (producti < nproducts) { let root: i32 = products[producti].root; - let variantroot: i32 = products[producti].variantroot; + if (istest != 0 && products[producti].notests) { + if (sepstageproductstatus(&products[producti]) != 0) { + cerr("ww: cannot stage package-test product\n"); + return sepfinishfail(&entries, ntxn); + }; + producti += 1; + continue; + }; if (istest == 0 && !seprootiscommand(&g.pkg[root])) { if (sepstageproductstatus(&products[producti]) != 0) { cerr("ww: cannot stage package-build product\n"); @@ -5533,11 +5899,6 @@ fn sepfinishrequest(selfdir: *u8, l6: *u8, c6: *u8, a6: *u8, let li: i32 = nlink - 1; for (li >= 0) { let pi: i32 = linkorder[li]; - if (variantroot >= 0 - && sepinternalreplacesproduction(g, variantroot, pi)) { - li -= 1; - continue; - }; let suffix: str = ".a"; if (warm && g.pkg[pi].archivestaged) { suffix = ".a.new"; }; largv[pos] = sepfname(g, pi, scratch, suffix); @@ -5889,6 +6250,9 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, let producti: i32 = 0; for (producti < nproducts) { products[producti].support = -1; + products[producti].productionroot = -1; + products[producti].ptest = -1; + products[producti].pxtest = -1; products[producti].stageout = nil; products[producti].stageiface = nil; products[producti].stagestatus = nil; @@ -5902,10 +6266,6 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, }; let contextroot: *u8 = entry; if (entryisdir == 0) { contextroot = srcd.ptr; }; - let selector: *u8 = products[producti].testpackage; - if (products[producti].variant == SEP_VARIANT_PRODUCTION) { - selector = nil; - }; let requestedpath: *u8 = "\0".ptr; if (products[producti].identity != nil) { requestedpath = products[producti].identity; @@ -5921,21 +6281,98 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, if (inferred < 0) { return 1; }; if (inferred > 0) { rootpath = inferredpath; }; }; - products[producti].root = sepfindoraddvariant(g, rootpath, entry, - entryisdir, products[producti].variant, - selector, - SEP_ROLE_NORMAL, products[producti].artifact, true); + if (products[producti].directoryproduct) { + if (products[producti].productionpackage != nil) { + products[producti].productionroot = sepfindoraddvariant(g, + rootpath, entry, 1, SEP_VARIANT_PRODUCTION, nil, + SEP_ROLE_NORMAL, nil, true); + if (products[producti].productionroot < 0) { return 1; }; + }; + if (products[producti].internalpackage != nil) { + products[producti].ptest = sepfindoraddvariant(g, rootpath, + entry, 1, SEP_VARIANT_SAME_TEST, + products[producti].internalpackage, + SEP_ROLE_NORMAL, nil, true); + if (products[producti].ptest < 0) { return 1; }; + } else { if (istest != 0 && !products[producti].notests) { + products[producti].ptest = products[producti].productionroot; + }; }; + if (products[producti].externalpackage != nil) { + products[producti].pxtest = sepfindoraddvariant(g, rootpath, + entry, 1, SEP_VARIANT_EXTERNAL, + products[producti].externalpackage, + SEP_ROLE_NORMAL, nil, true); + if (products[producti].pxtest < 0) { return 1; }; + }; + if (istest == 0 || products[producti].notests) { + products[producti].root = products[producti].productionroot; + } else { if (products[producti].ptest >= 0) { + products[producti].root = products[producti].ptest; + } else { + products[producti].root = products[producti].pxtest; + }; }; + products[producti].variantroot = products[producti].ptest; + } else { + let selector: *u8 = products[producti].testpackage; + if (products[producti].variant == SEP_VARIANT_PRODUCTION) { + selector = nil; + }; + products[producti].root = sepfindoraddvariant(g, rootpath, entry, + entryisdir, products[producti].variant, selector, + SEP_ROLE_NORMAL, products[producti].artifact, true); + products[producti].variantroot = products[producti].root; + }; if (products[producti].root < 0) { return 1; }; - products[producti].variantroot = products[producti].root; + producti += 1; + }; + producti = 0; + for (producti < nproducts) { + if (products[producti].directoryproduct) { + let a: i32 = products[producti].productionroot; + if (a < 0) { a = products[producti].ptest; }; + if (a < 0) { a = products[producti].pxtest; }; + let previous: i32 = 0; + for (previous < producti) { + if (products[previous].directoryproduct) { + let b: i32 = products[previous].productionroot; + if (b < 0) { b = products[previous].ptest; }; + if (b < 0) { b = products[previous].pxtest; }; + let duplicate: bool = a >= 0 && b >= 0 && a == b; + if (!duplicate && a >= 0 && b >= 0) { + let aid: *u8 = g.pkg[a].importbase; + let bid: *u8 = g.pkg[b].importbase; + if (aid != nil && bid != nil) { + duplicate = cstreq(aid, bid); + } else { if (aid == nil && bid == nil) { + duplicate = cstreq(g.pkg[a].canon, + g.pkg[b].canon); + }; }; + }; + if (duplicate) { + cerr("ww test: duplicate --ww-package-test product for canonical directory\n"); + return 1; + }; + }; + previous += 1; + }; + }; producti += 1; }; let testsupportmodule: str = "test"; + let haverunnabletests: bool = false; + producti = 0; + for (producti < nproducts) { + if (istest != 0 && !products[producti].notests) { + haverunnabletests = true; + }; + producti += 1; + }; // -T generates a dispatcher whose support qualifier is selected by the // command. Represent that requirement as a direct generated-main edge. It // normally coalesces with an explicit toolchain `import test`; // when user source occupies that identity, the reserved graph alias keeps // it distinct. The linker receives the same support archive closure. - if (istest != 0) { + if (haverunnabletests) { let td: i32 = 1; let tp: *u8 = locateimport(toolsrcdir, "test".ptr, "test".len: u64); @@ -5951,7 +6388,11 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, let collision: bool = false; producti = 0; for (producti < nproducts) { - let root: i32 = products[producti].root; + if (products[producti].notests) { + producti += 1; continue; + }; + let root: i32 = products[producti].ptest; + if (root < 0) { root = products[producti].pxtest; }; let rootissupport: bool = entryisdir != 0 && os.samefile(pathstr(tp), pathstr(g.pkg[root].entry)); @@ -5965,6 +6406,10 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, }; producti = 0; for (producti < nproducts && !collision) { + if (products[producti].notests) { + producti += 1; + continue; + }; let up: *u8 = locateimport( g.context[products[producti].context].searchpath, "test".ptr, @@ -5977,7 +6422,11 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, if (collision) { testsupportmodule = SEP_TEST_SUPPORT_MODULE; }; producti = 0; for (producti < nproducts) { - let root: i32 = products[producti].root; + if (products[producti].notests) { + producti += 1; continue; + }; + let root: i32 = products[producti].ptest; + if (root < 0) { root = products[producti].pxtest; }; let rootissupport: bool = entryisdir != 0 && os.samefile(pathstr(tp), pathstr(g.pkg[root].entry)); @@ -5985,8 +6434,7 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, // and its source imports. An external test still needs the // colocated production node, also its support dependency. if (rootissupport - && syntax.streq(testsupportmodule, "test") - && products[producti].variant != SEP_VARIANT_EXTERNAL) { + && syntax.streq(testsupportmodule, "test")) { products[producti].support = root; producti += 1; continue; @@ -6008,37 +6456,74 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, }; producti = 0; for (producti < nproducts) { - let root: i32 = products[producti].variantroot; + let roots: [3]i32; + roots[0] = products[producti].productionroot; + roots[1] = products[producti].ptest; + roots[2] = products[producti].pxtest; + let selectors: [3]*u8; + selectors[0] = products[producti].productionpackage; + selectors[1] = products[producti].internalpackage; + selectors[2] = products[producti].externalpackage; + let nroots: i32 = 3; + if (!products[producti].directoryproduct) { + nroots = 1; + roots[0] = products[producti].variantroot; + selectors[0] = products[producti].testpackage; + if (products[producti].variant == SEP_VARIANT_PRODUCTION) { + selectors[0] = nil; + }; + }; // Raw single-file tests retain the explicit fixture exception: test-main // synthesis stays in that action and support remains a direct export. if (istest != 0 && entryisdir == 0) { let support: i32 = products[producti].support; - if (support >= 0 && support != root - && !sepadddep(g, root, support)) { return 1; }; - g.pkg[root].linkentry = true; + if (support >= 0 && support != roots[0] + && !sepadddep(g, roots[0], support)) { return 1; }; + g.pkg[roots[0]].linkentry = true; }; - let loadresult: i32 = seploadpkg(g, root, - products[producti].context); - if (loadresult == -2) { return 1; }; - if (loadresult == SEP_LOAD_INTERNAL - || loadresult == SEP_LOAD_VENDOR) { return 1; }; - if (loadresult < 0) { - g.pkg[root].failed = true; - producti += 1; - continue; - }; - if (products[producti].testpackage != nil - && !cstreq(g.pkg[root].name, - products[producti].testpackage)) { - cerr("ww: package-test selector does not match loaded package\n"); - g.pkg[root].failed = true; + let ri: i32 = 0; + for (ri < nroots) { + let root: i32 = roots[ri]; + if (root < 0) { ri += 1; continue; }; + if (ri > 0 && products[producti].productionroot >= 0 + && g.pkg[products[producti].productionroot].failed) { + g.pkg[root].failed = true; + ri += 1; + continue; + }; + let duplicate: bool = false; + let rj: i32 = 0; + for (rj < ri) { + if (roots[rj] == root) { duplicate = true; }; + rj += 1; + }; + if (duplicate) { ri += 1; continue; }; + let loadresult: i32 = seploadpkg(g, root, + products[producti].context); + if (loadresult == -2) { return 1; }; + if (loadresult == SEP_LOAD_INTERNAL + || loadresult == SEP_LOAD_VENDOR) { return 1; }; + if (loadresult < 0) { + g.pkg[root].failed = true; + ri += 1; continue; + }; + if (selectors[ri] != nil + && !cstreq(g.pkg[root].name, selectors[ri])) { + cerr("ww: package-test selector does not match loaded package\n"); + g.pkg[root].failed = true; + }; + ri += 1; }; producti += 1; }; - if (istest != 0) { + if (haverunnabletests) { producti = 0; for (producti < nproducts) { - let variant: i32 = products[producti].variantroot; + if (products[producti].notests) { + producti += 1; continue; + }; + let variant: i32 = products[producti].ptest; + if (variant < 0) { variant = products[producti].pxtest; }; let support: i32 = products[producti].support; if (support >= 0 && support != variant) { let loadresult: i32 = seploadpkg(g, support, @@ -6058,7 +6543,7 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, if (istest == 0) { producti = 0; for (producti < nproducts) { - let root: i32 = products[producti].variantroot; + let root: i32 = products[producti].root; if (!g.pkg[root].failed) { g.pkg[root].linkentry = seprootiscommand(&g.pkg[root]); @@ -6069,11 +6554,25 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, if (istest != 0 && entryisdir != 0) { producti = 0; for (producti < nproducts) { - let variant: i32 = products[producti].variantroot; + if (products[producti].notests) { + producti += 1; continue; + }; + let variant: i32 = products[producti].ptest; + if (variant < 0) { variant = products[producti].pxtest; }; let support: i32 = products[producti].support; - if (g.pkg[variant].failed + let failed: bool = g.pkg[variant].failed; + if (products[producti].productionroot >= 0 + && g.pkg[products[producti].productionroot].failed) { + failed = true; + }; + if (products[producti].ptest >= 0 + && g.pkg[products[producti].ptest].failed) { failed = true; }; + if (products[producti].pxtest >= 0 + && g.pkg[products[producti].pxtest].failed) { failed = true; }; + if (failed || (support >= 0 && g.pkg[support].failed)) { products[producti].root = variant; + g.pkg[variant].failed = true; producti += 1; continue; }; @@ -6083,6 +6582,15 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, products[producti].root = mainpkg; producti += 1; }; + producti = 0; + for (producti < nproducts) { + if (!products[producti].notests + && !g.pkg[products[producti].root].failed + && seprecompilefortest(g, &products[producti]) < 0) { + return 1; + }; + producti += 1; + }; }; let initpi: i32 = 0; for (initpi < g.n) { @@ -6144,8 +6652,7 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, if (!g.pkg[root].failed) { let initcheck: []i32; let ninitcheck: i32 = 0; - if (sepinitorder(g, root, products[producti].variantroot, - &initcheck, &ninitcheck) < 0) { + if (sepinitorder(g, root, &initcheck, &ninitcheck) < 0) { g.pkg[root].failed = true; }; if (initcheck.ptr != nil) { @@ -6498,6 +7005,17 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, mapk += 1; }; let alen: i32 = 18; + if (g.pkg[pi].ngeneratedtargets + > (SEP_COUNT_MAX - alen) / 2) { + sepfailsize(); + sepdiscardactionstaging(warm, unitnew, wwinew, + asmnew, objnew, anew); + sepdiscardinitstaging(warm, initunitnew, + initasmnew, initobjnew); + return seprejectrequest(g, scratch, warm, products, nproducts, + &createdwork, &createdoutput, scratchout); + }; + alen += g.pkg[pi].ngeneratedtargets * 2; if (g.pkg[pi].ndeps > (SEP_COUNT_MAX - alen) / 3) { sepfailsize(); sepdiscardactionstaging(warm, unitnew, wwinew, asmnew, @@ -6540,8 +7058,13 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, append(argv, "--test-support-module"); append(argv, testsupportmodule); if (g.pkg[pi].generatedmain) { - append(argv, "--test-target-package"); - append(argv, pathstr(g.pkg[g.pkg[pi].generatedtarget].path)); + let targetk: i32 = 0; + for (targetk < g.pkg[pi].ngeneratedtargets) { + append(argv, "--test-target-package"); + append(argv, pathstr(g.pkg[ + g.pkg[pi].generatedtargets[targetk]].path)); + targetk += 1; + }; }; } else { if (testpkg) { append(argv, "--test-package"); }; @@ -6730,12 +7253,17 @@ fn buildonesep(selfdir: *u8, src: *u8, entryisdir: i32, product.out = out; product.identity = rootidentity; product.testpackage = testpackage; + product.productionpackage = nil; + product.internalpackage = nil; + product.externalpackage = nil; product.status = nil; product.artifact = nil; if (entryisdir == 0) { product.artifact = "__root\0".ptr; }; product.variant = rootvariant; + product.directoryproduct = false; + product.notests = false; product.root = -1; product.variantroot = -1; product.support = -1; @@ -7544,33 +8072,38 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { continue; }; if (cstreqlit(p, "--ww-package-test")) { - if (i + 5 >= argc) { - cerr("ww test: --ww-package-test needs kind, package, directory, output, and status\n"); + if (i + 8 >= argc) { + cerr("ww test: --ww-package-test needs kind, package, production, internal, external, directory, output, and status\n"); return 2; }; let kind: *u8 = argv[i + 1]; let name: *u8 = argv[i + 2]; - let dir: *u8 = argv[i + 3]; - let output: *u8 = argv[i + 4]; - let status: *u8 = argv[i + 5]; + let production: *u8 = argv[i + 3]; + let internal: *u8 = argv[i + 4]; + let external: *u8 = argv[i + 5]; + let dir: *u8 = argv[i + 6]; + let output: *u8 = argv[i + 7]; + let status: *u8 = argv[i + 8]; let pn: u64 = cstrlen(name); - let variant: i32 = SEP_VARIANT_EXTERNAL; - if (cstreqlit(kind, "production")) { - variant = SEP_VARIANT_PRODUCTION; - } else { if (cstreqlit(kind, "same")) { - variant = SEP_VARIANT_SAME_TEST; - }; }; - if ((!cstreqlit(kind, "production") - && !cstreqlit(kind, "same") - && !cstreqlit(kind, "external")) + let buildproduct: bool = cstreqlit(kind, "build"); + let testproduct: bool = cstreqlit(kind, "test"); + let hasproduction: bool = !cstreqlit(production, "-"); + let hasinternal: bool = !cstreqlit(internal, "-"); + let hasexternal: bool = !cstreqlit(external, "-"); + if ((!buildproduct && !testproduct) || pn == 0u64 || dir[0u64] == 0u8 || output[0u64] == 0u8 || status[0u64] == 0u8 - || (cstreqlit(kind, "external") - && (pn <= 5u64 - || !cstrendswithlit(name, - "_test")))) { - cerr("ww test: invalid --ww-package-test variant\n"); + || (hasproduction && !cstreq(production, name)) + || (hasinternal && !cstreq(internal, name)) + || (hasexternal && (cstrlen(external) != pn + 5u64 + || !strings.hasprefix(pathstr(external), pathstr(name)) + || !cstrendswithlit(external, "_test"))) + || (buildproduct && (!hasproduction + || hasinternal || hasexternal)) + || (testproduct && !hasproduction + && !hasinternal && !hasexternal)) { + cerr("ww test: invalid --ww-package-test product\n"); return 2; }; let product: sepproduct; @@ -7578,11 +8111,23 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { product.out = output; product.identity = nil; product.testpackage = name; + product.productionpackage = nil; + if (hasproduction) { product.productionpackage = production; }; + product.internalpackage = nil; + if (hasinternal) { product.internalpackage = internal; }; + product.externalpackage = nil; + if (hasexternal) { product.externalpackage = external; }; product.status = status; product.artifact = nil; - product.variant = variant; + product.variant = SEP_VARIANT_TEST_MAIN; + if (buildproduct) { product.variant = SEP_VARIANT_PRODUCTION; }; + product.directoryproduct = true; + product.notests = testproduct && !hasinternal && !hasexternal; product.root = -1; product.variantroot = -1; + product.productionroot = -1; + product.ptest = -1; + product.pxtest = -1; product.support = -1; if (products.len == SEP_COUNT_MAX) { sepfailsize(); @@ -7591,7 +8136,7 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { if (!sepreserveproducts(&products, products.len + 1)) { return 1; }; append(products, product); - i += 6; + i += 9; continue; }; if (p[1u64] == 73u8) { // '-I' @@ -7719,11 +8264,8 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { for (producti < products.len) { let product: sepproduct = products[producti]; let j: i32 = producti; - for (j > 0 && (strings.compare(pathstr(products[j - 1].dir), - pathstr(product.dir)) > 0 - || (strings.compare(pathstr(products[j - 1].dir), - pathstr(product.dir)) == 0 - && products[j - 1].variant > product.variant))) { + for (j > 0 && strings.compare(pathstr(products[j - 1].dir), + pathstr(product.dir)) > 0) { products[j] = products[j - 1]; j -= 1; }; @@ -7733,10 +8275,8 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { producti = 0; for (producti < products.len) { if (producti > 0 - && cstreq(products[producti - 1].dir, products[producti].dir) - && products[producti - 1].variant - == products[producti].variant) { - cerr("ww test: duplicate --ww-package-test variant for directory\n"); + && cstreq(products[producti - 1].dir, products[producti].dir)) { + cerr("ww test: duplicate --ww-package-test product for directory\n"); return 2; }; // Artifact identity is finalized from canonical package identity. @@ -7781,6 +8321,16 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { }; producti += 1; }; + } else { + producti = 0; + for (producti < products.len) { + if (products[producti].directoryproduct + && products[producti].variant == SEP_VARIANT_PRODUCTION) { + cerr("ww test: package-test products must use test kind\n"); + return 2; + }; + producti += 1; + }; }; if (patarg != nil) { let first: os.filestat; diff --git a/selfhost/cmd/wwdump/main.ww b/selfhost/cmd/wwdump/main.ww index 29d70d52..2c1a79c1 100644 --- a/selfhost/cmd/wwdump/main.ww +++ b/selfhost/cmd/wwdump/main.ww @@ -148,7 +148,8 @@ export fn main(argc: i32, argv: **u8) i32 = { // silently). Mirrors w6c main.ww:162 / cmd/w6c/main.c. if (l.errs > 0 || ps.errs > 0) { return 1; }; let empty: str; - if (wcc.compilefile(f, 0, 0, empty, empty, 0, -1, 0, 0, + let emptytargets: []str; + if (wcc.compilefile(f, 0, 0, empty, emptytargets, 0, -1, 0, 0, empty, empty) != 0) { return 1; }; };};};};