@hominis/fireforge 0.30.1 → 0.32.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 +36 -0
- package/README.md +22 -0
- package/dist/src/commands/export-all.js +9 -16
- package/dist/src/commands/export-flow.d.ts +6 -0
- package/dist/src/commands/export-flow.js +6 -1
- package/dist/src/commands/export-placement-gate.d.ts +38 -0
- package/dist/src/commands/export-placement-gate.js +105 -0
- package/dist/src/commands/export-shared.d.ts +28 -0
- package/dist/src/commands/export-shared.js +46 -1
- package/dist/src/commands/export.js +52 -113
- package/dist/src/commands/furnace/chrome-doc-templates.d.ts +0 -13
- package/dist/src/commands/furnace/chrome-doc-templates.js +1 -1
- package/dist/src/commands/furnace/create-dry-run.d.ts +1 -1
- package/dist/src/commands/furnace/create.d.ts +1 -2
- package/dist/src/commands/furnace/deploy.js +36 -114
- package/dist/src/commands/furnace/refresh.js +52 -32
- package/dist/src/commands/furnace/sync.js +2 -0
- package/dist/src/commands/import.js +108 -73
- package/dist/src/commands/lint-per-patch.d.ts +3 -1
- package/dist/src/commands/lint-per-patch.js +265 -74
- package/dist/src/commands/lint.d.ts +1 -58
- package/dist/src/commands/lint.js +193 -88
- package/dist/src/commands/patch/compact.d.ts +5 -2
- package/dist/src/commands/patch/compact.js +85 -25
- package/dist/src/commands/patch/delete.js +17 -17
- package/dist/src/commands/patch/index.js +2 -0
- package/dist/src/commands/patch/lint-ignore.js +3 -16
- package/dist/src/commands/patch/move-files.js +2 -0
- package/dist/src/commands/patch/patch-context.d.ts +41 -0
- package/dist/src/commands/patch/patch-context.js +53 -0
- package/dist/src/commands/patch/rename.js +10 -15
- package/dist/src/commands/patch/reorder.d.ts +0 -2
- package/dist/src/commands/patch/reorder.js +18 -19
- package/dist/src/commands/patch/split-plan.d.ts +66 -0
- package/dist/src/commands/patch/split-plan.js +178 -0
- package/dist/src/commands/patch/split.d.ts +30 -0
- package/dist/src/commands/patch/split.js +283 -0
- package/dist/src/commands/patch/staged-dependency.d.ts +1 -7
- package/dist/src/commands/patch/staged-dependency.js +4 -17
- package/dist/src/commands/patch/tier.js +4 -17
- package/dist/src/commands/re-export-files.js +4 -1
- package/dist/src/commands/re-export-scan.js +8 -1
- package/dist/src/commands/re-export.js +8 -1
- package/dist/src/commands/rebase/summary.d.ts +1 -5
- package/dist/src/commands/rebase/summary.js +1 -1
- package/dist/src/commands/status-output.js +77 -68
- package/dist/src/commands/test-diagnose.d.ts +23 -0
- package/dist/src/commands/test-diagnose.js +210 -0
- package/dist/src/commands/test-run.d.ts +68 -0
- package/dist/src/commands/test-run.js +97 -0
- package/dist/src/commands/test.js +214 -263
- package/dist/src/commands/token.js +15 -1
- package/dist/src/commands/wire.js +109 -78
- package/dist/src/core/build-audit.d.ts +1 -1
- package/dist/src/core/build-audit.js +2 -46
- package/dist/src/core/build-baseline-types.d.ts +38 -0
- package/dist/src/core/build-baseline-types.js +10 -0
- package/dist/src/core/build-baseline.d.ts +1 -31
- package/dist/src/core/build-prepare.d.ts +1 -1
- package/dist/src/core/build-prepare.js +2 -45
- package/dist/src/core/config-paths.d.ts +0 -8
- package/dist/src/core/config-paths.js +4 -4
- package/dist/src/core/config-state.d.ts +0 -6
- package/dist/src/core/config-state.js +1 -1
- package/dist/src/core/config-validate-patch-policy.js +12 -13
- package/dist/src/core/config-validate.js +74 -28
- package/dist/src/core/engine-changes.d.ts +24 -0
- package/dist/src/core/engine-changes.js +64 -0
- package/dist/src/core/firefox-cache.d.ts +0 -5
- package/dist/src/core/firefox-cache.js +1 -1
- package/dist/src/core/firefox-download.d.ts +0 -6
- package/dist/src/core/firefox-download.js +1 -1
- package/dist/src/core/furnace-apply-helpers.d.ts +1 -8
- package/dist/src/core/furnace-apply-helpers.js +11 -20
- package/dist/src/core/furnace-apply.d.ts +1 -1
- package/dist/src/core/furnace-apply.js +1 -1
- package/dist/src/core/furnace-checksum-utils.d.ts +7 -0
- package/dist/src/core/furnace-checksum-utils.js +15 -0
- package/dist/src/core/furnace-config-validate.d.ts +31 -0
- package/dist/src/core/furnace-config-validate.js +133 -0
- package/dist/src/core/furnace-config.d.ts +4 -32
- package/dist/src/core/furnace-config.js +15 -111
- package/dist/src/core/furnace-constants.d.ts +0 -10
- package/dist/src/core/furnace-constants.js +2 -2
- package/dist/src/core/furnace-css-fragments.d.ts +79 -0
- package/dist/src/core/furnace-css-fragments.js +243 -0
- package/dist/src/core/furnace-jsconfig.d.ts +63 -0
- package/dist/src/core/furnace-jsconfig.js +191 -0
- package/dist/src/core/furnace-validate-helpers.d.ts +16 -14
- package/dist/src/core/furnace-validate-helpers.js +40 -1
- package/dist/src/core/furnace-validate-registration.js +16 -1
- package/dist/src/core/furnace-validate.js +54 -2
- package/dist/src/core/git-base.d.ts +15 -0
- package/dist/src/core/git-base.js +32 -0
- package/dist/src/core/git-diff.d.ts +8 -0
- package/dist/src/core/git-diff.js +224 -59
- package/dist/src/core/git-file-ops.d.ts +39 -12
- package/dist/src/core/git-file-ops.js +84 -3
- package/dist/src/core/lint-cache.d.ts +0 -13
- package/dist/src/core/lint-cache.js +5 -5
- package/dist/src/core/mach.d.ts +22 -1
- package/dist/src/core/mach.js +27 -2
- package/dist/src/core/manifest-register.d.ts +5 -16
- package/dist/src/core/manifest-register.js +3 -1
- package/dist/src/core/patch-lint-checkjs.d.ts +75 -21
- package/dist/src/core/patch-lint-checkjs.js +263 -71
- package/dist/src/core/patch-lint-css.d.ts +23 -0
- package/dist/src/core/patch-lint-css.js +172 -0
- package/dist/src/core/patch-lint-jsdoc.js +63 -4
- package/dist/src/core/patch-lint-observer.d.ts +37 -0
- package/dist/src/core/patch-lint-observer.js +168 -0
- package/dist/src/core/patch-lint.d.ts +34 -11
- package/dist/src/core/patch-lint.js +24 -161
- package/dist/src/core/patch-manifest-io.d.ts +16 -0
- package/dist/src/core/patch-manifest-io.js +44 -2
- package/dist/src/core/patch-manifest-validate.d.ts +1 -8
- package/dist/src/core/patch-manifest-validate.js +1 -1
- package/dist/src/core/patch-manifest.d.ts +1 -1
- package/dist/src/core/patch-manifest.js +1 -1
- package/dist/src/core/patch-policy.d.ts +0 -4
- package/dist/src/core/patch-policy.js +10 -4
- package/dist/src/core/register-browser-content.d.ts +1 -1
- package/dist/src/core/register-module.d.ts +1 -1
- package/dist/src/core/register-result.d.ts +21 -0
- package/dist/src/core/register-result.js +9 -0
- package/dist/src/core/register-shared-css.d.ts +1 -1
- package/dist/src/core/register-test-manifest.d.ts +1 -1
- package/dist/src/core/test-harness-crash.d.ts +61 -0
- package/dist/src/core/test-harness-crash.js +140 -0
- package/dist/src/core/test-stale-check.d.ts +1 -1
- package/dist/src/core/test-stale-check.js +2 -46
- package/dist/src/core/test-xpcshell-retry.d.ts +9 -2
- package/dist/src/core/test-xpcshell-retry.js +10 -3
- package/dist/src/core/token-dark-mode.js +14 -26
- package/dist/src/core/token-manager.d.ts +4 -0
- package/dist/src/core/token-manager.js +70 -16
- package/dist/src/core/typecheck-shim.d.ts +3 -22
- package/dist/src/core/typecheck-shim.js +69 -7
- package/dist/src/core/wire-utils.js +37 -44
- package/dist/src/types/commands/index.d.ts +1 -1
- package/dist/src/types/commands/options.d.ts +122 -0
- package/dist/src/types/config.d.ts +11 -2
- package/dist/src/types/furnace.d.ts +12 -1
- package/dist/src/utils/elapsed.d.ts +0 -2
- package/dist/src/utils/elapsed.js +1 -1
- package/dist/src/utils/fs.d.ts +0 -5
- package/dist/src/utils/fs.js +1 -1
- package/dist/src/utils/regex.d.ts +0 -6
- package/dist/src/utils/regex.js +3 -3
- package/dist/src/utils/validation.d.ts +0 -8
- package/dist/src/utils/validation.js +2 -2
- package/package.json +6 -4
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
-
import { join } from 'node:path';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
3
|
import { prepareBuildEnvironment } from '../core/build-prepare.js';
|
|
4
4
|
import { getProjectPaths, loadConfig } from '../core/config.js';
|
|
5
|
-
import { buildArtifactMismatchMessage, buildUI, hasBuildArtifacts, hasRunnableBundle,
|
|
5
|
+
import { buildArtifactMismatchMessage, buildUI, hasBuildArtifacts, hasRunnableBundle, withBuildLock, } from '../core/mach.js';
|
|
6
6
|
import { assertMarionettePortAvailable, extractForwardedMarionettePort, forwardedMachArgsIncludeMarionetteClient, shouldAutoForwardMarionettePortToMach, } from '../core/marionette-port.js';
|
|
7
7
|
import { formatMarionettePreflightLine, reportMarionettePreflight, runMarionettePreflight, } from '../core/marionette-preflight.js';
|
|
8
|
-
import {
|
|
8
|
+
import { buildHarnessCrashMessage, detectHarnessCrashSignature, } from '../core/test-harness-crash.js';
|
|
9
|
+
import { createPostRebuildFailureContext } from '../core/test-harness-output.js';
|
|
9
10
|
import { checkStaleBuildForTest, formatStaleBuildWarning } from '../core/test-stale-check.js';
|
|
10
|
-
import { retryAfterXpcshellSymlinkRepair } from '../core/test-xpcshell-retry.js';
|
|
11
11
|
import { findNearestXpcshellManifest } from '../core/xpcshell-appdir.js';
|
|
12
12
|
import { GeneralError } from '../errors/base.js';
|
|
13
13
|
import { AmbiguousBuildArtifactsError, BuildError } from '../errors/build.js';
|
|
@@ -15,7 +15,8 @@ import { pathExists } from '../utils/fs.js';
|
|
|
15
15
|
import { info, intro, outro, spinner, success, warn } from '../utils/logger.js';
|
|
16
16
|
import { pickDefined } from '../utils/options.js';
|
|
17
17
|
import { stripEnginePrefix } from '../utils/paths.js';
|
|
18
|
-
import {
|
|
18
|
+
import { diagnoseShardOutcome, finalizeSingleRunOutcome } from './test-diagnose.js';
|
|
19
|
+
import { DEFAULT_HARNESS_RETRIES, runShardedTests, runTestsWithRetries, } from './test-run.js';
|
|
19
20
|
async function assertTestPathsExist(engineDir, testPaths) {
|
|
20
21
|
const missingPaths = [];
|
|
21
22
|
for (const testPath of testPaths) {
|
|
@@ -29,21 +30,6 @@ async function assertTestPathsExist(engineDir, testPaths) {
|
|
|
29
30
|
throw new GeneralError(`Test path${missingPaths.length === 1 ? '' : 's'} not found under engine/: ${missingPaths.join(', ')}\n\n` +
|
|
30
31
|
'If you expected these files to come from your patch stack, run "fireforge import" first.');
|
|
31
32
|
}
|
|
32
|
-
function buildUnknownTestMessage(testPaths) {
|
|
33
|
-
return (`mach could not discover the requested test path${testPaths.length === 1 ? '' : 's'}: ${testPaths.join(', ')}\n\n` +
|
|
34
|
-
'The file may exist, but Firefox does not currently resolve it as a runnable test.\n\n' +
|
|
35
|
-
'Check the nearest test manifest (for example browser.toml or xpcshell.toml), confirm the file is listed under the correct test type, and make sure each parent moz.build registers that manifest before retrying.');
|
|
36
|
-
}
|
|
37
|
-
function buildStaleBuildMessage(postRebuild) {
|
|
38
|
-
if (postRebuild) {
|
|
39
|
-
return ('Firefox test runtime still reported stale-artifact-shaped resource failures after the rebuild completed.\n\n' +
|
|
40
|
-
'FireForge already ran the requested rebuild before this focused test, so treat the remaining failure as a real runtime, registration, routing, or test-contract regression rather than another stale deployed-artifact-only blocker.\n\n' +
|
|
41
|
-
'Check the first post-rebuild failure above and the raw mach output for the concrete path or module that still fails.');
|
|
42
|
-
}
|
|
43
|
-
return ('Firefox test runtime appears to be using stale build artifacts.\n\n' +
|
|
44
|
-
'The failing output referenced missing branding or distribution resources, which usually means the current obj-* build does not match recent engine or branding changes.\n\n' +
|
|
45
|
-
'Re-run "fireforge build --ui" or "fireforge test --build" and then retry.');
|
|
46
|
-
}
|
|
47
33
|
async function classifyTestHarnesses(engineDir, normalizedPaths) {
|
|
48
34
|
const result = { xpcshell: [], nonXpcshell: [] };
|
|
49
35
|
for (const testPath of normalizedPaths) {
|
|
@@ -57,6 +43,25 @@ async function classifyTestHarnesses(engineDir, normalizedPaths) {
|
|
|
57
43
|
}
|
|
58
44
|
return result;
|
|
59
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Picks the mach dispatch target for a (non-mixed) run. A single-suite run
|
|
48
|
+
* auto-routes to the suite-specific command (`mach xpcshell-test` /
|
|
49
|
+
* `mach mochitest`), which degrades a broken host resource monitor to a
|
|
50
|
+
* warning instead of crashing generic `mach test` at startup (E1). Mixed runs
|
|
51
|
+
* are rejected before this point; a path-less "run all" or an explicit
|
|
52
|
+
* `--generic-mach-test` opt-out stays on the generic command.
|
|
53
|
+
*/
|
|
54
|
+
function resolveTestSuite(classification, forceGeneric) {
|
|
55
|
+
if (forceGeneric)
|
|
56
|
+
return 'generic';
|
|
57
|
+
if (classification.xpcshell.length > 0 && classification.nonXpcshell.length === 0) {
|
|
58
|
+
return 'xpcshell';
|
|
59
|
+
}
|
|
60
|
+
if (classification.nonXpcshell.length > 0 && classification.xpcshell.length === 0) {
|
|
61
|
+
return 'mochitest';
|
|
62
|
+
}
|
|
63
|
+
return 'generic';
|
|
64
|
+
}
|
|
60
65
|
function buildMixedHarnessMessage(classification) {
|
|
61
66
|
return ('FireForge cannot run xpcshell and browser/mochitest paths in the same mach invocation.\n\n' +
|
|
62
67
|
'Split this into separate `fireforge test` commands so each manifest selects its own harness:\n' +
|
|
@@ -81,75 +86,6 @@ function filterRedundantXpcshellFlavorArgs(machArgs, classification) {
|
|
|
81
86
|
}
|
|
82
87
|
return filtered;
|
|
83
88
|
}
|
|
84
|
-
function hasStaleBuildArtifactsSignal(output) {
|
|
85
|
-
// Deliberately narrow: only fire on branding-specific resource paths
|
|
86
|
-
// that are always a stale-artifact symptom. The earlier pattern also
|
|
87
|
-
// matched `resource:///modules/distribution.sys.mjs`, which surfaced on
|
|
88
|
-
// real packaging / module-resolution failures too (e.g. a fork's
|
|
89
|
-
// `MyBrowserStore.sys.mjs` missing from the installed app dir after a
|
|
90
|
-
// successful build). That false-positive pushed operators toward
|
|
91
|
-
// "rebuild" advice for what was actually a module-registration issue.
|
|
92
|
-
return (/chrome:\/\/branding\/locale\/brand\.properties/i.test(output) ||
|
|
93
|
-
/browser\/branding\/[^/\s]+\/moz\.build/i.test(output));
|
|
94
|
-
}
|
|
95
|
-
/**
|
|
96
|
-
* Fork-module-not-registered signal. 2026-04-21 eval Finding #14:
|
|
97
|
-
* a fork's test failed with `Failed to load resource:///modules/mybrowser/
|
|
98
|
-
* MyBrowserStore.sys.mjs`. The branding pattern happened to also match
|
|
99
|
-
* because the test harness printed a branding warning during its
|
|
100
|
-
* teardown, and the stale-build branch won by precedence — telling the
|
|
101
|
-
* operator to rebuild when the real fix is to register the module in
|
|
102
|
-
* the fork's `browser/modules/<binary>/moz.build`. Match a
|
|
103
|
-
* `resource:///modules/<binaryName>/` pattern so fork-owned module
|
|
104
|
-
* failures surface the right diagnosis.
|
|
105
|
-
*/
|
|
106
|
-
function hasForkModuleSignal(output, binaryName) {
|
|
107
|
-
const pattern = new RegExp(`Failed to load resource:\\/\\/\\/modules\\/${binaryName.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\$&')}\\/`, 'i');
|
|
108
|
-
return pattern.test(output);
|
|
109
|
-
}
|
|
110
|
-
function buildForkModuleMessage(binaryName) {
|
|
111
|
-
return (`Test failed to load a fork-owned module at resource:///modules/${binaryName}/*.sys.mjs.\n\n` +
|
|
112
|
-
'This is almost always a module-registration issue, not a stale build. The fork module directory is missing an entry that maps its file into the resource URI tree, so `ChromeUtils.importESModule` cannot resolve it.\n\n' +
|
|
113
|
-
'Check that:\n' +
|
|
114
|
-
` - browser/modules/${binaryName}/moz.build lists the missing module in EXTRA_JS_MODULES.\n` +
|
|
115
|
-
` - browser/modules/moz.build references the ${binaryName}/ subdirectory (DIRS += [...]).\n` +
|
|
116
|
-
' - The last `fireforge build` (or `fireforge build --ui`) completed successfully against the current manifests. If the registration is new, the UI-faster build path may not pick it up — a full build may be required.\n\n' +
|
|
117
|
-
'Use `fireforge register browser/modules/' +
|
|
118
|
-
binaryName +
|
|
119
|
-
'/<file>.sys.mjs` to add the EXTRA_JS_MODULES entry if it is missing.');
|
|
120
|
-
}
|
|
121
|
-
// Detects the broader xpcshell symptom where every `resource:///modules/...`
|
|
122
|
-
// import fails — the signature of xpcshell running with the wrong app-dir on
|
|
123
|
-
// a manifest that sets `firefox-appdir = "browser"`. Checked AFTER the
|
|
124
|
-
// stale-build signal (which matches the narrower `distribution.sys.mjs`
|
|
125
|
-
// path) so the more specific diagnosis wins when both patterns apply.
|
|
126
|
-
function hasXpcshellAppdirSignal(output) {
|
|
127
|
-
return /Failed to load resource:\/\/\/modules\//i.test(output);
|
|
128
|
-
}
|
|
129
|
-
function buildXpcshellAppdirMessage(injectionAttempted) {
|
|
130
|
-
const isMacos = process.platform === 'darwin';
|
|
131
|
-
const macosNote = isMacos
|
|
132
|
-
? 'Detected: macOS host. On macOS the xpcshell harness binds `-a` to `<obj>/dist/<App>.app/Contents/Resources` by default and frequently ignores `--app-path` overrides when the `.app` bundle is present — the surest fix is the `<appname>-appdir` migration below rather than trying to force a different path.\n\n'
|
|
133
|
-
: '';
|
|
134
|
-
const triggerLines = injectionAttempted
|
|
135
|
-
? 'FireForge auto-injected `--app-path=<absolute>` against the resolved obj-dir before mach test ran, but the failure persists. The injected path either does not match the appdir layout your harness expects, or (on macOS) the harness bound `-a` to the `.app/Contents/Resources` default and ignored the override.\n\n'
|
|
136
|
-
: 'Likely triggers:\n' +
|
|
137
|
-
' - The nearest xpcshell.toml sets `firefox-appdir = "browser"` but the harness reads `<appname>-appdir` instead — the literal `firefox-appdir` directive is silently ignored on rebranded forks (appname != "firefox").\n' +
|
|
138
|
-
' - FireForge could not find an xpcshell.toml above the test path, so the auto-injection never ran.\n\n';
|
|
139
|
-
return ('xpcshell failed to load core resource:///modules/*.sys.mjs imports.\n\n' +
|
|
140
|
-
'This is the canonical symptom of xpcshell running with the wrong app directory: the runtime resolves `resource:///modules/` against the parent of the expected app root, so every `ChromeUtils.importESModule("resource:///modules/…")` throws.\n\n' +
|
|
141
|
-
macosNote +
|
|
142
|
-
triggerLines +
|
|
143
|
-
'Options:\n' +
|
|
144
|
-
' - Add `<appname>-appdir = "browser"` alongside `firefox-appdir = "browser"` in the xpcshell.toml [DEFAULT] so the harness reads the appname-keyed value directly. This is the most reliable fix on rebranded macOS builds.\n' +
|
|
145
|
-
' - Pass overrides through `fireforge test <path> --mach-arg="--app-path=<absolute>"` to inject the path verbatim (operator overrides always win over auto-injection, but see the macOS caveat above).\n' +
|
|
146
|
-
' - Remove `firefox-appdir = "browser"` from the xpcshell.toml [DEFAULT] and move browser-chrome dependencies into a browser-chrome mochitest (see `fireforge furnace create --test-style=browser-chrome`).\n' +
|
|
147
|
-
' - If the test only touches toolkit chrome (chrome://global/*), drop the `firefox-appdir` setting entirely — toolkit chrome is registered without it.');
|
|
148
|
-
}
|
|
149
|
-
function buildHarnessSymlinkMessage() {
|
|
150
|
-
return ('mach failed while preparing test harness symlinks before the requested tests ran.\n\n' +
|
|
151
|
-
'This usually means the objdir contains stale harness setup from an earlier run. Re-run with `fireforge test --build` to refresh the harness state, or remove the stale harness symlink in the active obj-* directory before retrying.');
|
|
152
|
-
}
|
|
153
89
|
async function resolveLaunchablePathForTests(engineDir, binaryName, objDir) {
|
|
154
90
|
if (!objDir)
|
|
155
91
|
return undefined;
|
|
@@ -164,16 +100,31 @@ async function resolveLaunchablePathForTests(engineDir, binaryName, objDir) {
|
|
|
164
100
|
}
|
|
165
101
|
return bundleCheck.expectedPath;
|
|
166
102
|
}
|
|
167
|
-
async function runPreTestBuild(projectRoot, paths, projectConfig) {
|
|
103
|
+
async function runPreTestBuild(projectRoot, paths, projectConfig, harnessRetries) {
|
|
168
104
|
await withBuildLock(projectRoot, async () => {
|
|
169
105
|
await prepareBuildEnvironment(projectRoot, paths, projectConfig);
|
|
170
106
|
const s = spinner('Running incremental build...');
|
|
171
|
-
|
|
172
|
-
|
|
107
|
+
// The pre-test build runs through mach too, so the same resource-monitor
|
|
108
|
+
// startup crash that aborts `mach test` can abort `mach build faster`.
|
|
109
|
+
// Run the harness-crash classifier over the build output and retry within
|
|
110
|
+
// the same `--harness-retries` budget rather than hard-failing with a
|
|
111
|
+
// bare "Pre-test build failed" (field report E2).
|
|
112
|
+
const maxAttempts = Math.max(1, harnessRetries + 1);
|
|
113
|
+
for (let attempt = 1;; attempt += 1) {
|
|
114
|
+
const buildResult = await buildUI(paths.engine);
|
|
115
|
+
if (buildResult.exitCode === 0) {
|
|
116
|
+
s.stop('Build complete');
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const signature = detectHarnessCrashSignature(`${buildResult.stdout}\n${buildResult.stderr}`);
|
|
120
|
+
if (signature && attempt < maxAttempts) {
|
|
121
|
+
s.message(`Pre-test build hit a harness crash (${signature.reason}); ` +
|
|
122
|
+
`retrying (attempt ${attempt + 1} of ${maxAttempts})...`);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
173
125
|
s.error('Pre-test build failed');
|
|
174
|
-
throw new BuildError('Pre-test build failed', 'mach build faster');
|
|
126
|
+
throw new BuildError(signature ? buildHarnessCrashMessage(signature, attempt) : 'Pre-test build failed', 'mach build faster');
|
|
175
127
|
}
|
|
176
|
-
s.stop('Build complete');
|
|
177
128
|
});
|
|
178
129
|
}
|
|
179
130
|
function logTestSelection(normalizedPaths) {
|
|
@@ -185,79 +136,116 @@ function logTestSelection(normalizedPaths) {
|
|
|
185
136
|
}
|
|
186
137
|
info('');
|
|
187
138
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
'This is almost always a symptom of `chrome://branding` not registering correctly in your fork — the mochitest harness lazy-initializes `http3Server` only after branding resolves, and a missing branding registration short-circuits setup. The cleanup path then trips the AttributeError, masking the real error.\n\n' +
|
|
199
|
-
'Check that:\n' +
|
|
200
|
-
" - Your fork's branding directory is listed in `browser/branding/moz.build` (or equivalent) and ships a `brand.properties` / `brand.ftl`.\n" +
|
|
201
|
-
' - `chrome://branding/locale/brand.properties` resolves at runtime (try `fireforge run` and inspect the Browser Console).\n' +
|
|
202
|
-
" - The `BROWSER_CHROME_MANIFESTS` entry for your fork's chrome.manifest is registered.\n\n" +
|
|
203
|
-
'This is an upstream Firefox harness interaction; FireForge can only diagnose it.');
|
|
204
|
-
}
|
|
205
|
-
function handleNonZeroTestExit(result, normalizedPaths, appdirInjectionAttempted, binaryName, postRebuildContext) {
|
|
206
|
-
if (result.exitCode === 0 || result.exitCode === 130)
|
|
207
|
-
return;
|
|
208
|
-
const combinedOutput = `${result.stdout}\n${result.stderr}`;
|
|
209
|
-
const failureContext = postRebuildContext
|
|
210
|
-
? completePostRebuildFailureContext(postRebuildContext, combinedOutput)
|
|
211
|
-
: undefined;
|
|
212
|
-
const withContext = (message) => prependPostRebuildFailureContext(message, failureContext);
|
|
213
|
-
const throwGeneral = (message) => {
|
|
214
|
-
throw new GeneralError(withContext(message));
|
|
215
|
-
};
|
|
216
|
-
if (/UNKNOWN TEST\b/i.test(combinedOutput)) {
|
|
217
|
-
throwGeneral(buildUnknownTestMessage(normalizedPaths));
|
|
218
|
-
}
|
|
219
|
-
const earlyExit = classifyHarnessEarlyExit(combinedOutput, normalizedPaths);
|
|
220
|
-
if (earlyExit) {
|
|
221
|
-
throwGeneral(buildHarnessEarlyExitMessage(earlyExit, normalizedPaths));
|
|
222
|
-
}
|
|
223
|
-
// Fork-owned module load failures must beat the branding stale-build
|
|
224
|
-
// branch: 2026-04-21 eval (Finding #14) saw a fork's test fail with
|
|
225
|
-
// `Failed to load resource:///modules/mybrowser/MyBrowserStore.sys.mjs`
|
|
226
|
-
// while the harness teardown printed a branding warning that the old
|
|
227
|
-
// stale-build pattern matched, so the operator was told to rebuild
|
|
228
|
-
// when the real fix is to register the missing module.
|
|
229
|
-
if (hasForkModuleSignal(combinedOutput, binaryName)) {
|
|
230
|
-
throwGeneral(buildForkModuleMessage(binaryName));
|
|
139
|
+
/**
|
|
140
|
+
* Validates the build-artifact preconditions for running tests: rejects
|
|
141
|
+
* ambiguous multi-objdir checkouts, platform-mismatched artifacts, and
|
|
142
|
+
* missing/incomplete builds with the actionable message for each. Returns
|
|
143
|
+
* the successful artifact probe for downstream objdir use.
|
|
144
|
+
*/
|
|
145
|
+
async function assertTestBuildArtifacts(engineDir) {
|
|
146
|
+
const buildCheck = await hasBuildArtifacts(engineDir);
|
|
147
|
+
if (buildCheck.ambiguous && buildCheck.objDirs && buildCheck.objDirs.length > 0) {
|
|
148
|
+
throw new AmbiguousBuildArtifactsError(buildCheck.objDirs);
|
|
231
149
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
// But the stale-build check is now narrower — it no longer matches
|
|
236
|
-
// `resource:///modules/distribution.sys.mjs` alone, which was producing
|
|
237
|
-
// false-positive rebuild advice on fork-custom module-load failures
|
|
238
|
-
// (the eval saw this for `MyBrowserStore.sys.mjs`). Cases that once
|
|
239
|
-
// landed on `distribution.sys.mjs` fall through to xpcshell-appdir,
|
|
240
|
-
// which is the more useful diagnosis in practice for `Failed to load
|
|
241
|
-
// resource:///modules/…`.
|
|
242
|
-
if (hasStaleBuildArtifactsSignal(combinedOutput)) {
|
|
243
|
-
throwGeneral(buildStaleBuildMessage(Boolean(failureContext)));
|
|
150
|
+
const mismatchMessage = buildArtifactMismatchMessage(engineDir, buildCheck, 'Tests');
|
|
151
|
+
if (mismatchMessage) {
|
|
152
|
+
throw new GeneralError(mismatchMessage);
|
|
244
153
|
}
|
|
245
|
-
if (
|
|
246
|
-
|
|
154
|
+
if (!buildCheck.exists) {
|
|
155
|
+
const detail = buildCheck.objDir
|
|
156
|
+
? `Build artifacts incomplete in ${buildCheck.objDir}/`
|
|
157
|
+
: 'No build artifacts found (obj-*/ directory missing)';
|
|
158
|
+
throw new GeneralError(`Tests require a completed build. ${detail}\n\n` +
|
|
159
|
+
"Run 'fireforge build' first, then run 'fireforge test'.");
|
|
247
160
|
}
|
|
248
|
-
|
|
249
|
-
|
|
161
|
+
return buildCheck;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Runs the `--doctor` marionette handshake probe. With no test paths the
|
|
165
|
+
* probe is the entire command (returns `'stop'` after reporting); with
|
|
166
|
+
* paths it gates the mach invocation — a FAIL throws before mach runs.
|
|
167
|
+
*/
|
|
168
|
+
async function runDoctorPreflight(args) {
|
|
169
|
+
const { engineDir, effectivePort, hasTestPaths, objDir, binaryName, launchablePath } = args;
|
|
170
|
+
// Write the "Running marionette preflight..." banner via
|
|
171
|
+
// `process.stdout.write` directly before `info()` so non-TTY captures
|
|
172
|
+
// always see the banner even if clack's renderer defers output in
|
|
173
|
+
// pipe mode. `info()` is still called so TTY users keep the normal
|
|
174
|
+
// clack box-drawing framing.
|
|
175
|
+
process.stdout.write('Running marionette preflight...\n');
|
|
176
|
+
info('Running marionette preflight...');
|
|
177
|
+
const preflight = effectivePort !== undefined
|
|
178
|
+
? await runMarionettePreflight(engineDir, { port: effectivePort })
|
|
179
|
+
: await runMarionettePreflight(engineDir);
|
|
180
|
+
// 2026-04-24 eval Finding 7: the pre-0.18.1 code used
|
|
181
|
+
// `success()` + `outro()` + a direct `process.stdout.write` as a
|
|
182
|
+
// belt-and-suspenders but still reproducibly dropped the PASS summary
|
|
183
|
+
// under non-TTY capture (observed: `tee`-wrapped eval output saw only
|
|
184
|
+
// the intro). The fix writes the authoritative PASS/FAIL line via
|
|
185
|
+
// `process.stdout.write` as the very first output after the probe
|
|
186
|
+
// returns, so the captured stream has an unambiguous summary no
|
|
187
|
+
// matter what clack does on top. The clack-rendered banner
|
|
188
|
+
// (`info`/`warn`) is retained so TTY users keep the visual framing.
|
|
189
|
+
const directLine = formatMarionettePreflightLine(preflight);
|
|
190
|
+
process.stdout.write(`${directLine}\n`);
|
|
191
|
+
process.stdout.write(`Marionette preflight environment: objdir=${objDir ?? '(none)'}; binary=${binaryName}; app=${launchablePath ? `engine/${launchablePath}` : '(unknown)'}; port=${effectivePort ?? 2828}; elapsed=${preflight.durationMs}ms\n`);
|
|
192
|
+
reportMarionettePreflight(preflight);
|
|
193
|
+
if (!hasTestPaths) {
|
|
194
|
+
if (!preflight.ok) {
|
|
195
|
+
throw new GeneralError('Marionette preflight reported FAIL — see output above.');
|
|
196
|
+
}
|
|
197
|
+
success(directLine);
|
|
198
|
+
outro('Test completed');
|
|
199
|
+
return 'stop';
|
|
250
200
|
}
|
|
251
|
-
if (
|
|
252
|
-
|
|
253
|
-
throwGeneral(buildHarnessSymlinkMessage());
|
|
201
|
+
if (!preflight.ok) {
|
|
202
|
+
throw new GeneralError('Marionette preflight reported FAIL — see output above. Aborting before mach test runs.');
|
|
254
203
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
204
|
+
return 'continue';
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Auto-forwards `--marionette-port` to mach (`--setpref=marionette.port`
|
|
208
|
+
* for the listener, `--marionette=127.0.0.1:<n>` for the mochitest
|
|
209
|
+
* client), skipping each piece the operator already forwarded via
|
|
210
|
+
* `--mach-arg` and the xpcshell flavor that ignores the pref entirely.
|
|
211
|
+
* Mutates `extraArgs` in place.
|
|
212
|
+
*/
|
|
213
|
+
function appendMarionetteForwardingArgs(extraArgs, options, forwardedPort) {
|
|
214
|
+
// Auto-forward the Marionette port to mach when `--marionette-port` is
|
|
215
|
+
// set. `--setpref=marionette.port=<n>` configures where the browser
|
|
216
|
+
// listener binds; `--marionette=127.0.0.1:<n>` tells the mochitest harness
|
|
217
|
+
// client to connect there (default client is 127.0.0.1:2828). xpcshell
|
|
218
|
+
// ignores both for browser Marionette.
|
|
219
|
+
//
|
|
220
|
+
// Skip setpref forwarding when the operator already supplied an equivalent
|
|
221
|
+
// arg via `--mach-arg` — duplicates would be confusing without changing
|
|
222
|
+
// semantics. Skip when mach args explicitly request `--flavor=xpcshell`
|
|
223
|
+
// (or `xpcshell-tests`): the preflight still honours `--marionette-port`,
|
|
224
|
+
// but mach does not use the marionette.port pref on that harness. Any
|
|
225
|
+
// other arg shape still forwards so toolkit widget paths and mixed suites
|
|
226
|
+
// stay aligned with the probe without duplicate `--mach-arg` flags.
|
|
227
|
+
//
|
|
228
|
+
// Skip auto `--marionette=...` when `--mach-arg` already includes a client
|
|
229
|
+
// `--marionette=...` (or two-token `--marionette host:port`).
|
|
230
|
+
if (options.marionettePort === undefined)
|
|
231
|
+
return;
|
|
232
|
+
{
|
|
233
|
+
const operatorAlreadyForwarded = forwardedPort !== undefined;
|
|
234
|
+
const machArgs = options.machArg ?? [];
|
|
235
|
+
if (operatorAlreadyForwarded) {
|
|
236
|
+
info(`--marionette-port=${options.marionettePort} set, but the same port is already forwarded via --mach-arg; skipping auto-forward.`);
|
|
237
|
+
}
|
|
238
|
+
else if (shouldAutoForwardMarionettePortToMach(machArgs)) {
|
|
239
|
+
extraArgs.push(`--setpref=marionette.port=${options.marionettePort}`);
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
info(`--marionette-port=${options.marionettePort} applied to the preflight probe, but --flavor=xpcshell is set — mach is not auto-configured with --setpref=marionette.port or --marionette (xpcshell ignores the browser Marionette path). Pass --mach-arg --setpref=marionette.port=${options.marionettePort} explicitly if you still need mach to see the port.`);
|
|
243
|
+
}
|
|
244
|
+
if (shouldAutoForwardMarionettePortToMach(machArgs) &&
|
|
245
|
+
!forwardedMachArgsIncludeMarionetteClient(machArgs)) {
|
|
246
|
+
extraArgs.push(`--marionette=127.0.0.1:${options.marionettePort}`);
|
|
247
|
+
}
|
|
259
248
|
}
|
|
260
|
-
throw new BuildError(withContext(`Tests failed with exit code ${result.exitCode}. Check the output above for details.`), 'mach test');
|
|
261
249
|
}
|
|
262
250
|
/**
|
|
263
251
|
* Runs the test command to execute mach tests.
|
|
@@ -272,22 +260,7 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
|
|
|
272
260
|
if (!(await pathExists(paths.engine))) {
|
|
273
261
|
throw new GeneralError('Firefox source not found. Run "fireforge download" first.');
|
|
274
262
|
}
|
|
275
|
-
|
|
276
|
-
const buildCheck = await hasBuildArtifacts(paths.engine);
|
|
277
|
-
if (buildCheck.ambiguous && buildCheck.objDirs && buildCheck.objDirs.length > 0) {
|
|
278
|
-
throw new AmbiguousBuildArtifactsError(buildCheck.objDirs);
|
|
279
|
-
}
|
|
280
|
-
const mismatchMessage = buildArtifactMismatchMessage(paths.engine, buildCheck, 'Tests');
|
|
281
|
-
if (mismatchMessage) {
|
|
282
|
-
throw new GeneralError(mismatchMessage);
|
|
283
|
-
}
|
|
284
|
-
if (!buildCheck.exists) {
|
|
285
|
-
const detail = buildCheck.objDir
|
|
286
|
-
? `Build artifacts incomplete in ${buildCheck.objDir}/`
|
|
287
|
-
: 'No build artifacts found (obj-*/ directory missing)';
|
|
288
|
-
throw new GeneralError(`Tests require a completed build. ${detail}\n\n` +
|
|
289
|
-
"Run 'fireforge build' first, then run 'fireforge test'.");
|
|
290
|
-
}
|
|
263
|
+
const buildCheck = await assertTestBuildArtifacts(paths.engine);
|
|
291
264
|
// Load the project config once so both the build and the port
|
|
292
265
|
// probe have access to `binaryName` (the port probe uses it to
|
|
293
266
|
// recognise a fork-branded browser holding the Marionette port).
|
|
@@ -301,9 +274,10 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
|
|
|
301
274
|
// missing-bundle path instead of a cryptic `Browser process exited
|
|
302
275
|
// during spawn (exit code 1, signal none). stderr tail: (empty)`.
|
|
303
276
|
const launchablePath = await resolveLaunchablePathForTests(paths.engine, projectConfig.binaryName, buildCheck.objDir);
|
|
277
|
+
const harnessRetries = options.harnessRetries ?? DEFAULT_HARNESS_RETRIES;
|
|
304
278
|
// Run incremental build if requested
|
|
305
279
|
if (options.build) {
|
|
306
|
-
await runPreTestBuild(projectRoot, paths, projectConfig);
|
|
280
|
+
await runPreTestBuild(projectRoot, paths, projectConfig, harnessRetries);
|
|
307
281
|
info('');
|
|
308
282
|
}
|
|
309
283
|
else {
|
|
@@ -343,45 +317,17 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
|
|
|
343
317
|
// `-marionette` process from `fresh/` poisoned a later test run in
|
|
344
318
|
// the sibling `mybrowser/` workspace.
|
|
345
319
|
await assertMarionettePortAvailable(effectivePort, { binaryName: projectConfig.binaryName });
|
|
346
|
-
// `--doctor` runs a short marionette handshake probe. When test paths are
|
|
347
|
-
// supplied the probe gates the mach test invocation (a FAIL bails out). When
|
|
348
|
-
// no paths are supplied this is the only step — it's the fastest way to tell
|
|
349
|
-
// marionette-wedged apart from test-discovery-failure.
|
|
350
320
|
if (options.doctor) {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
: await runMarionettePreflight(paths.engine);
|
|
361
|
-
// 2026-04-24 eval Finding 7: the pre-0.18.1 code used
|
|
362
|
-
// `success()` + `outro()` + a direct `process.stdout.write` as a
|
|
363
|
-
// belt-and-suspenders but still reproducibly dropped the PASS summary
|
|
364
|
-
// under non-TTY capture (observed: `tee`-wrapped eval output saw only
|
|
365
|
-
// the intro). The fix writes the authoritative PASS/FAIL line via
|
|
366
|
-
// `process.stdout.write` as the very first output after the probe
|
|
367
|
-
// returns, so the captured stream has an unambiguous summary no
|
|
368
|
-
// matter what clack does on top. The clack-rendered banner
|
|
369
|
-
// (`info`/`warn`) is retained so TTY users keep the visual framing.
|
|
370
|
-
const directLine = formatMarionettePreflightLine(preflight);
|
|
371
|
-
process.stdout.write(`${directLine}\n`);
|
|
372
|
-
process.stdout.write(`Marionette preflight environment: objdir=${buildCheck.objDir ?? '(none)'}; binary=${projectConfig.binaryName}; app=${launchablePath ? `engine/${launchablePath}` : '(unknown)'}; port=${effectivePort ?? 2828}; elapsed=${preflight.durationMs}ms\n`);
|
|
373
|
-
reportMarionettePreflight(preflight);
|
|
374
|
-
if (testPaths.length === 0) {
|
|
375
|
-
if (!preflight.ok) {
|
|
376
|
-
throw new GeneralError('Marionette preflight reported FAIL — see output above.');
|
|
377
|
-
}
|
|
378
|
-
success(directLine);
|
|
379
|
-
outro('Test completed');
|
|
321
|
+
const doctorOutcome = await runDoctorPreflight({
|
|
322
|
+
engineDir: paths.engine,
|
|
323
|
+
effectivePort,
|
|
324
|
+
hasTestPaths: testPaths.length > 0,
|
|
325
|
+
objDir: buildCheck.objDir,
|
|
326
|
+
binaryName: projectConfig.binaryName,
|
|
327
|
+
launchablePath,
|
|
328
|
+
});
|
|
329
|
+
if (doctorOutcome === 'stop')
|
|
380
330
|
return;
|
|
381
|
-
}
|
|
382
|
-
if (!preflight.ok) {
|
|
383
|
-
throw new GeneralError('Marionette preflight reported FAIL — see output above. Aborting before mach test runs.');
|
|
384
|
-
}
|
|
385
331
|
}
|
|
386
332
|
const normalizedPaths = testPaths.map((p) => stripEnginePrefix(p).trim());
|
|
387
333
|
await assertTestPathsExist(paths.engine, normalizedPaths);
|
|
@@ -389,6 +335,7 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
|
|
|
389
335
|
if (classification.xpcshell.length > 0 && classification.nonXpcshell.length > 0) {
|
|
390
336
|
throw new GeneralError(buildMixedHarnessMessage(classification));
|
|
391
337
|
}
|
|
338
|
+
const suite = resolveTestSuite(classification, options.genericMachTest === true);
|
|
392
339
|
const forwardedMachArgs = options.machArg && options.machArg.length > 0
|
|
393
340
|
? filterRedundantXpcshellFlavorArgs(options.machArg, classification)
|
|
394
341
|
: [];
|
|
@@ -404,60 +351,54 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
|
|
|
404
351
|
if (forwardedMachArgs.length > 0) {
|
|
405
352
|
extraArgs.push(...forwardedMachArgs);
|
|
406
353
|
}
|
|
407
|
-
|
|
408
|
-
//
|
|
409
|
-
//
|
|
410
|
-
//
|
|
411
|
-
//
|
|
412
|
-
//
|
|
413
|
-
// Skip setpref forwarding when the operator already supplied an equivalent
|
|
414
|
-
// arg via `--mach-arg` — duplicates would be confusing without changing
|
|
415
|
-
// semantics. Skip when mach args explicitly request `--flavor=xpcshell`
|
|
416
|
-
// (or `xpcshell-tests`): the preflight still honours `--marionette-port`,
|
|
417
|
-
// but mach does not use the marionette.port pref on that harness. Any
|
|
418
|
-
// other arg shape still forwards so toolkit widget paths and mixed suites
|
|
419
|
-
// stay aligned with the probe without duplicate `--mach-arg` flags.
|
|
420
|
-
//
|
|
421
|
-
// Skip auto `--marionette=...` when `--mach-arg` already includes a client
|
|
422
|
-
// `--marionette=...` (or two-token `--marionette host:port`).
|
|
423
|
-
if (options.marionettePort !== undefined) {
|
|
424
|
-
const operatorAlreadyForwarded = forwardedPort !== undefined;
|
|
425
|
-
const machArgs = options.machArg ?? [];
|
|
426
|
-
if (operatorAlreadyForwarded) {
|
|
427
|
-
info(`--marionette-port=${options.marionettePort} set, but the same port is already forwarded via --mach-arg; skipping auto-forward.`);
|
|
428
|
-
}
|
|
429
|
-
else if (shouldAutoForwardMarionettePortToMach(machArgs)) {
|
|
430
|
-
extraArgs.push(`--setpref=marionette.port=${options.marionettePort}`);
|
|
431
|
-
}
|
|
432
|
-
else {
|
|
433
|
-
info(`--marionette-port=${options.marionettePort} applied to the preflight probe, but --flavor=xpcshell is set — mach is not auto-configured with --setpref=marionette.port or --marionette (xpcshell ignores the browser Marionette path). Pass --mach-arg --setpref=marionette.port=${options.marionettePort} explicitly if you still need mach to see the port.`);
|
|
434
|
-
}
|
|
435
|
-
if (shouldAutoForwardMarionettePortToMach(machArgs) &&
|
|
436
|
-
!forwardedMachArgsIncludeMarionetteClient(machArgs)) {
|
|
437
|
-
extraArgs.push(`--marionette=127.0.0.1:${options.marionettePort}`);
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
// xpcshell appdir auto-injection — see src/core/xpcshell-appdir.ts for the
|
|
441
|
-
// full motivation. On rebranded forks (appname != "firefox") the upstream
|
|
442
|
-
// harness silently ignores `firefox-appdir = "browser"` directives in the
|
|
443
|
-
// xpcshell.toml, so every `resource:///modules/…` import throws. We probe
|
|
444
|
-
// the nearest manifest, compute the absolute appdir under obj-*/dist/, and
|
|
445
|
-
// inject `--app-path=<abs>` so the harness uses the right root. Operator
|
|
446
|
-
// overrides via `--mach-arg=--app-path=…` always win — we skip injection
|
|
447
|
-
// when the operator already passed one.
|
|
448
|
-
const appdirInjection = await maybeInjectAppdirArg(paths.engine, normalizedPaths, buildCheck.objDir, extraArgs);
|
|
354
|
+
appendMarionetteForwardingArgs(extraArgs, options, forwardedPort);
|
|
355
|
+
// xpcshell appdir auto-injection happens per harness invocation inside
|
|
356
|
+
// `runTestsWithRetries` (src/commands/test-run.ts) so sharded runs probe
|
|
357
|
+
// the manifest for each file individually. See src/core/xpcshell-appdir.ts
|
|
358
|
+
// for the full motivation.
|
|
449
359
|
logTestSelection(normalizedPaths);
|
|
450
|
-
|
|
360
|
+
const perfSampleEnv = buildPerfSampleEnv(projectRoot, projectConfig.binaryName, options.perfSamples);
|
|
361
|
+
const runCtx = {
|
|
362
|
+
engineDir: paths.engine,
|
|
363
|
+
objDir: buildCheck.objDir,
|
|
364
|
+
classification,
|
|
365
|
+
suite,
|
|
366
|
+
baseExtraArgs: extraArgs,
|
|
367
|
+
harnessRetries,
|
|
368
|
+
...(perfSampleEnv ? { env: perfSampleEnv } : {}),
|
|
369
|
+
};
|
|
370
|
+
const postRebuildContext = options.build
|
|
371
|
+
? createPostRebuildFailureContext('fireforge test --build', normalizedPaths)
|
|
372
|
+
: undefined;
|
|
373
|
+
// Multi-file requests shard into sequential single-file harness runs by
|
|
374
|
+
// default (field report C3): one shared mochitest profile across files
|
|
375
|
+
// bleeds pref/media-query state into later files. --no-shard restores
|
|
376
|
+
// the combined invocation.
|
|
377
|
+
if (normalizedPaths.length > 1 && options.shard !== false) {
|
|
378
|
+
await runShardedTests(runCtx, normalizedPaths, (outcome, path) => diagnoseShardOutcome(outcome, path, projectConfig.binaryName, postRebuildContext));
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
let outcome;
|
|
451
382
|
try {
|
|
452
|
-
|
|
383
|
+
outcome = await runTestsWithRetries(runCtx, normalizedPaths);
|
|
453
384
|
}
|
|
454
385
|
catch (error) {
|
|
455
386
|
throw new BuildError('Test process failed to start', 'mach test', error instanceof Error ? error : undefined);
|
|
456
387
|
}
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
388
|
+
finalizeSingleRunOutcome(outcome, normalizedPaths, projectConfig.binaryName, postRebuildContext);
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Builds the perf-sample env contract for the harness run (field report
|
|
392
|
+
* C4): `--perf-samples <path>` exports `<BINARYNAME>_PERF_SAMPLE_JSON`
|
|
393
|
+
* naming the artifact file a budget checker consumes after the run.
|
|
394
|
+
*/
|
|
395
|
+
function buildPerfSampleEnv(projectRoot, binaryName, perfSamples) {
|
|
396
|
+
if (!perfSamples)
|
|
397
|
+
return undefined;
|
|
398
|
+
const envName = `${binaryName.toUpperCase().replace(/[^A-Z0-9]/g, '_')}_PERF_SAMPLE_JSON`;
|
|
399
|
+
const artifactPath = resolve(projectRoot, perfSamples);
|
|
400
|
+
info(`Perf sample contract: ${envName}=${artifactPath}`);
|
|
401
|
+
return { [envName]: artifactPath };
|
|
461
402
|
}
|
|
462
403
|
/** Registers the test command on the CLI program. */
|
|
463
404
|
export function registerTest(program, { getProjectRoot, withErrorHandling }) {
|
|
@@ -471,6 +412,16 @@ export function registerTest(program, { getProjectRoot, withErrorHandling }) {
|
|
|
471
412
|
acc.push(value);
|
|
472
413
|
return acc;
|
|
473
414
|
}, [])
|
|
415
|
+
.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)}.`, (raw) => {
|
|
416
|
+
const n = Number.parseInt(raw, 10);
|
|
417
|
+
if (!Number.isFinite(n) || n < 0 || n > 10) {
|
|
418
|
+
throw new GeneralError(`--harness-retries must be an integer in 0..10 (got "${raw}")`);
|
|
419
|
+
}
|
|
420
|
+
return n;
|
|
421
|
+
})
|
|
422
|
+
.option('--generic-mach-test', 'Force dispatch through generic `mach test` instead of the suite-specific `mach xpcshell-test` / `mach mochitest` a single-suite run auto-selects (the suite-specific commands skip the mozlog resource monitor that crashes `mach test` on some hosts).')
|
|
423
|
+
.option('--no-shard', 'Run multiple test paths in one combined mach invocation instead of sequential per-file shards')
|
|
424
|
+
.option('--perf-samples <path>', 'Publish a perf-sample artifact path to the harness via <BINARYNAME>_PERF_SAMPLE_JSON (resolved against the project root)')
|
|
474
425
|
.option('--marionette-port <port>', 'Override the Marionette control port (default 2828) for the stale-browser probe, the --doctor preflight, and (unless --mach-arg includes --flavor=xpcshell) auto-forwarded mach args: --setpref=marionette.port=<n> (browser listener) and --marionette=127.0.0.1:<n> (mochitest client). Omits the client flag when --mach-arg already sets --marionette. Use when 2828 is busy or CI assigns another port.', (raw) => {
|
|
475
426
|
const n = Number.parseInt(raw, 10);
|
|
476
427
|
if (!Number.isFinite(n) || n < 1 || n > 65535) {
|