diff --git a/cmd/ww/main.c b/cmd/ww/main.c index d32a2c5e..c124c349 100644 --- a/cmd/ww/main.c +++ b/cmd/ww/main.c @@ -211,12 +211,12 @@ exec_package_command(int argc, char **argv, const char *target, if (fallback == NULL) return 1; prog = fallback; } - if (argc < 0 || argc > INT_MAX - 10) { + if (argc < 0 || argc > INT_MAX - 11) { free(fallback); sep_fail_size(); return 1; } - char **xargv = calloc((size_t)argc + 10, sizeof *xargv); + char **xargv = calloc((size_t)argc + 11, sizeof *xargv); if (xargv == NULL) { fprintf(stderr, "ww %s: cannot allocate package coordinator arguments\n", @@ -233,6 +233,8 @@ exec_package_command(int argc, char **argv, const char *target, } xargv[n++] = "--ww-driver"; xargv[n++] = (char *)self_path; + if (!build_only && !add_dot) + xargv[n++] = "--ww-explicit-test-target"; if (root_identity != NULL) { xargv[n++] = "--ww-root-identity"; xargv[n++] = (char *)root_identity; @@ -7985,6 +7987,8 @@ do_test(int argc, char **argv) if (!resolve_module(target, incs, resolved, sizeof resolved, &is_dir)) { fprintf(stderr, "ww test: cannot find %s\n", target); + if (src != NULL && !compileonly && !emit_asm) + fputs("FAIL\n", stdout); return 1; } if (is_dir) { @@ -8064,6 +8068,7 @@ do_test(int argc, char **argv) fputs("ww: cannot remove temporary output\n", stderr); if (owntmp && rmdir(tmpdir) != 0) fputs("ww: cannot remove temporary directory\n", stderr); + if (!compileonly && !emit_asm) fputs("FAIL\n", stdout); return 1; } if (compileonly || emit_asm) { @@ -8079,6 +8084,7 @@ do_test(int argc, char **argv) return cleanfail ? 1 : 0; } int rc = run_test_bin(outp, pattern); + int test_failed = rc != 0; if (rc == 0 && deferred_install && sep_install_test_output(outp, outstem) != 0) rc = 1; @@ -8090,6 +8096,7 @@ do_test(int argc, char **argv) fputs("ww: cannot remove temporary directory\n", stderr); if (rc == 0) rc = 1; } + if (test_failed) fputs("FAIL\n", stdout); return rc; } if (S_ISREG(st.st_mode)) { @@ -8139,6 +8146,7 @@ do_test(int argc, char **argv) fputs("ww: cannot remove temporary output\n", stderr); if (owntmp && rmdir(tmpdir) != 0) fputs("ww: cannot remove temporary directory\n", stderr); + if (!compileonly && !emit_asm) fputs("FAIL\n", stdout); return 1; } if (compileonly || emit_asm) { @@ -8154,6 +8162,7 @@ do_test(int argc, char **argv) return cleanfail ? 1 : 0; } int rc = run_test_bin(outp, pattern); + int test_failed = rc != 0; if (rc == 0 && deferred_install && sep_install_test_output(outp, outstem) != 0) rc = 1; @@ -8165,6 +8174,7 @@ do_test(int argc, char **argv) fputs("ww: cannot remove temporary directory\n", stderr); if (rc == 0) rc = 1; } + if (test_failed) fputs("FAIL\n", stdout); return rc; } if (!S_ISDIR(st.st_mode)) { diff --git a/docs/build-system.md b/docs/build-system.md index 9146bec5..413362db 100644 --- a/docs/build-system.md +++ b/docs/build-system.md @@ -7190,6 +7190,159 @@ must match exactly. Existing package execution, timeout, interruption, transaction, persistence, byte-identity, and cleanup owners continue to prove the mechanisms this slice does not alter. +### 11.34 Implemented final `FAIL` for explicit ordinary test failures + +An ordinary `ww test` request with an explicit target now ends its ordered +standard output with exactly one command-owned `FAIL\n` when test setup, build, +or execution fails. The line follows every package result, including successful +packages ordered after an earlier failure. It applies to one or many explicit +directory, recursive, dotted-directory, or raw-file targets; filters and list +mode; and the private execution of a retained test. It does not apply to bare +implicit-current-directory `ww test`, `-c`, `-S`, `ww build`, command-line +usage/shape or output preflight rejection, publication-only failure, +capture-only failure, cleanup-only failure, allocation/systemic coordinator +failure, or later direct execution of a retained binary. + +#### Pinned Go evidence and classification + +The sole authority is official Go 1.26.5 at commit +`c19862e5f8415b4f24b189d065ed739517c548ba`: + +- `runTest` reports setup errors, writes the per-package setup-failure result, + and sets the command exit status + ([`cmd/go/internal/test/test.go`, lines 1010–1061](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/test/test.go#L1010-L1061)). + It then creates a root `go test` action owned by `printExitStatus`, orders all + package print actions, and executes that root + ([lines 1099–1124](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/test/test.go#L1099-L1124)). +- `builderTest` constructs the ordinary build/run/clean/print chain and gives + the run and print boundaries the failure handling needed to reach ordered + output; its compile-only branch instead uses a dependency-sensitive nop + print action + ([lines 1133–1169 and 1185–1366](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/test/test.go#L1133-L1366)). +- `(*runTestActor).Act` turns a dependency build failure into a package test + result and sets exit status 1 + ([lines 1436–1521](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/test/test.go#L1436-L1521)). + Every non-nil execution error, including nonzero exit or abnormal/start + failure, likewise sets the exit status and emits the package failure result + ([lines 1644–1774](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/test/test.go#L1644-L1774)). +- `builderCleanTest` and `builderPrintTest` put cleanup and captured package + output before the root status action + ([lines 2237–2259](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/test/test.go#L2237-L2259)). + `printExitStatus` then prints exactly `FAIL\n` when at least one package + argument was explicit and the global exit status is nonzero + ([lines 2262–2284](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/test/test.go#L2262-L2284)). +- Official `test_status.txt` requires a failing package, a later successful + package, and a final standalone `FAIL\n` + ([lines 3–6](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/testdata/script/test_status.txt#L3-L6)). + Official `test_syntax_error_says_fail.txt` requires `FAIL` for an explicit + test build/setup syntax failure + ([lines 1–13](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/testdata/script/test_syntax_error_says_fail.txt#L1-L13)). +- `testFlags` records explicit file operands in `pkgArgs` + ([`cmd/go/internal/test/testflag.go`, lines 219–290](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/test/testflag.go#L219-L290)); + `PackagesAndErrors` turns those files into the command-line package + ([`cmd/go/internal/load/pkg.go`, lines 2895–2918](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/load/pkg.go#L2895-L2918)), + which `GoFilesPackage` constructs + ([lines 3244–3315](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/load/pkg.go#L3244-L3315)). + +Those source rules and official assertions are behavior directly implemented +or asserted by pinned Go. The single-explicit-package and raw-file cases are +derived from `len(pkgArgs) != 0` and the explicit-file loading path. The bare +implicit exclusion is derived from empty `pkgArgs`. The compile-only and +publication-only exclusions are derived from their uncleared dependency +failure preventing the root actor under the pinned work executor +([`cmd/go/internal/work/exec.go`, lines 134–205](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/work/exec.go#L134-L205)). + +#### Fresh four-axis audit and direct pre-fix measurements + +The bounded audit considered all four permanent axes and selected only this +test-command status gap: + +- **Go-like build:** pinned unresolved-symbol handling and its undeclared-main + case are owned by `(*ErrorReporter).errorUnresolved` + ([`cmd/link/internal/ld/errors.go`, lines 29–65](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/link/internal/ld/errors.go#L29-L65)), + with official assertions in `TestUndefinedRelocErrors` + ([`cmd/link/internal/ld/ld_test.go`, lines 19–45](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/link/internal/ld/ld_test.go#L19-L45)) + and `issue10978/main.go` + ([lines 5–27](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/link/internal/ld/testdata/issue10978/main.go#L5-L27)). + Direct Cstage and WWstage no-main builds both exited 1 with identical linker + diagnostics and no output. This candidate was aligned. +- **Go-like test:** an explicit failing directory made both stages exit 1 with + empty stderr and byte-identical 207-byte stdout (SHA-256 + `3d0d44d5d9c4d4d95446382807ee092a611f19b43dc3013cf8df76f135cb5c46`), + ending at its package failure rather than a standalone marker. A failing then + successful `-j 2` request had byte-identical 384-byte stdout (SHA-256 + `4623567924df78375fcca84ff797b7cb89d06d3fd5382704d8b9ebb79916b9d9`) + ending at the successful `ok` result. A raw-file failure had byte-identical + 121-byte stdout (SHA-256 + `8db356ffc5b3a6d9df3308bfba8301329c5c07ce30c5ccd5cc5ee22a6c43331c`) + ending at harness accounting. Explicit missing-import build failure likewise + lacked the final marker. These directly measured pre-fix WW facts establish + the selected external difference. Bare implicit and `-c` failures already + omitted the marker and were aligned exclusions. +- **Go-like package:** `MultiplePackageError` and package scanning implement + conflicting selected declarations + ([`go/build/build.go`, lines 538–548 and 931–967](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/go/build/build.go#L538-L548)); + `TestMultiplePackageImport` asserts the rule + ([`go/build/build_test.go`, lines 105–133](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/go/build/build_test.go#L105-L133)). + Both WW stages rejected an `alpha`/`beta` directory identically before tools. + This candidate was aligned. +- **Go-like import:** `unusedImports` and `errorUnusedPkg` implement the unused + renamed-import diagnostic + ([`cmd/compile/internal/types2/resolver.go`, lines 706–740](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/compile/internal/types2/resolver.go#L706-L740)); + official `importdecl0` asserts ordinary and renamed forms + ([lines 9–27](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/internal/types/testdata/check/importdecl0/importdecl0a.go#L9-L27)). + After normalizing only PID-bearing scratch roots, both WW stages produced the + same unused-renamed-import diagnostic and no output. This candidate was + aligned. + +The command results in that list are directly measured WW behavior; the linked +source and tests are behavior directly implemented or asserted by pinned Go. +The conclusion that only test presentation changes while build, package, and +import identity remain fixed is behavior derived from the pinned action and +final-status placement. + +#### Ownership, final behavior, and preserved boundaries + +The directory owner is `internal/wwpackage.packagecommand`: its private +explicit-target bit affects only final status, and `pkgemitplan`/`pkgemitgroup` +record attributable build/run failure while preserving canonical result order. +After all package captures, package results, install attempts, and temporary-root +cleanup, the coordinator emits one final line. Attributable setup/load rejection +uses the same status helper. The two public drivers own the raw-file equivalent: +they remember the producer or process result, finish their existing cleanup, +then emit the line. A successful run followed only by install or cleanup failure +does not acquire test-failure status. + +Loading and source selection, canonical package and import identity, graph +nodes, action construction, scheduling, compiler/assembler/linker invocation, +generated main, child argv/environment/cwd/stdin, capture bytes, package +diagnostics, and diagnostic precedence are unchanged. Runtime nonzero exit, +signal, timeout, and executable-start failure keep their existing +classification; only the command status line follows. One command-global bit +is isolated from every product-local capture, so parallel completion order +cannot duplicate or reorder it. If the coordinator itself is interrupted before +final emission no post-termination output is promised; an observed child +interruption is an ordinary run failure. + +Artifact construction and bytes are unchanged. A failing running-retained test +still preserves prior public bytes and creates no new executable; successful +products in a mixed request retain their existing independent install results. +Producer failure, publication rejection, transaction rollback, cleanup, and +residue ownership are unchanged, and the marker creates no file. Physical +directories and the private explicit-target signal do not enter package, +import, graph, action, artifact, symbol, `.wwi`, publication, or persistence +identity. No persisted byte or key changed, so build workdir format remains +`18`, test workdir format remains `19`, and semantic storage format remains `3`. + +The WW-native `explicit_test_failure_has_final_status` observer covers both +stages for single and concurrent failing/succeeding packages, filters, list +mode, raw files, setup/build and runtime failure, success, bare implicit and +compile-only exclusions, running retained rollback, artifact-byte equality, +diagnostic equality, exact cardinality/order, and `.new`/transaction cleanup. +Existing directory execution observers cover signals, timeouts, child cleanup, +and canonical-order behavior with the new final line, while build-mode controls +prove that the other command axis remains silent. + ## 12. Candidate architectures and hard-gate decision Five candidates were developed as coherent systems, not as feature bins. diff --git a/docs/spec.md b/docs/spec.md index 2c8c5fee..1a882c85 100644 --- a/docs/spec.md +++ b/docs/spec.md @@ -790,6 +790,19 @@ linker, and other build failures retain their diagnostic standard-error channel; build-action stdout and stderr remain separate. Parallel products own independent captures and are still emitted in canonical product order. +After those ordered results, an ordinary explicit `ww test` request that records +an attributable setup, build, or execution failure emits exactly one standalone +`FAIL\n` on standard output. The line is command-owned: it follows even a later +successful package result, and a single explicit directory, recursive or dotted +directory, or raw source file is sufficient. Runtime nonzero exit, signal, +timeout, or executable-start failure all set that status. Filters, list mode, +and the private execution of a retained test keep the same rule. Bare implicit +current-directory `ww test`, `-c`, `-S`, build requests, CLI usage/shape or +output preflight, publication-only failure, capture-only failure, cleanup-only +failure, and later direct execution of a retained binary do not emit it. The +marker changes no capture, diagnostic, executable, package/import/action +identity, publication decision, or persistent byte. + The coordinator does not change its own cwd or environment. Parallel products receive independent child environments and each uses its own source directory. Relative ordinary files, `testdata`, and writes resolve there for every diff --git a/docs/test-system-v2.md b/docs/test-system-v2.md index 2efb3ecd..0748a476 100644 --- a/docs/test-system-v2.md +++ b/docs/test-system-v2.md @@ -322,6 +322,18 @@ has a distinct capture, while canonical group-order emission remains byte-stable across `-j` values. Build-plan stdout and stderr stay distinct, and loader/build diagnostics stay on stderr. +After canonical result emission, an ordinary test command with any explicit +directory, recursive, dotted-directory, or raw-file target emits exactly one +final standalone `FAIL\n` when attributable setup, build, or test execution +failed. It follows later successful package output and applies to filters, list +mode, signals, timeouts, executable-start failures, and running retained tests. +The bare implicit-current-directory form, `-c`, `-S`, build requests, CLI +usage/shape or output preflight, publication-only, capture-only, cleanup-only, +and direct retained-binary execution do not receive the command marker. +Directory coordination and both raw-file drivers own only this final +presentation state; test captures, result trailers, diagnostics, scheduling, +rollback, artifact bytes, and persisted action identity remain unchanged. + The physical directory remains distinct from exact dotted package identity and from production, internal, external, recompiled, support, and generated-main action identity. Only product execution uses it. Compiler, assembler, archiver, diff --git a/internal/wwpackage/package.ww b/internal/wwpackage/package.ww index 3d6665a7..4699bb54 100644 --- a/internal/wwpackage/package.ww +++ b/internal/wwpackage/package.ww @@ -372,6 +372,17 @@ fn pkgputln(fd: i32, s: str) void = { pkgput(fd, "\n"); }; +// Go's final test-status action is command-owned: after an explicit ordinary +// package request records a setup, build, or run failure, it prints one final +// FAIL after the ordered package results. Implicit current-directory testing +// and compile-only testing deliberately do not use this action. +fn pkgteststatusfail(explicitstatus: bool, compileonly: bool) int = { + if (explicitstatus && !compileonly) { + pkgputln(os.STDOUT_FILENO, "FAIL"); + }; + return 1; +}; + fn pkgputhexbyte(fd: i32, value: u8) void = { let digits: str = "0123456789abcdef"; let encoded: [2]u8; @@ -1704,7 +1715,8 @@ fn pkgemitinstall(g: *pkggroup) bool = { return true; }; -fn pkgemitgroup(g: *pkggroup, compileonly: bool) bool = { +fn pkgemitgroup(g: *pkggroup, compileonly: bool, + statusfailed: *bool) bool = { if (g.notests) { pkgput(os.STDOUT_FILENO, "? "); pkgput(os.STDOUT_FILENO, g.dir); @@ -1728,6 +1740,7 @@ fn pkgemitgroup(g: *pkggroup, compileonly: bool) bool = { pkgput(os.STDOUT_FILENO, "\n"); }; if (!pkgrunok(g)) { + *statusfailed = true; pkgreportcommand(os.STDOUT_FILENO, "test", g, &g.runres); return false; }; @@ -1740,7 +1753,7 @@ fn pkgemitgroup(g: *pkggroup, compileonly: bool) bool = { // A directory build capture is emitted once, followed by its independently // executed products in their existing byte-sorted group order. fn pkgemitplan(p: *pkgplan, groups: []pkggroup, - compileonly: bool, buildonly: bool) i32 = { + compileonly: bool, buildonly: bool, statusfailed: *bool) i32 = { let buildcaptures: bool = pkgemitfile(p.buildout, os.STDOUT_FILENO); buildcaptures = pkgemitfile(p.builderr, os.STDERR_FILENO) && buildcaptures; if (!buildcaptures) { @@ -1755,11 +1768,14 @@ fn pkgemitplan(p: *pkgplan, groups: []pkggroup, pkgreportcommand(os.STDERR_FILENO, "build", &groups[i], &p.buildres); }; + if (!buildonly && !compileonly) { *statusfailed = true; }; failed += 1; } else if (buildonly) { void; } else { - if (!pkgemitgroup(&groups[i], compileonly)) { failed += 1; }; + if (!pkgemitgroup(&groups[i], compileonly, statusfailed)) { + failed += 1; + }; if (!compileonly && !pkgemitinstall(&groups[i])) { failed += 1; }; }; i += 1; @@ -1774,6 +1790,7 @@ export fn packagecommand(args: []str) int = { let jobs: i32 = 1; let afterdash: bool = false; let buildrootseen: bool = false; + let explicitstatus: bool = false; if (args.len == PKG_COUNT_MAX) { pkgputln(os.STDERR_FILENO, "wwtest package: package graph is too large"); @@ -1943,6 +1960,12 @@ export fn packagecommand(args: []str) int = { i += 2; continue; }; + if (strings.compare(a, "--ww-explicit-test-target") == 0) { + if (explicitstatus || buildonly) { pkgusage(); return 2; }; + explicitstatus = true; + i += 1; + continue; + }; if (strings.compare(a, "--ww-operation") == 0) { if (i + 1 >= args.len || strings.compare(args[i + 1], "build") != 0 || buildonly) { @@ -2052,7 +2075,10 @@ export fn packagecommand(args: []str) int = { }; i += 1; }; - if (ds.errors != 0) { return 1; }; + if (ds.errors != 0) { + if (ds.fatal) { return 1; }; + return pkgteststatusfail(explicitstatus, compileonly); + }; pkgsortstrings(directroots.paths); directroots.paths = pkgdedup(directroots.paths); i = 0; @@ -2063,7 +2089,7 @@ export fn packagecommand(args: []str) int = { &canonicaloom)) { if (canonicaloom) { return 1; }; pkgfailpath(ds.paths[i], "cannot canonicalize package directory"); - return 1; + return pkgteststatusfail(explicitstatus, compileonly); }; let canonicalsource: str; if (!pkgstring(&canonicalsource, canonicaldir, "/", @@ -2115,7 +2141,7 @@ export fn packagecommand(args: []str) int = { let pn: str; if (!pkgread(ds.paths[i], &body) || !pkgclause(body, &pn)) { pkgfailpath(ds.paths[i], "invalid or missing package clause"); - return 1; + return pkgteststatusfail(explicitstatus, compileonly); }; if (!pkgstring(&s.pkg, pn)) { return 1; }; append(srcs, s); @@ -2221,7 +2247,8 @@ export fn packagecommand(args: []str) int = { } else { pkgfailpath(srcs[j].path, "test package must match production package or _test"); - return 1; + return pkgteststatusfail(explicitstatus, + compileonly); }; }; j += 1; @@ -2242,7 +2269,8 @@ export fn packagecommand(args: []str) int = { && strings.compare(srcs[j].pkg, secondpkg) != 0) { pkgfailpath(srcs[j].path, "test package must match production package or _test"); - return 1; + return pkgteststatusfail(explicitstatus, + compileonly); }; }; j += 1; @@ -2253,7 +2281,8 @@ export fn packagecommand(args: []str) int = { if (family.len == 0) { pkgfailpath(f.path, "external test package has empty base name"); - return 1; + return pkgteststatusfail(explicitstatus, + compileonly); }; hasexternal = true; externalpkg = firstpkg; @@ -2284,7 +2313,7 @@ export fn packagecommand(args: []str) int = { } else { pkgfailpath(f.path, "test package must match production package or _test"); - return 1; + return pkgteststatusfail(explicitstatus, compileonly); }; }; }; @@ -2511,6 +2540,7 @@ export fn packagecommand(args: []str) int = { let planout: str = outname; if (buildnull) { planout = ""; }; let failed: i32 = 0; + let statusfailed: bool = false; i = 0; for (i < plans.len) { if (!pkgsetplanpaths(&plans[i], groups, tmproot, i, @@ -2675,7 +2705,8 @@ export fn packagecommand(args: []str) int = { i = 0; for (i < plans.len) { - failed += pkgemitplan(&plans[i], groups, compileonly, buildonly); + failed += pkgemitplan(&plans[i], groups, compileonly, buildonly, + &statusfailed); i += 1; }; if (!pkgremoveall(tmproot)) { @@ -2683,6 +2714,9 @@ export fn packagecommand(args: []str) int = { pkgputln(os.STDERR_FILENO, tmproot); failed += 1; }; + if (statusfailed && explicitstatus && !compileonly) { + pkgputln(os.STDOUT_FILENO, "FAIL"); + }; if (failed != 0) { return 1; }; return 0; }; diff --git a/selfhost/cmd/ww/main.ww b/selfhost/cmd/ww/main.ww index aca243d4..eb352b0b 100644 --- a/selfhost/cmd/ww/main.ww +++ b/selfhost/cmd/ww/main.ww @@ -273,11 +273,11 @@ fn execpackagetests(selfdir: *u8, argv: **u8, argc: i32, start: i32, if (prog == nil) { return 1; }; }; - if (start < 0 || start > argc || argc - start > SEP_COUNT_MAX - 10) { + if (start < 0 || start > argc || argc - start > SEP_COUNT_MAX - 11) { sepfailsize(); return 1; }; - let cap: i32 = argc - start + 10; + let cap: i32 = argc - start + 11; let execargv: []*u8; let argvallocation: ([]*u8 | nomem) = sepallocptrs(cap); match (argvallocation) { @@ -300,6 +300,9 @@ fn execpackagetests(selfdir: *u8, argv: **u8, argc: i32, start: i32, }; execargv[n] = "--ww-driver".ptr; n += 1; execargv[n] = selfpath; n += 1; + if (!buildonly && !adddot) { + execargv[n] = "--ww-explicit-test-target".ptr; n += 1; + }; if (rootidentity != nil) { execargv[n] = "--ww-root-identity".ptr; n += 1; execargv[n] = rootidentity; n += 1; @@ -9243,6 +9246,9 @@ fn runsingletest(selfdir: *u8, src: *u8, incs: *u8, compileonly: i32, cerr("ww: cannot remove temporary directory\n"); }; }; + if (compileonly == 0 && emitasm == 0) { + os.write(1, "FAIL\n".ptr, 5u64); + }; return 1; }; if (compileonly != 0 || emitasm != 0) { @@ -9292,6 +9298,7 @@ fn runsingletest(selfdir: *u8, src: *u8, incs: *u8, compileonly: i32, if (toolbin != nil) { os.free(toolbin: *void, cstrlen(toolbin) + 1u64); }; + let testfailed: bool = rc != 0; if (rc == 0 && deferredinstall && sepinstalltestoutput(outp, outstem) != 0) { rc = 1; }; if (owntmp) { @@ -9305,6 +9312,7 @@ fn runsingletest(selfdir: *u8, src: *u8, incs: *u8, compileonly: i32, if (rc == 0) { rc = 1; }; }; }; + if (testfailed) { os.write(1, "FAIL\n".ptr, 5u64); }; return rc; }; @@ -9804,6 +9812,9 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { if (resolved == nil) { cerr("ww test: cannot find "); os.write(2, target, cstrlen(target)); cerr("\n"); + if (targetindex >= 0 && compileonly == 0 && emitasm == 0) { + os.write(1, "FAIL\n".ptr, 5u64); + }; return 1; }; }; diff --git a/test/package/package_test.ww b/test/package/package_test.ww index 5cb226ba..03060e12 100644 --- a/test/package/package_test.ww +++ b/test/package/package_test.ww @@ -1159,6 +1159,7 @@ fn cwdwritedata(dir: str, label: str) void = { assert(outc.stderr.len == 0); assert(has(outc.stdout, strings.concat("FAIL ", p7, " [p7] (test exit 1)\n"))); + assert(strings.hassuffix(outc.stdout, "\nFAIL\n")); let timeoutc: []str = [driver("ww"), "test", "-timeout-ms=50", "-I", tree, "-I", failtree, p8]; @@ -1180,6 +1181,7 @@ fn cwdwritedata(dir: str, label: str) void = { assert(outc.stderr.len == 0); assert(has(outc.stdout, strings.concat("FAIL ", p8, " [p8] (test exit 1)\n"))); + assert(strings.hassuffix(outc.stdout, "\nFAIL\n")); let signalc: []str = [driver("ww"), "test", "-I", tree, "-I", failtree, p9]; @@ -1199,6 +1201,7 @@ fn cwdwritedata(dir: str, label: str) void = { assert(outc.stderr.len == 0); assert(has(outc.stdout, strings.concat("FAIL ", p9, " [p9] (test exit 1)\n"))); + assert(strings.hassuffix(outc.stdout, "\nFAIL\n")); // Source-class rejection precedes every captured build or test child, so a // nonempty caller input cannot create a partial persistent generation. @@ -1216,7 +1219,7 @@ fn cwdwritedata(dir: str, label: str) void = { stdinpath, (60i64 * (time.second: i64)): time.duration, &outw); expectexit(&outc, 1); expectexit(&outw, 1); - assert(outc.stdout.len == 0 && outw.stdout.len == 0); + assert(same(outc.stdout, "FAIL\n") && same(outw.stdout, "FAIL\n")); assert(same(outc.stderr, outw.stderr)); assert(has(outc.stderr, "test package must match production package or _test\n")); @@ -1360,6 +1363,7 @@ fn cwdwritedata(dir: str, label: str) void = { (90i64 * (time.second: i64)): time.duration, &outc); expectexit(&outc, 1); cwdassertpackage(outc.stdout, "p7-fail", p7, "p7-data", "p7-testdata"); + assert(strings.hassuffix(outc.stdout, "\nFAIL\n")); assert(same(retainedbytes, readfile(retained))); runcommandinputenvdir(root, "path-retain-fail-ww", retainfailw, wenv, caller, stdinpath, @@ -1735,6 +1739,7 @@ fn cwdwritedata(dir: str, label: str) void = { (90i64 * (time.second: i64)): time.duration, &out); expectexit(&out, 1); assert(out.stderr.len == 0); + assert(strings.hassuffix(out.stdout, "\nFAIL\n")); assert(has(out.stdout, "partial-success\nok ")); assert(!has(out.stdout, "partial-successok ")); assert(has(out.stdout, "partial-failure\nFAIL ")); @@ -1792,6 +1797,133 @@ fn cwdwritedata(dir: str, label: str) void = { clean(root); }; +@test fn explicit_test_failure_has_final_status() void = { + let root: str = fresh(); + let fail: str = strings.concat(root, "/fail"); + let pass: str = strings.concat(root, "/pass"); + let setup: str = strings.concat(root, "/setup"); + mkdirall(fail); mkdirall(pass); mkdirall(setup); + let failfile: str = strings.concat(fail, "/fail_test.ww"); + writefile(failfile, strings.concat( + "package final_fail;\n", + "@test fn fails() void = { assert(false); };\n")); + writefile(strings.concat(pass, "/pass_test.ww"), strings.concat( + "package final_pass;\n", + "@test fn passes() void = { assert(true); };\n")); + writefile(strings.concat(setup, "/bad_test.ww"), strings.concat( + "package final_setup;\nimport nowhere;\n", + "@test fn unreachable() void = { void; };\n")); + + let stages: []str = ["ww", "ww_ww"]; + let tags: []str = ["c", "ww"]; + let multiout: []str = ["", ""]; + let multierr: []str = ["", ""]; + let rawout: []str = ["", ""]; + let setuperr: []str = ["", ""]; + let out: commandout; + let i: i32 = 0; + for (i < stages.len) { + let multiav: []str = [driver(stages[i]), "test", "-j", "2", + fail, pass]; + runcommand(root, strings.concat("final-multi-", tags[i]), multiav, + (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(out.stderr.len == 0); + assert(strings.hassuffix(out.stdout, "\nFAIL\n")); + assert(occurrences(out.stdout, "\nFAIL\n") == 1); + assert(has(out.stdout, strings.concat("ok ", pass, + " [final_pass]\nFAIL\n"))); + multiout[i] = strings.dup(out.stdout); + multierr[i] = strings.dup(out.stderr); + + let filterav: []str = [driver(stages[i]), "test", "-run", "fails", + fail]; + runcommand(root, strings.concat("final-filter-", tags[i]), filterav, + (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(out.stderr.len == 0); + assert(strings.hassuffix(out.stdout, "\nFAIL\n")); + + let listav: []str = [driver(stages[i]), "test", "-list", setup]; + runcommand(root, strings.concat("final-list-", tags[i]), listav, + (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(same(out.stdout, "FAIL\n")); + assert(has(out.stderr, "cannot find package nowhere\n")); + + let rawav: []str = [driver(stages[i]), "test", failfile]; + runcommand(root, strings.concat("final-raw-", tags[i]), rawav, + (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(out.stderr.len == 0); + assert(strings.hassuffix(out.stdout, "\nFAIL\n")); + assert(!has(out.stdout, strings.concat("FAIL ", fail, " ["))); + rawout[i] = strings.dup(out.stdout); + + let setupav: []str = [driver(stages[i]), "test", setup]; + runcommand(root, strings.concat("final-setup-", tags[i]), setupav, + (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(same(out.stdout, "FAIL\n")); + assert(has(out.stderr, "cannot find package nowhere\n")); + assert(has(out.stderr, strings.concat("FAIL ", setup, + " [final_setup] (build exit 1)\n"))); + setuperr[i] = strings.dup(out.stderr); + + let missing: str = strings.concat(root, "/missing"); + let missingav: []str = [driver(stages[i]), "test", missing]; + runcommand(root, strings.concat("final-missing-", tags[i]), missingav, + (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(same(out.stdout, "FAIL\n")); + assert(same(out.stderr, strings.concat("ww test: cannot find ", + missing, "\n"))); + + let implicitav: []str = [driver(stages[i]), "test"]; + runcommanddir(root, strings.concat("final-implicit-", tags[i]), fail, + implicitav, (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(out.stderr.len == 0); + assert(!strings.hassuffix(out.stdout, "\nFAIL\n")); + + let compileav: []str = [driver(stages[i]), "test", "-c", setup]; + runcommand(root, strings.concat("final-compile-", tags[i]), compileav, + (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(out.stdout.len == 0); + assert(has(out.stderr, "cannot find package nowhere\n")); + + let successav: []str = [driver(stages[i]), "test", pass]; + runcommand(root, strings.concat("final-success-", tags[i]), successav, + (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stderr.len == 0); + assert(!strings.hassuffix(out.stdout, "\nFAIL\n")); + + let retained: str = strings.concat(root, "/retained-", tags[i], ".test"); + writefile(retained, "prior-test-binary\n"); + let retainav: []str = [driver(stages[i]), "test", "-o", retained, + fail]; + runcommand(root, strings.concat("final-retained-", tags[i]), retainav, + (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(out.stderr.len == 0); + assert(strings.hassuffix(out.stdout, "\nFAIL\n")); + assert(same(readfile(retained), "prior-test-binary\n")); + assert(!os.exists(strings.concat(retained, ".new"))); + assert(!directoryhasfragment(root, ".wwtxn.")); + i += 1; + }; + assert(same(multiout[0], multiout[1])); + assert(same(multierr[0], multierr[1])); + assert(same(rawout[0], rawout[1])); + assert(same(setuperr[0], setuperr[1])); + assert(same(readfile(strings.concat(root, "/retained-c.test")), + readfile(strings.concat(root, "/retained-ww.test")))); + assert(!directoryhasnew(root)); + clean(root); +}; + @test fn imported_dependency_tests_do_not_leak() void = { let root: str = fresh(); let base: str = packagepath("dependency"); @@ -4967,7 +5099,7 @@ fn cwdwritedata(dir: str, label: str) void = { runcommand(root, strings.concat("multidir-failure-", stages[i]), av, (120i64 * (time.second: i64)): time.duration, &out); expectexit(&out, 1); - assert(out.stdout.len == 0); + assert(same(out.stdout, "FAIL\n")); assert(has(out.stderr, "has no exported declaration 'hidden'")); assert(has(out.stderr, strings.concat("FAIL ", alpha, " [alpha] (build exit 1)\n"))); @@ -5033,7 +5165,7 @@ fn cwdwritedata(dir: str, label: str) void = { runcommand(root, strings.concat("isolated-external-", tags[i]), externalav, (120i64 * (time.second: i64)): time.duration, &out); expectexit(&out, 1); - assert(out.stdout.len == 0); + assert(same(out.stdout, "FAIL\n")); assert(has(out.stderr, "has no exported declaration 'hidden'")); assert(has(out.stderr, @@ -5049,7 +5181,7 @@ fn cwdwritedata(dir: str, label: str) void = { runcommand(root, strings.concat("isolated-same-", tags[i]), sameav, (120i64 * (time.second: i64)): time.duration, &out); expectexit(&out, 1); - assert(out.stdout.len == 0); + assert(same(out.stdout, "FAIL\n")); assert(has(out.stderr, "undefined: missing")); assert(has(out.stderr, strings.concat("FAIL ", badsame, " [badsame] (build exit 1)\n"))); @@ -8168,12 +8300,15 @@ fn cwdwritedata(dir: str, label: str) void = { (120i64 * (time.second: i64)): time.duration, &out); expectexit(&out, 1); let foldfullwant: str = foldwants[fi]; + let foldstdoutwant: str = ""; if (same(foldmodes[fi], "test")) { foldfullwant = strings.concat(foldwants[fi], "FAIL ", foldtargets[fi], " [", foldpackages[fi], "] (build exit 1)\n"); + foldstdoutwant = "FAIL\n"; }; - assert(out.stdout.len == 0 && same(out.stderr, foldfullwant)); + assert(same(out.stdout, foldstdoutwant) + && same(out.stderr, foldfullwant)); assert(readfile(compilertrace).len == 0); assert(readfile(assemblertrace).len == 0); assert(readfile(linkertrace).len == 0);