@hominis/fireforge 0.28.4 → 0.29.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 +5 -0
- package/dist/src/commands/test.js +38 -34
- package/dist/src/core/firefox-ignorefile.d.ts +17 -0
- package/dist/src/core/firefox-ignorefile.js +31 -0
- package/dist/src/core/git.js +5 -0
- package/dist/src/core/mach.js +5 -0
- package/dist/src/core/test-harness-output.d.ts +13 -0
- package/dist/src/core/test-harness-output.js +39 -0
- package/dist/src/core/test-xpcshell-retry.d.ts +7 -0
- package/dist/src/core/test-xpcshell-retry.js +16 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.29.0
|
|
4
|
+
|
|
5
|
+
- Improved `fireforge test --build` failure reporting so post-rebuild focused test failures name the rebuild command, requested paths, and first failure line separately from stale-artifact rebuild advice.
|
|
6
|
+
|
|
3
7
|
## 0.28.0
|
|
4
8
|
|
|
9
|
+
- Restored mach lint compatibility for FireForge-managed Git-backed Firefox checkouts by materializing a `.hgignore` copy of `.gitignore` when Firefox's ignorefile linter config is present.
|
|
5
10
|
- Added the product-resolved Firefox source archive URL to `source set` output so pinned checksums can be verified against the exact archive target before download.
|
|
6
11
|
- Added dry-run locking for `re-export` so parallel previews serialize engine git inspection instead of racing on `.git/index.lock`.
|
|
7
12
|
- Added `re-export --scan --scan-files <manifest>` for dry-runnable bulk generated-file assignment across owner patches, with ambiguity and ownership refusals.
|
|
@@ -5,9 +5,9 @@ import { getProjectPaths, loadConfig } from '../core/config.js';
|
|
|
5
5
|
import { buildArtifactMismatchMessage, buildUI, hasBuildArtifacts, hasRunnableBundle, testWithOutput, withBuildLock, } from '../core/mach.js';
|
|
6
6
|
import { assertMarionettePortAvailable, extractForwardedMarionettePort, forwardedMachArgsIncludeMarionetteClient, shouldAutoForwardMarionettePortToMach, } from '../core/marionette-port.js';
|
|
7
7
|
import { formatMarionettePreflightLine, reportMarionettePreflight, runMarionettePreflight, } from '../core/marionette-preflight.js';
|
|
8
|
-
import { buildHarnessEarlyExitMessage, classifyHarnessEarlyExit, } from '../core/test-harness-output.js';
|
|
8
|
+
import { buildHarnessEarlyExitMessage, classifyHarnessEarlyExit, completePostRebuildFailureContext, createPostRebuildFailureContext, prependPostRebuildFailureContext, } from '../core/test-harness-output.js';
|
|
9
9
|
import { checkStaleBuildForTest, formatStaleBuildWarning } from '../core/test-stale-check.js';
|
|
10
|
-
import {
|
|
10
|
+
import { retryAfterXpcshellSymlinkRepair } from '../core/test-xpcshell-retry.js';
|
|
11
11
|
import { findNearestXpcshellManifest } from '../core/xpcshell-appdir.js';
|
|
12
12
|
import { GeneralError } from '../errors/base.js';
|
|
13
13
|
import { AmbiguousBuildArtifactsError, BuildError } from '../errors/build.js';
|
|
@@ -34,7 +34,12 @@ function buildUnknownTestMessage(testPaths) {
|
|
|
34
34
|
'The file may exist, but Firefox does not currently resolve it as a runnable test.\n\n' +
|
|
35
35
|
'Check the nearest test manifest (for example browser.toml or xpcshell.toml), confirm the file is listed under the correct test type, and make sure each parent moz.build registers that manifest before retrying.');
|
|
36
36
|
}
|
|
37
|
-
function buildStaleBuildMessage() {
|
|
37
|
+
function buildStaleBuildMessage(postRebuild) {
|
|
38
|
+
if (postRebuild) {
|
|
39
|
+
return ('Firefox test runtime still reported stale-artifact-shaped resource failures after the rebuild completed.\n\n' +
|
|
40
|
+
'FireForge already ran the requested rebuild before this focused test, so treat the remaining failure as a real runtime, registration, routing, or test-contract regression rather than another stale deployed-artifact-only blocker.\n\n' +
|
|
41
|
+
'Check the first post-rebuild failure above and the raw mach output for the concrete path or module that still fails.');
|
|
42
|
+
}
|
|
38
43
|
return ('Firefox test runtime appears to be using stale build artifacts.\n\n' +
|
|
39
44
|
'The failing output referenced missing branding or distribution resources, which usually means the current obj-* build does not match recent engine or branding changes.\n\n' +
|
|
40
45
|
'Re-run "fireforge build --ui" or "fireforge test --build" and then retry.');
|
|
@@ -171,6 +176,15 @@ async function runPreTestBuild(projectRoot, paths, projectConfig) {
|
|
|
171
176
|
s.stop('Build complete');
|
|
172
177
|
});
|
|
173
178
|
}
|
|
179
|
+
function logTestSelection(normalizedPaths) {
|
|
180
|
+
if (normalizedPaths.length > 0) {
|
|
181
|
+
info(`Running tests: ${normalizedPaths.join(', ')}`);
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
info('Running all tests...');
|
|
185
|
+
}
|
|
186
|
+
info('');
|
|
187
|
+
}
|
|
174
188
|
// Detects the `AttributeError: 'MochitestDesktop' object has no attribute
|
|
175
189
|
// 'http3Server'` teardown crash. The attribute is lazy-initialized inside
|
|
176
190
|
// harness code paths that presume chrome://branding resolves correctly; a
|
|
@@ -188,16 +202,23 @@ function buildMochitestHttp3ServerMessage() {
|
|
|
188
202
|
" - The `BROWSER_CHROME_MANIFESTS` entry for your fork's chrome.manifest is registered.\n\n" +
|
|
189
203
|
'This is an upstream Firefox harness interaction; FireForge can only diagnose it.');
|
|
190
204
|
}
|
|
191
|
-
function handleNonZeroTestExit(result, normalizedPaths, appdirInjectionAttempted, binaryName) {
|
|
205
|
+
function handleNonZeroTestExit(result, normalizedPaths, appdirInjectionAttempted, binaryName, postRebuildContext) {
|
|
192
206
|
if (result.exitCode === 0 || result.exitCode === 130)
|
|
193
207
|
return;
|
|
194
208
|
const combinedOutput = `${result.stdout}\n${result.stderr}`;
|
|
209
|
+
const failureContext = postRebuildContext
|
|
210
|
+
? completePostRebuildFailureContext(postRebuildContext, combinedOutput)
|
|
211
|
+
: undefined;
|
|
212
|
+
const withContext = (message) => prependPostRebuildFailureContext(message, failureContext);
|
|
213
|
+
const throwGeneral = (message) => {
|
|
214
|
+
throw new GeneralError(withContext(message));
|
|
215
|
+
};
|
|
195
216
|
if (/UNKNOWN TEST\b/i.test(combinedOutput)) {
|
|
196
|
-
|
|
217
|
+
throwGeneral(buildUnknownTestMessage(normalizedPaths));
|
|
197
218
|
}
|
|
198
219
|
const earlyExit = classifyHarnessEarlyExit(combinedOutput, normalizedPaths);
|
|
199
220
|
if (earlyExit) {
|
|
200
|
-
|
|
221
|
+
throwGeneral(buildHarnessEarlyExitMessage(earlyExit, normalizedPaths));
|
|
201
222
|
}
|
|
202
223
|
// Fork-owned module load failures must beat the branding stale-build
|
|
203
224
|
// branch: 2026-04-21 eval (Finding #14) saw a fork's test fail with
|
|
@@ -206,7 +227,7 @@ function handleNonZeroTestExit(result, normalizedPaths, appdirInjectionAttempted
|
|
|
206
227
|
// stale-build pattern matched, so the operator was told to rebuild
|
|
207
228
|
// when the real fix is to register the missing module.
|
|
208
229
|
if (hasForkModuleSignal(combinedOutput, binaryName)) {
|
|
209
|
-
|
|
230
|
+
throwGeneral(buildForkModuleMessage(binaryName));
|
|
210
231
|
}
|
|
211
232
|
// Branding-specific stale-build signals keep priority over the broader
|
|
212
233
|
// xpcshell-appdir hint: when `chrome://branding/locale/brand.properties`
|
|
@@ -219,24 +240,24 @@ function handleNonZeroTestExit(result, normalizedPaths, appdirInjectionAttempted
|
|
|
219
240
|
// which is the more useful diagnosis in practice for `Failed to load
|
|
220
241
|
// resource:///modules/…`.
|
|
221
242
|
if (hasStaleBuildArtifactsSignal(combinedOutput)) {
|
|
222
|
-
|
|
243
|
+
throwGeneral(buildStaleBuildMessage(Boolean(failureContext)));
|
|
223
244
|
}
|
|
224
245
|
if (hasXpcshellAppdirSignal(combinedOutput)) {
|
|
225
|
-
|
|
246
|
+
throwGeneral(buildXpcshellAppdirMessage(appdirInjectionAttempted));
|
|
226
247
|
}
|
|
227
248
|
if (hasMochitestHttp3ServerSignal(combinedOutput)) {
|
|
228
|
-
|
|
249
|
+
throwGeneral(buildMochitestHttp3ServerMessage());
|
|
229
250
|
}
|
|
230
251
|
if (/FileExistsError/i.test(combinedOutput) &&
|
|
231
252
|
/(mochitest|xpcshell|_tests)/i.test(combinedOutput)) {
|
|
232
|
-
|
|
253
|
+
throwGeneral(buildHarnessSymlinkMessage());
|
|
233
254
|
}
|
|
234
255
|
if (/invalid filename/i.test(combinedOutput) ||
|
|
235
256
|
/chrome:\/\/mochitests.*not found/i.test(combinedOutput)) {
|
|
236
257
|
info('Hint: The test file may not be registered in browser.toml or jar.mn.');
|
|
237
258
|
info('Run "fireforge register <test-path>" to register it.');
|
|
238
259
|
}
|
|
239
|
-
throw new BuildError(`Tests failed with exit code ${result.exitCode}. Check the output above for details
|
|
260
|
+
throw new BuildError(withContext(`Tests failed with exit code ${result.exitCode}. Check the output above for details.`), 'mach test');
|
|
240
261
|
}
|
|
241
262
|
/**
|
|
242
263
|
* Runs the test command to execute mach tests.
|
|
@@ -362,10 +383,6 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
|
|
|
362
383
|
throw new GeneralError('Marionette preflight reported FAIL — see output above. Aborting before mach test runs.');
|
|
363
384
|
}
|
|
364
385
|
}
|
|
365
|
-
// Normalize test paths (strip engine/ prefix if present). Uses the
|
|
366
|
-
// shared `stripEnginePrefix` helper so `test`, `register`, `lint`, and
|
|
367
|
-
// `export` all accept the same prefix forms. Also trim to match the
|
|
368
|
-
// previous case-insensitive + leading-whitespace-tolerant contract.
|
|
369
386
|
const normalizedPaths = testPaths.map((p) => stripEnginePrefix(p).trim());
|
|
370
387
|
await assertTestPathsExist(paths.engine, normalizedPaths);
|
|
371
388
|
const classification = await classifyTestHarnesses(paths.engine, normalizedPaths);
|
|
@@ -375,7 +392,6 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
|
|
|
375
392
|
const forwardedMachArgs = options.machArg && options.machArg.length > 0
|
|
376
393
|
? filterRedundantXpcshellFlavorArgs(options.machArg, classification)
|
|
377
394
|
: [];
|
|
378
|
-
// Build extra args
|
|
379
395
|
const extraArgs = [];
|
|
380
396
|
if (options.headless) {
|
|
381
397
|
extraArgs.push('--headless');
|
|
@@ -430,14 +446,7 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
|
|
|
430
446
|
// overrides via `--mach-arg=--app-path=…` always win — we skip injection
|
|
431
447
|
// when the operator already passed one.
|
|
432
448
|
const appdirInjection = await maybeInjectAppdirArg(paths.engine, normalizedPaths, buildCheck.objDir, extraArgs);
|
|
433
|
-
|
|
434
|
-
if (normalizedPaths.length > 0) {
|
|
435
|
-
info(`Running tests: ${normalizedPaths.join(', ')}`);
|
|
436
|
-
}
|
|
437
|
-
else {
|
|
438
|
-
info('Running all tests...');
|
|
439
|
-
}
|
|
440
|
-
info('');
|
|
449
|
+
logTestSelection(normalizedPaths);
|
|
441
450
|
let result;
|
|
442
451
|
try {
|
|
443
452
|
result = await testWithOutput(paths.engine, normalizedPaths, extraArgs);
|
|
@@ -445,15 +454,10 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
|
|
|
445
454
|
catch (error) {
|
|
446
455
|
throw new BuildError('Test process failed to start', 'mach test', error instanceof Error ? error : undefined);
|
|
447
456
|
}
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
if (repaired) {
|
|
453
|
-
result = await testWithOutput(paths.engine, normalizedPaths, extraArgs);
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
handleNonZeroTestExit(result, normalizedPaths, appdirInjection, projectConfig.binaryName);
|
|
457
|
+
result = await retryAfterXpcshellSymlinkRepair(paths.engine, buildCheck.objDir, result, classification, normalizedPaths, extraArgs);
|
|
458
|
+
handleNonZeroTestExit(result, normalizedPaths, appdirInjection, projectConfig.binaryName, options.build
|
|
459
|
+
? createPostRebuildFailureContext('fireforge test --build', normalizedPaths)
|
|
460
|
+
: undefined);
|
|
457
461
|
}
|
|
458
462
|
/** Registers the test command on the CLI program. */
|
|
459
463
|
export function registerTest(program, { getProjectRoot, withErrorHandling }) {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Result of normalizing Firefox's Mercurial ignore file for Git-backed
|
|
3
|
+
* checkouts.
|
|
4
|
+
*/
|
|
5
|
+
export type FirefoxIgnorefileCompatibilityResult = 'created' | 'existing' | 'skipped';
|
|
6
|
+
/**
|
|
7
|
+
* Ensures Firefox's mozlint ignorefile configuration can be parsed in
|
|
8
|
+
* Git-backed source trees.
|
|
9
|
+
*
|
|
10
|
+
* Firefox's `tools/lint/ignorefile.yml` includes both `.gitignore` and
|
|
11
|
+
* `.hgignore`. Source archives and some Git mirrors may omit `.hgignore`,
|
|
12
|
+
* which makes `mach lint --fix <files>` fail before it reaches the scoped
|
|
13
|
+
* linter. For FireForge-managed Git checkouts, copying `.gitignore` gives
|
|
14
|
+
* mozlint the missing include and keeps the ignorefile linter's pattern
|
|
15
|
+
* comparison equivalent without patching upstream lint configuration.
|
|
16
|
+
*/
|
|
17
|
+
export declare function ensureFirefoxIgnorefileCompatibility(engineDir: string): Promise<FirefoxIgnorefileCompatibilityResult>;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { pathExists, readText, writeText } from '../utils/fs.js';
|
|
4
|
+
/**
|
|
5
|
+
* Ensures Firefox's mozlint ignorefile configuration can be parsed in
|
|
6
|
+
* Git-backed source trees.
|
|
7
|
+
*
|
|
8
|
+
* Firefox's `tools/lint/ignorefile.yml` includes both `.gitignore` and
|
|
9
|
+
* `.hgignore`. Source archives and some Git mirrors may omit `.hgignore`,
|
|
10
|
+
* which makes `mach lint --fix <files>` fail before it reaches the scoped
|
|
11
|
+
* linter. For FireForge-managed Git checkouts, copying `.gitignore` gives
|
|
12
|
+
* mozlint the missing include and keeps the ignorefile linter's pattern
|
|
13
|
+
* comparison equivalent without patching upstream lint configuration.
|
|
14
|
+
*/
|
|
15
|
+
export async function ensureFirefoxIgnorefileCompatibility(engineDir) {
|
|
16
|
+
const lintConfigPath = join(engineDir, 'tools', 'lint', 'ignorefile.yml');
|
|
17
|
+
if (!(await pathExists(lintConfigPath))) {
|
|
18
|
+
return 'skipped';
|
|
19
|
+
}
|
|
20
|
+
const hgignorePath = join(engineDir, '.hgignore');
|
|
21
|
+
if (await pathExists(hgignorePath)) {
|
|
22
|
+
return 'existing';
|
|
23
|
+
}
|
|
24
|
+
const gitignorePath = join(engineDir, '.gitignore');
|
|
25
|
+
if (!(await pathExists(gitignorePath))) {
|
|
26
|
+
return 'skipped';
|
|
27
|
+
}
|
|
28
|
+
await writeText(hgignorePath, await readText(gitignorePath));
|
|
29
|
+
return 'created';
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=firefox-ignorefile.js.map
|
package/dist/src/core/git.js
CHANGED
|
@@ -7,6 +7,7 @@ import { toError } from '../utils/errors.js';
|
|
|
7
7
|
import { pathExists, removeFile } from '../utils/fs.js';
|
|
8
8
|
import { verbose } from '../utils/logger.js';
|
|
9
9
|
import { exec } from '../utils/process.js';
|
|
10
|
+
import { ensureFirefoxIgnorefileCompatibility } from './firefox-ignorefile.js';
|
|
10
11
|
import { configureGitPerformance, ensureGit, git, GIT_ADD_CHUNK_TIMEOUT_ENV_VAR, GIT_ADD_CHUNK_TIMEOUT_MS, GIT_ADD_TIMEOUT_MS, } from './git-base.js';
|
|
11
12
|
import { getWorkingTreeStatus } from './git-status.js';
|
|
12
13
|
// ── Functions that remain in this file ──
|
|
@@ -291,6 +292,8 @@ export async function initRepository(dir, branchName = 'main', options = {}) {
|
|
|
291
292
|
reportProgress('Configuring origin remote for build compatibility...');
|
|
292
293
|
await git(['remote', 'add', 'origin', 'https://github.com/mozilla-firefox/firefox'], dir);
|
|
293
294
|
reportProgress('Git phase complete: source git repository metadata initialized.');
|
|
295
|
+
reportProgress('Normalizing Firefox ignore files for Git-backed mach lint compatibility...');
|
|
296
|
+
await ensureFirefoxIgnorefileCompatibility(dir);
|
|
294
297
|
// Add all files
|
|
295
298
|
reportProgress('Indexing Firefox source with git add -A (this can take several minutes on large trees)...');
|
|
296
299
|
await assertNoGitIndexLock(dir);
|
|
@@ -327,6 +330,8 @@ export async function resumeRepository(dir, options = {}) {
|
|
|
327
330
|
await cleanupIndexLock(dir);
|
|
328
331
|
// Ensure origin remote exists (may have been added before the interrupt)
|
|
329
332
|
await ensureOriginRemote(dir);
|
|
333
|
+
reportProgress('Normalizing Firefox ignore files for Git-backed mach lint compatibility...');
|
|
334
|
+
await ensureFirefoxIgnorefileCompatibility(dir);
|
|
330
335
|
// Stage all files
|
|
331
336
|
reportProgress('Indexing Firefox source (resuming)...');
|
|
332
337
|
await assertNoGitIndexLock(dir);
|
package/dist/src/core/mach.js
CHANGED
|
@@ -5,6 +5,7 @@ import { pathExists } from '../utils/fs.js';
|
|
|
5
5
|
import { warn } from '../utils/logger.js';
|
|
6
6
|
import { exec, execInherit, execInheritCapture, execSmokeRun, execStream, } from '../utils/process.js';
|
|
7
7
|
import { createSiblingLockPath, withFileLock } from './file-lock.js';
|
|
8
|
+
import { ensureFirefoxIgnorefileCompatibility } from './firefox-ignorefile.js';
|
|
8
9
|
import { explainMachError } from './mach-error-hints.js';
|
|
9
10
|
import { getPython } from './mach-python.js';
|
|
10
11
|
// Re-export sub-modules so existing `from './mach.js'` imports keep working.
|
|
@@ -32,6 +33,7 @@ export async function ensureMach(engineDir) {
|
|
|
32
33
|
export async function runMach(args, engineDir, options = {}) {
|
|
33
34
|
const python = await getPython(engineDir);
|
|
34
35
|
await ensureMach(engineDir);
|
|
36
|
+
await ensureFirefoxIgnorefileCompatibility(engineDir);
|
|
35
37
|
const machPath = join(engineDir, 'mach');
|
|
36
38
|
const execOptions = {
|
|
37
39
|
cwd: engineDir,
|
|
@@ -59,6 +61,7 @@ const CAPTURE_TAIL_LIMIT = 2 * 1024 * 1024;
|
|
|
59
61
|
export async function runMachCapture(args, engineDir, options = {}) {
|
|
60
62
|
const python = await getPython(engineDir);
|
|
61
63
|
await ensureMach(engineDir);
|
|
64
|
+
await ensureFirefoxIgnorefileCompatibility(engineDir);
|
|
62
65
|
const machPath = join(engineDir, 'mach');
|
|
63
66
|
let stdout = '';
|
|
64
67
|
let stderr = '';
|
|
@@ -89,6 +92,7 @@ export async function runMachCapture(args, engineDir, options = {}) {
|
|
|
89
92
|
export async function runMachInheritCapture(args, engineDir, options = {}) {
|
|
90
93
|
const python = await getPython(engineDir);
|
|
91
94
|
await ensureMach(engineDir);
|
|
95
|
+
await ensureFirefoxIgnorefileCompatibility(engineDir);
|
|
92
96
|
const machPath = join(engineDir, 'mach');
|
|
93
97
|
return execInheritCapture(python, [machPath, ...args], {
|
|
94
98
|
cwd: engineDir,
|
|
@@ -237,6 +241,7 @@ export async function run(engineDir, args = []) {
|
|
|
237
241
|
export async function runMachSmoke(args, engineDir, options) {
|
|
238
242
|
const python = await getPython(engineDir);
|
|
239
243
|
await ensureMach(engineDir);
|
|
244
|
+
await ensureFirefoxIgnorefileCompatibility(engineDir);
|
|
240
245
|
const machPath = join(engineDir, 'mach');
|
|
241
246
|
return execSmokeRun(python, [machPath, ...args], {
|
|
242
247
|
cwd: engineDir,
|
|
@@ -3,6 +3,19 @@ export interface HarnessEarlyExit {
|
|
|
3
3
|
kind: HarnessEarlyExitKind;
|
|
4
4
|
line: string;
|
|
5
5
|
}
|
|
6
|
+
export interface PostRebuildFailureContext {
|
|
7
|
+
rebuildCommand: string;
|
|
8
|
+
requestedPaths: readonly string[];
|
|
9
|
+
firstFailureLine?: string;
|
|
10
|
+
}
|
|
11
|
+
/** Finds the first high-signal failure line from captured mach test output. */
|
|
12
|
+
export declare function findFirstUsefulFailureLine(output: string): string | undefined;
|
|
13
|
+
/** Starts a post-rebuild context block for a focused test failure. */
|
|
14
|
+
export declare function createPostRebuildFailureContext(rebuildCommand: string, requestedPaths: readonly string[]): PostRebuildFailureContext;
|
|
15
|
+
/** Adds the first useful failure line from captured output to an existing context block. */
|
|
16
|
+
export declare function completePostRebuildFailureContext(context: PostRebuildFailureContext, output: string): PostRebuildFailureContext;
|
|
17
|
+
/** Prepends post-rebuild context when the test failure happened after a successful rebuild. */
|
|
18
|
+
export declare function prependPostRebuildFailureContext(message: string, context: PostRebuildFailureContext | undefined): string;
|
|
6
19
|
/** Classifies mach output where no requested test actually began running. */
|
|
7
20
|
export declare function classifyHarnessEarlyExit(output: string, normalizedPaths: readonly string[]): HarnessEarlyExit | undefined;
|
|
8
21
|
/** Builds the user-facing message for a harness startup or zero-run failure. */
|
|
@@ -8,6 +8,45 @@ function getNonEmptyOutputLines(output) {
|
|
|
8
8
|
function findFirstMatchingLine(lines, patterns) {
|
|
9
9
|
return lines.find((line) => patterns.some((pattern) => pattern.test(line)));
|
|
10
10
|
}
|
|
11
|
+
/** Finds the first high-signal failure line from captured mach test output. */
|
|
12
|
+
export function findFirstUsefulFailureLine(output) {
|
|
13
|
+
const lines = getNonEmptyOutputLines(output);
|
|
14
|
+
const matched = findFirstMatchingLine(lines, [
|
|
15
|
+
/\bTEST-UNEXPECTED-[A-Z-]+\b/,
|
|
16
|
+
/\bPROCESS-CRASH\b/i,
|
|
17
|
+
/\bTIMEOUT\b/i,
|
|
18
|
+
/timed out/i,
|
|
19
|
+
/HominisBrowserUnavailableError/i,
|
|
20
|
+
/Marionette.*(?:session|startup|start).*fail/i,
|
|
21
|
+
/(?:failed|unable) to (?:start|create|open).*Marionette/i,
|
|
22
|
+
/SessionNotCreatedException/i,
|
|
23
|
+
/Browser process exited during spawn/i,
|
|
24
|
+
/Failed to load (?:resource|chrome):\/\//i,
|
|
25
|
+
/\b(?:Error|Exception|TypeError|ReferenceError|SyntaxError):\s+/,
|
|
26
|
+
/AttributeError:\s+/,
|
|
27
|
+
]);
|
|
28
|
+
return matched ?? lines[0];
|
|
29
|
+
}
|
|
30
|
+
/** Starts a post-rebuild context block for a focused test failure. */
|
|
31
|
+
export function createPostRebuildFailureContext(rebuildCommand, requestedPaths) {
|
|
32
|
+
return { rebuildCommand, requestedPaths };
|
|
33
|
+
}
|
|
34
|
+
/** Adds the first useful failure line from captured output to an existing context block. */
|
|
35
|
+
export function completePostRebuildFailureContext(context, output) {
|
|
36
|
+
const firstFailureLine = findFirstUsefulFailureLine(output);
|
|
37
|
+
return firstFailureLine ? { ...context, firstFailureLine } : context;
|
|
38
|
+
}
|
|
39
|
+
/** Prepends post-rebuild context when the test failure happened after a successful rebuild. */
|
|
40
|
+
export function prependPostRebuildFailureContext(message, context) {
|
|
41
|
+
if (!context)
|
|
42
|
+
return message;
|
|
43
|
+
const requestedPaths = context.requestedPaths.length > 0 ? context.requestedPaths.join(', ') : '(all tests)';
|
|
44
|
+
return ('Post-rebuild test failure:\n\n' +
|
|
45
|
+
`Rebuild command: ${context.rebuildCommand}\n` +
|
|
46
|
+
`Requested paths: ${requestedPaths}\n` +
|
|
47
|
+
`First post-rebuild failure: ${context.firstFailureLine ?? '(no captured output)'}\n\n` +
|
|
48
|
+
message);
|
|
49
|
+
}
|
|
11
50
|
/** Classifies mach output where no requested test actually began running. */
|
|
12
51
|
export function classifyHarnessEarlyExit(output, normalizedPaths) {
|
|
13
52
|
const lines = getNonEmptyOutputLines(output);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type MachCommandResult } from './mach.js';
|
|
2
|
+
export interface XpcshellRetryClassification {
|
|
3
|
+
xpcshell: readonly string[];
|
|
4
|
+
nonXpcshell: readonly string[];
|
|
5
|
+
}
|
|
6
|
+
/** Removes a stale xpcshell install symlink and retries the focused mach test once. */
|
|
7
|
+
export declare function retryAfterXpcshellSymlinkRepair(engineDir: string, objDir: string | undefined, result: MachCommandResult, classification: XpcshellRetryClassification, normalizedPaths: string[], extraArgs: string[]): Promise<MachCommandResult>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
import { testWithOutput } from './mach.js';
|
|
3
|
+
import { tryRepairStaleXpcshellTestSymlink } from './test-stale-symlink.js';
|
|
4
|
+
/** Removes a stale xpcshell install symlink and retries the focused mach test once. */
|
|
5
|
+
export async function retryAfterXpcshellSymlinkRepair(engineDir, objDir, result, classification, normalizedPaths, extraArgs) {
|
|
6
|
+
if (result.exitCode !== 0 &&
|
|
7
|
+
classification.xpcshell.length > 0 &&
|
|
8
|
+
classification.nonXpcshell.length === 0) {
|
|
9
|
+
const repaired = await tryRepairStaleXpcshellTestSymlink(engineDir, objDir, `${result.stdout}\n${result.stderr}`);
|
|
10
|
+
if (repaired) {
|
|
11
|
+
return testWithOutput(engineDir, normalizedPaths, extraArgs);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return result;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=test-xpcshell-retry.js.map
|