@hominis/fireforge 0.30.0 → 0.31.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 +26 -1
- package/README.md +22 -5
- package/dist/src/commands/export-all.js +5 -15
- 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 +36 -0
- package/dist/src/commands/export.js +47 -112
- 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 +1 -1
- package/dist/src/commands/lint-per-patch.js +119 -78
- package/dist/src/commands/lint.d.ts +1 -58
- package/dist/src/commands/lint.js +96 -84
- 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-scan.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 +58 -0
- package/dist/src/commands/test-run.js +88 -0
- package/dist/src/commands/test.js +169 -257
- 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 +48 -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 +171 -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-file-ops.d.ts +0 -12
- package/dist/src/core/git-file-ops.js +2 -2
- package/dist/src/core/lint-cache.d.ts +3 -13
- package/dist/src/core/lint-cache.js +11 -5
- package/dist/src/core/mach.d.ts +5 -1
- package/dist/src/core/mach.js +6 -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.js +53 -7
- 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.js +132 -125
- 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 +1 -1
- package/dist/src/core/test-xpcshell-retry.js +4 -2
- 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 +0 -21
- package/dist/src/core/typecheck-shim.js +26 -4
- 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 +105 -0
- 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
|
@@ -105,82 +105,91 @@ export async function renderUnmanagedOnly(unmanagedFiles, totalModified, project
|
|
|
105
105
|
? 'No unmanaged changes'
|
|
106
106
|
: `${unmanagedFiles.length} unmanaged change${unmanagedFiles.length === 1 ? '' : 's'}`);
|
|
107
107
|
}
|
|
108
|
+
/** Renders the cross-patch ownership conflict section. */
|
|
109
|
+
function renderConflictSection(conflict) {
|
|
110
|
+
warn('Cross-patch ownership conflicts (same file claimed by multiple patches):');
|
|
111
|
+
printStatusGroups(conflict);
|
|
112
|
+
for (const entry of conflict) {
|
|
113
|
+
if (entry.claimedBy && entry.claimedBy.length > 0) {
|
|
114
|
+
info(` ${entry.file} — claimed by ${entry.claimedBy.join(', ')}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
info('Run "fireforge status --ownership" for the full conflict table, then repartition with "fireforge re-export --files <paths> <patch>".');
|
|
118
|
+
}
|
|
108
119
|
/** Renders the default classified status buckets. */
|
|
109
120
|
export async function renderDefaultStatus(totalModified, buckets, projectRoot, binaryName) {
|
|
110
121
|
const { conflict, unmanaged, patchBacked, patchOwnedDrift, branding, furnace } = buckets;
|
|
111
122
|
info(`${totalModified} modified file${totalModified === 1 ? '' : 's'}:\n`);
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
123
|
+
// Sections render in this fixed order, separated by a blank line
|
|
124
|
+
// whenever an earlier section already printed (the pre-refactor code
|
|
125
|
+
// expressed the same rule as per-section "any earlier bucket
|
|
126
|
+
// non-empty" conditions).
|
|
127
|
+
const sections = [
|
|
128
|
+
{
|
|
129
|
+
files: conflict,
|
|
130
|
+
label: 'conflict',
|
|
131
|
+
render: () => {
|
|
132
|
+
renderConflictSection(conflict);
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
files: unmanaged,
|
|
137
|
+
label: 'unmanaged',
|
|
138
|
+
render: async () => {
|
|
139
|
+
warn('Unmanaged changes:');
|
|
140
|
+
printStatusGroups(unmanaged);
|
|
141
|
+
await printUnregisteredWarnings(unmanaged, projectRoot, binaryName);
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
files: patchBacked,
|
|
146
|
+
label: 'patch-backed',
|
|
147
|
+
render: () => {
|
|
148
|
+
warn('Patch-backed materialized changes:');
|
|
149
|
+
printStatusGroups(patchBacked);
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
files: patchOwnedDrift,
|
|
154
|
+
label: 'patch-owned drift',
|
|
155
|
+
render: () => {
|
|
156
|
+
warn('Patch-owned drift:');
|
|
157
|
+
printStatusGroups(patchOwnedDrift);
|
|
158
|
+
info('These files are claimed by exactly one patch, but engine/ no longer matches that patch output. Re-export the owning patch after reviewing the manual resolution.');
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
files: branding,
|
|
163
|
+
label: 'branding',
|
|
164
|
+
render: () => {
|
|
165
|
+
warn('Tool-managed branding changes:');
|
|
166
|
+
printStatusGroups(branding);
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
files: furnace,
|
|
171
|
+
label: 'furnace',
|
|
172
|
+
render: () => {
|
|
173
|
+
warn('Furnace-managed component changes:');
|
|
174
|
+
printStatusGroups(furnace);
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
];
|
|
178
|
+
let printedAny = false;
|
|
179
|
+
for (const section of sections) {
|
|
180
|
+
if (section.files.length === 0)
|
|
181
|
+
continue;
|
|
182
|
+
if (printedAny)
|
|
131
183
|
info('');
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}
|
|
135
|
-
if (patchOwnedDrift.length > 0) {
|
|
136
|
-
if (conflict.length > 0 || unmanaged.length > 0 || patchBacked.length > 0)
|
|
137
|
-
info('');
|
|
138
|
-
warn('Patch-owned drift:');
|
|
139
|
-
printStatusGroups(patchOwnedDrift);
|
|
140
|
-
info('These files are claimed by exactly one patch, but engine/ no longer matches that patch output. Re-export the owning patch after reviewing the manual resolution.');
|
|
141
|
-
}
|
|
142
|
-
if (branding.length > 0) {
|
|
143
|
-
if (conflict.length > 0 ||
|
|
144
|
-
unmanaged.length > 0 ||
|
|
145
|
-
patchBacked.length > 0 ||
|
|
146
|
-
patchOwnedDrift.length > 0) {
|
|
147
|
-
info('');
|
|
148
|
-
}
|
|
149
|
-
warn('Tool-managed branding changes:');
|
|
150
|
-
printStatusGroups(branding);
|
|
151
|
-
}
|
|
152
|
-
if (furnace.length > 0) {
|
|
153
|
-
if (conflict.length > 0 ||
|
|
154
|
-
unmanaged.length > 0 ||
|
|
155
|
-
patchBacked.length > 0 ||
|
|
156
|
-
patchOwnedDrift.length > 0 ||
|
|
157
|
-
branding.length > 0) {
|
|
158
|
-
info('');
|
|
159
|
-
}
|
|
160
|
-
warn('Furnace-managed component changes:');
|
|
161
|
-
printStatusGroups(furnace);
|
|
184
|
+
await section.render();
|
|
185
|
+
printedAny = true;
|
|
162
186
|
}
|
|
163
|
-
if (
|
|
164
|
-
unmanaged.length === 0 &&
|
|
165
|
-
patchBacked.length === 0 &&
|
|
166
|
-
patchOwnedDrift.length === 0 &&
|
|
167
|
-
branding.length === 0 &&
|
|
168
|
-
furnace.length === 0) {
|
|
187
|
+
if (!printedAny) {
|
|
169
188
|
info('No changes');
|
|
170
189
|
}
|
|
171
|
-
const parts =
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
if (unmanaged.length > 0)
|
|
175
|
-
parts.push(`${unmanaged.length} unmanaged`);
|
|
176
|
-
if (patchBacked.length > 0)
|
|
177
|
-
parts.push(`${patchBacked.length} patch-backed`);
|
|
178
|
-
if (patchOwnedDrift.length > 0)
|
|
179
|
-
parts.push(`${patchOwnedDrift.length} patch-owned drift`);
|
|
180
|
-
if (branding.length > 0)
|
|
181
|
-
parts.push(`${branding.length} branding`);
|
|
182
|
-
if (furnace.length > 0)
|
|
183
|
-
parts.push(`${furnace.length} furnace`);
|
|
190
|
+
const parts = sections
|
|
191
|
+
.filter((section) => section.files.length > 0)
|
|
192
|
+
.map((section) => `${section.files.length} ${section.label}`);
|
|
184
193
|
outro(parts.join(', '));
|
|
185
194
|
}
|
|
186
195
|
//# sourceMappingURL=status-output.js.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Failure diagnosis for `fireforge test`: maps captured mach output to
|
|
3
|
+
* actionable operator messages (unknown test paths, stale build artifacts,
|
|
4
|
+
* fork-module registration, xpcshell appdir, harness symlinks, mochitest
|
|
5
|
+
* branding interactions), and applies harness-run verdicts from
|
|
6
|
+
* `test-harness-crash.ts` for single and sharded invocations. Split out of
|
|
7
|
+
* `test.ts` to keep both files within the per-file line budget.
|
|
8
|
+
*/
|
|
9
|
+
import { type PostRebuildFailureContext } from '../core/test-harness-output.js';
|
|
10
|
+
import type { TestRunOutcome } from './test-run.js';
|
|
11
|
+
/**
|
|
12
|
+
* Applies the harness-run verdict for a single (non-sharded) invocation:
|
|
13
|
+
* exhausted harness-crash retries and silent zero-TEST-START runs are
|
|
14
|
+
* harness problems with their own messages; everything else flows into
|
|
15
|
+
* the regular non-zero-exit diagnosis chain.
|
|
16
|
+
*/
|
|
17
|
+
export declare function finalizeSingleRunOutcome(outcome: TestRunOutcome, normalizedPaths: string[], binaryName: string, postRebuildContext: PostRebuildFailureContext | undefined): void;
|
|
18
|
+
/**
|
|
19
|
+
* Shard-mode adapter over {@link handleNonZeroTestExit}: produces the
|
|
20
|
+
* diagnosis text as a string (to warn per shard) instead of throwing, so
|
|
21
|
+
* later shards still run and the aggregate error stays singular.
|
|
22
|
+
*/
|
|
23
|
+
export declare function diagnoseShardOutcome(outcome: TestRunOutcome, path: string, binaryName: string, postRebuildContext: PostRebuildFailureContext | undefined): string | undefined;
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Failure diagnosis for `fireforge test`: maps captured mach output to
|
|
4
|
+
* actionable operator messages (unknown test paths, stale build artifacts,
|
|
5
|
+
* fork-module registration, xpcshell appdir, harness symlinks, mochitest
|
|
6
|
+
* branding interactions), and applies harness-run verdicts from
|
|
7
|
+
* `test-harness-crash.ts` for single and sharded invocations. Split out of
|
|
8
|
+
* `test.ts` to keep both files within the per-file line budget.
|
|
9
|
+
*/
|
|
10
|
+
import { buildHarnessCrashMessage, buildNoTestsRanMessage } from '../core/test-harness-crash.js';
|
|
11
|
+
import { buildHarnessEarlyExitMessage, classifyHarnessEarlyExit, completePostRebuildFailureContext, prependPostRebuildFailureContext, } from '../core/test-harness-output.js';
|
|
12
|
+
import { GeneralError } from '../errors/base.js';
|
|
13
|
+
import { BuildError } from '../errors/build.js';
|
|
14
|
+
import { info } from '../utils/logger.js';
|
|
15
|
+
function buildUnknownTestMessage(testPaths) {
|
|
16
|
+
return (`mach could not discover the requested test path${testPaths.length === 1 ? '' : 's'}: ${testPaths.join(', ')}\n\n` +
|
|
17
|
+
'The file may exist, but Firefox does not currently resolve it as a runnable test.\n\n' +
|
|
18
|
+
'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.');
|
|
19
|
+
}
|
|
20
|
+
function buildStaleBuildMessage(postRebuild) {
|
|
21
|
+
if (postRebuild) {
|
|
22
|
+
return ('Firefox test runtime still reported stale-artifact-shaped resource failures after the rebuild completed.\n\n' +
|
|
23
|
+
'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' +
|
|
24
|
+
'Check the first post-rebuild failure above and the raw mach output for the concrete path or module that still fails.');
|
|
25
|
+
}
|
|
26
|
+
return ('Firefox test runtime appears to be using stale build artifacts.\n\n' +
|
|
27
|
+
'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' +
|
|
28
|
+
'Re-run "fireforge build --ui" or "fireforge test --build" and then retry.');
|
|
29
|
+
}
|
|
30
|
+
function hasStaleBuildArtifactsSignal(output) {
|
|
31
|
+
// Deliberately narrow: only fire on branding-specific resource paths
|
|
32
|
+
// that are always a stale-artifact symptom. The earlier pattern also
|
|
33
|
+
// matched `resource:///modules/distribution.sys.mjs`, which surfaced on
|
|
34
|
+
// real packaging / module-resolution failures too (e.g. a fork's
|
|
35
|
+
// `MyBrowserStore.sys.mjs` missing from the installed app dir after a
|
|
36
|
+
// successful build). That false-positive pushed operators toward
|
|
37
|
+
// "rebuild" advice for what was actually a module-registration issue.
|
|
38
|
+
return (/chrome:\/\/branding\/locale\/brand\.properties/i.test(output) ||
|
|
39
|
+
/browser\/branding\/[^/\s]+\/moz\.build/i.test(output));
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Fork-module-not-registered signal. 2026-04-21 eval Finding #14:
|
|
43
|
+
* a fork's test failed with `Failed to load resource:///modules/mybrowser/
|
|
44
|
+
* MyBrowserStore.sys.mjs`. The branding pattern happened to also match
|
|
45
|
+
* because the test harness printed a branding warning during its
|
|
46
|
+
* teardown, and the stale-build branch won by precedence — telling the
|
|
47
|
+
* operator to rebuild when the real fix is to register the module in
|
|
48
|
+
* the fork's `browser/modules/<binary>/moz.build`. Match a
|
|
49
|
+
* `resource:///modules/<binaryName>/` pattern so fork-owned module
|
|
50
|
+
* failures surface the right diagnosis.
|
|
51
|
+
*/
|
|
52
|
+
function hasForkModuleSignal(output, binaryName) {
|
|
53
|
+
const pattern = new RegExp(`Failed to load resource:\\/\\/\\/modules\\/${binaryName.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\$&')}\\/`, 'i');
|
|
54
|
+
return pattern.test(output);
|
|
55
|
+
}
|
|
56
|
+
function buildForkModuleMessage(binaryName) {
|
|
57
|
+
return (`Test failed to load a fork-owned module at resource:///modules/${binaryName}/*.sys.mjs.\n\n` +
|
|
58
|
+
'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' +
|
|
59
|
+
'Check that:\n' +
|
|
60
|
+
` - browser/modules/${binaryName}/moz.build lists the missing module in EXTRA_JS_MODULES.\n` +
|
|
61
|
+
` - browser/modules/moz.build references the ${binaryName}/ subdirectory (DIRS += [...]).\n` +
|
|
62
|
+
' - 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' +
|
|
63
|
+
'Use `fireforge register browser/modules/' +
|
|
64
|
+
binaryName +
|
|
65
|
+
'/<file>.sys.mjs` to add the EXTRA_JS_MODULES entry if it is missing.');
|
|
66
|
+
}
|
|
67
|
+
// Detects the broader xpcshell symptom where every `resource:///modules/...`
|
|
68
|
+
// import fails — the signature of xpcshell running with the wrong app-dir on
|
|
69
|
+
// a manifest that sets `firefox-appdir = "browser"`. Checked AFTER the
|
|
70
|
+
// stale-build signal (which matches the narrower `distribution.sys.mjs`
|
|
71
|
+
// path) so the more specific diagnosis wins when both patterns apply.
|
|
72
|
+
function hasXpcshellAppdirSignal(output) {
|
|
73
|
+
return /Failed to load resource:\/\/\/modules\//i.test(output);
|
|
74
|
+
}
|
|
75
|
+
function buildXpcshellAppdirMessage(injectionAttempted) {
|
|
76
|
+
const isMacos = process.platform === 'darwin';
|
|
77
|
+
const macosNote = isMacos
|
|
78
|
+
? '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'
|
|
79
|
+
: '';
|
|
80
|
+
const triggerLines = injectionAttempted
|
|
81
|
+
? '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'
|
|
82
|
+
: 'Likely triggers:\n' +
|
|
83
|
+
' - 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' +
|
|
84
|
+
' - FireForge could not find an xpcshell.toml above the test path, so the auto-injection never ran.\n\n';
|
|
85
|
+
return ('xpcshell failed to load core resource:///modules/*.sys.mjs imports.\n\n' +
|
|
86
|
+
'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' +
|
|
87
|
+
macosNote +
|
|
88
|
+
triggerLines +
|
|
89
|
+
'Options:\n' +
|
|
90
|
+
' - 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' +
|
|
91
|
+
' - 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' +
|
|
92
|
+
' - 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' +
|
|
93
|
+
' - If the test only touches toolkit chrome (chrome://global/*), drop the `firefox-appdir` setting entirely — toolkit chrome is registered without it.');
|
|
94
|
+
}
|
|
95
|
+
function buildHarnessSymlinkMessage() {
|
|
96
|
+
return ('mach failed while preparing test harness symlinks before the requested tests ran.\n\n' +
|
|
97
|
+
'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.');
|
|
98
|
+
}
|
|
99
|
+
// Detects the `AttributeError: 'MochitestDesktop' object has no attribute
|
|
100
|
+
// 'http3Server'` teardown crash. The attribute is lazy-initialized inside
|
|
101
|
+
// harness code paths that presume chrome://branding resolves correctly; a
|
|
102
|
+
// missing or miswired branding registration short-circuits the setup and
|
|
103
|
+
// leaves the cleanup path looking up an attribute that was never assigned.
|
|
104
|
+
function hasMochitestHttp3ServerSignal(output) {
|
|
105
|
+
return /'MochitestDesktop' object has no attribute 'http3Server'/.test(output);
|
|
106
|
+
}
|
|
107
|
+
function buildMochitestHttp3ServerMessage() {
|
|
108
|
+
return ("Mochitest raised `AttributeError: 'MochitestDesktop' object has no attribute 'http3Server'`.\n\n" +
|
|
109
|
+
'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' +
|
|
110
|
+
'Check that:\n' +
|
|
111
|
+
" - Your fork's branding directory is listed in `browser/branding/moz.build` (or equivalent) and ships a `brand.properties` / `brand.ftl`.\n" +
|
|
112
|
+
' - `chrome://branding/locale/brand.properties` resolves at runtime (try `fireforge run` and inspect the Browser Console).\n' +
|
|
113
|
+
" - The `BROWSER_CHROME_MANIFESTS` entry for your fork's chrome.manifest is registered.\n\n" +
|
|
114
|
+
'This is an upstream Firefox harness interaction; FireForge can only diagnose it.');
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
*
|
|
118
|
+
*/
|
|
119
|
+
function handleNonZeroTestExit(result, normalizedPaths, appdirInjectionAttempted, binaryName, postRebuildContext) {
|
|
120
|
+
if (result.exitCode === 0 || result.exitCode === 130)
|
|
121
|
+
return;
|
|
122
|
+
const combinedOutput = `${result.stdout}\n${result.stderr}`;
|
|
123
|
+
const failureContext = postRebuildContext
|
|
124
|
+
? completePostRebuildFailureContext(postRebuildContext, combinedOutput)
|
|
125
|
+
: undefined;
|
|
126
|
+
const withContext = (message) => prependPostRebuildFailureContext(message, failureContext);
|
|
127
|
+
const throwGeneral = (message) => {
|
|
128
|
+
throw new GeneralError(withContext(message));
|
|
129
|
+
};
|
|
130
|
+
if (/UNKNOWN TEST\b/i.test(combinedOutput)) {
|
|
131
|
+
throwGeneral(buildUnknownTestMessage(normalizedPaths));
|
|
132
|
+
}
|
|
133
|
+
const earlyExit = classifyHarnessEarlyExit(combinedOutput, normalizedPaths);
|
|
134
|
+
if (earlyExit) {
|
|
135
|
+
throwGeneral(buildHarnessEarlyExitMessage(earlyExit, normalizedPaths));
|
|
136
|
+
}
|
|
137
|
+
// Fork-owned module load failures must beat the branding stale-build
|
|
138
|
+
// branch: 2026-04-21 eval (Finding #14) saw a fork's test fail with
|
|
139
|
+
// `Failed to load resource:///modules/mybrowser/MyBrowserStore.sys.mjs`
|
|
140
|
+
// while the harness teardown printed a branding warning that the old
|
|
141
|
+
// stale-build pattern matched, so the operator was told to rebuild
|
|
142
|
+
// when the real fix is to register the missing module.
|
|
143
|
+
if (hasForkModuleSignal(combinedOutput, binaryName)) {
|
|
144
|
+
throwGeneral(buildForkModuleMessage(binaryName));
|
|
145
|
+
}
|
|
146
|
+
// Branding-specific stale-build signals keep priority over the broader
|
|
147
|
+
// xpcshell-appdir hint: when `chrome://branding/locale/brand.properties`
|
|
148
|
+
// fails to resolve, the fix really is "rebuild", not "pass --app-path".
|
|
149
|
+
// But the stale-build check is now narrower — it no longer matches
|
|
150
|
+
// `resource:///modules/distribution.sys.mjs` alone, which was producing
|
|
151
|
+
// false-positive rebuild advice on fork-custom module-load failures
|
|
152
|
+
// (the eval saw this for `MyBrowserStore.sys.mjs`). Cases that once
|
|
153
|
+
// landed on `distribution.sys.mjs` fall through to xpcshell-appdir,
|
|
154
|
+
// which is the more useful diagnosis in practice for `Failed to load
|
|
155
|
+
// resource:///modules/…`.
|
|
156
|
+
if (hasStaleBuildArtifactsSignal(combinedOutput)) {
|
|
157
|
+
throwGeneral(buildStaleBuildMessage(Boolean(failureContext)));
|
|
158
|
+
}
|
|
159
|
+
if (hasXpcshellAppdirSignal(combinedOutput)) {
|
|
160
|
+
throwGeneral(buildXpcshellAppdirMessage(appdirInjectionAttempted));
|
|
161
|
+
}
|
|
162
|
+
if (hasMochitestHttp3ServerSignal(combinedOutput)) {
|
|
163
|
+
throwGeneral(buildMochitestHttp3ServerMessage());
|
|
164
|
+
}
|
|
165
|
+
if (/FileExistsError/i.test(combinedOutput) &&
|
|
166
|
+
/(mochitest|xpcshell|_tests)/i.test(combinedOutput)) {
|
|
167
|
+
throwGeneral(buildHarnessSymlinkMessage());
|
|
168
|
+
}
|
|
169
|
+
if (/invalid filename/i.test(combinedOutput) ||
|
|
170
|
+
/chrome:\/\/mochitests.*not found/i.test(combinedOutput)) {
|
|
171
|
+
info('Hint: The test file may not be registered in browser.toml or jar.mn.');
|
|
172
|
+
info('Run "fireforge register <test-path>" to register it.');
|
|
173
|
+
}
|
|
174
|
+
throw new BuildError(withContext(`Tests failed with exit code ${result.exitCode}. Check the output above for details.`), 'mach test');
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Applies the harness-run verdict for a single (non-sharded) invocation:
|
|
178
|
+
* exhausted harness-crash retries and silent zero-TEST-START runs are
|
|
179
|
+
* harness problems with their own messages; everything else flows into
|
|
180
|
+
* the regular non-zero-exit diagnosis chain.
|
|
181
|
+
*/
|
|
182
|
+
export function finalizeSingleRunOutcome(outcome, normalizedPaths, binaryName, postRebuildContext) {
|
|
183
|
+
if (outcome.verdict.kind === 'harness-crash' && outcome.verdict.signature) {
|
|
184
|
+
throw new GeneralError(buildHarnessCrashMessage(outcome.verdict.signature, outcome.attempts));
|
|
185
|
+
}
|
|
186
|
+
if (outcome.verdict.kind === 'no-tests' && outcome.result.exitCode === 0) {
|
|
187
|
+
// The silent false green: exit 0 plus a "Passed: 0"-style summary with
|
|
188
|
+
// zero TEST-START lines must fail, not pass.
|
|
189
|
+
throw new GeneralError(buildNoTestsRanMessage(0, normalizedPaths));
|
|
190
|
+
}
|
|
191
|
+
handleNonZeroTestExit(outcome.result, normalizedPaths, outcome.appdirInjectionAttempted, binaryName, postRebuildContext);
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Shard-mode adapter over {@link handleNonZeroTestExit}: produces the
|
|
195
|
+
* diagnosis text as a string (to warn per shard) instead of throwing, so
|
|
196
|
+
* later shards still run and the aggregate error stays singular.
|
|
197
|
+
*/
|
|
198
|
+
export function diagnoseShardOutcome(outcome, path, binaryName, postRebuildContext) {
|
|
199
|
+
if (outcome.verdict.kind === 'no-tests' && outcome.result.exitCode === 0) {
|
|
200
|
+
return buildNoTestsRanMessage(0, [path]);
|
|
201
|
+
}
|
|
202
|
+
try {
|
|
203
|
+
handleNonZeroTestExit(outcome.result, [path], outcome.appdirInjectionAttempted, binaryName, postRebuildContext);
|
|
204
|
+
return undefined;
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
return error instanceof Error ? error.message : String(error);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
//# sourceMappingURL=test-diagnose.js.map
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run orchestration for `fireforge test`: bounded harness-crash retries
|
|
3
|
+
* (field reports C1/C2) and sequential per-file sharding of multi-path
|
|
4
|
+
* invocations (field report C3).
|
|
5
|
+
*
|
|
6
|
+
* Sharding exists because passing several browser-chrome files to one
|
|
7
|
+
* mach invocation destabilizes later files — cross-file profile/pref
|
|
8
|
+
* bleed in the shared mochitest profile made the second file time out at
|
|
9
|
+
* window-open while each file passed in isolation. Sequential single-file
|
|
10
|
+
* harness runs cost startup time but make results reproducible; the
|
|
11
|
+
* combined invocation stays available via `--no-shard`.
|
|
12
|
+
*/
|
|
13
|
+
import { type MachCommandResult } from '../core/mach.js';
|
|
14
|
+
import { type HarnessRunVerdict } from '../core/test-harness-crash.js';
|
|
15
|
+
/** Default bounded retry budget for recognized harness crashes. */
|
|
16
|
+
export declare const DEFAULT_HARNESS_RETRIES = 2;
|
|
17
|
+
/** Inputs shared by every harness invocation in one `fireforge test` run. */
|
|
18
|
+
export interface TestRunContext {
|
|
19
|
+
engineDir: string;
|
|
20
|
+
objDir: string | undefined;
|
|
21
|
+
classification: {
|
|
22
|
+
xpcshell: string[];
|
|
23
|
+
nonXpcshell: string[];
|
|
24
|
+
};
|
|
25
|
+
/** Extra mach args before per-shard appdir injection. */
|
|
26
|
+
baseExtraArgs: readonly string[];
|
|
27
|
+
/** Bounded harness-crash retry budget (0 disables retries). */
|
|
28
|
+
harnessRetries: number;
|
|
29
|
+
/** Extra environment variables for the mach process. */
|
|
30
|
+
env?: Record<string, string>;
|
|
31
|
+
}
|
|
32
|
+
/** Outcome of one (possibly retried) harness invocation. */
|
|
33
|
+
export interface TestRunOutcome {
|
|
34
|
+
result: MachCommandResult;
|
|
35
|
+
verdict: HarnessRunVerdict;
|
|
36
|
+
attempts: number;
|
|
37
|
+
/** Whether xpcshell appdir injection was attempted for this invocation. */
|
|
38
|
+
appdirInjectionAttempted: boolean;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Runs one mach test invocation for `paths`, retrying recognized harness
|
|
42
|
+
* crashes up to the configured budget. Every attempt goes through the
|
|
43
|
+
* stale-xpcshell-symlink repair path the single-run flow already used.
|
|
44
|
+
*/
|
|
45
|
+
export declare function runTestsWithRetries(ctx: TestRunContext, paths: string[]): Promise<TestRunOutcome>;
|
|
46
|
+
/** Per-shard summary entry. */
|
|
47
|
+
export interface ShardOutcome {
|
|
48
|
+
path: string;
|
|
49
|
+
outcome: TestRunOutcome;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Runs each requested path as its own sequential harness invocation and
|
|
53
|
+
* prints an aggregate report. Per-shard failures are diagnosed via
|
|
54
|
+
* `diagnoseShardFailure` (which receives the throwing diagnosis chain from
|
|
55
|
+
* the command layer) but downgraded to warnings so every shard runs; a
|
|
56
|
+
* single aggregate error is thrown at the end when any shard did not pass.
|
|
57
|
+
*/
|
|
58
|
+
export declare function runShardedTests(ctx: TestRunContext, paths: string[], diagnoseShardFailure: (outcome: TestRunOutcome, path: string) => string | undefined): Promise<void>;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Run orchestration for `fireforge test`: bounded harness-crash retries
|
|
4
|
+
* (field reports C1/C2) and sequential per-file sharding of multi-path
|
|
5
|
+
* invocations (field report C3).
|
|
6
|
+
*
|
|
7
|
+
* Sharding exists because passing several browser-chrome files to one
|
|
8
|
+
* mach invocation destabilizes later files — cross-file profile/pref
|
|
9
|
+
* bleed in the shared mochitest profile made the second file time out at
|
|
10
|
+
* window-open while each file passed in isolation. Sequential single-file
|
|
11
|
+
* harness runs cost startup time but make results reproducible; the
|
|
12
|
+
* combined invocation stays available via `--no-shard`.
|
|
13
|
+
*/
|
|
14
|
+
import { testWithOutput } from '../core/mach.js';
|
|
15
|
+
import { buildHarnessCrashMessage, classifyHarnessRun, } from '../core/test-harness-crash.js';
|
|
16
|
+
import { retryAfterXpcshellSymlinkRepair } from '../core/test-xpcshell-retry.js';
|
|
17
|
+
import { BuildError } from '../errors/build.js';
|
|
18
|
+
import { info, note, warn } from '../utils/logger.js';
|
|
19
|
+
import { maybeInjectAppdirArg } from './test-appdir.js';
|
|
20
|
+
/** Default bounded retry budget for recognized harness crashes. */
|
|
21
|
+
export const DEFAULT_HARNESS_RETRIES = 2;
|
|
22
|
+
/**
|
|
23
|
+
* Runs one mach test invocation for `paths`, retrying recognized harness
|
|
24
|
+
* crashes up to the configured budget. Every attempt goes through the
|
|
25
|
+
* stale-xpcshell-symlink repair path the single-run flow already used.
|
|
26
|
+
*/
|
|
27
|
+
export async function runTestsWithRetries(ctx, paths) {
|
|
28
|
+
const extraArgs = [...ctx.baseExtraArgs];
|
|
29
|
+
const appdirInjectionAttempted = await maybeInjectAppdirArg(ctx.engineDir, paths, ctx.objDir, extraArgs);
|
|
30
|
+
const maxAttempts = Math.max(1, ctx.harnessRetries + 1);
|
|
31
|
+
let attempts = 0;
|
|
32
|
+
let result;
|
|
33
|
+
let verdict;
|
|
34
|
+
for (;;) {
|
|
35
|
+
attempts += 1;
|
|
36
|
+
result = ctx.env
|
|
37
|
+
? await testWithOutput(ctx.engineDir, paths, extraArgs, ctx.env)
|
|
38
|
+
: await testWithOutput(ctx.engineDir, paths, extraArgs);
|
|
39
|
+
result = await retryAfterXpcshellSymlinkRepair(ctx.engineDir, ctx.objDir, result, ctx.classification, paths, extraArgs, ctx.env);
|
|
40
|
+
const combined = `${result.stdout}\n${result.stderr}`;
|
|
41
|
+
verdict = classifyHarnessRun(result.exitCode, combined, paths);
|
|
42
|
+
if (verdict.kind !== 'harness-crash' || attempts >= maxAttempts)
|
|
43
|
+
break;
|
|
44
|
+
warn(`Harness crash detected (${verdict.signature?.reason ?? 'unknown shape'}): ` +
|
|
45
|
+
`${verdict.signature?.line ?? ''}\n` +
|
|
46
|
+
`Retrying (attempt ${attempts + 1} of ${maxAttempts})...`);
|
|
47
|
+
}
|
|
48
|
+
return { result, verdict, attempts, appdirInjectionAttempted };
|
|
49
|
+
}
|
|
50
|
+
const SHARD_STATUS_LABEL = {
|
|
51
|
+
'tests-ran-ok': 'PASS',
|
|
52
|
+
'test-failures': 'FAIL',
|
|
53
|
+
'harness-crash': 'CRASH',
|
|
54
|
+
'no-tests': 'NO-TESTS',
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Runs each requested path as its own sequential harness invocation and
|
|
58
|
+
* prints an aggregate report. Per-shard failures are diagnosed via
|
|
59
|
+
* `diagnoseShardFailure` (which receives the throwing diagnosis chain from
|
|
60
|
+
* the command layer) but downgraded to warnings so every shard runs; a
|
|
61
|
+
* single aggregate error is thrown at the end when any shard did not pass.
|
|
62
|
+
*/
|
|
63
|
+
export async function runShardedTests(ctx, paths, diagnoseShardFailure) {
|
|
64
|
+
const shards = [];
|
|
65
|
+
for (const [index, path] of paths.entries()) {
|
|
66
|
+
info(`— Shard ${index + 1}/${paths.length}: ${path}`);
|
|
67
|
+
const outcome = await runTestsWithRetries(ctx, [path]);
|
|
68
|
+
shards.push({ path, outcome });
|
|
69
|
+
if (outcome.verdict.kind === 'harness-crash' && outcome.verdict.signature) {
|
|
70
|
+
warn(buildHarnessCrashMessage(outcome.verdict.signature, outcome.attempts));
|
|
71
|
+
}
|
|
72
|
+
else if (outcome.verdict.kind !== 'tests-ran-ok') {
|
|
73
|
+
const diagnosis = diagnoseShardFailure(outcome, path);
|
|
74
|
+
if (diagnosis)
|
|
75
|
+
warn(diagnosis);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const lines = shards.map(({ path, outcome }) => `${SHARD_STATUS_LABEL[outcome.verdict.kind].padEnd(8)} ${path}` +
|
|
79
|
+
(outcome.attempts > 1 ? ` (${outcome.attempts} attempts)` : ''));
|
|
80
|
+
const failing = shards.filter(({ outcome }) => outcome.verdict.kind !== 'tests-ran-ok');
|
|
81
|
+
note(`${lines.join('\n')}\n\n${shards.length - failing.length}/${shards.length} shard(s) passed`, 'Sharded Test Summary');
|
|
82
|
+
if (failing.length > 0) {
|
|
83
|
+
throw new BuildError(`${failing.length} of ${shards.length} sharded test run(s) did not pass: ` +
|
|
84
|
+
`${failing.map(({ path }) => path).join(', ')}. ` +
|
|
85
|
+
'See the per-shard diagnosis above. Use --no-shard to reproduce the combined single-invocation behaviour.', 'mach test');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=test-run.js.map
|