@hominis/fireforge 0.44.3 → 0.45.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 +47 -0
- package/dist/build-info.json +4 -4
- package/dist/src/commands/build.js +9 -4
- package/dist/src/commands/doctor-source-pin.d.ts +29 -0
- package/dist/src/commands/doctor-source-pin.js +81 -0
- package/dist/src/commands/doctor.js +2 -0
- package/dist/src/commands/export-flow.js +3 -18
- package/dist/src/commands/furnace/create-browser-test.d.ts +9 -0
- package/dist/src/commands/furnace/create-browser-test.js +6 -6
- package/dist/src/commands/furnace/create-dry-run.d.ts +10 -0
- package/dist/src/commands/furnace/create-dry-run.js +16 -11
- package/dist/src/commands/furnace/create-xpcshell.js +2 -2
- package/dist/src/commands/furnace/create.js +5 -0
- package/dist/src/commands/patch/move-files-into.js +3 -6
- package/dist/src/commands/patch/split-plan.js +3 -6
- package/dist/src/commands/re-export-adjacent.d.ts +8 -0
- package/dist/src/commands/re-export-adjacent.js +26 -1
- package/dist/src/commands/re-export-drift.d.ts +8 -0
- package/dist/src/commands/re-export-drift.js +61 -0
- package/dist/src/commands/re-export-files.js +3 -6
- package/dist/src/commands/re-export-options.js +19 -5
- package/dist/src/commands/re-export-register.js +5 -1
- package/dist/src/commands/re-export-scan.js +3 -6
- package/dist/src/commands/re-export.js +16 -16
- package/dist/src/commands/test-build-phase.js +3 -2
- package/dist/src/commands/test-diagnose.js +14 -5
- package/dist/src/commands/test-register.js +1 -0
- package/dist/src/commands/test-run.js +4 -3
- package/dist/src/commands/test-stale-gate.js +5 -2
- package/dist/src/commands/test.js +9 -2
- package/dist/src/core/build-audit.d.ts +20 -0
- package/dist/src/core/build-audit.js +69 -3
- package/dist/src/core/build-baseline-types.d.ts +28 -0
- package/dist/src/core/build-baseline.d.ts +16 -1
- package/dist/src/core/build-baseline.js +38 -2
- package/dist/src/core/build-jar-escalation.d.ts +64 -0
- package/dist/src/core/build-jar-escalation.js +127 -0
- package/dist/src/core/build-prepare.d.ts +12 -1
- package/dist/src/core/build-prepare.js +68 -6
- package/dist/src/core/config-validate-file-size.d.ts +12 -0
- package/dist/src/core/config-validate-file-size.js +65 -0
- package/dist/src/core/config-validate.js +6 -0
- package/dist/src/core/engine-changes.d.ts +18 -0
- package/dist/src/core/engine-changes.js +34 -0
- package/dist/src/core/furnace-constants.d.ts +21 -0
- package/dist/src/core/furnace-constants.js +26 -1
- package/dist/src/core/harness-orphans.d.ts +73 -0
- package/dist/src/core/harness-orphans.js +186 -0
- package/dist/src/core/license-headers.d.ts +9 -5
- package/dist/src/core/license-headers.js +33 -7
- package/dist/src/core/mach-known-noise-filter.d.ts +21 -0
- package/dist/src/core/mach-known-noise-filter.js +24 -1
- package/dist/src/core/mach.d.ts +7 -0
- package/dist/src/core/mach.js +18 -2
- package/dist/src/core/patch-apply.js +49 -20
- package/dist/src/core/patch-lint-checkjs.js +6 -1
- package/dist/src/core/patch-lint-file-size.d.ts +52 -0
- package/dist/src/core/patch-lint-file-size.js +53 -0
- package/dist/src/core/patch-lint-prettier.d.ts +36 -0
- package/dist/src/core/patch-lint-prettier.js +126 -0
- package/dist/src/core/patch-lint-unmanaged-companion.d.ts +106 -0
- package/dist/src/core/patch-lint-unmanaged-companion.js +214 -0
- package/dist/src/core/patch-lint.js +23 -24
- package/dist/src/core/patch-transform.d.ts +21 -0
- package/dist/src/core/patch-transform.js +38 -0
- package/dist/src/core/smoke-patterns.js +12 -0
- package/dist/src/core/test-harness-crash.js +61 -48
- package/dist/src/core/test-harness-verdict-notes.d.ts +55 -0
- package/dist/src/core/test-harness-verdict-notes.js +48 -0
- package/dist/src/core/test-stale-check.d.ts +23 -6
- package/dist/src/core/test-stale-check.js +77 -15
- package/dist/src/core/test-stall-triage.js +37 -19
- package/dist/src/errors/build.d.ts +28 -0
- package/dist/src/errors/build.js +49 -0
- package/dist/src/types/commands/options.d.ts +19 -0
- package/dist/src/types/commands/patches.d.ts +17 -6
- package/dist/src/types/config.d.ts +41 -0
- package/dist/src/utils/fs.d.ts +4 -2
- package/dist/src/utils/fs.js +3 -1
- package/docs/configuration.md +47 -0
- package/docs/exit-codes.md +4 -2
- package/docs/testing.md +83 -0
- package/package.json +1 -1
|
@@ -66,13 +66,30 @@ export declare const FULL_SUITE_REQUEST = "(entire suite)";
|
|
|
66
66
|
*/
|
|
67
67
|
export declare function findUncoveredRequestPaths(coverage: TestPackagingCoverage | undefined, requestedPaths: readonly string[]): string[];
|
|
68
68
|
/**
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
69
|
+
* Test manifests changed since the recorded build that sit in (or above)
|
|
70
|
+
* the directory of an uncovered request path — i.e. the manifests that
|
|
71
|
+
* EXPLAIN why this run needs coverage the build never claimed.
|
|
72
|
+
*
|
|
73
|
+
* Best-effort: a probe failure returns an empty list, so the refusal
|
|
74
|
+
* degrades to exactly its previous text rather than failing differently.
|
|
75
|
+
*
|
|
76
|
+
* @param engineDir - Absolute engine directory
|
|
77
|
+
* @param baseline - Last successful build's baseline
|
|
78
|
+
* @param uncovered - Uncovered request paths from
|
|
79
|
+
* {@link findUncoveredRequestPaths}
|
|
80
|
+
* @returns Engine-relative manifest paths, capped at
|
|
81
|
+
* {@link STALE_PATHS_LIMIT}
|
|
82
|
+
*/
|
|
83
|
+
export declare function findChangedTestManifestsForPaths(engineDir: string, baseline: BuildBaseline | undefined, uncovered: readonly string[]): Promise<string[]>;
|
|
84
|
+
/**
|
|
85
|
+
* @param uncovered - Request paths the recorded packaging does not cover
|
|
86
|
+
* @param coverage - The recorded scoped coverage claim
|
|
87
|
+
* @param changedManifests - Test manifests changed since the recorded
|
|
88
|
+
* build that explain an uncovered path (from
|
|
89
|
+
* {@link findChangedTestManifestsForPaths}); omitted or empty renders
|
|
90
|
+
* nothing
|
|
74
91
|
*/
|
|
75
|
-
export declare function formatTestCoverageRefusal(uncovered: string[], coverage: string[]): string;
|
|
92
|
+
export declare function formatTestCoverageRefusal(uncovered: string[], coverage: string[], changedManifests?: readonly string[]): string;
|
|
76
93
|
/** Result of the compiled-StaticComponents staleness probe. */
|
|
77
94
|
export interface StaticComponentsStaleResult {
|
|
78
95
|
/** True when at least one `components.conf` genuinely diverged from the anchor. */
|
|
@@ -27,7 +27,7 @@ import { verbose, warn } from '../utils/logger.js';
|
|
|
27
27
|
import { isPackageablePath, isXpcomManifestPath } from './build-audit.js';
|
|
28
28
|
import { readBuildBaseline } from './build-baseline.js';
|
|
29
29
|
import { hashEngineFile } from './coverage-extend.js';
|
|
30
|
-
import { collectChangedEnginePaths } from './engine-changes.js';
|
|
30
|
+
import { collectChangedEnginePaths, dropPathsMatchingFingerprints } from './engine-changes.js';
|
|
31
31
|
export { isXpcomManifestPath };
|
|
32
32
|
/** Cap on the number of changed paths rendered inline. */
|
|
33
33
|
const STALE_PATHS_LIMIT = 10;
|
|
@@ -60,18 +60,7 @@ export async function checkStaleBuildForTest(projectRoot, engineDir) {
|
|
|
60
60
|
// and the stale check fires immediately after a successful one. The
|
|
61
61
|
// fingerprints capture "these files had this content when the build ran";
|
|
62
62
|
// a path stays stale only when its live hash diverges.
|
|
63
|
-
|
|
64
|
-
if (fingerprints) {
|
|
65
|
-
const staleAfterHashCheck = [];
|
|
66
|
-
for (const path of packageable) {
|
|
67
|
-
const recorded = fingerprints[path];
|
|
68
|
-
const live = await hashEngineFile(engineDir, path);
|
|
69
|
-
if (recorded === undefined || live === undefined || recorded !== live) {
|
|
70
|
-
staleAfterHashCheck.push(path);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
packageable = staleAfterHashCheck;
|
|
74
|
-
}
|
|
63
|
+
packageable = await dropPathsMatchingFingerprints(engineDir, packageable, baseline.packageableFingerprints);
|
|
75
64
|
if (packageable.length === 0) {
|
|
76
65
|
return { stale: false, changedPaths: [], truncated: 0, baseline };
|
|
77
66
|
}
|
|
@@ -158,7 +147,74 @@ function normalizeCoveragePath(path) {
|
|
|
158
147
|
* separate from the matcher so tests can pin structure and copy
|
|
159
148
|
* independently (same split as {@link formatStaleBuildWarning}).
|
|
160
149
|
*/
|
|
161
|
-
|
|
150
|
+
/**
|
|
151
|
+
* Basenames of the test manifests whose entries decide what a packaged test
|
|
152
|
+
* runtime stages. A manifest that gained an entry is the single most common
|
|
153
|
+
* reason a run needs coverage the last build never claimed — and it is a
|
|
154
|
+
* fact FireForge already holds but never said, leaving the operator to
|
|
155
|
+
* rediscover it after a correct refusal.
|
|
156
|
+
*/
|
|
157
|
+
const TEST_MANIFEST_BASENAMES = new Set([
|
|
158
|
+
'xpcshell.toml',
|
|
159
|
+
'xpcshell.ini',
|
|
160
|
+
'browser.toml',
|
|
161
|
+
'browser.ini',
|
|
162
|
+
'mochitest.toml',
|
|
163
|
+
'mochitest.ini',
|
|
164
|
+
'chrome.toml',
|
|
165
|
+
'chrome.ini',
|
|
166
|
+
'a11y.toml',
|
|
167
|
+
'a11y.ini',
|
|
168
|
+
]);
|
|
169
|
+
/** True for a test manifest path (by basename). */
|
|
170
|
+
function isTestManifestPath(path) {
|
|
171
|
+
const base = path.slice(path.lastIndexOf('/') + 1);
|
|
172
|
+
return TEST_MANIFEST_BASENAMES.has(base);
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Test manifests changed since the recorded build that sit in (or above)
|
|
176
|
+
* the directory of an uncovered request path — i.e. the manifests that
|
|
177
|
+
* EXPLAIN why this run needs coverage the build never claimed.
|
|
178
|
+
*
|
|
179
|
+
* Best-effort: a probe failure returns an empty list, so the refusal
|
|
180
|
+
* degrades to exactly its previous text rather than failing differently.
|
|
181
|
+
*
|
|
182
|
+
* @param engineDir - Absolute engine directory
|
|
183
|
+
* @param baseline - Last successful build's baseline
|
|
184
|
+
* @param uncovered - Uncovered request paths from
|
|
185
|
+
* {@link findUncoveredRequestPaths}
|
|
186
|
+
* @returns Engine-relative manifest paths, capped at
|
|
187
|
+
* {@link STALE_PATHS_LIMIT}
|
|
188
|
+
*/
|
|
189
|
+
export async function findChangedTestManifestsForPaths(engineDir, baseline, uncovered) {
|
|
190
|
+
let changed;
|
|
191
|
+
try {
|
|
192
|
+
changed = await collectChangedEnginePaths(engineDir, baseline, 'Coverage refusal');
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
verbose(`Coverage refusal: manifest probe failed — ${toError(error).message}`);
|
|
196
|
+
return [];
|
|
197
|
+
}
|
|
198
|
+
const requestDirs = uncovered
|
|
199
|
+
.filter((p) => p !== FULL_SUITE_REQUEST)
|
|
200
|
+
.map((p) => toManifestGranule(normalizeCoveragePath(p)));
|
|
201
|
+
const hits = changed.filter((path) => {
|
|
202
|
+
if (!isTestManifestPath(path))
|
|
203
|
+
return false;
|
|
204
|
+
const dir = path.slice(0, Math.max(0, path.lastIndexOf('/')));
|
|
205
|
+
return requestDirs.some((req) => req === dir || req.startsWith(`${dir}/`));
|
|
206
|
+
});
|
|
207
|
+
return hits.slice(0, STALE_PATHS_LIMIT);
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* @param uncovered - Request paths the recorded packaging does not cover
|
|
211
|
+
* @param coverage - The recorded scoped coverage claim
|
|
212
|
+
* @param changedManifests - Test manifests changed since the recorded
|
|
213
|
+
* build that explain an uncovered path (from
|
|
214
|
+
* {@link findChangedTestManifestsForPaths}); omitted or empty renders
|
|
215
|
+
* nothing
|
|
216
|
+
*/
|
|
217
|
+
export function formatTestCoverageRefusal(uncovered, coverage, changedManifests = []) {
|
|
162
218
|
const cap = (paths) => {
|
|
163
219
|
const head = paths.slice(0, STALE_PATHS_LIMIT);
|
|
164
220
|
const truncated = paths.length - head.length;
|
|
@@ -177,10 +233,16 @@ export function formatTestCoverageRefusal(uncovered, coverage) {
|
|
|
177
233
|
const rebuildHint = rebuildTargets.length > 0
|
|
178
234
|
? `Rerun "fireforge test --build ${rebuildTargets.slice(0, STALE_PATHS_LIMIT).join(' ')}" to package them, or run "fireforge build" for full coverage.${extendHint}`
|
|
179
235
|
: 'Run "fireforge build" (or a path-less "fireforge test --build") for full coverage first.';
|
|
236
|
+
// Why THIS run needs new coverage, when a changed manifest explains it: a
|
|
237
|
+
// manifest that gained an entry is not covered by a build claim recorded
|
|
238
|
+
// for other paths, and a browser-chrome claim never covers xpcshell.
|
|
239
|
+
const manifestLine = changedManifests.length > 0
|
|
240
|
+
? ` This run needs coverage the recorded build never claimed because a test manifest changed since it: ${changedManifests.map((m) => `engine/${m}`).join(', ')} — a manifest that gained an entry stages new support fixtures, and a claim recorded for other paths (or another harness) does not cover them.`
|
|
241
|
+
: '';
|
|
180
242
|
return (`The packaged test runtime was produced by a scoped "fireforge test --build" covering only: ${cap(coverage)}.\n` +
|
|
181
243
|
`The requested run needs ${cap(uncovered)}, which that packaging does not cover — support ` +
|
|
182
244
|
'fixtures for those manifests may be missing from obj-*/_tests/, and the run can hang ' +
|
|
183
|
-
`rather than fail
|
|
245
|
+
`rather than fail.${manifestLine} ${rebuildHint} ` +
|
|
184
246
|
'(--allow-stale-build does not bypass this check — it accepts stale content, not missing coverage.)');
|
|
185
247
|
}
|
|
186
248
|
/**
|
|
@@ -38,33 +38,51 @@ const NO_OUTPUT_STALL_CONTROL_STEP = 'First, run a known-good control test. If t
|
|
|
38
38
|
* its manifest from `server.js` on 8888, and a survivor of an
|
|
39
39
|
* interrupted run keeps the port. Its probe is exact, and the preflight
|
|
40
40
|
* in `mochitest-server-port.ts` now refuses it before a run starts.
|
|
41
|
-
* - (3) is
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
* `
|
|
47
|
-
*
|
|
48
|
-
*
|
|
41
|
+
* - (3) is now MECHANICAL and root-caused, replacing the correlation an
|
|
42
|
+
* earlier revision recorded. A `chrome://` (or `resource://`) URL that
|
|
43
|
+
* resolves to nothing reaches `CheckForBrokenChromeURL`
|
|
44
|
+
* (netwerk/base/nsNetUtil.cpp), which outside automation only
|
|
45
|
+
* `printf_stderr`s `Missing chrome or resource URL: <uri>` — but under
|
|
46
|
+
* `xpc::IsInAutomation()` the same condition is
|
|
47
|
+
* `MOZ_CRASH_UNSAFE_PRINTF("Missing chrome or resource URLs: %s")`. A
|
|
48
|
+
* downstream reproduction symbolicated the faulting frame as
|
|
49
|
+
* `CheckForBrokenChromeURL`, which is the established diagnosis the bar
|
|
50
|
+
* in `mach-error-hints.ts` asks for. The discredited "stalls first
|
|
51
|
+
* paint" story an even earlier revision asserted must not come back: a
|
|
52
|
+
* failed image load in Gecko fires `error` and does not gate the
|
|
53
|
+
* document load event.
|
|
49
54
|
*
|
|
50
|
-
* Detection
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
55
|
+
* Detection by log line is deliberately NOT specified for (3), and the
|
|
56
|
+
* reason is the mechanism itself: in the reproduction the crash landed in a
|
|
57
|
+
* CONTENT process (`plugin-container`, a child of the browser) with the
|
|
58
|
+
* crash reporter compiled out, so the harness log carried neither the crash
|
|
59
|
+
* message nor a crash line — only the no-output timeout, `Ran 0 checks`,
|
|
60
|
+
* and mochitest's "Can't trigger Breakpad, just killing process". Under
|
|
61
|
+
* automation the message is in the process that DIED, not in the log, so
|
|
62
|
+
* the census points at the two artefacts that do carry it: the OS crash
|
|
63
|
+
* report (whose faulting frame names `CheckForBrokenChromeURL`) and the
|
|
64
|
+
* out-of-automation smoke probe (which prints the non-crashing spelling
|
|
65
|
+
* and, since 0.45.0, counts it as an unallowed error so the exit code
|
|
66
|
+
* agrees with the capture).
|
|
54
67
|
*/
|
|
55
68
|
const NO_OUTPUT_STALL_TRIAGE = [
|
|
56
69
|
' 1. A sleeping or locked display on an unattended HEADED run (macOS). The browser never ' +
|
|
57
70
|
'paints and never reaches its first test.',
|
|
58
71
|
' 2. A headless SWGL compositor failure — re-run with --headless to separate this from (1): ' +
|
|
59
72
|
'if --headless passes, the stall was display/compositor-side, not product-side.',
|
|
60
|
-
' 3.
|
|
61
|
-
'
|
|
62
|
-
'
|
|
63
|
-
'
|
|
73
|
+
' 3. A broken `chrome://` or `resource://` URL reached from the startup document. Under ' +
|
|
74
|
+
'automation this is a MOZ_CRASH in whichever process opened the channel ' +
|
|
75
|
+
'(CheckForBrokenChromeURL, netwerk/base/nsNetUtil.cpp), so the browser dies before the ' +
|
|
76
|
+
'harness sees anything. Expect the log to name NOTHING: with the crash in a content ' +
|
|
77
|
+
'process and the crash reporter compiled out, the only artefacts are the OS crash report ' +
|
|
78
|
+
'(macOS: ~/Library/Logs/DiagnosticReports/plugin-container-*.ips — the faulting frame ' +
|
|
79
|
+
'reads CheckForBrokenChromeURL) and the smoke probe below. Discriminate with ' +
|
|
64
80
|
'"fireforge run --smoke-exit 60 --headless --capture-console <file>", which launches the ' +
|
|
65
|
-
'same browser OUTSIDE
|
|
66
|
-
'
|
|
67
|
-
'
|
|
81
|
+
'same browser OUTSIDE automation, where the same condition only PRINTS ' +
|
|
82
|
+
'"Missing chrome or resource URL: <uri>" — a line the probe counts as an unallowed ' +
|
|
83
|
+
'error, so its exit code and its capture now agree. A clean window with no unallowed ' +
|
|
84
|
+
'console errors means product-side startup is healthy and the stall is harness- or ' +
|
|
85
|
+
'test-side.',
|
|
68
86
|
' 4. A stale mochitest httpd holding the harness server port (127.0.0.1:8888). A fresh ' +
|
|
69
87
|
"browser connects to THAT server, which cannot serve this run's manifest, so the run " +
|
|
70
88
|
'stalls before TEST_START. Probe with "lsof -nP -iTCP:8888": any listener that is not ' +
|
|
@@ -9,6 +9,34 @@ export declare class BuildError extends FireForgeError {
|
|
|
9
9
|
constructor(message: string, command?: string | undefined, cause?: Error);
|
|
10
10
|
get userMessage(): string;
|
|
11
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* A test SUITE went red — assertions failed, or sharded runs did not all
|
|
14
|
+
* pass. Distinct from its {@link BuildError} parent only in what it TELLS
|
|
15
|
+
* the operator, deliberately not in its exit code: `docs/exit-codes.md`
|
|
16
|
+
* documents 5 as "a suite that FAILED", and consumers key CI on it.
|
|
17
|
+
*
|
|
18
|
+
* The parent's remedies are all build remedies — "check the build output",
|
|
19
|
+
* "fireforge bootstrap", "delete obj-* directories" — and none of them apply
|
|
20
|
+
* to a failing assertion. Worse, the first points at the wrong half of the
|
|
21
|
+
* log, which is exactly the half a `tail` keeps. Ground truth for a test red
|
|
22
|
+
* is the harness's own output, so the remedies here name the failing tests,
|
|
23
|
+
* the `FIREFORGE-VERDICT:` line, and the run log that survives a pipe.
|
|
24
|
+
*
|
|
25
|
+
* Note that `machineErrorCode` derives the `--json` error code from the class
|
|
26
|
+
* name, so this reports as `test-failure` rather than `build`.
|
|
27
|
+
*/
|
|
28
|
+
export declare class TestFailureError extends BuildError {
|
|
29
|
+
/** Rendered TEST-UNEXPECTED blocks, when the verdict carried any. */
|
|
30
|
+
readonly failureBlocks?: string | undefined;
|
|
31
|
+
/** The run's complete raw log, when one was opened. */
|
|
32
|
+
readonly logPath?: string | undefined;
|
|
33
|
+
constructor(message: string, command?: string,
|
|
34
|
+
/** Rendered TEST-UNEXPECTED blocks, when the verdict carried any. */
|
|
35
|
+
failureBlocks?: string | undefined,
|
|
36
|
+
/** The run's complete raw log, when one was opened. */
|
|
37
|
+
logPath?: string | undefined);
|
|
38
|
+
get userMessage(): string;
|
|
39
|
+
}
|
|
12
40
|
/**
|
|
13
41
|
* Error thrown when mach is not available.
|
|
14
42
|
*/
|
package/dist/src/errors/build.js
CHANGED
|
@@ -23,6 +23,55 @@ export class BuildError extends FireForgeError {
|
|
|
23
23
|
return msg;
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* A test SUITE went red — assertions failed, or sharded runs did not all
|
|
28
|
+
* pass. Distinct from its {@link BuildError} parent only in what it TELLS
|
|
29
|
+
* the operator, deliberately not in its exit code: `docs/exit-codes.md`
|
|
30
|
+
* documents 5 as "a suite that FAILED", and consumers key CI on it.
|
|
31
|
+
*
|
|
32
|
+
* The parent's remedies are all build remedies — "check the build output",
|
|
33
|
+
* "fireforge bootstrap", "delete obj-* directories" — and none of them apply
|
|
34
|
+
* to a failing assertion. Worse, the first points at the wrong half of the
|
|
35
|
+
* log, which is exactly the half a `tail` keeps. Ground truth for a test red
|
|
36
|
+
* is the harness's own output, so the remedies here name the failing tests,
|
|
37
|
+
* the `FIREFORGE-VERDICT:` line, and the run log that survives a pipe.
|
|
38
|
+
*
|
|
39
|
+
* Note that `machineErrorCode` derives the `--json` error code from the class
|
|
40
|
+
* name, so this reports as `test-failure` rather than `build`.
|
|
41
|
+
*/
|
|
42
|
+
export class TestFailureError extends BuildError {
|
|
43
|
+
failureBlocks;
|
|
44
|
+
logPath;
|
|
45
|
+
constructor(message, command,
|
|
46
|
+
/** Rendered TEST-UNEXPECTED blocks, when the verdict carried any. */
|
|
47
|
+
failureBlocks,
|
|
48
|
+
/** The run's complete raw log, when one was opened. */
|
|
49
|
+
logPath) {
|
|
50
|
+
super(message, command);
|
|
51
|
+
this.failureBlocks = failureBlocks;
|
|
52
|
+
this.logPath = logPath;
|
|
53
|
+
}
|
|
54
|
+
get userMessage() {
|
|
55
|
+
let msg = `Test Failure: ${this.message}`;
|
|
56
|
+
if (this.command) {
|
|
57
|
+
msg += `\n\nCommand: ${this.command}`;
|
|
58
|
+
}
|
|
59
|
+
if (this.failureBlocks) {
|
|
60
|
+
msg += `\n\n${this.failureBlocks}`;
|
|
61
|
+
}
|
|
62
|
+
msg += '\n\nTo fix this:\n';
|
|
63
|
+
msg += this.failureBlocks
|
|
64
|
+
? ' 1. Fix the failing assertions listed above, then re-run just those test paths\n'
|
|
65
|
+
: ' 1. Find the TEST-UNEXPECTED-FAIL lines in the output above — they name what broke\n';
|
|
66
|
+
msg +=
|
|
67
|
+
' 2. Read the "FIREFORGE-VERDICT:" line for the run\'s ground truth (checks, unexpected)\n';
|
|
68
|
+
msg +=
|
|
69
|
+
this.logPath !== undefined
|
|
70
|
+
? ` 3. The complete raw output is at ${this.logPath} — it survives a piped/tailed terminal`
|
|
71
|
+
: ' 3. Re-run without piping the output, so the failure lines are not cut off by "tail"';
|
|
72
|
+
return msg;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
26
75
|
/**
|
|
27
76
|
* Error thrown when mach is not available.
|
|
28
77
|
*/
|
|
@@ -322,6 +322,16 @@ export interface ReExportOptions {
|
|
|
322
322
|
* else still refuses.
|
|
323
323
|
*/
|
|
324
324
|
expect?: string[];
|
|
325
|
+
/**
|
|
326
|
+
* Engine-relative files that are an APPROVED unmanaged exception
|
|
327
|
+
* (`--expect-unmanaged`, repeatable; requires
|
|
328
|
+
* `--refuse-adjacent-unmanaged`). The adjacency refusal is all-or-nothing
|
|
329
|
+
* without it: a project with a reviewed, deliberately unmanaged path beside
|
|
330
|
+
* a patch's ownership has to drop the flag entirely and hand-read the
|
|
331
|
+
* notice, which is a weaker belt than the one being disarmed. Approved
|
|
332
|
+
* paths are still LISTED — they are carved out of the refusal, not hidden.
|
|
333
|
+
*/
|
|
334
|
+
expectUnmanaged?: string[];
|
|
325
335
|
}
|
|
326
336
|
/**
|
|
327
337
|
* Options for the `fireforge patch tier` subcommand. Sets or clears the
|
|
@@ -601,6 +611,15 @@ export interface TestOptions {
|
|
|
601
611
|
marionettePort?: number;
|
|
602
612
|
/** Kill a recognized stale browser process holding the Marionette port, then continue. */
|
|
603
613
|
killStaleMarionette?: boolean;
|
|
614
|
+
/**
|
|
615
|
+
* Terminate harness helper processes (`xpcshell` httpd, `pywebsocket`,
|
|
616
|
+
* `ssltunnel`, `moz-http2`) that survived an earlier run in this
|
|
617
|
+
* project's objdir. The census itself runs on EVERY test dispatch and is
|
|
618
|
+
* report-only; this flag opts into the kill. Survivors slow later runs
|
|
619
|
+
* without appearing in their output, so the visibility is the default and
|
|
620
|
+
* the termination is the choice.
|
|
621
|
+
*/
|
|
622
|
+
reapOrphans?: boolean;
|
|
604
623
|
/** Permit tests against packageable engine edits newer than the last successful build. */
|
|
605
624
|
allowStaleBuild?: boolean;
|
|
606
625
|
/**
|
|
@@ -32,13 +32,24 @@ export interface PatchResult {
|
|
|
32
32
|
/** Whether the patch was auto-resolved (new file vs existing file conflict) */
|
|
33
33
|
autoResolved?: boolean;
|
|
34
34
|
/**
|
|
35
|
-
* Pre-existing
|
|
36
|
-
* engine-relative path. Kept for the RUN's
|
|
37
|
-
* if a later patch fails and
|
|
38
|
-
* reversing a new-file patch
|
|
39
|
-
* only way to restore what was
|
|
35
|
+
* Pre-existing BYTES of files the auto-resolve overwrote (or, for a binary
|
|
36
|
+
* new file, removed), keyed by engine-relative path. Kept for the RUN's
|
|
37
|
+
* duration (not just the retry): if a later patch fails and
|
|
38
|
+
* `rollbackPatches` reverse-applies this one, reversing a new-file patch
|
|
39
|
+
* DELETES the file — these snapshots are the only way to restore what was
|
|
40
|
+
* there before the auto-resolve.
|
|
41
|
+
*
|
|
42
|
+
* Raw bytes rather than decoded text, because the same snapshot has to
|
|
43
|
+
* restore a binary target byte-for-byte.
|
|
44
|
+
*
|
|
45
|
+
* Typed `Uint8Array`, not `Buffer`: this interface is published, and the
|
|
46
|
+
* emitted `.d.ts` naming a Node global made a consumer without
|
|
47
|
+
* `@types/node` fail to compile against the package (`TS2591` in the
|
|
48
|
+
* pack smoke test). Every `Buffer` assigns to it — `Buffer` extends
|
|
49
|
+
* `Uint8Array` — and `fs.writeFile` accepts it, so nothing at the call
|
|
50
|
+
* sites changes.
|
|
40
51
|
*/
|
|
41
|
-
autoResolvedOriginals?: Map<string,
|
|
52
|
+
autoResolvedOriginals?: Map<string, Uint8Array>;
|
|
42
53
|
}
|
|
43
54
|
/**
|
|
44
55
|
* Extended patch information with metadata.
|
|
@@ -226,6 +226,35 @@ export interface PatchLintCheckJsCompilerOptions {
|
|
|
226
226
|
/** Module-resolution `paths` mapping (pattern → targets, engine-relative). */
|
|
227
227
|
paths?: Record<string, string[]>;
|
|
228
228
|
}
|
|
229
|
+
/**
|
|
230
|
+
* Line-count thresholds for one `file-too-large` tier.
|
|
231
|
+
*
|
|
232
|
+
* Every field is optional; an omitted field keeps the built-in default.
|
|
233
|
+
* The three must stay ordered `notice <= warning <= error`, which the
|
|
234
|
+
* config validator enforces — an out-of-order triple silently disables a
|
|
235
|
+
* band rather than failing anything.
|
|
236
|
+
*/
|
|
237
|
+
export interface PatchLintFileSizeTier {
|
|
238
|
+
notice?: number;
|
|
239
|
+
warning?: number;
|
|
240
|
+
error?: number;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Overrides for the `file-too-large` thresholds, per file class.
|
|
244
|
+
*
|
|
245
|
+
* These were module constants until 0.45.0, and nothing in `fireforge.json`
|
|
246
|
+
* could move them. That is a problem specifically because the recommended
|
|
247
|
+
* gate posture is `--max-warnings 0`: under it the `warning` band is not a
|
|
248
|
+
* soft limit at all, so a project whose controllers legitimately sit a few
|
|
249
|
+
* lines over 750 has a hard failure with no dial, at exactly the moment
|
|
250
|
+
* someone is deciding whether to restructure a file.
|
|
251
|
+
*/
|
|
252
|
+
export interface PatchLintFileSizeThresholds {
|
|
253
|
+
/** Non-test files. Defaults: notice 500, warning 750, error 900. */
|
|
254
|
+
general?: PatchLintFileSizeTier;
|
|
255
|
+
/** Test files. Defaults: notice 1200, warning 1400, error 1600. */
|
|
256
|
+
test?: PatchLintFileSizeTier;
|
|
257
|
+
}
|
|
229
258
|
/**
|
|
230
259
|
* Configuration for patch lint rules.
|
|
231
260
|
*/
|
|
@@ -272,6 +301,18 @@ export interface PatchLintConfig {
|
|
|
272
301
|
checkJsTestShim?: string;
|
|
273
302
|
/** File paths exempt from the raw-color-value check (exact or basename match) */
|
|
274
303
|
rawColorAllowlist?: string[];
|
|
304
|
+
/**
|
|
305
|
+
* Per-tier overrides for the `file-too-large` line-count thresholds.
|
|
306
|
+
* Unset fields keep the built-in defaults.
|
|
307
|
+
*/
|
|
308
|
+
fileSizeThresholds?: PatchLintFileSizeThresholds;
|
|
309
|
+
/**
|
|
310
|
+
* Run the project's Prettier over patch-owned `.sys.mjs` modules, from
|
|
311
|
+
* inside `engine/` so the engine's own `.prettierrc*`/`.prettierignore`
|
|
312
|
+
* decide. Default `'off'`: when off, formatting is explicitly out of
|
|
313
|
+
* scope for the per-patch tier.
|
|
314
|
+
*/
|
|
315
|
+
prettier?: PatchLintSeverityGate;
|
|
275
316
|
/** Enforce JSDoc on class-method exports in patch-owned .sys.mjs files. Default: 'off'. */
|
|
276
317
|
jsdocClassMethods?: PatchLintSeverityGate;
|
|
277
318
|
/** Require ≥1 assertion in any patch-touched browser_*.js test file (new or modified). Default: 'off'. */
|
package/dist/src/utils/fs.d.ts
CHANGED
|
@@ -76,9 +76,11 @@ export declare function writeTextIfChanged(path: string, content: string): Promi
|
|
|
76
76
|
* Writes content atomically using a temp-file-and-rename strategy.
|
|
77
77
|
* Temp files are created in the destination directory so rename stays atomic.
|
|
78
78
|
* @param path - Destination file path
|
|
79
|
-
* @param content - Content to write
|
|
79
|
+
* @param content - Content to write. `Uint8Array` rather than `Buffer` so
|
|
80
|
+
* published types never name a Node global (every `Buffer` still assigns
|
|
81
|
+
* to it, and `fs.writeFile` accepts both).
|
|
80
82
|
*/
|
|
81
|
-
export declare function writeFileAtomic(path: string, content: string |
|
|
83
|
+
export declare function writeFileAtomic(path: string, content: string | Uint8Array): Promise<void>;
|
|
82
84
|
/**
|
|
83
85
|
* Copies a directory recursively.
|
|
84
86
|
* @param src - Source directory path
|
package/dist/src/utils/fs.js
CHANGED
|
@@ -165,7 +165,9 @@ export async function writeTextIfChanged(path, content) {
|
|
|
165
165
|
* Writes content atomically using a temp-file-and-rename strategy.
|
|
166
166
|
* Temp files are created in the destination directory so rename stays atomic.
|
|
167
167
|
* @param path - Destination file path
|
|
168
|
-
* @param content - Content to write
|
|
168
|
+
* @param content - Content to write. `Uint8Array` rather than `Buffer` so
|
|
169
|
+
* published types never name a Node global (every `Buffer` still assigns
|
|
170
|
+
* to it, and `fs.writeFile` accepts both).
|
|
169
171
|
*/
|
|
170
172
|
export async function writeFileAtomic(path, content) {
|
|
171
173
|
await ensureParentDir(path);
|
package/docs/configuration.md
CHANGED
|
@@ -7,6 +7,24 @@
|
|
|
7
7
|
whose behaviour needs more than a schema line, plus the environment variables
|
|
8
8
|
FireForge reads.
|
|
9
9
|
|
|
10
|
+
## `firefox` — the source pin
|
|
11
|
+
|
|
12
|
+
`firefox.version` (with `firefox.product`, and optionally `firefox.sha256` and
|
|
13
|
+
`firefox.candidate`) is the source pin `fireforge download` resolves an archive
|
|
14
|
+
from. `fireforge source set` writes it.
|
|
15
|
+
|
|
16
|
+
The pin lives in `fireforge.json` beside hand-maintained policy sections, which
|
|
17
|
+
is worth knowing when reverting that file: `git checkout -- fireforge.json`
|
|
18
|
+
after an accidental reformat also reverts an **uncommitted** pin, and the tree
|
|
19
|
+
is then building a different version than the config claims.
|
|
20
|
+
|
|
21
|
+
`fireforge doctor` reports the pin beside what the checkout actually is — the
|
|
22
|
+
engine's own `browser/config/version.txt` and the version the last download
|
|
23
|
+
recorded — and warns when they disagree. It is a report, not a lock: a tree
|
|
24
|
+
may legitimately be mid-migration. `product`, `sha256` and `candidate` have no
|
|
25
|
+
recorded counterpart on disk, so the check does not claim to have verified
|
|
26
|
+
them.
|
|
27
|
+
|
|
10
28
|
## External toolchains
|
|
11
29
|
|
|
12
30
|
Projects with generated asset prerequisites can declare opt-in
|
|
@@ -33,6 +51,35 @@ shim that tracks upstream WebIDL additions per release.
|
|
|
33
51
|
- `patchLint.checkJsTestShim` points at a project `.d.ts` whose typed
|
|
34
52
|
declarations (e.g. a real `TestUtils` interface) override the loose
|
|
35
53
|
baseline, so calls to nonexistent harness members fail at export time.
|
|
54
|
+
- `patchLint.prettier` (`'off'` | `'warning'` | `'error'`, default `'off'`)
|
|
55
|
+
runs the project's configured Prettier over patch-owned `.sys.mjs`
|
|
56
|
+
modules. It runs from inside `engine/`, so `engine/.prettierrc*` and
|
|
57
|
+
`engine/.prettierignore` decide — the same command run from the repo root
|
|
58
|
+
can report a false pass when the root `.prettierignore` excludes
|
|
59
|
+
`engine/`. Prettier is resolved from `engine/node_modules`, then the
|
|
60
|
+
project's, then `npx --no-install`. **Left `'off'`, formatting is out of
|
|
61
|
+
scope for the per-patch tier**; no other tier checks `.sys.mjs`
|
|
62
|
+
formatting either.
|
|
63
|
+
- `patchLint.fileSizeThresholds` tunes the `file-too-large` line counts per
|
|
64
|
+
file class:
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
{
|
|
68
|
+
"patchLint": {
|
|
69
|
+
"fileSizeThresholds": {
|
|
70
|
+
"general": { "notice": 500, "warning": 800, "error": 900 },
|
|
71
|
+
"test": { "warning": 1500 }
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Every field is optional and merges over the defaults (general
|
|
78
|
+
`500/750/900`, test `1200/1400/1600`). The merged triple must satisfy
|
|
79
|
+
`notice <= warning <= error`, which is validated at config load. Note that
|
|
80
|
+
under the recommended gate posture `--max-warnings 0` the `warning` band is
|
|
81
|
+
a hard failure, not a soft limit — which is what these thresholds exist to
|
|
82
|
+
let you move.
|
|
36
83
|
|
|
37
84
|
## `patchPolicy`
|
|
38
85
|
|
package/docs/exit-codes.md
CHANGED
|
@@ -14,7 +14,7 @@ they mean and which class produces each.
|
|
|
14
14
|
| 2 | `CONFIG_ERROR` | `fireforge.json` missing or invalid. | `ConfigError` and subclasses |
|
|
15
15
|
| 3 | `DOWNLOAD_ERROR` | Could not download or extract Firefox source. | `DownloadError` and subclasses |
|
|
16
16
|
| 4 | `GIT_ERROR` | A git operation failed. | `GitError` and subclasses |
|
|
17
|
-
| 5 | `BUILD_ERROR` | `mach build` failed.
|
|
17
|
+
| 5 | `BUILD_ERROR` | `mach build` failed, or a test suite went red. | `BuildError` and subclasses, `TestFailureError` |
|
|
18
18
|
| 6 | `PATCH_ERROR` | A patch failed to apply, or the patch queue could not be mutated. | `PatchError` |
|
|
19
19
|
| 7 | `MISSING_DEPENDENCY` | A required tool (python3, git, tar) was not found. | `PythonNotFoundError`, `GitNotFoundError`, `MachNotFoundError` |
|
|
20
20
|
| 8 | `INVALID_ARGUMENT` | The flags were wrong. Usage problem, not an environment one. | `InvalidArgumentError` |
|
|
@@ -35,7 +35,9 @@ change would have helped — escalate it, do not retry.
|
|
|
35
35
|
|
|
36
36
|
**Red vs never-ran vs thrown-away.** These are three different facts and,
|
|
37
37
|
before 0.44.0, two of them shared exit 1. `BUILD_ERROR` (5) is a suite that
|
|
38
|
-
FAILED
|
|
38
|
+
FAILED — thrown as `TestFailureError` since 0.45.0, which keeps the code but
|
|
39
|
+
carries test remedies rather than build ones (`--json` reports it as
|
|
40
|
+
`test-failure`). `LOCK_TIMEOUT` (15) is a run that never started, because another
|
|
39
41
|
FireForge process held the lock for the whole budget — nothing about the
|
|
40
42
|
request was wrong, so re-queue it. `INCONCLUSIVE` (14) is a suite whose
|
|
41
43
|
result was DISCARDED, because `engine/` moved underneath it — the code may
|