@hominis/fireforge 0.47.1 → 0.49.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 +83 -0
- package/dist/bin/fireforge.js +5 -0
- package/dist/build-info.json +4 -4
- package/dist/src/cli.js +16 -28
- package/dist/src/commands/build.js +30 -4
- package/dist/src/commands/doctor-engine-eslint.d.ts +3 -0
- package/dist/src/commands/doctor-engine-eslint.js +12 -0
- package/dist/src/commands/doctor-macos-sdk.d.ts +8 -0
- package/dist/src/commands/doctor-macos-sdk.js +158 -0
- package/dist/src/commands/doctor-source-pin.js +9 -2
- package/dist/src/commands/doctor.js +4 -0
- package/dist/src/commands/download.js +64 -3
- package/dist/src/commands/furnace/apply.js +10 -4
- package/dist/src/commands/furnace/create-templates.js +0 -3
- package/dist/src/commands/import.js +5 -2
- package/dist/src/commands/lint-per-patch.js +44 -6
- package/dist/src/commands/lint-per-run-checkjs.d.ts +3 -0
- package/dist/src/commands/lint-per-run-checkjs.js +1 -0
- package/dist/src/commands/lint.js +14 -0
- package/dist/src/commands/manifest.d.ts +1 -2
- package/dist/src/commands/patch/delete.js +18 -4
- package/dist/src/commands/re-export-lint.d.ts +17 -2
- package/dist/src/commands/re-export-lint.js +33 -4
- package/dist/src/commands/re-export-options.js +7 -0
- package/dist/src/commands/re-export-register.js +1 -0
- package/dist/src/commands/re-export-removed.d.ts +4 -0
- package/dist/src/commands/re-export-removed.js +49 -0
- package/dist/src/commands/re-export-single.js +4 -1
- package/dist/src/commands/re-export.js +8 -2
- package/dist/src/commands/rebase/confirm.d.ts +12 -1
- package/dist/src/commands/rebase/confirm.js +6 -3
- package/dist/src/commands/rebase/index.js +77 -13
- package/dist/src/commands/run.js +43 -7
- package/dist/src/commands/status.js +5 -3
- package/dist/src/commands/test-browser-preflight.d.ts +28 -0
- package/dist/src/commands/test-browser-preflight.js +181 -0
- package/dist/src/commands/test-build-phase.js +30 -7
- package/dist/src/commands/test-checkjs.d.ts +3 -0
- package/dist/src/commands/test-checkjs.js +27 -0
- package/dist/src/commands/test-diagnose.js +3 -0
- package/dist/src/commands/test-harness-env.d.ts +5 -6
- package/dist/src/commands/test-harness-env.js +27 -7
- package/dist/src/commands/test-modes.d.ts +2 -4
- package/dist/src/commands/test-register.js +11 -5
- package/dist/src/commands/test-run.d.ts +2 -0
- package/dist/src/commands/test-run.js +21 -1
- package/dist/src/commands/test-verdict.d.ts +2 -2
- package/dist/src/commands/test.js +83 -211
- package/dist/src/commands/typecheck.d.ts +5 -0
- package/dist/src/commands/typecheck.js +7 -1
- package/dist/src/core/build-baseline-types.d.ts +12 -14
- package/dist/src/core/build-baseline-validate.d.ts +3 -0
- package/dist/src/core/build-baseline-validate.js +53 -0
- package/dist/src/core/build-baseline.d.ts +4 -3
- package/dist/src/core/build-baseline.js +23 -11
- package/dist/src/core/build-prepare.d.ts +2 -0
- package/dist/src/core/build-prepare.js +67 -35
- package/dist/src/core/component-source-guard.d.ts +20 -0
- package/dist/src/core/component-source-guard.js +68 -0
- package/dist/src/core/config-paths.d.ts +1 -1
- package/dist/src/core/config-paths.js +1 -0
- package/dist/src/core/config-validate.js +7 -0
- package/dist/src/core/coverage-extend.d.ts +5 -34
- package/dist/src/core/coverage-extend.js +26 -37
- package/dist/src/core/discard-baseline.js +2 -1
- package/dist/src/core/engine-write-boundary.d.ts +5 -0
- package/dist/src/core/engine-write-boundary.js +41 -0
- package/dist/src/core/forward-registrations.d.ts +33 -0
- package/dist/src/core/forward-registrations.js +16 -0
- package/dist/src/core/furnace-apply-helpers.js +8 -2
- package/dist/src/core/furnace-apply.js +7 -0
- package/dist/src/core/furnace-validate-accessibility.js +9 -12
- package/dist/src/core/furnace-validate.js +0 -43
- package/dist/src/core/gecko-dom-types.d.ts +2 -0
- package/dist/src/core/gecko-dom-types.js +17 -0
- package/dist/src/core/git-file-ops.d.ts +21 -0
- package/dist/src/core/git-file-ops.js +75 -0
- package/dist/src/core/git-status.d.ts +14 -0
- package/dist/src/core/git-status.js +41 -2
- package/dist/src/core/lit-template-regions.d.ts +4 -0
- package/dist/src/core/lit-template-regions.js +50 -0
- package/dist/src/core/mach-build-artifacts.d.ts +6 -6
- package/dist/src/core/mach-build-artifacts.js +13 -7
- package/dist/src/core/mach-mozconfig.d.ts +2 -1
- package/dist/src/core/mach-mozconfig.js +2 -0
- package/dist/src/core/mach-resource-shim.d.ts +1 -1
- package/dist/src/core/mach-resource-shim.js +2 -0
- package/dist/src/core/marionette-port.d.ts +2 -0
- package/dist/src/core/marionette-port.js +38 -53
- package/dist/src/core/mochitest-server-port.d.ts +1 -1
- package/dist/src/core/mochitest-server-port.js +20 -17
- package/dist/src/core/objdir-maintenance.d.ts +10 -0
- package/dist/src/core/objdir-maintenance.js +151 -0
- package/dist/src/core/patch-apply-fuzz.d.ts +12 -0
- package/dist/src/core/patch-apply-fuzz.js +27 -6
- package/dist/src/core/patch-apply.d.ts +13 -0
- package/dist/src/core/patch-apply.js +27 -2
- package/dist/src/core/patch-lint-checkjs-cache.d.ts +18 -0
- package/dist/src/core/patch-lint-checkjs-cache.js +45 -0
- package/dist/src/core/patch-lint-checkjs.d.ts +6 -0
- package/dist/src/core/patch-lint-checkjs.js +10 -2
- package/dist/src/core/patch-lint-cross.d.ts +7 -1
- package/dist/src/core/patch-lint-cross.js +13 -12
- package/dist/src/core/patch-lint-forward-registration-extended.d.ts +38 -0
- package/dist/src/core/patch-lint-forward-registration-extended.js +240 -0
- package/dist/src/core/patch-lint-forward-registration.d.ts +18 -5
- package/dist/src/core/patch-lint-forward-registration.js +83 -37
- package/dist/src/core/patch-lint.d.ts +11 -0
- package/dist/src/core/patch-lint.js +20 -2
- package/dist/src/core/patch-transform.d.ts +14 -13
- package/dist/src/core/patch-transform.js +29 -17
- package/dist/src/core/preflight-wait.d.ts +2 -0
- package/dist/src/core/preflight-wait.js +24 -0
- package/dist/src/core/process-owner.d.ts +9 -0
- package/dist/src/core/process-owner.js +43 -0
- package/dist/src/core/rebase-dry-run.d.ts +48 -0
- package/dist/src/core/rebase-dry-run.js +105 -0
- package/dist/src/core/run-profile.d.ts +50 -0
- package/dist/src/core/run-profile.js +131 -0
- package/dist/src/core/status-binary.d.ts +1 -1
- package/dist/src/core/status-binary.js +3 -1
- package/dist/src/core/status-classify.js +29 -2
- package/dist/src/core/test-harness-crash.d.ts +1 -1
- package/dist/src/core/test-harness-crash.js +5 -5
- package/dist/src/core/test-host-state.d.ts +14 -0
- package/dist/src/core/test-host-state.js +80 -0
- package/dist/src/core/test-profile-files.d.ts +17 -0
- package/dist/src/core/test-profile-files.js +75 -0
- package/dist/src/core/test-stale-check.d.ts +6 -7
- package/dist/src/core/test-stale-check.js +9 -32
- package/dist/src/core/token-category.d.ts +7 -26
- package/dist/src/core/token-category.js +68 -121
- package/dist/src/core/token-inventory.js +12 -7
- package/dist/src/core/toolchain-preflight.d.ts +6 -0
- package/dist/src/core/toolchain-preflight.js +1 -1
- package/dist/src/core/typecheck.d.ts +11 -1
- package/dist/src/core/typecheck.js +86 -12
- package/dist/src/errors/rebase.d.ts +9 -0
- package/dist/src/errors/rebase.js +20 -0
- package/dist/src/index.d.ts +3 -1
- package/dist/src/index.js +2 -1
- package/dist/src/types/commands/options.d.ts +29 -3
- package/dist/src/types/config.d.ts +10 -0
- package/dist/src/utils/assert.js +2 -7
- package/dist/src/utils/child-env.d.ts +0 -3
- package/dist/src/utils/child-env.js +0 -3
- package/dist/src/utils/concurrency.d.ts +2 -2
- package/dist/src/utils/concurrency.js +15 -4
- package/dist/src/utils/fs.js +11 -1
- package/dist/src/utils/options.d.ts +16 -1
- package/dist/src/utils/options.js +5 -3
- package/dist/src/utils/paths.js +3 -2
- package/dist/src/utils/process-group.d.ts +1 -2
- package/dist/src/utils/process-group.js +1 -2
- package/dist/src/utils/process-input.d.ts +27 -0
- package/dist/src/utils/process-input.js +46 -0
- package/dist/src/utils/process.d.ts +5 -2
- package/dist/src/utils/process.js +21 -8
- package/dist/src/utils/validation.d.ts +11 -0
- package/dist/src/utils/validation.js +43 -0
- package/docs/configuration.md +82 -0
- package/docs/exit-codes.md +1 -1
- package/docs/lifecycle-invariants.md +13 -8
- package/docs/machine-output.md +36 -19
- package/docs/patch-workflow.md +50 -1
- package/docs/testing.md +77 -12
- package/package.json +10 -5
|
@@ -11,19 +11,20 @@
|
|
|
11
11
|
* Supports `--continue` (resume after manual fix) and `--abort` (cancel).
|
|
12
12
|
*/
|
|
13
13
|
import { Command } from 'commander';
|
|
14
|
-
import { getProjectPaths, loadConfig } from '../../core/config.js';
|
|
14
|
+
import { getProjectPaths, loadConfig, loadState } from '../../core/config.js';
|
|
15
15
|
import { assertEngineGitReady } from '../../core/engine-precondition.js';
|
|
16
16
|
import { clearAppliedFurnaceState } from '../../core/furnace-config.js';
|
|
17
17
|
import { getHead, resetChanges } from '../../core/git.js';
|
|
18
18
|
import { discoverPatches } from '../../core/patch-files.js';
|
|
19
19
|
import { loadPatchesManifest } from '../../core/patch-manifest.js';
|
|
20
20
|
import { getPatchSourceVersion } from '../../core/patch-source-metadata.js';
|
|
21
|
+
import { replayQueueIndexOnly } from '../../core/rebase-dry-run.js';
|
|
21
22
|
import { getRebaseSessionPath, readRebaseSession, saveRebaseSession, } from '../../core/rebase-session.js';
|
|
22
23
|
import { GeneralError } from '../../errors/base.js';
|
|
23
|
-
import { CorruptRebaseSessionError, RebaseSessionExistsError } from '../../errors/rebase.js';
|
|
24
|
-
import { info, intro, outro, spinner } from '../../utils/logger.js';
|
|
24
|
+
import { CorruptRebaseSessionError, RebaseDryRunRejectError, RebaseSessionExistsError, } from '../../errors/rebase.js';
|
|
25
|
+
import { info, intro, outro, spinner, warn } from '../../utils/logger.js';
|
|
25
26
|
import { addWaitLockOption, commanderArgParser, pickDefined, resolveWaitLockSeconds, } from '../../utils/options.js';
|
|
26
|
-
import { parsePositiveIntegerFlag } from '../../utils/validation.js';
|
|
27
|
+
import { compareFirefoxVersions, parsePositiveIntegerFlag } from '../../utils/validation.js';
|
|
27
28
|
import { handleAbort } from './abort.js';
|
|
28
29
|
import { confirmDirtyEngineReset } from './confirm.js';
|
|
29
30
|
import { handleContinue } from './continue.js';
|
|
@@ -57,23 +58,51 @@ async function handleFreshStart(projectRoot, options) {
|
|
|
57
58
|
if (!manifest || manifest.patches.length === 0) {
|
|
58
59
|
throw new GeneralError('No patches found in manifest. Nothing to rebase.');
|
|
59
60
|
}
|
|
60
|
-
// Determine the "from" version from the
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
// Determine the "from" version from the patch stamps. A queue can carry
|
|
62
|
+
// several: patches re-exported since the last hop are stamped newer than
|
|
63
|
+
// untouched ones. The oldest stamp is "from", compared semantically (a
|
|
64
|
+
// lexical sort put 153.10.0esr before 153.2.0esr).
|
|
65
|
+
const versionCounts = new Map();
|
|
66
|
+
for (const patch of manifest.patches) {
|
|
67
|
+
const version = getPatchSourceVersion(patch);
|
|
68
|
+
versionCounts.set(version, (versionCounts.get(version) ?? 0) + 1);
|
|
69
|
+
}
|
|
70
|
+
const sortedVersions = [...versionCounts.keys()].sort(compareFirefoxVersions);
|
|
64
71
|
const fromVersion = sortedVersions[0] ?? currentVersion;
|
|
65
|
-
const fromProduct =
|
|
66
|
-
|
|
72
|
+
const fromProduct = manifest.patches.find((p) => getPatchSourceVersion(p) === fromVersion)?.sourceProduct ??
|
|
73
|
+
config.firefox.product;
|
|
74
|
+
if (versionCounts.size === 1 && fromVersion === currentVersion) {
|
|
67
75
|
info('All patches already match the current Firefox version. Nothing to rebase.');
|
|
68
76
|
outro('Rebase not needed');
|
|
69
77
|
return;
|
|
70
78
|
}
|
|
71
79
|
info(`Rebasing patches: ${fromVersion} → ${currentVersion}`);
|
|
80
|
+
if (versionCounts.size > 1) {
|
|
81
|
+
info(`Patch stamps: ${sortedVersions.map((v) => `${versionCounts.get(v)} at ${v}`).join(', ')}`);
|
|
82
|
+
}
|
|
83
|
+
// The rebase replays onto whatever engine/ holds. The pin is the target,
|
|
84
|
+
// so an engine still on another version means the download step has not
|
|
85
|
+
// run yet and every verdict below would be against the wrong tree.
|
|
86
|
+
const engineVersion = (await loadState(projectRoot)).downloadedVersion;
|
|
87
|
+
if (engineVersion !== undefined && engineVersion !== currentVersion) {
|
|
88
|
+
warn(`engine/ holds ${engineVersion}, not the pinned target ${currentVersion}. ` +
|
|
89
|
+
'Run "fireforge download --force" first, or the rebase applies onto the old source.');
|
|
90
|
+
}
|
|
72
91
|
info(`Found ${manifest.patches.length} patch(es)`);
|
|
73
92
|
info(`Max context-reduction steps (fuzz-like): ${maxFuzz}`);
|
|
74
93
|
if (isDryRun) {
|
|
75
|
-
|
|
76
|
-
|
|
94
|
+
const patches = await discoverPatches(paths.patches);
|
|
95
|
+
const replaySpinner = spinner(`Replaying ${patches.length} patch(es) on a private index...`);
|
|
96
|
+
let replay;
|
|
97
|
+
try {
|
|
98
|
+
replay = await replayQueueIndexOnly(paths.engine, patches, maxFuzz);
|
|
99
|
+
replaySpinner.stop('Replay finished; engine/ untouched');
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
replaySpinner.error('Replay failed');
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
reportDryRunReplay(replay);
|
|
77
106
|
return;
|
|
78
107
|
}
|
|
79
108
|
if (!(await confirmDirtyEngineReset({
|
|
@@ -114,6 +143,41 @@ async function handleFreshStart(projectRoot, options) {
|
|
|
114
143
|
// Run the patch loop
|
|
115
144
|
await runPatchLoop(projectRoot, session, paths, maxFuzz, resolveWaitLockSeconds(options.waitLock));
|
|
116
145
|
}
|
|
146
|
+
/**
|
|
147
|
+
* Prints one line per patch and a tally, then fails the run when any patch
|
|
148
|
+
* rejects: a dry run that exits 0 must mean the real rebase would apply the
|
|
149
|
+
* whole queue.
|
|
150
|
+
*/
|
|
151
|
+
function reportDryRunReplay(replay) {
|
|
152
|
+
let reduced = 0;
|
|
153
|
+
const rejected = [];
|
|
154
|
+
for (const [index, verdict] of replay.verdicts.entries()) {
|
|
155
|
+
const cascade = replay.firstRejectIndex !== undefined && index > replay.firstRejectIndex
|
|
156
|
+
? ' (after an earlier reject; may cascade from it)'
|
|
157
|
+
: '';
|
|
158
|
+
switch (verdict.outcome) {
|
|
159
|
+
case 'clean':
|
|
160
|
+
info(` clean ${verdict.filename}`);
|
|
161
|
+
break;
|
|
162
|
+
case 'reduced-context':
|
|
163
|
+
reduced += 1;
|
|
164
|
+
info(` reduced context ${verdict.filename} (step ${verdict.step}, ${verdict.contextArg})`);
|
|
165
|
+
break;
|
|
166
|
+
case 'reject': {
|
|
167
|
+
rejected.push(verdict.filename);
|
|
168
|
+
const files = verdict.files.length > 0 ? verdict.files.join(', ') : 'see git output';
|
|
169
|
+
warn(` reject ${verdict.filename}: ${files}${cascade}`);
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const clean = replay.verdicts.length - reduced - rejected.length;
|
|
175
|
+
info(`[dry-run] ${replay.verdicts.length} patch(es): ${clean} clean, ${reduced} with reduced context, ${rejected.length} rejected.`);
|
|
176
|
+
if (rejected.length > 0) {
|
|
177
|
+
throw new RebaseDryRunRejectError(rejected);
|
|
178
|
+
}
|
|
179
|
+
outro('Dry run complete: every patch applies');
|
|
180
|
+
}
|
|
117
181
|
// ── Public API ──
|
|
118
182
|
/**
|
|
119
183
|
* Runs the rebase command to orchestrate a Firefox source version upgrade.
|
|
@@ -136,7 +200,7 @@ export function registerRebase(program, { getProjectRoot, withErrorHandling }) {
|
|
|
136
200
|
.description('Semi-automated Firefox source version upgrade — apply patches with drift tolerance and re-export')
|
|
137
201
|
.option('--continue', 'Resume after manually resolving a failed patch')
|
|
138
202
|
.option('--abort', 'Cancel the rebase and restore engine to pre-rebase state')
|
|
139
|
-
.option('--dry-run', '
|
|
203
|
+
.option('--dry-run', 'Replay the queue onto the engine base commit in a private index and report each patch as clean, reduced-context or reject (exit 6 on any reject); engine/ is not touched')
|
|
140
204
|
.option('--max-fuzz <n>', 'Maximum context-reduction steps for git apply -C<n> (fuzz-like drift tolerance; default: 3)', commanderArgParser((v) => parsePositiveIntegerFlag('--max-fuzz', v)))
|
|
141
205
|
.option('-y, --yes', 'Skip dirty-tree confirmation prompt');
|
|
142
206
|
addWaitLockOption(command).action(withErrorHandling(async (options) => {
|
package/dist/src/commands/run.js
CHANGED
|
@@ -8,6 +8,7 @@ import { assertEngineExists } from '../core/engine-precondition.js';
|
|
|
8
8
|
import { warnIfFurnaceStale } from '../core/furnace-staleness.js';
|
|
9
9
|
import { hasBuildArtifacts, hasRunnableBundle, run, runMachSmoke } from '../core/mach.js';
|
|
10
10
|
import { assertBuildArtifacts } from '../core/mach-build-artifacts.js';
|
|
11
|
+
import { resolveRunProfile, sweepAbandonedRunProfiles, } from '../core/run-profile.js';
|
|
11
12
|
import { compileAllowlistFromFile, compileAllowlistFromStrings, findOverCapAllowlistEntries, matchAllowlist, matchesSmokeError, } from '../core/smoke-patterns.js';
|
|
12
13
|
import { GeneralError, InvalidArgumentError } from '../errors/base.js';
|
|
13
14
|
import { BuildError } from '../errors/build.js';
|
|
@@ -97,12 +98,38 @@ export async function runCommand(projectRoot, options = {}) {
|
|
|
97
98
|
await warnIfFurnaceStale(projectRoot);
|
|
98
99
|
// Clean stale profile state to prevent silent startup failures
|
|
99
100
|
await cleanDevProfile(paths.engine);
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
// Reclaim temporary profiles a SIGKILLed FireForge could not remove.
|
|
102
|
+
const swept = await sweepAbandonedRunProfiles();
|
|
103
|
+
if (swept > 0)
|
|
104
|
+
verbose(`Removed ${swept} abandoned temporary run profile(s).`);
|
|
105
|
+
const profile = await resolveRunProfile({
|
|
106
|
+
profile: options.profile,
|
|
107
|
+
tempProfile: options.tempProfile,
|
|
108
|
+
smoke: options.smokeExit !== undefined,
|
|
109
|
+
});
|
|
110
|
+
try {
|
|
111
|
+
if (options.smokeExit !== undefined) {
|
|
112
|
+
await runSmokeExit(paths.engine, options, profile);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
info('Launching browser...\n');
|
|
116
|
+
await runInteractive(paths.engine, options, profile);
|
|
117
|
+
}
|
|
118
|
+
finally {
|
|
119
|
+
await profile.dispose();
|
|
103
120
|
}
|
|
104
|
-
|
|
105
|
-
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Launches the browser under `mach run` for an interactive session.
|
|
124
|
+
* @param engineDir - Path to the engine directory
|
|
125
|
+
* @param options - Run options
|
|
126
|
+
* @param profile - The profile this run launches with
|
|
127
|
+
*/
|
|
128
|
+
async function runInteractive(engineDir, options, profile) {
|
|
129
|
+
const exitCode = await run(engineDir, [
|
|
130
|
+
...(options.headless ? ['--headless'] : []),
|
|
131
|
+
...profile.args,
|
|
132
|
+
]);
|
|
106
133
|
// Exit-code whitelist:
|
|
107
134
|
// 0: clean shutdown
|
|
108
135
|
// 130: SIGINT (Ctrl+C), user-initiated termination
|
|
@@ -121,7 +148,7 @@ export async function runCommand(projectRoot, options = {}) {
|
|
|
121
148
|
* exit contract. The deadline-fires-SIGTERM path is treated as a clean
|
|
122
149
|
* window iff no unallowed errors were observed.
|
|
123
150
|
*/
|
|
124
|
-
async function runSmokeExit(engineDir, options) {
|
|
151
|
+
async function runSmokeExit(engineDir, options, profile) {
|
|
125
152
|
// Windows lacks the POSIX process-group primitives --smoke-exit leans on to
|
|
126
153
|
// SIGTERM the whole mach → python → firefox tree. Running through anyway
|
|
127
154
|
// would only kill the top-level wrapper and orphan Firefox content
|
|
@@ -194,10 +221,17 @@ async function runSmokeExit(engineDir, options) {
|
|
|
194
221
|
};
|
|
195
222
|
warnIfHeadedOnDesktop(options.headless === true);
|
|
196
223
|
info(`Launching browser (smoke-exit after ${smokeExit}s)...\n`);
|
|
224
|
+
if (profile.temporary) {
|
|
225
|
+
info(`Profile: fresh temporary profile (removed after the run; --profile <path> to use a named one)`);
|
|
226
|
+
}
|
|
227
|
+
else if (profile.dir !== undefined) {
|
|
228
|
+
info(`Profile: ${profile.dir}`);
|
|
229
|
+
}
|
|
197
230
|
const startedAt = Date.now();
|
|
198
231
|
let result;
|
|
199
232
|
try {
|
|
200
|
-
|
|
233
|
+
const machArgs = ['run', ...(options.headless ? ['--headless'] : []), ...profile.args];
|
|
234
|
+
result = await runMachSmoke(machArgs, engineDir, {
|
|
201
235
|
smokeTimeoutMs,
|
|
202
236
|
onStdoutLine: (line) => {
|
|
203
237
|
handleLine('stdout', line);
|
|
@@ -358,6 +392,8 @@ export function registerRun(program, { getProjectRoot, withErrorHandling }) {
|
|
|
358
392
|
.option('--console-allow-file <path>', 'Newline-delimited allowlist regex file. Blank lines and # comments are ignored. A "# max-hits: N" comment directly above an entry caps how many lines it may match in one run; more than that fails the run with exit 12.')
|
|
359
393
|
.option('--capture-console <file>', 'Mirror captured console output to <file> for post-exit inspection.')
|
|
360
394
|
.option('--headless', 'Launch the browser with --headless. Recommended for --smoke-exit on a shared desktop: input into a headed smoke window contaminates the console capture.')
|
|
395
|
+
.option('--profile <path>', 'Launch with this profile directory (passed to Firefox as -profile). Left in place after the run. With --smoke-exit it replaces the default fresh profile.')
|
|
396
|
+
.option('--temp-profile', 'Launch with a fresh temporary profile that is removed when the run ends. The default for --smoke-exit.')
|
|
361
397
|
.action(withErrorHandling(
|
|
362
398
|
// `args` is the variadic positional, not a Commander flag, so it
|
|
363
399
|
// never appears in this object.
|
|
@@ -6,7 +6,7 @@ import { assertEngineExists } from '../core/engine-precondition.js';
|
|
|
6
6
|
import { formatEngineSessionLockStatus, readEngineSessionLockStatus, } from '../core/engine-session-lock.js';
|
|
7
7
|
import { collectFurnaceManagedPrefixes } from '../core/furnace-config.js';
|
|
8
8
|
import { getHead, getStatusWithCodes, isGitRepository, isMissingHeadError } from '../core/git.js';
|
|
9
|
-
import {
|
|
9
|
+
import { listUntrackedFilesInDirs, resolveMaxUntrackedFilesPerDir } from '../core/git-status.js';
|
|
10
10
|
import { renderOwnershipTable } from '../core/ownership-table.js';
|
|
11
11
|
import { loadPatchesManifest } from '../core/patch-manifest.js';
|
|
12
12
|
import { readProcessCpuSeconds } from '../core/process-cpu-time.js';
|
|
@@ -49,9 +49,11 @@ function renderTruncationBanner(truncations) {
|
|
|
49
49
|
async function expandDirectoryEntries(files, engineDir) {
|
|
50
50
|
const expanded = [];
|
|
51
51
|
const truncations = [];
|
|
52
|
+
const isCollapsedUntracked = (entry) => entry.file.endsWith('/') && entry.status.includes('?');
|
|
53
|
+
const untrackedByDir = await listUntrackedFilesInDirs(engineDir, files.filter(isCollapsedUntracked).map((entry) => entry.file));
|
|
52
54
|
for (const entry of files) {
|
|
53
|
-
if (
|
|
54
|
-
const individualFiles =
|
|
55
|
+
if (isCollapsedUntracked(entry)) {
|
|
56
|
+
const individualFiles = untrackedByDir.get(entry.file) ?? [];
|
|
55
57
|
const cap = getMaxUntrackedFilesPerDir();
|
|
56
58
|
if (individualFiles.length > cap) {
|
|
57
59
|
// Recorded once here, reported once by renderTruncationBanner. The
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { TestOptions } from '../types/commands/index.js';
|
|
2
|
+
import type { FireForgeConfig } from '../types/config.js';
|
|
3
|
+
/**
|
|
4
|
+
* Runs the `--doctor` marionette handshake probe. With no test paths the
|
|
5
|
+
* probe is the entire command (returns `'stop'` after reporting). With
|
|
6
|
+
* paths it gates the mach invocation, where a FAIL throws before mach runs.
|
|
7
|
+
*/
|
|
8
|
+
export declare function runDoctorPreflight(args: {
|
|
9
|
+
engineDir: string;
|
|
10
|
+
effectivePort: number | undefined;
|
|
11
|
+
hasTestPaths: boolean;
|
|
12
|
+
objDir: string | undefined;
|
|
13
|
+
binaryName: string;
|
|
14
|
+
launchablePath: string | undefined;
|
|
15
|
+
}): Promise<'stop' | 'continue'>;
|
|
16
|
+
/**
|
|
17
|
+
* Auto-forwards `--marionette-port` to mach (`--setpref=marionette.port`
|
|
18
|
+
* for the listener, `--marionette=127.0.0.1:<n>` for the mochitest
|
|
19
|
+
* client), skipping each piece the operator already forwarded via
|
|
20
|
+
* `--mach-arg` and the xpcshell flavor that ignores the pref entirely.
|
|
21
|
+
* Mutates `extraArgs` in place.
|
|
22
|
+
*/
|
|
23
|
+
export declare function appendMarionetteForwardingArgs(extraArgs: string[], options: TestOptions, forwardedPort: number | undefined, xpcshellOnly?: boolean): void;
|
|
24
|
+
/** Prepares profile overlays and browser/port ownership preconditions. */
|
|
25
|
+
export declare function ensureTestBrowserEnvironment(engineDir: string, launchablePath: string | undefined, xpcshellOnly: boolean, projectConfig: FireForgeConfig, options: TestOptions, objDir: string | undefined): Promise<{
|
|
26
|
+
forwardedPort: number | undefined;
|
|
27
|
+
effectivePort: number | undefined;
|
|
28
|
+
}>;
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { reportOrphanedHarnessProcesses } from '../core/harness-orphans.js';
|
|
4
|
+
import { assertMarionettePortAvailable, ensureLaunchableBrowserNotRunning, ensureMarionettePortAvailable, extractForwardedMarionettePort, forwardedMachArgsIncludeMarionetteClient, shouldAutoForwardMarionettePortToMach, } from '../core/marionette-port.js';
|
|
5
|
+
import { formatMarionettePreflightLine, reportMarionettePreflight, runMarionettePreflight, } from '../core/marionette-preflight.js';
|
|
6
|
+
import { ensureMochitestServerPortAvailable } from '../core/mochitest-server-port.js';
|
|
7
|
+
import { waitForPreflight } from '../core/preflight-wait.js';
|
|
8
|
+
import { GeneralError } from '../errors/base.js';
|
|
9
|
+
import { info, outro, verbose } from '../utils/logger.js';
|
|
10
|
+
import { resolveWaitLockSeconds } from '../utils/options.js';
|
|
11
|
+
import { addVerdictRunCount, emitFailVerdict, emitPassVerdict } from './test-verdict.js';
|
|
12
|
+
/**
|
|
13
|
+
* Runs the `--doctor` marionette handshake probe. With no test paths the
|
|
14
|
+
* probe is the entire command (returns `'stop'` after reporting). With
|
|
15
|
+
* paths it gates the mach invocation, where a FAIL throws before mach runs.
|
|
16
|
+
*/
|
|
17
|
+
export async function runDoctorPreflight(args) {
|
|
18
|
+
const { engineDir, effectivePort, hasTestPaths, objDir, binaryName, launchablePath } = args;
|
|
19
|
+
// Non-TTY captures need the banner even if clack's renderer defers output
|
|
20
|
+
// in pipe mode. TTY users need the clack framing. Gated rather than
|
|
21
|
+
// written twice: the unconditional pair printed the same line twice on a
|
|
22
|
+
// terminal.
|
|
23
|
+
if (process.stdout.isTTY) {
|
|
24
|
+
info('Running marionette preflight...');
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
process.stdout.write('Running marionette preflight...\n');
|
|
28
|
+
}
|
|
29
|
+
const preflight = effectivePort !== undefined
|
|
30
|
+
? await runMarionettePreflight(engineDir, { port: effectivePort })
|
|
31
|
+
: await runMarionettePreflight(engineDir);
|
|
32
|
+
// The authoritative PASS/FAIL line is written with `process.stdout.write`
|
|
33
|
+
// as the first output after the probe returns, because clack's renderer
|
|
34
|
+
// can drop the summary under non-TTY capture.
|
|
35
|
+
//
|
|
36
|
+
// Gated on non-TTY: unconditional, it stacks with the two clack renderings
|
|
37
|
+
// below and the same line appears three times on a terminal. Captured
|
|
38
|
+
// streams are exactly where this branch still fires.
|
|
39
|
+
const directLine = formatMarionettePreflightLine(preflight);
|
|
40
|
+
if (!process.stdout.isTTY) {
|
|
41
|
+
process.stdout.write(`${directLine}\n`);
|
|
42
|
+
}
|
|
43
|
+
process.stdout.write(`Marionette preflight environment: objdir=${objDir ?? '(none)'}; binary=${binaryName}; app=${launchablePath ? `engine/${launchablePath}` : '(unknown)'}; port=${effectivePort ?? 2828}; elapsed=${preflight.durationMs}ms\n`);
|
|
44
|
+
reportMarionettePreflight(preflight);
|
|
45
|
+
if (!hasTestPaths) {
|
|
46
|
+
if (!preflight.ok) {
|
|
47
|
+
emitFailVerdict('preflight');
|
|
48
|
+
throw new GeneralError('Marionette preflight reported FAIL — see output above.');
|
|
49
|
+
}
|
|
50
|
+
// Doctor-only runs end here, so they carry their own verdict line
|
|
51
|
+
// (reason=preflight on failure). With test paths the verdict comes
|
|
52
|
+
// from the actual harness run downstream.
|
|
53
|
+
emitPassVerdict();
|
|
54
|
+
outro('Test completed');
|
|
55
|
+
return 'stop';
|
|
56
|
+
}
|
|
57
|
+
if (!preflight.ok) {
|
|
58
|
+
emitFailVerdict('preflight');
|
|
59
|
+
throw new GeneralError('Marionette preflight reported FAIL — see output above. Aborting before mach test runs.');
|
|
60
|
+
}
|
|
61
|
+
return 'continue';
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Auto-forwards `--marionette-port` to mach (`--setpref=marionette.port`
|
|
65
|
+
* for the listener, `--marionette=127.0.0.1:<n>` for the mochitest
|
|
66
|
+
* client), skipping each piece the operator already forwarded via
|
|
67
|
+
* `--mach-arg` and the xpcshell flavor that ignores the pref entirely.
|
|
68
|
+
* Mutates `extraArgs` in place.
|
|
69
|
+
*/
|
|
70
|
+
export function appendMarionetteForwardingArgs(extraArgs, options, forwardedPort, xpcshellOnly = false) {
|
|
71
|
+
// Auto-forward the Marionette port to mach when `--marionette-port` is set.
|
|
72
|
+
// `--setpref=marionette.port=<n>` configures where the browser listener
|
|
73
|
+
// binds. `--marionette=127.0.0.1:<n>` tells the mochitest harness client to
|
|
74
|
+
// connect there (default client is 127.0.0.1:2828). xpcshell ignores both
|
|
75
|
+
// for browser Marionette.
|
|
76
|
+
//
|
|
77
|
+
// Skip setpref forwarding when the operator already supplied an equivalent
|
|
78
|
+
// arg via `--mach-arg`: duplicates would confuse without changing
|
|
79
|
+
// semantics. Skip when mach args explicitly request `--flavor=xpcshell` (or
|
|
80
|
+
// `xpcshell-tests`): the preflight still honours `--marionette-port`, but
|
|
81
|
+
// mach does not use the marionette.port pref on that harness. Any other arg
|
|
82
|
+
// shape still forwards so toolkit widget paths and mixed suites stay
|
|
83
|
+
// aligned with the probe without duplicate `--mach-arg` flags.
|
|
84
|
+
//
|
|
85
|
+
// Skip auto `--marionette=...` when `--mach-arg` already includes a client
|
|
86
|
+
// `--marionette=...` (or two-token `--marionette host:port`).
|
|
87
|
+
if (options.marionettePort === undefined)
|
|
88
|
+
return;
|
|
89
|
+
if (xpcshellOnly) {
|
|
90
|
+
// Manifest classification says every requested path is xpcshell.
|
|
91
|
+
// xpcshell ignores the browser Marionette path entirely, and forwarding
|
|
92
|
+
// the mochitest client flags here makes mach reject the dispatch.
|
|
93
|
+
info(`--marionette-port=${options.marionettePort} applied to the preflight probe only: the requested paths are xpcshell-only, and xpcshell ignores the browser Marionette port. Not forwarding --setpref=marionette.port or --marionette to mach.`);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
{
|
|
97
|
+
const operatorAlreadyForwarded = forwardedPort !== undefined;
|
|
98
|
+
const machArgs = options.machArg ?? [];
|
|
99
|
+
if (operatorAlreadyForwarded) {
|
|
100
|
+
info(`--marionette-port=${options.marionettePort} set, but the same port is already forwarded via --mach-arg; skipping auto-forward.`);
|
|
101
|
+
}
|
|
102
|
+
else if (shouldAutoForwardMarionettePortToMach(machArgs)) {
|
|
103
|
+
extraArgs.push(`--setpref=marionette.port=${options.marionettePort}`);
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
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.`);
|
|
107
|
+
}
|
|
108
|
+
if (shouldAutoForwardMarionettePortToMach(machArgs) &&
|
|
109
|
+
!forwardedMachArgsIncludeMarionetteClient(machArgs)) {
|
|
110
|
+
extraArgs.push(`--marionette=127.0.0.1:${options.marionettePort}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
async function ensureTestMarionettePortAvailable(port, binaryName, options, skip) {
|
|
115
|
+
// Refuse a stale listener before mach surfaces a generic bind failure.
|
|
116
|
+
// This also recognizes a fork-branded browser via binaryName.
|
|
117
|
+
if (skip.xpcshellOnly && !skip.doctor) {
|
|
118
|
+
// xpcshell does not bind the browser Marionette port, so a developer's
|
|
119
|
+
// interactive browser holding 2828 must not kill an xpcshell run.
|
|
120
|
+
// --doctor keeps the preflight: its probe launches a
|
|
121
|
+
// Marionette browser regardless of the requested harness.
|
|
122
|
+
const message = 'Skipping the Marionette stale-port preflight: all requested paths are xpcshell ' +
|
|
123
|
+
'(xpcshell does not bind the browser Marionette port).';
|
|
124
|
+
if (options.marionettePort !== undefined || options.killStaleMarionette === true) {
|
|
125
|
+
info(message);
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
verbose(message);
|
|
129
|
+
}
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (options.killStaleMarionette === true) {
|
|
133
|
+
await ensureMarionettePortAvailable(port, { binaryName, killStaleBrowser: true });
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
await assertMarionettePortAvailable(port, { binaryName });
|
|
137
|
+
}
|
|
138
|
+
/** Prepares profile overlays and browser/port ownership preconditions. */
|
|
139
|
+
export async function ensureTestBrowserEnvironment(engineDir, launchablePath, xpcshellOnly, projectConfig, options, objDir) {
|
|
140
|
+
// A timed-out mochitest can leave the built app alive after its Marionette
|
|
141
|
+
// listener has disappeared. The port probe cannot see that case, but the
|
|
142
|
+
// survivor can still steal focus and wedge every later headed run.
|
|
143
|
+
if (!xpcshellOnly && launchablePath) {
|
|
144
|
+
await waitForPreflight(() => ensureLaunchableBrowserNotRunning(join(engineDir, launchablePath), {
|
|
145
|
+
killStaleBrowser: options.killStaleMarionette === true,
|
|
146
|
+
}), options.waitBrowser === undefined ? undefined : resolveWaitLockSeconds(options.waitBrowser));
|
|
147
|
+
}
|
|
148
|
+
// Packaging-only runs must protect a running objdir, but do not bind any
|
|
149
|
+
// harness ports or launch tests, so peer listeners cannot block packaging.
|
|
150
|
+
if (options.buildOnly)
|
|
151
|
+
return { forwardedPort: undefined, effectivePort: undefined };
|
|
152
|
+
// A zombie mochitest httpd squatting the server port makes a fresh
|
|
153
|
+
// browser connect to a server that cannot serve this run's manifest,
|
|
154
|
+
// which surfaces as a 370s "Ran 0 checks" stall naming nothing. xpcshell
|
|
155
|
+
// does not use the httpd, so an xpcshell-only run is never blocked by it.
|
|
156
|
+
if (!xpcshellOnly) {
|
|
157
|
+
await waitForPreflight(() => ensureMochitestServerPortAvailable(undefined, {
|
|
158
|
+
engineDir,
|
|
159
|
+
killStaleServer: options.killStaleMarionette === true,
|
|
160
|
+
}), options.waitPort === undefined ? undefined : resolveWaitLockSeconds(options.waitPort));
|
|
161
|
+
}
|
|
162
|
+
// Helpers that outlived an earlier run (httpd, pywebsocket, ssltunnel,
|
|
163
|
+
// moz-http2) slow every later run without appearing in its output. Both
|
|
164
|
+
// harnesses are affected, so this census is not gated on xpcshellOnly.
|
|
165
|
+
// Reaping is opt-in per invocation (--reap-orphans) or per repo
|
|
166
|
+
// (test.reapOrphans: "reap"); a reap is stamped on the verdict line so a
|
|
167
|
+
// green after one is not mistaken for a green on a quiet machine.
|
|
168
|
+
const census = await reportOrphanedHarnessProcesses(objDir, {
|
|
169
|
+
reap: options.reapOrphans === true || projectConfig.test?.reapOrphans === 'reap',
|
|
170
|
+
});
|
|
171
|
+
addVerdictRunCount('orphans-reaped', census.reaped);
|
|
172
|
+
const forwardedPort = options.machArg
|
|
173
|
+
? extractForwardedMarionettePort(options.machArg)
|
|
174
|
+
: undefined;
|
|
175
|
+
const effectivePort = options.marionettePort ?? forwardedPort;
|
|
176
|
+
await waitForPreflight(() => ensureTestMarionettePortAvailable(effectivePort, projectConfig.binaryName, options, {
|
|
177
|
+
xpcshellOnly,
|
|
178
|
+
doctor: options.doctor === true,
|
|
179
|
+
}), options.waitPort === undefined ? undefined : resolveWaitLockSeconds(options.waitPort));
|
|
180
|
+
return { forwardedPort, effectivePort };
|
|
181
|
+
}
|
|
@@ -7,12 +7,15 @@
|
|
|
7
7
|
import { readBuildBaseline, writeBuildBaseline } from '../core/build-baseline.js';
|
|
8
8
|
import { prepareBuildEnvironment } from '../core/build-prepare.js';
|
|
9
9
|
import { checkExtendCoverageAnchor, checkExtendMozconfigAnchor, formatExtendCoverageRefusal, unionTestPackagingCoverage, } from '../core/coverage-extend.js';
|
|
10
|
+
import { hasBuildArtifacts } from '../core/mach.js';
|
|
10
11
|
import { runProtectedMachBuild, withBuildLock } from '../core/mach.js';
|
|
12
|
+
import { assertHealthyPartialConfig, pruneDanglingTestLinks } from '../core/objdir-maintenance.js';
|
|
11
13
|
import { buildHarnessCrashMessage } from '../core/test-harness-crash.js';
|
|
12
14
|
import { GeneralError, InvalidArgumentError } from '../errors/base.js';
|
|
13
15
|
import { BuildError } from '../errors/build.js';
|
|
14
16
|
import { toError } from '../utils/errors.js';
|
|
15
17
|
import { info, notice, spinner, verbose } from '../utils/logger.js';
|
|
18
|
+
import { checkRequestedTestTypes } from './test-checkjs.js';
|
|
16
19
|
import { reportBuildOnlyCompletion } from './test-modes.js';
|
|
17
20
|
import { enforceStaleBuildGate, enforceStaticComponentsGate } from './test-stale-gate.js';
|
|
18
21
|
import { emitPassVerdict } from './test-verdict.js';
|
|
@@ -31,6 +34,14 @@ async function runPreTestBuild(options) {
|
|
|
31
34
|
// `mach build faster` would: a failed pre-test build followed by a full
|
|
32
35
|
// rebuild is an hour where an incremental one would have sufficed.
|
|
33
36
|
const previousBaseline = await readBuildBaseline(projectRoot);
|
|
37
|
+
const artifacts = await hasBuildArtifacts(paths.engine);
|
|
38
|
+
if (artifacts.objDir) {
|
|
39
|
+
await assertHealthyPartialConfig(paths.engine, artifacts.objDir);
|
|
40
|
+
const pruned = await pruneDanglingTestLinks(paths.engine, artifacts.objDir);
|
|
41
|
+
if (pruned > 0)
|
|
42
|
+
notice(`Pruned ${pruned} dangling _tests symlink(s).`);
|
|
43
|
+
}
|
|
44
|
+
await checkRequestedTestTypes(paths, projectConfig, testPackagingCoverage === 'full' ? [] : testPackagingCoverage);
|
|
34
45
|
const preparation = await prepareBuildEnvironment(projectRoot, paths, projectConfig, {
|
|
35
46
|
previousBaseline,
|
|
36
47
|
refuseUnexportedDrift,
|
|
@@ -39,10 +50,22 @@ async function runPreTestBuild(options) {
|
|
|
39
50
|
// only be checked once prepareBuildEnvironment has regenerated
|
|
40
51
|
// engine/mozconfig (the file this build will configure with), and must
|
|
41
52
|
// still be checked before mach, so a refusal costs no build.
|
|
42
|
-
|
|
53
|
+
let retainCoverage = false;
|
|
54
|
+
if (testPackagingCoverage !== 'full' && previousBaseline) {
|
|
55
|
+
const anchor = await checkExtendCoverageAnchor(paths.engine, previousBaseline, testPackagingCoverage);
|
|
43
56
|
const mozconfigAnchor = await checkExtendMozconfigAnchor(paths.engine, previousBaseline);
|
|
44
|
-
|
|
45
|
-
|
|
57
|
+
retainCoverage =
|
|
58
|
+
anchor.ok && mozconfigAnchor.ok && previousBaseline.testInputFingerprints !== undefined;
|
|
59
|
+
if (extend !== undefined && (!anchor.ok || !mozconfigAnchor.ok)) {
|
|
60
|
+
throw new GeneralError(formatExtendCoverageRefusal(!anchor.ok ? anchor : mozconfigAnchor));
|
|
61
|
+
}
|
|
62
|
+
if (!retainCoverage) {
|
|
63
|
+
const old = previousBaseline.testPackagingCoverage;
|
|
64
|
+
const dropped = old === undefined || old === 'full'
|
|
65
|
+
? ['full coverage outside this scope']
|
|
66
|
+
: old.filter((file) => !testPackagingCoverage.includes(file));
|
|
67
|
+
if (dropped.length)
|
|
68
|
+
notice(`Scoped build drops previous packaging coverage: ${dropped.join(', ')}. Earlier staging inputs changed or have no complete fingerprint anchor; rebuild those paths together to retain them.`);
|
|
46
69
|
}
|
|
47
70
|
}
|
|
48
71
|
const buildKind = preparation.fullBuildRequired ? 'full' : 'faster';
|
|
@@ -85,8 +108,8 @@ async function runPreTestBuild(options) {
|
|
|
85
108
|
// is always carried forward: that union can evaluate to 'full'
|
|
86
109
|
// while the build behind it was a scoped `mach build faster` that
|
|
87
110
|
// did not rebake the compiled table.
|
|
88
|
-
const recordedCoverage =
|
|
89
|
-
? unionTestPackagingCoverage(previousBaseline?.testPackagingCoverage,
|
|
111
|
+
const recordedCoverage = retainCoverage
|
|
112
|
+
? unionTestPackagingCoverage(previousBaseline?.testPackagingCoverage, testPackagingCoverage === 'full' ? [] : testPackagingCoverage)
|
|
90
113
|
: testPackagingCoverage;
|
|
91
114
|
await writeBuildBaseline({
|
|
92
115
|
projectRoot,
|
|
@@ -97,7 +120,7 @@ async function runPreTestBuild(options) {
|
|
|
97
120
|
recordedBy: describeBuildInvocation(extend !== undefined, testPackagingCoverage),
|
|
98
121
|
staticComponentsHandling: preparation.fullBuildRequired
|
|
99
122
|
? 'refresh'
|
|
100
|
-
:
|
|
123
|
+
: retainCoverage
|
|
101
124
|
? 'carry-forward'
|
|
102
125
|
: 'auto',
|
|
103
126
|
buildKind,
|
|
@@ -176,7 +199,7 @@ export async function runTestBuildPhase(projectRoot, paths, projectConfig, harne
|
|
|
176
199
|
if (extending) {
|
|
177
200
|
// Head/fingerprint half of the anchor, before the build lock: a
|
|
178
201
|
// refusal here costs neither build time nor the lock.
|
|
179
|
-
const anchor = await checkExtendCoverageAnchor(paths.engine, previousBaseline);
|
|
202
|
+
const anchor = await checkExtendCoverageAnchor(paths.engine, previousBaseline, normalizedPaths);
|
|
180
203
|
if (!anchor.ok) {
|
|
181
204
|
throw new GeneralError(formatExtendCoverageRefusal(anchor));
|
|
182
205
|
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { FireForgeConfig, ProjectPaths } from '../types/config.js';
|
|
2
|
+
/** Runs the configured patch test checkJs pass before paying for a proving build. */
|
|
3
|
+
export declare function checkRequestedTestTypes(paths: ProjectPaths, config: FireForgeConfig, requested: string[]): Promise<void>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
import { runCheckJsTestFilesGrouped } from '../core/patch-lint-checkjs.js';
|
|
3
|
+
import { isTestScriptFile } from '../core/patch-lint-ownership.js';
|
|
4
|
+
import { loadPatchesManifest } from '../core/patch-manifest.js';
|
|
5
|
+
import { analyzeTestPathScopes } from '../core/test-path-scope.js';
|
|
6
|
+
import { GeneralError } from '../errors/base.js';
|
|
7
|
+
import { notice } from '../utils/logger.js';
|
|
8
|
+
/** Runs the configured patch test checkJs pass before paying for a proving build. */
|
|
9
|
+
export async function checkRequestedTestTypes(paths, config, requested) {
|
|
10
|
+
if (config.patchLint?.checkJs !== true ||
|
|
11
|
+
config.patchLint.checkJsTestFiles !== true ||
|
|
12
|
+
requested.length === 0)
|
|
13
|
+
return;
|
|
14
|
+
const manifest = await loadPatchesManifest(paths.patches);
|
|
15
|
+
const owned = new Set(manifest?.patches.flatMap((patch) => patch.filesAffected.filter(isTestScriptFile)) ?? []);
|
|
16
|
+
const scopes = await analyzeTestPathScopes(paths.engine, requested);
|
|
17
|
+
const selected = new Set(scopes.flatMap((scope) => scope.dispatchPaths.filter(isTestScriptFile)));
|
|
18
|
+
for (const file of selected)
|
|
19
|
+
owned.add(file);
|
|
20
|
+
const result = await runCheckJsTestFilesGrouped(paths.engine, owned, config.patchLint, paths.root, selected);
|
|
21
|
+
const issues = [...result.global, ...[...result.byFile.values()].flat()];
|
|
22
|
+
for (const issue of issues)
|
|
23
|
+
notice(`Pre-build checkJs: ${issue.file}: ${issue.message}`);
|
|
24
|
+
if (issues.some((issue) => issue.severity === 'error')) {
|
|
25
|
+
throw new GeneralError('Named test files failed the configured patch checkJs pass before build. Fix the type errors above before running the proof.');
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -143,6 +143,9 @@ verdict) {
|
|
|
143
143
|
if (result.exitCode === 0 || result.exitCode === 130)
|
|
144
144
|
return;
|
|
145
145
|
const combinedOutput = `${result.stdout}\n${result.stderr}`;
|
|
146
|
+
if (/copyExtraFilesToProfile/.test(combinedOutput) && /FileExistsError/.test(combinedOutput)) {
|
|
147
|
+
throw new GeneralError('Harness argument failure: --extra-profile-file collided with the existing profile directory. Use --profile-file source=chrome/userChrome.css to merge an individual file; rebuilding does not repair this argument.');
|
|
148
|
+
}
|
|
146
149
|
const failureContext = postRebuildContext
|
|
147
150
|
? completePostRebuildFailureContext(postRebuildContext, combinedOutput)
|
|
148
151
|
: undefined;
|
|
@@ -11,14 +11,13 @@ export declare function buildPerfSampleEnv(projectRoot: string, binaryName: stri
|
|
|
11
11
|
*/
|
|
12
12
|
export declare function mergeHarnessEnv(...parts: (Record<string, string> | undefined)[]): Record<string, string> | undefined;
|
|
13
13
|
/**
|
|
14
|
-
* Resolves the
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* shuffling could not be reproduced. FireForge draws the seed, exports it
|
|
18
|
-
* as `FIREFORGE_SHUFFLE_SEED` for harness code that reorders inside a file,
|
|
19
|
-
* prints it, and stamps it on the verdict line.
|
|
14
|
+
* Resolves the seed for FireForge's isolated shard order and exports it for
|
|
15
|
+
* custom in-file task code. Native mach file shuffling remains unseeded;
|
|
16
|
+
* replaying this seed guarantees only the FireForge shard permutation.
|
|
20
17
|
*
|
|
21
18
|
* Mochitest-only: the xpcshell harness has no shuffle, and generic `mach
|
|
22
19
|
* test` dispatch would forward the flag to every suite it fans out to.
|
|
23
20
|
*/
|
|
24
21
|
export declare function resolveShuffleSeed(shuffle: number | boolean | undefined, suite: TestSuite): number | undefined;
|
|
22
|
+
/** Returns a deterministic Fisher–Yates permutation without mutating the selection. */
|
|
23
|
+
export declare function shuffleTestGroups<T>(groups: readonly T[], seed: number): T[];
|