@hominis/fireforge 0.48.0 → 0.49.1
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 +29 -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.js +7 -5
- package/dist/src/commands/doctor-source-pin.js +9 -2
- package/dist/src/commands/doctor.js +2 -0
- package/dist/src/commands/furnace/apply.js +10 -4
- package/dist/src/commands/furnace/create-templates.js +0 -3
- package/dist/src/commands/lint.js +4 -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-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 +4 -0
- 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/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/component-source-guard.d.ts +20 -0
- package/dist/src/core/component-source-guard.js +68 -0
- package/dist/src/core/coverage-extend.d.ts +5 -34
- package/dist/src/core/coverage-extend.js +26 -37
- 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/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/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-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 +167 -0
- package/dist/src/core/patch-lint-checkjs.js +2 -1
- package/dist/src/core/patch-parse.js +8 -6
- 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/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 +91 -117
- package/dist/src/core/token-inventory.js +12 -7
- package/dist/src/core/typecheck.js +6 -6
- package/dist/src/types/commands/options.d.ts +14 -3
- 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.js +0 -3
- package/dist/src/utils/process.d.ts +3 -2
- package/dist/src/utils/process.js +19 -7
- package/docs/configuration.md +12 -11
- package/docs/lifecycle-invariants.md +9 -5
- package/docs/machine-output.md +36 -19
- package/docs/patch-workflow.md +15 -0
- package/docs/testing.md +77 -12
- package/package.json +6 -5
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
import { exec } from '../utils/process.js';
|
|
3
|
+
/** Parent evidence; an unreadable parent is never proof of an orphan. */
|
|
4
|
+
export async function readProcessOwner(pid) {
|
|
5
|
+
if (process.platform === 'win32')
|
|
6
|
+
return undefined;
|
|
7
|
+
try {
|
|
8
|
+
const result = await exec('ps', ['-p', String(pid), '-o', 'ppid='], { timeout: 5000 });
|
|
9
|
+
const parentPid = Number(result.stdout.trim());
|
|
10
|
+
if (result.exitCode !== 0 || !Number.isSafeInteger(parentPid) || parentPid < 1)
|
|
11
|
+
return undefined;
|
|
12
|
+
if (parentPid === 1)
|
|
13
|
+
return { parentPid, owner: 'init/launchd (parent exited)' };
|
|
14
|
+
const parent = await exec('ps', ['-p', String(parentPid), '-o', 'args='], { timeout: 5000 });
|
|
15
|
+
if (parent.exitCode !== 0 || !parent.stdout.trim())
|
|
16
|
+
return undefined;
|
|
17
|
+
return { parentPid, owner: parent.stdout.trim() };
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/** Harness arguments alone never prove abandonment: require a dead parent too. */
|
|
24
|
+
export function isOrphanedHarness(commandLine, parentPid) {
|
|
25
|
+
return (parentPid === 1 &&
|
|
26
|
+
(/\s-marionette(?:\s|$)/.test(commandLine) ||
|
|
27
|
+
/\s-profile(?:\s|=)[^\n]*(?:fireforge-|mochitest|rust_mozprofile)/.test(commandLine)));
|
|
28
|
+
}
|
|
29
|
+
/** Refreshes the exact command and orphan evidence immediately before signaling. */
|
|
30
|
+
export async function isStillOrphanedProcess(pid, commandLine) {
|
|
31
|
+
// Windows ownership has no verified implementation; never infer it from
|
|
32
|
+
// command-line appearance or enable a destructive PowerShell fallback.
|
|
33
|
+
if (process.platform === 'win32')
|
|
34
|
+
return false;
|
|
35
|
+
try {
|
|
36
|
+
const result = await exec('ps', ['-p', String(pid), '-o', 'ppid=,args='], { timeout: 5000 });
|
|
37
|
+
const match = /^\s*(\d+)\s+(.+?)\s*$/.exec(result.stdout);
|
|
38
|
+
return result.exitCode === 0 && match?.[1] === '1' && match[2] === commandLine;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -25,7 +25,7 @@ export type { HarnessCrashSignature } from './test-harness-signature.js';
|
|
|
25
25
|
import type { HarnessCrashSignature } from './test-harness-signature.js';
|
|
26
26
|
export { headedDisplayAsleepVerdictNote, headedNoOutputTimeoutHint } from './test-stall-triage.js';
|
|
27
27
|
/** How a completed harness run should be interpreted. */
|
|
28
|
-
export type HarnessRunClassification = 'tests-ran-ok' | 'test-failures' | 'harness-crash' | 'no-tests';
|
|
28
|
+
export type HarnessRunClassification = 'tests-ran-ok' | 'test-failures' | 'harness-crash' | 'harness-arguments' | 'no-tests';
|
|
29
29
|
/** Numeric counts parsed from the harness's embedded result summary. */
|
|
30
30
|
export interface HarnessSummaryCounts {
|
|
31
31
|
/** Total checks the suite ran (`Ran N checks`), when the summary prints one. */
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
*/
|
|
25
25
|
import { hasKnownTeardownNoise } from './mach-known-noise-filter.js';
|
|
26
26
|
import { truncateEvidence, unmarkedFailureEvidenceNote, } from './test-harness-verdict-notes.js';
|
|
27
|
+
import { classifyProfileArgumentFailure } from './test-profile-files.js';
|
|
27
28
|
// The no-output-stall triage text lives in `test-stall-triage.ts` (it is
|
|
28
29
|
// operator advice, not classification) and is re-exported here so the
|
|
29
30
|
// command layer keeps importing its harness diagnostics from one module.
|
|
@@ -472,6 +473,9 @@ export function detectHarnessCrashSignature(output) {
|
|
|
472
473
|
*/
|
|
473
474
|
export function classifyHarnessRun(exitCode, output, requestedPaths) {
|
|
474
475
|
const counts = extractSummaryCounts(output);
|
|
476
|
+
const profileFailure = classifyProfileArgumentFailure(output);
|
|
477
|
+
if (profileFailure)
|
|
478
|
+
return { ...profileFailure, ...counts };
|
|
475
479
|
const realFailures = realUnexpectedFailureLines(output);
|
|
476
480
|
const firstRealFailure = realFailures[0];
|
|
477
481
|
const failureBlocks = collectUnexpectedFailureBlocks(output);
|
|
@@ -660,11 +664,7 @@ export function formatFireforgeVerdictLine(verdict, shards) {
|
|
|
660
664
|
if (verdict.kind === 'tests-ran-ok') {
|
|
661
665
|
return `FIREFORGE-VERDICT: PASS${counts}${shardSuffix}${note}`;
|
|
662
666
|
}
|
|
663
|
-
const reason = verdict.kind === 'harness-crash'
|
|
664
|
-
? 'crash'
|
|
665
|
-
: verdict.kind === 'no-tests'
|
|
666
|
-
? 'no-tests'
|
|
667
|
-
: 'test-failures';
|
|
667
|
+
const reason = verdict.kind === 'harness-crash' ? 'crash' : verdict.kind;
|
|
668
668
|
return `FIREFORGE-VERDICT: FAIL reason=${reason}${counts}${shardSuffix}${note}`;
|
|
669
669
|
}
|
|
670
670
|
/**
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface TestHostState {
|
|
2
|
+
load: number;
|
|
3
|
+
topProcess?: {
|
|
4
|
+
cpu: number;
|
|
5
|
+
command: string;
|
|
6
|
+
};
|
|
7
|
+
power: 'ac' | 'battery' | 'unknown';
|
|
8
|
+
}
|
|
9
|
+
/** Samples load/top process and the macOS power SOURCE (including charging on AC). */
|
|
10
|
+
export declare function sampleTestHost(): Promise<TestHostState>;
|
|
11
|
+
/** Reports host contention alongside a suite's result, without inventing a test failure. */
|
|
12
|
+
export declare function reportTestHost(state: TestHostState, phase: string): boolean;
|
|
13
|
+
/** Stamps each perf artifact before the verdict, preserving the producer's schema and data. */
|
|
14
|
+
export declare function stampPerfHost(env: Record<string, string> | undefined, start: TestHostState, end: TestHostState): Promise<boolean>;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
import { loadavg } from 'node:os';
|
|
3
|
+
import { toError } from '../utils/errors.js';
|
|
4
|
+
import { pathExists, readJson, writeJson } from '../utils/fs.js';
|
|
5
|
+
import { notice } from '../utils/logger.js';
|
|
6
|
+
import { exec } from '../utils/process.js';
|
|
7
|
+
import { isObject } from '../utils/validation.js';
|
|
8
|
+
/** Samples load/top process and the macOS power SOURCE (including charging on AC). */
|
|
9
|
+
export async function sampleTestHost() {
|
|
10
|
+
const state = { load: loadavg()[0] ?? 0, power: 'unknown' };
|
|
11
|
+
try {
|
|
12
|
+
const result = await exec('ps', ['-axo', 'pcpu=,comm='], { timeout: 5000 });
|
|
13
|
+
const processes = result.stdout
|
|
14
|
+
.split('\n')
|
|
15
|
+
.flatMap((line) => {
|
|
16
|
+
const match = /^\s*([\d.]+)\s+(.+)$/.exec(line);
|
|
17
|
+
return match ? [{ cpu: Number(match[1]), command: match[2] ?? '' }] : [];
|
|
18
|
+
})
|
|
19
|
+
.sort((a, b) => b.cpu - a.cpu);
|
|
20
|
+
if (processes[0])
|
|
21
|
+
state.topProcess = processes[0];
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
/* Host diagnostics remain best effort. */
|
|
25
|
+
}
|
|
26
|
+
if (process.platform === 'darwin') {
|
|
27
|
+
try {
|
|
28
|
+
const result = await exec('pmset', ['-g', 'ps'], { timeout: 5000 });
|
|
29
|
+
state.power = /'AC Power'/.test(result.stdout)
|
|
30
|
+
? 'ac'
|
|
31
|
+
: /'Battery Power'/.test(result.stdout)
|
|
32
|
+
? 'battery'
|
|
33
|
+
: 'unknown';
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
/* Keep unknown when power tooling is unavailable. */
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return state;
|
|
40
|
+
}
|
|
41
|
+
/** Reports host contention alongside a suite's result, without inventing a test failure. */
|
|
42
|
+
export function reportTestHost(state, phase) {
|
|
43
|
+
const pegged = (state.topProcess?.cpu ?? 0) >= 90;
|
|
44
|
+
if (state.load < 4 && !pegged)
|
|
45
|
+
return false;
|
|
46
|
+
notice(`Host load at ${phase}: ${state.load.toFixed(2)}; top process ${state.topProcess?.command ?? '(unknown)'} ${state.topProcess?.cpu ?? 0}% CPU. Timing failures may reflect host contention.`);
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
/** Stamps each perf artifact before the verdict, preserving the producer's schema and data. */
|
|
50
|
+
export async function stampPerfHost(env, start, end) {
|
|
51
|
+
if (!env)
|
|
52
|
+
return false;
|
|
53
|
+
const artifacts = [
|
|
54
|
+
...new Set(Object.entries(env)
|
|
55
|
+
.filter(([key]) => key.endsWith('_PERF_SAMPLE_JSON'))
|
|
56
|
+
.map(([, path]) => path)),
|
|
57
|
+
];
|
|
58
|
+
if (artifacts.length === 0)
|
|
59
|
+
return false;
|
|
60
|
+
const changed = start.power !== 'unknown' && end.power !== 'unknown' && start.power !== end.power;
|
|
61
|
+
if (changed)
|
|
62
|
+
notice(`Perf power source changed during the run (${start.power} → ${end.power}); this sample is inconclusive.`);
|
|
63
|
+
for (const artifact of artifacts) {
|
|
64
|
+
try {
|
|
65
|
+
if (!(await pathExists(artifact)))
|
|
66
|
+
continue;
|
|
67
|
+
const sample = await readJson(artifact);
|
|
68
|
+
if (!isObject(sample))
|
|
69
|
+
throw new Error('sample JSON is not an object');
|
|
70
|
+
await writeJson(artifact, {
|
|
71
|
+
...sample,
|
|
72
|
+
fireforgeHost: { start, end, powerSourceChanged: changed },
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
notice(`Could not annotate perf artifact ${artifact}: ${toError(error).message}. The harness outcome is preserved; inspect or regenerate this artifact before using it as perf evidence.`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return changed;
|
|
80
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Stages explicit profile overlays with relative destinations; cleanup belongs to the caller. */
|
|
2
|
+
export interface StagedProfileFiles {
|
|
3
|
+
root: string;
|
|
4
|
+
args: string[];
|
|
5
|
+
env: Record<string, string>;
|
|
6
|
+
}
|
|
7
|
+
/** Prepares profile overlays and browser/port ownership preconditions. */
|
|
8
|
+
export declare function stageProfileFiles(projectRoot: string, files: readonly string[]): Promise<StagedProfileFiles>;
|
|
9
|
+
/** Python startup overlay: allow merging ONLY the explicitly staged profile directories. */
|
|
10
|
+
export declare const PROFILE_MERGE_PYTHON = "\nimport os\nimport shutil\n_fireforge_profile_root = os.environ.get(\"FIREFORGE_PROFILE_MERGE_ROOT\")\nif _fireforge_profile_root and not getattr(shutil.copytree, \"_fireforge_profile_merge\", False):\n _fireforge_copytree = shutil.copytree\n def _fireforge_merge_copytree(src, dst, *args, **kwargs):\n if os.path.realpath(src).startswith(os.path.realpath(_fireforge_profile_root) + os.sep):\n # Python's recursive calls may supply dirs_exist_ok positionally.\n if len(args) >= 5:\n args = args[:4] + (True,) + args[5:]\n else:\n kwargs[\"dirs_exist_ok\"] = True\n return _fireforge_copytree(src, dst, *args, **kwargs)\n _fireforge_merge_copytree._fireforge_profile_merge = True\n shutil.copytree = _fireforge_merge_copytree\n";
|
|
11
|
+
/** Classifies the profile setup traceback separately from stale packaging. */
|
|
12
|
+
export declare function classifyProfileArgumentFailure(output: string): {
|
|
13
|
+
kind: 'harness-arguments';
|
|
14
|
+
note: string;
|
|
15
|
+
} | undefined;
|
|
16
|
+
/** Cleanup is independent of the harness verdict and cannot hide its primary error. */
|
|
17
|
+
export declare function cleanupProfileFiles(root: string): Promise<void>;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
import { copyFile, mkdir, mkdtemp, rm } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { dirname, join, resolve } from 'node:path';
|
|
5
|
+
import { InvalidArgumentError } from '../errors/base.js';
|
|
6
|
+
import { toError } from '../utils/errors.js';
|
|
7
|
+
import { warn } from '../utils/logger.js';
|
|
8
|
+
/** Prepares profile overlays and browser/port ownership preconditions. */
|
|
9
|
+
export async function stageProfileFiles(projectRoot, files) {
|
|
10
|
+
const root = await mkdtemp(join(tmpdir(), 'fireforge-profile-overlay-'));
|
|
11
|
+
const top = new Set();
|
|
12
|
+
try {
|
|
13
|
+
for (const file of files) {
|
|
14
|
+
const separator = file.indexOf('=');
|
|
15
|
+
const source = file.slice(0, separator);
|
|
16
|
+
const dest = file.slice(separator + 1).replaceAll('\\', '/');
|
|
17
|
+
if (separator <= 0 ||
|
|
18
|
+
!dest ||
|
|
19
|
+
dest.startsWith('/') ||
|
|
20
|
+
dest.includes(':') ||
|
|
21
|
+
dest.split('/').some((part) => !part || part === '.' || part === '..')) {
|
|
22
|
+
throw new InvalidArgumentError('--profile-file requires source=relative/destination with no traversal.', '--profile-file');
|
|
23
|
+
}
|
|
24
|
+
const target = join(root, dest);
|
|
25
|
+
await mkdir(dirname(target), { recursive: true });
|
|
26
|
+
await copyFile(resolve(projectRoot, source), target);
|
|
27
|
+
top.add(dest.split('/')[0] ?? dest);
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
root,
|
|
31
|
+
args: [...top].map((path) => `--extra-profile-file=${join(root, path)}`),
|
|
32
|
+
env: { FIREFORGE_PROFILE_MERGE_ROOT: root },
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
await cleanupProfileFiles(root);
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** Python startup overlay: allow merging ONLY the explicitly staged profile directories. */
|
|
41
|
+
export const PROFILE_MERGE_PYTHON = `
|
|
42
|
+
import os
|
|
43
|
+
import shutil
|
|
44
|
+
_fireforge_profile_root = os.environ.get("FIREFORGE_PROFILE_MERGE_ROOT")
|
|
45
|
+
if _fireforge_profile_root and not getattr(shutil.copytree, "_fireforge_profile_merge", False):
|
|
46
|
+
_fireforge_copytree = shutil.copytree
|
|
47
|
+
def _fireforge_merge_copytree(src, dst, *args, **kwargs):
|
|
48
|
+
if os.path.realpath(src).startswith(os.path.realpath(_fireforge_profile_root) + os.sep):
|
|
49
|
+
# Python's recursive calls may supply dirs_exist_ok positionally.
|
|
50
|
+
if len(args) >= 5:
|
|
51
|
+
args = args[:4] + (True,) + args[5:]
|
|
52
|
+
else:
|
|
53
|
+
kwargs["dirs_exist_ok"] = True
|
|
54
|
+
return _fireforge_copytree(src, dst, *args, **kwargs)
|
|
55
|
+
_fireforge_merge_copytree._fireforge_profile_merge = True
|
|
56
|
+
shutil.copytree = _fireforge_merge_copytree
|
|
57
|
+
`;
|
|
58
|
+
/** Classifies the profile setup traceback separately from stale packaging. */
|
|
59
|
+
export function classifyProfileArgumentFailure(output) {
|
|
60
|
+
if (!/copyExtraFilesToProfile/.test(output) || !/FileExistsError/.test(output))
|
|
61
|
+
return undefined;
|
|
62
|
+
return {
|
|
63
|
+
kind: 'harness-arguments',
|
|
64
|
+
note: 'profile directory collision; use --profile-file source=chrome/userChrome.css to merge into the existing profile',
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/** Cleanup is independent of the harness verdict and cannot hide its primary error. */
|
|
68
|
+
export async function cleanupProfileFiles(root) {
|
|
69
|
+
try {
|
|
70
|
+
await rm(root, { recursive: true, force: true });
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
warn(`Could not remove staged profile files ${root}: ${toError(error).message}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -27,8 +27,8 @@ export interface StaleBuildResult {
|
|
|
27
27
|
* Probes the engine tree for packageable changes since the last successful
|
|
28
28
|
* `fireforge build`. Returns a summary the `fireforge test` handler renders
|
|
29
29
|
* as an up-front warning when `--build` was not passed. The probe never
|
|
30
|
-
*
|
|
31
|
-
*
|
|
30
|
+
* refuses a malformed persisted baseline. Git failures and a missing baseline
|
|
31
|
+
* degrade to `stale: false` because they provide no comparison anchor.
|
|
32
32
|
*
|
|
33
33
|
* @param projectRoot Root directory of the project.
|
|
34
34
|
* @param engineDir Path to the engine directory.
|
|
@@ -52,11 +52,10 @@ export declare const FULL_SUITE_REQUEST = "(entire suite)";
|
|
|
52
52
|
* `_tests/` support fixtures and hang rather than fail.
|
|
53
53
|
*
|
|
54
54
|
* Coverage semantics: an absent claim and `'full'` cover everything. A
|
|
55
|
-
* scoped list covers a request path when the request equals a covered entry
|
|
56
|
-
* sits beneath
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
* covered file is packaged too). Both sides are normalized to forward
|
|
55
|
+
* scoped list covers a request path when the request equals a covered entry
|
|
56
|
+
* or sits beneath it. A file claim never widens to its containing directory:
|
|
57
|
+
* sibling files can belong to different manifests or harnesses, and a dot in
|
|
58
|
+
* a directory name does not make it a file. Both sides are normalized to forward
|
|
60
59
|
* slashes so Windows-style CLI input cannot defeat the prefix rule (baseline
|
|
61
60
|
* paths are POSIX by convention). A request with no paths is a full-suite
|
|
62
61
|
* run and is never covered by a scoped list, so the
|
|
@@ -35,14 +35,14 @@ const STALE_PATHS_LIMIT = 10;
|
|
|
35
35
|
* Probes the engine tree for packageable changes since the last successful
|
|
36
36
|
* `fireforge build`. Returns a summary the `fireforge test` handler renders
|
|
37
37
|
* as an up-front warning when `--build` was not passed. The probe never
|
|
38
|
-
*
|
|
39
|
-
*
|
|
38
|
+
* refuses a malformed persisted baseline. Git failures and a missing baseline
|
|
39
|
+
* degrade to `stale: false` because they provide no comparison anchor.
|
|
40
40
|
*
|
|
41
41
|
* @param projectRoot Root directory of the project.
|
|
42
42
|
* @param engineDir Path to the engine directory.
|
|
43
43
|
*/
|
|
44
44
|
export async function checkStaleBuildForTest(projectRoot, engineDir) {
|
|
45
|
-
const baseline = await readBuildBaseline(projectRoot);
|
|
45
|
+
const baseline = await readBuildBaseline(projectRoot, 'refuse');
|
|
46
46
|
if (!baseline) {
|
|
47
47
|
return { stale: false, changedPaths: [], truncated: 0, baseline: undefined };
|
|
48
48
|
}
|
|
@@ -93,11 +93,10 @@ export const FULL_SUITE_REQUEST = '(entire suite)';
|
|
|
93
93
|
* `_tests/` support fixtures and hang rather than fail.
|
|
94
94
|
*
|
|
95
95
|
* Coverage semantics: an absent claim and `'full'` cover everything. A
|
|
96
|
-
* scoped list covers a request path when the request equals a covered entry
|
|
97
|
-
* sits beneath
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
* covered file is packaged too). Both sides are normalized to forward
|
|
96
|
+
* scoped list covers a request path when the request equals a covered entry
|
|
97
|
+
* or sits beneath it. A file claim never widens to its containing directory:
|
|
98
|
+
* sibling files can belong to different manifests or harnesses, and a dot in
|
|
99
|
+
* a directory name does not make it a file. Both sides are normalized to forward
|
|
101
100
|
* slashes so Windows-style CLI input cannot defeat the prefix rule (baseline
|
|
102
101
|
* paths are POSIX by convention). A request with no paths is a full-suite
|
|
103
102
|
* run and is never covered by a scoped list, so the
|
|
@@ -114,29 +113,9 @@ export function findUncoveredRequestPaths(coverage, requestedPaths) {
|
|
|
114
113
|
}
|
|
115
114
|
return requestedPaths.filter((requested) => {
|
|
116
115
|
const path = normalizeCoveragePath(requested);
|
|
117
|
-
|
|
118
|
-
return !covered.some((c) => path === c || path.startsWith(`${c}/`) || granule === toManifestGranule(c));
|
|
116
|
+
return !covered.some((c) => path === c || path.startsWith(`${c}/`));
|
|
119
117
|
});
|
|
120
118
|
}
|
|
121
|
-
/**
|
|
122
|
-
* Maps a normalized request/coverage path to the "manifest granule" the
|
|
123
|
-
* packaged runtime actually staged: an extension-bearing basename (a test
|
|
124
|
-
* FILE) maps to its containing directory, a directory (no dot in the
|
|
125
|
-
* basename) maps to itself. Purely lexical: the directory-as-manifest
|
|
126
|
-
* approximation holds because xpcshell/mochitest manifests live next to
|
|
127
|
-
* their test files and a scoped `test --build` stages the whole manifest
|
|
128
|
-
* directory into `obj-*`/`_tests/`, not single files. Caveat: a directory
|
|
129
|
-
* whose basename contains a dot is misread as a file and mapped to its
|
|
130
|
-
* parent, which widens (never narrows) the covered granule.
|
|
131
|
-
*/
|
|
132
|
-
function toManifestGranule(path) {
|
|
133
|
-
const slash = path.lastIndexOf('/');
|
|
134
|
-
const base = slash === -1 ? path : path.slice(slash + 1);
|
|
135
|
-
if (!base.includes('.')) {
|
|
136
|
-
return path;
|
|
137
|
-
}
|
|
138
|
-
return slash === -1 ? '' : path.slice(0, slash);
|
|
139
|
-
}
|
|
140
119
|
/** Normalizes a path for coverage comparison: forward slashes, no trailing slash. */
|
|
141
120
|
function normalizeCoveragePath(path) {
|
|
142
121
|
return normalizePathSlashes(path.trim()).replace(/\/+$/, '');
|
|
@@ -196,9 +175,7 @@ export async function findChangedTestManifestsForPaths(engineDir, baseline, unco
|
|
|
196
175
|
verbose(`Coverage refusal: manifest probe failed — ${toError(error).message}`);
|
|
197
176
|
return [];
|
|
198
177
|
}
|
|
199
|
-
const requestDirs = uncovered
|
|
200
|
-
.filter((p) => p !== FULL_SUITE_REQUEST)
|
|
201
|
-
.map((p) => toManifestGranule(normalizeCoveragePath(p)));
|
|
178
|
+
const requestDirs = uncovered.filter((p) => p !== FULL_SUITE_REQUEST).map(normalizeCoveragePath);
|
|
202
179
|
const hits = changed.filter((path) => {
|
|
203
180
|
if (!isTestManifestPath(path))
|
|
204
181
|
return false;
|
|
@@ -5,32 +5,13 @@
|
|
|
5
5
|
* agree on what "exists" means.
|
|
6
6
|
*/
|
|
7
7
|
export declare function categoryHeaderExists(lines: string[], category: string): boolean;
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
* - Multi-line: `/* =====` opening, `Foo` on any of the next ~5 lines,
|
|
16
|
-
* closing `*\/`.
|
|
17
|
-
*
|
|
18
|
-
* This helper exists as a pure inspector. It never throws on malformed
|
|
19
|
-
* headers and silently skips shapes it cannot parse.
|
|
20
|
-
*/
|
|
21
|
-
/**
|
|
22
|
-
* Category name declared by the banner starting at `index`, or `undefined`
|
|
23
|
-
* when that line opens no banner.
|
|
24
|
-
*
|
|
25
|
-
* The single reader for both banner shapes, so the inventory walk, the
|
|
26
|
-
* "available categories" error body and the section finder cannot drift on
|
|
27
|
-
* what counts as a header.
|
|
28
|
-
*
|
|
29
|
-
* @param lines - Tokens CSS split into lines
|
|
30
|
-
* @param index - 0-based line index to test
|
|
31
|
-
* @returns The declared category name, or undefined
|
|
32
|
-
*/
|
|
33
|
-
export declare function categoryBannerNameAt(lines: string[], index: number): string | undefined;
|
|
8
|
+
export interface TokenCategorySection {
|
|
9
|
+
name: string;
|
|
10
|
+
categoryLine: number;
|
|
11
|
+
sectionEnd: number;
|
|
12
|
+
}
|
|
13
|
+
/** Parses category regions owned by the base root, including nested CSS rules. */
|
|
14
|
+
export declare function parseTokenCategorySections(lines: string[]): TokenCategorySection[];
|
|
34
15
|
/**
|
|
35
16
|
* Asserts the category banner exists (or that creation was requested).
|
|
36
17
|
* Throws a GeneralError naming the available categories and the
|