@hominis/fireforge 0.40.0 → 0.42.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +85 -0
- package/README.md +16 -10
- package/dist/bin/fireforge.js +25 -4
- package/dist/build-info.json +9 -0
- package/dist/src/cli.js +20 -3
- package/dist/src/commands/bootstrap.js +5 -1
- package/dist/src/commands/discard.js +2 -22
- package/dist/src/commands/doctor/post-rebase-audit.js +27 -4
- package/dist/src/commands/doctor-check-core.d.ts +20 -0
- package/dist/src/commands/doctor-check-core.js +28 -0
- package/dist/src/commands/doctor-external-toolchains.js +2 -0
- package/dist/src/commands/doctor-furnace-manifest-sync.js +144 -46
- package/dist/src/commands/doctor-furnace.js +13 -11
- package/dist/src/commands/doctor-orphaned-harness.js +2 -1
- package/dist/src/commands/doctor.js +2 -2
- package/dist/src/commands/export-all.js +9 -1
- package/dist/src/commands/export-flow.js +13 -3
- package/dist/src/commands/export-placement-conflicts.d.ts +17 -0
- package/dist/src/commands/export-placement-conflicts.js +74 -0
- package/dist/src/commands/export-placement-gate.js +1 -1
- package/dist/src/commands/export-shared.d.ts +8 -1
- package/dist/src/commands/export-shared.js +16 -7
- package/dist/src/commands/export.js +2 -2
- package/dist/src/commands/furnace/apply.js +4 -3
- package/dist/src/commands/furnace/chrome-doc-remove.js +13 -2
- package/dist/src/commands/furnace/chrome-doc.js +13 -2
- package/dist/src/commands/furnace/create-browser-test.d.ts +5 -0
- package/dist/src/commands/furnace/create-browser-test.js +5 -0
- package/dist/src/commands/furnace/create.js +2 -2
- package/dist/src/commands/furnace/diff.js +2 -1
- package/dist/src/commands/furnace/init.js +4 -6
- package/dist/src/commands/furnace/override.js +2 -2
- package/dist/src/commands/furnace/remove-mutations.d.ts +40 -0
- package/dist/src/commands/furnace/remove-mutations.js +171 -0
- package/dist/src/commands/furnace/remove.js +21 -128
- package/dist/src/commands/furnace/rename-browser-test.js +54 -5
- package/dist/src/commands/furnace/scan.js +27 -36
- package/dist/src/commands/furnace/status.js +30 -28
- package/dist/src/commands/furnace/validate.js +25 -18
- package/dist/src/commands/import.js +6 -20
- package/dist/src/commands/lint-per-patch.js +12 -59
- package/dist/src/commands/lint-per-run-checkjs.d.ts +64 -0
- package/dist/src/commands/lint-per-run-checkjs.js +102 -0
- package/dist/src/commands/lint.js +4 -4
- package/dist/src/commands/manifest.js +1 -1
- package/dist/src/commands/package.js +9 -16
- package/dist/src/commands/patch/compact.js +5 -5
- package/dist/src/commands/patch/delete.js +5 -5
- package/dist/src/commands/patch/lint-ignore.js +3 -2
- package/dist/src/commands/patch/move-files-create.js +22 -4
- package/dist/src/commands/patch/move-files-into.js +14 -10
- package/dist/src/commands/patch/move-files.js +6 -5
- package/dist/src/commands/patch/rename.d.ts +2 -2
- package/dist/src/commands/patch/rename.js +151 -50
- package/dist/src/commands/patch/reorder.d.ts +9 -0
- package/dist/src/commands/patch/reorder.js +14 -10
- package/dist/src/commands/patch/split-plan.d.ts +13 -6
- package/dist/src/commands/patch/split-plan.js +22 -12
- package/dist/src/commands/patch/split.d.ts +1 -1
- package/dist/src/commands/patch/split.js +23 -9
- package/dist/src/commands/patch/staged-dependency-validate.d.ts +15 -0
- package/dist/src/commands/patch/staged-dependency-validate.js +49 -0
- package/dist/src/commands/patch/staged-dependency.js +10 -0
- package/dist/src/commands/re-export-adjacent.js +5 -6
- package/dist/src/commands/re-export-bulk-scan.d.ts +13 -0
- package/dist/src/commands/re-export-bulk-scan.js +142 -2
- package/dist/src/commands/re-export-drift.d.ts +74 -0
- package/dist/src/commands/re-export-drift.js +234 -0
- package/dist/src/commands/re-export-files.js +5 -3
- package/dist/src/commands/re-export-lint.d.ts +97 -0
- package/dist/src/commands/re-export-lint.js +210 -0
- package/dist/src/commands/re-export-options.js +8 -0
- package/dist/src/commands/re-export-register.d.ts +18 -0
- package/dist/src/commands/re-export-register.js +61 -0
- package/dist/src/commands/re-export-scan.js +13 -7
- package/dist/src/commands/re-export-single.d.ts +20 -0
- package/dist/src/commands/re-export-single.js +261 -0
- package/dist/src/commands/re-export.d.ts +0 -4
- package/dist/src/commands/re-export.js +113 -268
- package/dist/src/commands/rebase/abort.js +13 -4
- package/dist/src/commands/rebase/continue.js +11 -4
- package/dist/src/commands/rebase/index.js +19 -29
- package/dist/src/commands/rebase/patch-loop.js +12 -0
- package/dist/src/commands/reset.js +4 -23
- package/dist/src/commands/resolve.js +2 -9
- package/dist/src/commands/run.js +11 -21
- package/dist/src/commands/source.js +3 -9
- package/dist/src/commands/status-check.d.ts +13 -0
- package/dist/src/commands/status-check.js +31 -13
- package/dist/src/commands/status-json.d.ts +31 -0
- package/dist/src/commands/status-json.js +76 -0
- package/dist/src/commands/status-output.d.ts +1 -0
- package/dist/src/commands/status-output.js +10 -1
- package/dist/src/commands/status-ownership.d.ts +52 -0
- package/dist/src/commands/status-ownership.js +57 -0
- package/dist/src/commands/status.js +184 -175
- package/dist/src/commands/test-build-phase.d.ts +19 -0
- package/dist/src/commands/test-build-phase.js +168 -0
- package/dist/src/commands/test-diagnose.js +30 -2
- package/dist/src/commands/test-modes.d.ts +9 -1
- package/dist/src/commands/test-modes.js +46 -13
- package/dist/src/commands/test-register.js +59 -27
- package/dist/src/commands/test-run.d.ts +29 -3
- package/dist/src/commands/test-run.js +41 -4
- package/dist/src/commands/test-verdict.d.ts +32 -0
- package/dist/src/commands/test-verdict.js +48 -0
- package/dist/src/commands/test.d.ts +9 -0
- package/dist/src/commands/test.js +100 -123
- package/dist/src/commands/token-coverage.js +4 -9
- package/dist/src/commands/tree.d.ts +3 -0
- package/dist/src/commands/tree.js +171 -25
- package/dist/src/commands/verify.js +2 -2
- package/dist/src/commands/watch.js +22 -21
- package/dist/src/commands/wire.js +1 -1
- package/dist/src/core/ast-utils.d.ts +11 -0
- package/dist/src/core/ast-utils.js +11 -0
- package/dist/src/core/branding.d.ts +16 -0
- package/dist/src/core/branding.js +18 -2
- package/dist/src/core/build-audit-platform.js +6 -1
- package/dist/src/core/build-audit-registration.js +2 -0
- package/dist/src/core/build-audit-resolve.d.ts +13 -0
- package/dist/src/core/build-audit-resolve.js +34 -13
- package/dist/src/core/build-audit.js +4 -20
- package/dist/src/core/build-baseline-types.d.ts +29 -8
- package/dist/src/core/build-baseline-types.js +2 -1
- package/dist/src/core/build-baseline.d.ts +10 -2
- package/dist/src/core/build-baseline.js +39 -8
- package/dist/src/core/build-prepare.d.ts +4 -0
- package/dist/src/core/build-prepare.js +7 -1
- package/dist/src/core/config-paths.d.ts +1 -1
- package/dist/src/core/config-paths.js +2 -0
- package/dist/src/core/config-validate-test-toolchains.js +3 -2
- package/dist/src/core/config-validate.js +34 -36
- package/dist/src/core/config.js +16 -3
- package/dist/src/core/coverage-extend.d.ts +89 -0
- package/dist/src/core/coverage-extend.js +188 -0
- package/dist/src/core/destructive.js +3 -1
- package/dist/src/core/discard-baseline.js +18 -4
- package/dist/src/core/engine-precondition.d.ts +35 -0
- package/dist/src/core/engine-precondition.js +50 -0
- package/dist/src/core/engine-session-lock.d.ts +38 -1
- package/dist/src/core/engine-session-lock.js +78 -4
- package/dist/src/core/file-lock.d.ts +7 -0
- package/dist/src/core/file-lock.js +20 -30
- package/dist/src/core/firefox.js +5 -2
- package/dist/src/core/furnace-apply-helpers.d.ts +12 -1
- package/dist/src/core/furnace-apply-helpers.js +12 -3
- package/dist/src/core/furnace-apply-output.js +9 -0
- package/dist/src/core/furnace-apply-overwrite-warn.d.ts +63 -0
- package/dist/src/core/furnace-apply-overwrite-warn.js +123 -0
- package/dist/src/core/furnace-apply-undeploy-actions.d.ts +11 -0
- package/dist/src/core/furnace-apply-undeploy-actions.js +49 -0
- package/dist/src/core/furnace-apply.d.ts +16 -4
- package/dist/src/core/furnace-apply.js +39 -43
- package/dist/src/core/furnace-config.js +2 -0
- package/dist/src/core/furnace-css-fragments.js +2 -0
- package/dist/src/core/furnace-jsconfig.js +14 -7
- package/dist/src/core/furnace-registration-ast.js +8 -8
- package/dist/src/core/furnace-registration-remove.js +6 -3
- package/dist/src/core/furnace-registration.js +9 -12
- package/dist/src/core/furnace-rollback.js +1 -1
- package/dist/src/core/furnace-scanner.js +2 -1
- package/dist/src/core/furnace-stale-export.js +2 -1
- package/dist/src/core/furnace-state-persist.d.ts +4 -6
- package/dist/src/core/furnace-validate-compatibility.js +4 -7
- package/dist/src/core/furnace-validate-helpers.js +33 -1
- package/dist/src/core/furnace-validate-registration.js +16 -0
- package/dist/src/core/furnace-validate.js +4 -2
- package/dist/src/core/git-diff.js +37 -2
- package/dist/src/core/lint-cache.d.ts +1 -1
- package/dist/src/core/lint-cache.js +33 -10
- package/dist/src/core/mach-build-artifacts.d.ts +63 -1
- package/dist/src/core/mach-build-artifacts.js +108 -2
- package/dist/src/core/mach-mozconfig.js +9 -1
- package/dist/src/core/mach-resource-shim.js +1 -0
- package/dist/src/core/marionette-port.d.ts +16 -0
- package/dist/src/core/marionette-port.js +93 -8
- package/dist/src/core/marionette-preflight.js +6 -5
- package/dist/src/core/patch-apply.d.ts +20 -13
- package/dist/src/core/patch-apply.js +32 -33
- package/dist/src/core/patch-export.js +3 -5
- package/dist/src/core/patch-lint-checkjs.d.ts +25 -3
- package/dist/src/core/patch-lint-checkjs.js +78 -7
- package/dist/src/core/patch-lint-chrome-jsdoc.js +9 -0
- package/dist/src/core/patch-lint-creators.d.ts +30 -0
- package/dist/src/core/patch-lint-creators.js +45 -0
- package/dist/src/core/patch-lint-cross.d.ts +1 -17
- package/dist/src/core/patch-lint-cross.js +16 -32
- package/dist/src/core/patch-lint-jsdoc.d.ts +3 -1
- package/dist/src/core/patch-lint-jsdoc.js +15 -5
- package/dist/src/core/patch-lint-module-registration.d.ts +15 -0
- package/dist/src/core/patch-lint-module-registration.js +88 -0
- package/dist/src/core/patch-lint-mozbuild.js +99 -11
- package/dist/src/core/patch-lint-reexports.d.ts +1 -0
- package/dist/src/core/patch-lint-reexports.js +1 -0
- package/dist/src/core/patch-lint.d.ts +8 -0
- package/dist/src/core/patch-lint.js +84 -6
- package/dist/src/core/patch-lock.d.ts +12 -1
- package/dist/src/core/patch-lock.js +40 -4
- package/dist/src/core/patch-manifest-consistency.js +15 -3
- package/dist/src/core/patch-parse.d.ts +8 -0
- package/dist/src/core/patch-parse.js +10 -0
- package/dist/src/core/patch-policy.d.ts +7 -0
- package/dist/src/core/patch-policy.js +4 -1
- package/dist/src/core/patch-transform.d.ts +10 -0
- package/dist/src/core/patch-transform.js +13 -2
- package/dist/src/core/rebase-session.d.ts +44 -2
- package/dist/src/core/rebase-session.js +114 -17
- package/dist/src/core/register-browser-content.js +2 -1
- package/dist/src/core/register-shared-css.js +2 -1
- package/dist/src/core/smoke-patterns.d.ts +0 -8
- package/dist/src/core/smoke-patterns.js +3 -2
- package/dist/src/core/status-binary.d.ts +55 -0
- package/dist/src/core/status-binary.js +124 -0
- package/dist/src/core/status-classify.d.ts +6 -1
- package/dist/src/core/status-classify.js +101 -73
- package/dist/src/core/test-harness-crash.d.ts +32 -1
- package/dist/src/core/test-harness-crash.js +59 -4
- package/dist/src/core/test-path-scope.js +2 -0
- package/dist/src/core/test-stale-check.js +1 -20
- package/dist/src/core/toolchain-preflight.js +24 -26
- package/dist/src/core/tree-guard.d.ts +1 -1
- package/dist/src/core/tree-guard.js +67 -12
- package/dist/src/core/tree-store.d.ts +86 -10
- package/dist/src/core/tree-store.js +310 -72
- package/dist/src/core/typecheck.js +2 -1
- package/dist/src/core/wire-destroy.js +14 -3
- package/dist/src/core/wire-init.js +37 -40
- package/dist/src/core/wire-subscript.js +11 -3
- package/dist/src/core/wire-utils.js +4 -4
- package/dist/src/core/xpcshell-appdir.js +3 -0
- package/dist/src/errors/base.d.ts +13 -0
- package/dist/src/errors/base.js +13 -0
- package/dist/src/errors/rebase.d.ts +13 -0
- package/dist/src/errors/rebase.js +17 -0
- package/dist/src/types/commands/options.d.ts +86 -1
- package/dist/src/types/commands/patches.d.ts +8 -0
- package/dist/src/types/furnace.d.ts +7 -0
- package/dist/src/utils/build-info.d.ts +31 -0
- package/dist/src/utils/build-info.js +98 -0
- package/dist/src/utils/concurrency.d.ts +8 -0
- package/dist/src/utils/concurrency.js +21 -0
- package/dist/src/utils/errors.d.ts +39 -0
- package/dist/src/utils/errors.js +55 -0
- package/dist/src/utils/fs.js +5 -12
- package/dist/src/utils/logger.d.ts +14 -2
- package/dist/src/utils/logger.js +43 -13
- package/dist/src/utils/options.d.ts +21 -4
- package/dist/src/utils/options.js +26 -6
- package/dist/src/utils/package-root.js +7 -4
- package/dist/src/utils/process-group.js +13 -2
- package/dist/src/utils/stdio-drain.d.ts +28 -0
- package/dist/src/utils/stdio-drain.js +54 -0
- package/dist/src/utils/validation.d.ts +23 -2
- package/dist/src/utils/validation.js +29 -3
- package/package.json +9 -9
|
@@ -11,8 +11,11 @@ import { buildGreenSummaryRejectedMessage, buildHarnessCrashMessage, buildNoTest
|
|
|
11
11
|
import { buildHarnessEarlyExitMessage, classifyHarnessEarlyExit, completePostRebuildFailureContext, prependPostRebuildFailureContext, } from '../core/test-harness-output.js';
|
|
12
12
|
import { GeneralError } from '../errors/base.js';
|
|
13
13
|
import { BuildError } from '../errors/build.js';
|
|
14
|
+
import { toError } from '../utils/errors.js';
|
|
14
15
|
import { info } from '../utils/logger.js';
|
|
15
16
|
import { getPlatform } from '../utils/platform.js';
|
|
17
|
+
import { escapeRegex } from '../utils/regex.js';
|
|
18
|
+
import { emitHarnessVerdict } from './test-verdict.js';
|
|
16
19
|
function buildUnknownTestMessage(testPaths) {
|
|
17
20
|
return (`mach could not discover the requested test path${testPaths.length === 1 ? '' : 's'}: ${testPaths.join(', ')}\n\n` +
|
|
18
21
|
'The file may exist, but Firefox does not currently resolve it as a runnable test.\n\n' +
|
|
@@ -51,7 +54,15 @@ function hasStaleBuildArtifactsSignal(output) {
|
|
|
51
54
|
* failures surface the right diagnosis.
|
|
52
55
|
*/
|
|
53
56
|
function hasForkModuleSignal(output, binaryName) {
|
|
54
|
-
|
|
57
|
+
// `binaryName` comes from fireforge.json, and config validation
|
|
58
|
+
// (`config-validate.ts:59-70`) bars only `..`, path separators, NUL and
|
|
59
|
+
// absolute paths — regex metacharacters get through. The inline escape
|
|
60
|
+
// this replaced wrote its character class as `[.*+?^${}()|[\\]\\\\]`,
|
|
61
|
+
// which closes early after the escaped backslash, so it escaped nothing:
|
|
62
|
+
// `my.browser` matched `myXbrowser`, and an unbalanced `(` or `[` threw a
|
|
63
|
+
// SyntaxError out of the failure-diagnosis path, replacing the real test
|
|
64
|
+
// failure with an opaque regex error.
|
|
65
|
+
const pattern = new RegExp(`Failed to load resource:\\/\\/\\/modules\\/${escapeRegex(binaryName)}\\/`, 'i');
|
|
55
66
|
return pattern.test(output);
|
|
56
67
|
}
|
|
57
68
|
function buildForkModuleMessage(binaryName) {
|
|
@@ -115,7 +126,12 @@ function buildMochitestHttp3ServerMessage() {
|
|
|
115
126
|
'This is an upstream Firefox harness interaction; FireForge can only diagnose it.');
|
|
116
127
|
}
|
|
117
128
|
/**
|
|
129
|
+
* Maps a non-zero mach exit to the most specific actionable diagnosis the
|
|
130
|
+
* captured output supports, throwing it as a domain error.
|
|
118
131
|
*
|
|
132
|
+
* The branch order is load-bearing: narrower signals are checked before
|
|
133
|
+
* broader ones so, for example, a fork-owned module failure is not reported as
|
|
134
|
+
* a stale build (2026-04-21 eval Finding #14).
|
|
119
135
|
*/
|
|
120
136
|
function handleNonZeroTestExit(result, normalizedPaths, appdirInjectionAttempted, binaryName, postRebuildContext) {
|
|
121
137
|
if (result.exitCode === 0 || result.exitCode === 130)
|
|
@@ -181,6 +197,18 @@ function handleNonZeroTestExit(result, normalizedPaths, appdirInjectionAttempted
|
|
|
181
197
|
* the regular non-zero-exit diagnosis chain.
|
|
182
198
|
*/
|
|
183
199
|
export function finalizeSingleRunOutcome(outcome, normalizedPaths, binaryName, postRebuildContext, headless = false) {
|
|
200
|
+
// The FIREFORGE-VERDICT line is the LAST stdout write of the run, on the
|
|
201
|
+
// pass return and every throw path alike — written raw (not via clack,
|
|
202
|
+
// whose renderer drops output under non-TTY capture) so harness-verdict
|
|
203
|
+
// consumers grep one stable line instead of mach internals.
|
|
204
|
+
try {
|
|
205
|
+
applySingleRunOutcome(outcome, normalizedPaths, binaryName, postRebuildContext, headless);
|
|
206
|
+
}
|
|
207
|
+
finally {
|
|
208
|
+
emitHarnessVerdict(outcome.verdict);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
function applySingleRunOutcome(outcome, normalizedPaths, binaryName, postRebuildContext, headless) {
|
|
184
212
|
if (outcome.verdict.kind === 'harness-crash' && outcome.verdict.signature) {
|
|
185
213
|
const base = buildHarnessCrashMessage(outcome.verdict.signature, outcome.attempts);
|
|
186
214
|
const hint = headedNoOutputTimeoutHint(outcome.verdict.signature, {
|
|
@@ -243,7 +271,7 @@ export function diagnoseShardOutcome(outcome, path, binaryName, postRebuildConte
|
|
|
243
271
|
return undefined;
|
|
244
272
|
}
|
|
245
273
|
catch (error) {
|
|
246
|
-
const diagnosis = error
|
|
274
|
+
const diagnosis = toError(error).message;
|
|
247
275
|
const blocks = formatFailureBlocks(outcome.verdict.realFailureBlocks);
|
|
248
276
|
return blocks !== undefined ? `${blocks}\n${diagnosis}` : diagnosis;
|
|
249
277
|
}
|
|
@@ -29,8 +29,16 @@ export interface HarnessClassification {
|
|
|
29
29
|
* nonXpcshell paths) gates the Marionette preflight and the mochitest
|
|
30
30
|
* client flags (FORGE F10); pathless runs keep the full-suite behavior.
|
|
31
31
|
*/
|
|
32
|
-
export declare function classifyBeforeDispatch(engineDir: string, normalizedPaths: string[]
|
|
32
|
+
export declare function classifyBeforeDispatch(engineDir: string, normalizedPaths: string[], options?: {
|
|
33
|
+
allowMixed?: boolean;
|
|
34
|
+
}): Promise<{
|
|
33
35
|
classification: HarnessClassification;
|
|
34
36
|
xpcshellOnly: boolean;
|
|
35
37
|
}>;
|
|
38
|
+
/**
|
|
39
|
+
* Prints the `--build-only` completion guidance: the union build is
|
|
40
|
+
* recorded as the packaging-coverage claim, so each harness half can now
|
|
41
|
+
* run build-less without tripping the stale/coverage gate.
|
|
42
|
+
*/
|
|
43
|
+
export declare function reportBuildOnlyCompletion(classification: HarnessClassification, normalizedPaths: readonly string[]): void;
|
|
36
44
|
export {};
|
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
import { loadConfig } from '../core/config.js';
|
|
3
3
|
import { findNearestXpcshellManifest } from '../core/xpcshell-appdir.js';
|
|
4
4
|
import { GeneralError } from '../errors/base.js';
|
|
5
|
-
import { success } from '../utils/logger.js';
|
|
5
|
+
import { info, success } from '../utils/logger.js';
|
|
6
|
+
import { emitHarnessVerdict } from './test-verdict.js';
|
|
6
7
|
/** Rejects pathless `fireforge test` unless the operator selected a pathless mode. */
|
|
7
8
|
export function assertPathlessTestMode(testPaths, options) {
|
|
8
9
|
if (testPaths.length === 0 &&
|
|
9
10
|
options.auto !== true &&
|
|
10
11
|
options.doctor !== true &&
|
|
12
|
+
options.buildOnly !== true &&
|
|
11
13
|
options.canary === undefined) {
|
|
12
14
|
throw new GeneralError(buildPathlessTestMessage());
|
|
13
15
|
}
|
|
@@ -24,6 +26,10 @@ export function assertTestModeCombinations(testPaths, options, canaryPath) {
|
|
|
24
26
|
throw new GeneralError('No test canary path is configured.\n\n' +
|
|
25
27
|
'Pass `fireforge test --canary <path>` or set `test.canaryPath` in fireforge.json.');
|
|
26
28
|
}
|
|
29
|
+
if (options.buildOnly === true &&
|
|
30
|
+
(options.doctor === true || options.canary !== undefined || options.auto === true)) {
|
|
31
|
+
throw new GeneralError('`fireforge test --build-only` builds and exits without dispatching tests; it cannot be combined with --doctor, --canary, or --auto.');
|
|
32
|
+
}
|
|
27
33
|
}
|
|
28
34
|
/** Resolves the canary path from CLI or config. */
|
|
29
35
|
export function resolveCanaryPath(options, projectConfig) {
|
|
@@ -39,18 +45,25 @@ export function canaryTimeoutSeconds(projectConfig) {
|
|
|
39
45
|
}
|
|
40
46
|
/** Emits or throws the one-word canary verdict. */
|
|
41
47
|
export function reportCanaryOutcome(outcome) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
// Written raw as the run's last stdout line on green and throw paths
|
|
49
|
+
// alike — see the FIREFORGE-VERDICT contract in test-harness-crash.ts.
|
|
50
|
+
try {
|
|
51
|
+
if (outcome.verdict.kind === 'tests-ran-ok') {
|
|
52
|
+
success('Canary: green');
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (outcome.verdict.kind === 'harness-crash' &&
|
|
56
|
+
outcome.verdict.signature?.reason.includes('no-output timeout')) {
|
|
57
|
+
throw new GeneralError(`Canary: hang\n\n${outcome.verdict.signature.line}`);
|
|
58
|
+
}
|
|
59
|
+
if (outcome.verdict.kind === 'harness-crash') {
|
|
60
|
+
throw new GeneralError(`Canary: crash\n\n${outcome.verdict.signature ? outcome.verdict.signature.line : 'Harness crashed before the canary completed.'}`);
|
|
61
|
+
}
|
|
62
|
+
throw new GeneralError('Canary: crash\n\nThe canary did not complete green; see mach output above.');
|
|
49
63
|
}
|
|
50
|
-
|
|
51
|
-
|
|
64
|
+
finally {
|
|
65
|
+
emitHarnessVerdict(outcome.verdict);
|
|
52
66
|
}
|
|
53
|
-
throw new GeneralError('Canary: crash\n\nThe canary did not complete green; see mach output above.');
|
|
54
67
|
}
|
|
55
68
|
function buildPathlessTestMessage() {
|
|
56
69
|
return ('`fireforge test` now requires an explicit test path unless you choose a pathless mode.\n\n' +
|
|
@@ -91,13 +104,33 @@ function buildMixedHarnessMessage(classification) {
|
|
|
91
104
|
* nonXpcshell paths) gates the Marionette preflight and the mochitest
|
|
92
105
|
* client flags (FORGE F10); pathless runs keep the full-suite behavior.
|
|
93
106
|
*/
|
|
94
|
-
export async function classifyBeforeDispatch(engineDir, normalizedPaths) {
|
|
107
|
+
export async function classifyBeforeDispatch(engineDir, normalizedPaths, options) {
|
|
95
108
|
const classification = await classifyTestHarnesses(engineDir, normalizedPaths);
|
|
96
109
|
if (classification.xpcshell.length > 0 && classification.nonXpcshell.length > 0) {
|
|
97
|
-
|
|
110
|
+
// `--build-only` never dispatches, so a mixed request is legal there —
|
|
111
|
+
// packaging the union is the whole point (FORGE J9).
|
|
112
|
+
if (options?.allowMixed !== true) {
|
|
113
|
+
throw new GeneralError(buildMixedHarnessMessage(classification));
|
|
114
|
+
}
|
|
98
115
|
}
|
|
99
116
|
return {
|
|
100
117
|
classification,
|
|
101
118
|
xpcshellOnly: normalizedPaths.length > 0 && classification.nonXpcshell.length === 0,
|
|
102
119
|
};
|
|
103
120
|
}
|
|
121
|
+
/**
|
|
122
|
+
* Prints the `--build-only` completion guidance: the union build is
|
|
123
|
+
* recorded as the packaging-coverage claim, so each harness half can now
|
|
124
|
+
* run build-less without tripping the stale/coverage gate.
|
|
125
|
+
*/
|
|
126
|
+
export function reportBuildOnlyCompletion(classification, normalizedPaths) {
|
|
127
|
+
info('Build complete. The recorded packaging coverage includes every requested path.');
|
|
128
|
+
if (classification.xpcshell.length > 0 && classification.nonXpcshell.length > 0) {
|
|
129
|
+
info('Run each harness separately without --build:');
|
|
130
|
+
info(` fireforge test ${classification.xpcshell.join(' ')}`);
|
|
131
|
+
info(` fireforge test ${classification.nonXpcshell.join(' ')}`);
|
|
132
|
+
}
|
|
133
|
+
else if (normalizedPaths.length > 0) {
|
|
134
|
+
info(`Run: fireforge test ${normalizedPaths.join(' ')}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -1,10 +1,49 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
2
|
import { Command } from 'commander';
|
|
3
3
|
import { withEngineSessionLock } from '../core/engine-session-lock.js';
|
|
4
|
-
import { GeneralError } from '../errors/base.js';
|
|
5
|
-
import { addWaitLockOption, commanderArgParser, pickDefined, resolveWaitLockSeconds, } from '../utils/options.js';
|
|
4
|
+
import { GeneralError, LockContentionError } from '../errors/base.js';
|
|
5
|
+
import { addWaitLockOption, commanderArgParser, pickDefined, resolveWaitLockSeconds, stringListOption, } from '../utils/options.js';
|
|
6
6
|
import { testCommand } from './test.js';
|
|
7
7
|
import { DEFAULT_HARNESS_RETRIES } from './test-run.js';
|
|
8
|
+
import { emitFailVerdict, resetVerdictEmission, verdictEmitted } from './test-verdict.js';
|
|
9
|
+
const TEST_HELP_TEXT = [
|
|
10
|
+
'',
|
|
11
|
+
'[paths...] semantics: a directory argument selects EXACTLY that',
|
|
12
|
+
'directory. FireForge enumerates the test files of exactly that',
|
|
13
|
+
'directory and passes the explicit file list to mach, because mach',
|
|
14
|
+
'resolves test paths by string prefix and a bare directory name',
|
|
15
|
+
'silently sweeps in sibling directories sharing the prefix (e.g.',
|
|
16
|
+
'foo also running foo-extras) — an explicit file list cannot',
|
|
17
|
+
'prefix-match anything. The directory still runs as ONE mach',
|
|
18
|
+
'invocation (one browser instance), so cross-file state carries',
|
|
19
|
+
'within it. When prefix-named siblings exist, the excluded',
|
|
20
|
+
'directories are listed with their test-file counts; pass them as',
|
|
21
|
+
'separate paths to include them.',
|
|
22
|
+
'',
|
|
23
|
+
'Multiple path arguments shard into sequential isolated harness',
|
|
24
|
+
'runs (one browser instance per argument) by default, which does',
|
|
25
|
+
'not exercise cross-argument state; --no-shard restores the',
|
|
26
|
+
'combined single-instance invocation.',
|
|
27
|
+
'',
|
|
28
|
+
'Machine-readable verdict: every run (single, sharded, --canary,',
|
|
29
|
+
'--doctor) ends with exactly one raw stdout line',
|
|
30
|
+
' FIREFORGE-VERDICT: PASS|FAIL [reason=crash|test-failures|',
|
|
31
|
+
' no-tests|preflight|inconclusive|lock-timeout] [checks=<n>]',
|
|
32
|
+
' [unexpected=<n>] [shards=<p>/<t>]',
|
|
33
|
+
'The status follows the harness classifier, not the raw exit code:',
|
|
34
|
+
'a crash-classified run says FAIL reason=crash even at exit 0, and',
|
|
35
|
+
'a green-summary-override pass says PASS despite a non-zero mach',
|
|
36
|
+
'exit. reason=preflight covers any failure before the harness ran',
|
|
37
|
+
'(missing/stale build, invalid paths, port conflicts, config',
|
|
38
|
+
'errors); reason=inconclusive means engine/ changed while the',
|
|
39
|
+
'tests ran, so the harness result cannot be trusted;',
|
|
40
|
+
'reason=lock-timeout means the run never started because another',
|
|
41
|
+
'engine-mutating command held the lock past the wait budget. A sharded',
|
|
42
|
+
'aggregate reason is the most structural failing shard (crash,',
|
|
43
|
+
'then no-tests, then test-failures). Count keys are omitted when',
|
|
44
|
+
'the embedded summary did not print them. Key on this line',
|
|
45
|
+
'instead of mach internals.',
|
|
46
|
+
].join('\n');
|
|
8
47
|
/** Registers the test command on the CLI program. */
|
|
9
48
|
export function registerTest(program, { getProjectRoot, withErrorHandling }) {
|
|
10
49
|
const test = program
|
|
@@ -12,16 +51,15 @@ export function registerTest(program, { getProjectRoot, withErrorHandling }) {
|
|
|
12
51
|
.description('Run tests via mach test')
|
|
13
52
|
.option('--headless', 'Run tests in headless mode')
|
|
14
53
|
.option('--build', 'Run incremental UI build before testing')
|
|
54
|
+
.option('--build-only', 'Run the scoped pre-test build (packaging exactly [paths...], mixed harnesses allowed) and exit without dispatching tests — one build covers both harness halves, then run each half without --build')
|
|
55
|
+
.option('--extend-coverage', 'With --build/--build-only and explicit paths, union those paths into the recorded test-packaging coverage instead of replacing it, so an earlier scoped build stays covered. Refused when the build anchor moved (engine HEAD, mozconfig, or a previously fingerprinted packageable file changed).')
|
|
15
56
|
.option('--auto', 'Forward mach test --auto. Valid only when no explicit paths are provided.')
|
|
16
57
|
.option('--allow-stale-build', 'Allow tests to run even when packageable engine files changed since the last successful FireForge build')
|
|
17
58
|
.option('--allow-stale-components', 'Run tests despite components.conf changes that only a full "fireforge build" compiles in (the packaged child process will resolve the OLD StaticComponents table)')
|
|
18
|
-
.option('--kill-stale-marionette', 'Terminate a recognized stale browser
|
|
59
|
+
.option('--kill-stale-marionette', 'Terminate a recognized stale browser from this objdir or one holding the Marionette port before running tests')
|
|
19
60
|
.option('--canary [path]', 'Run one short browser-chrome harness canary. Uses test.canaryPath from fireforge.json when no path is supplied.')
|
|
20
61
|
.option('--doctor', 'Run a marionette handshake preflight before tests (exit 1 on FAIL). With no paths, runs the preflight only.')
|
|
21
|
-
.option('--mach-arg <arg>', 'Forward this argument verbatim to `mach test` (repeatable). Escape valve for upstream xpcshell/mochitest flags FireForge does not model.', (
|
|
22
|
-
acc.push(value);
|
|
23
|
-
return acc;
|
|
24
|
-
}, [])
|
|
62
|
+
.option('--mach-arg <arg>', 'Forward this argument verbatim to `mach test` (repeatable). Escape valve for upstream xpcshell/mochitest flags FireForge does not model.', ...stringListOption())
|
|
25
63
|
.option('--harness-retries <n>', `Retry budget for recognized harness crashes (resource-monitor tracebacks, pre-test hangs, post-green shutdown re-entry). 0 disables retries. Default: ${String(DEFAULT_HARNESS_RETRIES)}.`, commanderArgParser((raw) => {
|
|
26
64
|
const n = Number.parseInt(raw, 10);
|
|
27
65
|
if (!Number.isFinite(n) || n < 0 || n > 10) {
|
|
@@ -39,27 +77,21 @@ export function registerTest(program, { getProjectRoot, withErrorHandling }) {
|
|
|
39
77
|
}
|
|
40
78
|
return n;
|
|
41
79
|
}))
|
|
42
|
-
.addHelpText('after',
|
|
43
|
-
'',
|
|
44
|
-
'[paths...] semantics: a directory argument selects EXACTLY that',
|
|
45
|
-
'directory. FireForge enumerates the test files of exactly that',
|
|
46
|
-
'directory and passes the explicit file list to mach, because mach',
|
|
47
|
-
'resolves test paths by string prefix and a bare directory name',
|
|
48
|
-
'silently sweeps in sibling directories sharing the prefix (e.g.',
|
|
49
|
-
'foo also running foo-extras) — an explicit file list cannot',
|
|
50
|
-
'prefix-match anything. The directory still runs as ONE mach',
|
|
51
|
-
'invocation (one browser instance), so cross-file state carries',
|
|
52
|
-
'within it. When prefix-named siblings exist, the excluded',
|
|
53
|
-
'directories are listed with their test-file counts; pass them as',
|
|
54
|
-
'separate paths to include them.',
|
|
55
|
-
'',
|
|
56
|
-
'Multiple path arguments shard into sequential isolated harness',
|
|
57
|
-
'runs (one browser instance per argument) by default, which does',
|
|
58
|
-
'not exercise cross-argument state; --no-shard restores the',
|
|
59
|
-
'combined single-instance invocation.',
|
|
60
|
-
].join('\n'));
|
|
80
|
+
.addHelpText('after', TEST_HELP_TEXT);
|
|
61
81
|
addWaitLockOption(test).action(withErrorHandling(async (paths, options) => {
|
|
62
82
|
const projectRoot = getProjectRoot();
|
|
63
|
-
|
|
83
|
+
// The engine session lock is acquired outside testCommand's own
|
|
84
|
+
// exactly-one-verdict guarantee, so a lock failure must emit here or
|
|
85
|
+
// the run ends with no FIREFORGE-VERDICT line at all.
|
|
86
|
+
resetVerdictEmission();
|
|
87
|
+
try {
|
|
88
|
+
await withEngineSessionLock(projectRoot, 'test', () => testCommand(projectRoot, paths, pickDefined(options)), { waitLockSeconds: resolveWaitLockSeconds(options.waitLock) });
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
if (!verdictEmitted()) {
|
|
92
|
+
emitFailVerdict(error instanceof LockContentionError ? 'lock-timeout' : 'preflight');
|
|
93
|
+
}
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
64
96
|
}));
|
|
65
97
|
}
|
|
@@ -74,13 +74,39 @@ export interface ShardOutcome {
|
|
|
74
74
|
label: string;
|
|
75
75
|
outcome: TestRunOutcome;
|
|
76
76
|
}
|
|
77
|
+
/** Aggregate result of a sharded run, verdict emission deferred to the caller. */
|
|
78
|
+
export interface ShardedRunSummary {
|
|
79
|
+
shards: ShardOutcome[];
|
|
80
|
+
passed: number;
|
|
81
|
+
total: number;
|
|
82
|
+
/** Aggregate classifier verdict — see {@link deriveAggregateShardVerdict}. */
|
|
83
|
+
aggregate: HarnessRunVerdict;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Derives the aggregate classification for a sharded run: all-green is
|
|
87
|
+
* `tests-ran-ok`; otherwise the most structural failure wins — a crashed
|
|
88
|
+
* shard makes the whole run `harness-crash`, then `no-tests`, then plain
|
|
89
|
+
* `test-failures` — so the aggregate `reason=` keeps the classifier-driven
|
|
90
|
+
* contract instead of collapsing every failure to `test-failures`.
|
|
91
|
+
*/
|
|
92
|
+
export declare function deriveAggregateShardVerdict(shards: ShardOutcome[]): HarnessRunVerdict;
|
|
77
93
|
/**
|
|
78
94
|
* Runs each requested path argument as its own sequential harness
|
|
79
95
|
* invocation (a directory argument's enumerated files stay together in
|
|
80
96
|
* one invocation — see {@link ShardGroup}) and prints an aggregate
|
|
81
97
|
* report. Per-shard failures are diagnosed via `diagnoseShardFailure`
|
|
82
98
|
* (which receives the throwing diagnosis chain from the command layer)
|
|
83
|
-
* but downgraded to warnings so every shard runs
|
|
84
|
-
*
|
|
99
|
+
* but downgraded to warnings so every shard runs. The aggregate verdict
|
|
100
|
+
* line and the aggregate error are NOT produced here — the caller runs
|
|
101
|
+
* the engine-generation integrity check first and then calls
|
|
102
|
+
* {@link finalizeShardedOutcome}, so a run invalidated by a concurrent
|
|
103
|
+
* `engine/` mutation can never print `PASS` before the check fails.
|
|
104
|
+
*/
|
|
105
|
+
export declare function runShardedTests(ctx: TestRunContext, groups: ShardGroup[], diagnoseShardFailure: (outcome: TestRunOutcome, label: string) => string | undefined): Promise<ShardedRunSummary>;
|
|
106
|
+
/**
|
|
107
|
+
* Emits the aggregate FIREFORGE-VERDICT line for a sharded run, then
|
|
108
|
+
* throws the aggregate error when any shard did not pass — the same
|
|
109
|
+
* emit-then-throw shape as `finalizeSingleRunOutcome`. Called only after
|
|
110
|
+
* the engine-generation check succeeded.
|
|
85
111
|
*/
|
|
86
|
-
export declare function
|
|
112
|
+
export declare function finalizeShardedOutcome(summary: ShardedRunSummary): void;
|
|
@@ -19,6 +19,7 @@ import { BuildError } from '../errors/build.js';
|
|
|
19
19
|
import { info, note, warn } from '../utils/logger.js';
|
|
20
20
|
import { getPlatform } from '../utils/platform.js';
|
|
21
21
|
import { maybeInjectAppdirArg } from './test-appdir.js';
|
|
22
|
+
import { emitHarnessVerdict } from './test-verdict.js';
|
|
22
23
|
/** Default bounded retry budget for recognized harness crashes. */
|
|
23
24
|
export const DEFAULT_HARNESS_RETRIES = 2;
|
|
24
25
|
/** Resolves the capturing mach dispatcher for a suite. */
|
|
@@ -78,14 +79,34 @@ const SHARD_STATUS_LABEL = {
|
|
|
78
79
|
'harness-crash': 'CRASH',
|
|
79
80
|
'no-tests': 'NO-TESTS',
|
|
80
81
|
};
|
|
82
|
+
/**
|
|
83
|
+
* Derives the aggregate classification for a sharded run: all-green is
|
|
84
|
+
* `tests-ran-ok`; otherwise the most structural failure wins — a crashed
|
|
85
|
+
* shard makes the whole run `harness-crash`, then `no-tests`, then plain
|
|
86
|
+
* `test-failures` — so the aggregate `reason=` keeps the classifier-driven
|
|
87
|
+
* contract instead of collapsing every failure to `test-failures`.
|
|
88
|
+
*/
|
|
89
|
+
export function deriveAggregateShardVerdict(shards) {
|
|
90
|
+
const kinds = new Set(shards.map(({ outcome }) => outcome.verdict.kind));
|
|
91
|
+
if (kinds.has('harness-crash'))
|
|
92
|
+
return { kind: 'harness-crash' };
|
|
93
|
+
if (kinds.has('no-tests'))
|
|
94
|
+
return { kind: 'no-tests' };
|
|
95
|
+
if (kinds.has('test-failures'))
|
|
96
|
+
return { kind: 'test-failures' };
|
|
97
|
+
return { kind: 'tests-ran-ok' };
|
|
98
|
+
}
|
|
81
99
|
/**
|
|
82
100
|
* Runs each requested path argument as its own sequential harness
|
|
83
101
|
* invocation (a directory argument's enumerated files stay together in
|
|
84
102
|
* one invocation — see {@link ShardGroup}) and prints an aggregate
|
|
85
103
|
* report. Per-shard failures are diagnosed via `diagnoseShardFailure`
|
|
86
104
|
* (which receives the throwing diagnosis chain from the command layer)
|
|
87
|
-
* but downgraded to warnings so every shard runs
|
|
88
|
-
*
|
|
105
|
+
* but downgraded to warnings so every shard runs. The aggregate verdict
|
|
106
|
+
* line and the aggregate error are NOT produced here — the caller runs
|
|
107
|
+
* the engine-generation integrity check first and then calls
|
|
108
|
+
* {@link finalizeShardedOutcome}, so a run invalidated by a concurrent
|
|
109
|
+
* `engine/` mutation can never print `PASS` before the check fails.
|
|
89
110
|
*/
|
|
90
111
|
export async function runShardedTests(ctx, groups, diagnoseShardFailure) {
|
|
91
112
|
const shards = [];
|
|
@@ -111,8 +132,24 @@ export async function runShardedTests(ctx, groups, diagnoseShardFailure) {
|
|
|
111
132
|
(outcome.attempts > 1 ? ` (${outcome.attempts} attempts)` : ''));
|
|
112
133
|
const failing = shards.filter(({ outcome }) => outcome.verdict.kind !== 'tests-ran-ok');
|
|
113
134
|
note(`${lines.join('\n')}\n\n${shards.length - failing.length}/${shards.length} shard(s) passed`, 'Sharded Test Summary');
|
|
114
|
-
|
|
115
|
-
|
|
135
|
+
return {
|
|
136
|
+
shards,
|
|
137
|
+
passed: shards.length - failing.length,
|
|
138
|
+
total: shards.length,
|
|
139
|
+
aggregate: deriveAggregateShardVerdict(shards),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Emits the aggregate FIREFORGE-VERDICT line for a sharded run, then
|
|
144
|
+
* throws the aggregate error when any shard did not pass — the same
|
|
145
|
+
* emit-then-throw shape as `finalizeSingleRunOutcome`. Called only after
|
|
146
|
+
* the engine-generation check succeeded.
|
|
147
|
+
*/
|
|
148
|
+
export function finalizeShardedOutcome(summary) {
|
|
149
|
+
emitHarnessVerdict(summary.aggregate, { passed: summary.passed, total: summary.total });
|
|
150
|
+
if (summary.passed < summary.total) {
|
|
151
|
+
const failing = summary.shards.filter(({ outcome }) => outcome.verdict.kind !== 'tests-ran-ok');
|
|
152
|
+
throw new BuildError(`${failing.length} of ${summary.total} sharded test run(s) did not pass: ` +
|
|
116
153
|
`${failing.map(({ label }) => label).join(', ')}. ` +
|
|
117
154
|
'See the per-shard diagnosis above. Use --no-shard to reproduce the combined single-invocation behaviour.', 'mach test');
|
|
118
155
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { HarnessRunVerdict } from '../core/test-harness-crash.js';
|
|
2
|
+
/**
|
|
3
|
+
* Reason codes the emission layer can put on a `FIREFORGE-VERDICT: FAIL`
|
|
4
|
+
* line. The first three mirror the harness classifier; `preflight` covers
|
|
5
|
+
* every failure before harness classification (missing/stale builds,
|
|
6
|
+
* invalid paths, port conflicts, config errors, spawn failures),
|
|
7
|
+
* `inconclusive` marks a run whose harness result exists but cannot be
|
|
8
|
+
* trusted because `engine/` changed (or became unprobeable one-sidedly)
|
|
9
|
+
* while the tests ran, and `lock-timeout` marks a run that never started
|
|
10
|
+
* because the engine session lock stayed contended past the wait budget.
|
|
11
|
+
*/
|
|
12
|
+
export type FireforgeVerdictReason = 'crash' | 'no-tests' | 'test-failures' | 'preflight' | 'inconclusive' | 'lock-timeout';
|
|
13
|
+
/**
|
|
14
|
+
* Re-arms the verdict sink for a new run. Called at `testCommand` entry so
|
|
15
|
+
* repeated programmatic invocations (and tests) each get their own
|
|
16
|
+
* exactly-one-line guarantee.
|
|
17
|
+
*/
|
|
18
|
+
export declare function resetVerdictEmission(): void;
|
|
19
|
+
/** True once this run has written its `FIREFORGE-VERDICT:` line. */
|
|
20
|
+
export declare function verdictEmitted(): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Emits the classifier-derived verdict for a completed harness run, with
|
|
23
|
+
* the aggregate `shards=<passed>/<total>` suffix for sharded runs.
|
|
24
|
+
*/
|
|
25
|
+
export declare function emitHarnessVerdict(verdict: HarnessRunVerdict, shards?: {
|
|
26
|
+
passed: number;
|
|
27
|
+
total: number;
|
|
28
|
+
}): void;
|
|
29
|
+
/** Emits the pathless `--doctor` PASS verdict. */
|
|
30
|
+
export declare function emitPassVerdict(): void;
|
|
31
|
+
/** Emits a FAIL verdict carrying an emission-layer reason code. */
|
|
32
|
+
export declare function emitFailVerdict(reason: FireforgeVerdictReason): void;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { formatFireforgeVerdictLine } from '../core/test-harness-crash.js';
|
|
2
|
+
import { setStdoutSealed } from '../utils/logger.js';
|
|
3
|
+
let emitted = false;
|
|
4
|
+
/**
|
|
5
|
+
* Re-arms the verdict sink for a new run. Called at `testCommand` entry so
|
|
6
|
+
* repeated programmatic invocations (and tests) each get their own
|
|
7
|
+
* exactly-one-line guarantee.
|
|
8
|
+
*/
|
|
9
|
+
export function resetVerdictEmission() {
|
|
10
|
+
emitted = false;
|
|
11
|
+
setStdoutSealed(false);
|
|
12
|
+
}
|
|
13
|
+
/** True once this run has written its `FIREFORGE-VERDICT:` line. */
|
|
14
|
+
export function verdictEmitted() {
|
|
15
|
+
return emitted;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Writes the run's single verdict line — raw stdout (clack's renderer can
|
|
19
|
+
* drop output under non-TTY capture), first write wins. Every later call
|
|
20
|
+
* is a no-op: the writer closest to the harness result runs first, so the
|
|
21
|
+
* fallback layers (the engine-generation guard, the preflight wrapper)
|
|
22
|
+
* can emit unconditionally without risking a second line. Writing also
|
|
23
|
+
* seals stdout: the verdict must stay the run's LAST stdout write, so all
|
|
24
|
+
* subsequent logger output — including the CLI-boundary error/cancel
|
|
25
|
+
* rendering after an emit-then-throw path — routes to stderr.
|
|
26
|
+
*/
|
|
27
|
+
function writeVerdictLine(line) {
|
|
28
|
+
if (emitted)
|
|
29
|
+
return;
|
|
30
|
+
emitted = true;
|
|
31
|
+
process.stdout.write(`${line}\n`);
|
|
32
|
+
setStdoutSealed(true);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Emits the classifier-derived verdict for a completed harness run, with
|
|
36
|
+
* the aggregate `shards=<passed>/<total>` suffix for sharded runs.
|
|
37
|
+
*/
|
|
38
|
+
export function emitHarnessVerdict(verdict, shards) {
|
|
39
|
+
writeVerdictLine(formatFireforgeVerdictLine(verdict, shards));
|
|
40
|
+
}
|
|
41
|
+
/** Emits the pathless `--doctor` PASS verdict. */
|
|
42
|
+
export function emitPassVerdict() {
|
|
43
|
+
writeVerdictLine('FIREFORGE-VERDICT: PASS');
|
|
44
|
+
}
|
|
45
|
+
/** Emits a FAIL verdict carrying an emission-layer reason code. */
|
|
46
|
+
export function emitFailVerdict(reason) {
|
|
47
|
+
writeVerdictLine(`FIREFORGE-VERDICT: FAIL reason=${reason}`);
|
|
48
|
+
}
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import type { TestOptions } from '../types/commands/index.js';
|
|
2
2
|
/**
|
|
3
3
|
* Runs the test command to execute mach tests.
|
|
4
|
+
*
|
|
5
|
+
* Owns the run's exactly-one-`FIREFORGE-VERDICT:`-line guarantee: the sink
|
|
6
|
+
* is re-armed at entry, and any failure that reaches this boundary without
|
|
7
|
+
* an inner writer having emitted — the preflight ladder (missing engine or
|
|
8
|
+
* build, config errors, stale-build and port gates, missing paths), and a
|
|
9
|
+
* harness process that failed to start — emits `FAIL reason=preflight`,
|
|
10
|
+
* since no harness classification exists for such a run. Writers closer to
|
|
11
|
+
* the harness (doctor, canary, single, sharded, the engine-generation
|
|
12
|
+
* guard) emit first and win.
|
|
4
13
|
* @param projectRoot - Root directory of the project
|
|
5
14
|
* @param testPaths - Test file or directory paths
|
|
6
15
|
* @param options - Test options
|