@hominis/fireforge 0.50.0 → 0.51.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +47 -0
- package/dist/build-info.json +4 -4
- package/dist/src/commands/check-report.d.ts +17 -4
- package/dist/src/commands/check-report.js +19 -6
- package/dist/src/commands/check-run.d.ts +5 -0
- package/dist/src/commands/check-run.js +9 -1
- package/dist/src/commands/check-steps.d.ts +8 -0
- package/dist/src/commands/check-steps.js +31 -6
- package/dist/src/commands/check.js +1 -1
- package/dist/src/commands/declarations-report.d.ts +43 -0
- package/dist/src/commands/declarations-report.js +76 -0
- package/dist/src/commands/export-placement-gate.js +8 -0
- package/dist/src/commands/furnace/rename-registrations.js +7 -2
- package/dist/src/commands/furnace/validate.js +26 -1
- package/dist/src/commands/lint-per-patch.js +6 -2
- package/dist/src/commands/lint-per-run-checkjs.js +2 -2
- package/dist/src/commands/lint-report.js +7 -2
- package/dist/src/commands/manifest.js +2 -0
- package/dist/src/commands/park.d.ts +22 -0
- package/dist/src/commands/park.js +232 -0
- package/dist/src/commands/patch/delete.js +11 -6
- package/dist/src/commands/patch/index.d.ts +3 -3
- package/dist/src/commands/patch/index.js +8 -4
- package/dist/src/commands/patch/lint-ignore-from.d.ts +44 -0
- package/dist/src/commands/patch/lint-ignore-from.js +142 -0
- package/dist/src/commands/patch/lint-ignore.js +39 -29
- package/dist/src/commands/patch/move-files-create.js +11 -4
- package/dist/src/commands/patch/move-files-into.js +10 -4
- package/dist/src/commands/patch/move-files.js +3 -1
- package/dist/src/commands/patch/removal-created-files.d.ts +48 -0
- package/dist/src/commands/patch/removal-created-files.js +83 -0
- package/dist/src/commands/patch/rename.js +15 -3
- package/dist/src/commands/patch/reserve.d.ts +33 -0
- package/dist/src/commands/patch/reserve.js +152 -0
- package/dist/src/commands/patch/retire.js +11 -6
- package/dist/src/commands/patch/split-plan.d.ts +10 -1
- package/dist/src/commands/patch/split-plan.js +16 -1
- package/dist/src/commands/patch/split.js +14 -5
- package/dist/src/commands/patch/stage.d.ts +34 -0
- package/dist/src/commands/patch/stage.js +153 -0
- package/dist/src/commands/patch/staged-dependency-remove.d.ts +31 -0
- package/dist/src/commands/patch/staged-dependency-remove.js +80 -0
- package/dist/src/commands/patch/staged-dependency.js +40 -98
- package/dist/src/commands/re-export-adjacent.d.ts +5 -0
- package/dist/src/commands/re-export-adjacent.js +4 -3
- package/dist/src/commands/re-export-bulk-scan.d.ts +2 -1
- package/dist/src/commands/re-export-bulk-scan.js +22 -7
- package/dist/src/commands/re-export-hunks.d.ts +35 -0
- package/dist/src/commands/re-export-hunks.js +220 -0
- package/dist/src/commands/re-export-options.js +29 -2
- package/dist/src/commands/re-export-purity-scope.d.ts +18 -0
- package/dist/src/commands/re-export-purity-scope.js +131 -0
- package/dist/src/commands/re-export-register.js +15 -1
- package/dist/src/commands/re-export-removed.d.ts +1 -1
- package/dist/src/commands/re-export-removed.js +17 -2
- package/dist/src/commands/re-export-scan.d.ts +2 -0
- package/dist/src/commands/re-export-scan.js +4 -1
- package/dist/src/commands/re-export-single.js +102 -42
- package/dist/src/commands/re-export-stale-registrations.d.ts +18 -0
- package/dist/src/commands/re-export-stale-registrations.js +62 -0
- package/dist/src/commands/re-export.js +35 -34
- package/dist/src/commands/test-build-furnace.d.ts +3 -2
- package/dist/src/commands/test-build-furnace.js +8 -2
- package/dist/src/commands/test-build-phase.js +24 -7
- package/dist/src/commands/test-checkjs.js +3 -2
- package/dist/src/commands/test-dispatch.d.ts +39 -0
- package/dist/src/commands/test-dispatch.js +211 -0
- package/dist/src/commands/test-furnace-reach.d.ts +37 -0
- package/dist/src/commands/test-furnace-reach.js +132 -0
- package/dist/src/commands/test-orchestrate.d.ts +9 -0
- package/dist/src/commands/test-orchestrate.js +22 -4
- package/dist/src/commands/test-perf-scope.d.ts +21 -0
- package/dist/src/commands/test-perf-scope.js +55 -0
- package/dist/src/commands/test-prelock.js +100 -4
- package/dist/src/commands/test-register.js +18 -3
- package/dist/src/commands/test-results.d.ts +62 -0
- package/dist/src/commands/test-results.js +62 -0
- package/dist/src/commands/test-run.d.ts +13 -55
- package/dist/src/commands/test-run.js +73 -101
- package/dist/src/commands/test-shards.d.ts +110 -0
- package/dist/src/commands/test-shards.js +255 -0
- package/dist/src/commands/test-stale-gate.d.ts +5 -1
- package/dist/src/commands/test-stale-gate.js +4 -2
- package/dist/src/commands/test-verdict.d.ts +2 -1
- package/dist/src/commands/test.d.ts +1 -1
- package/dist/src/commands/test.js +65 -75
- package/dist/src/commands/token-lint.js +48 -8
- package/dist/src/commands/verify.js +9 -2
- package/dist/src/core/build-prepare.d.ts +6 -0
- package/dist/src/core/build-prepare.js +29 -3
- package/dist/src/core/config-paths.d.ts +1 -1
- package/dist/src/core/config-paths.js +2 -0
- package/dist/src/core/config-validate-check.js +83 -7
- package/dist/src/core/config-validate-test-toolchains.js +14 -0
- package/dist/src/core/config-validate-token-scales.d.ts +3 -0
- package/dist/src/core/config-validate-token-scales.js +47 -5
- package/dist/src/core/config-validate.js +6 -0
- package/dist/src/core/coverage-extend.js +2 -2
- package/dist/src/core/coverage-records.d.ts +11 -0
- package/dist/src/core/coverage-records.js +34 -0
- package/dist/src/core/engine-session-lock.d.ts +6 -0
- package/dist/src/core/engine-session-lock.js +10 -2
- package/dist/src/core/file-lock-shared.d.ts +57 -0
- package/dist/src/core/file-lock-shared.js +117 -0
- package/dist/src/core/file-lock.d.ts +9 -0
- package/dist/src/core/file-lock.js +58 -2
- package/dist/src/core/furnace-apply.js +10 -10
- package/dist/src/core/furnace-config-validate-exceptions.d.ts +10 -3
- package/dist/src/core/furnace-config-validate-exceptions.js +39 -8
- package/dist/src/core/furnace-config-validate.js +7 -1
- package/dist/src/core/furnace-registration-jar-inplace.d.ts +12 -0
- package/dist/src/core/furnace-registration-jar-inplace.js +80 -0
- package/dist/src/core/furnace-validate-accessibility.d.ts +4 -2
- package/dist/src/core/furnace-validate-accessibility.js +61 -27
- package/dist/src/core/furnace-validate-exceptions.d.ts +12 -4
- package/dist/src/core/furnace-validate-exceptions.js +41 -4
- package/dist/src/core/furnace-validate-icon-button.js +30 -1
- package/dist/src/core/history-log-file.js +8 -1
- package/dist/src/core/line-prefixer.d.ts +18 -0
- package/dist/src/core/line-prefixer.js +34 -0
- package/dist/src/core/mach.d.ts +7 -0
- package/dist/src/core/mach.js +24 -9
- package/dist/src/core/module-resolution-preflight.d.ts +19 -3
- package/dist/src/core/module-resolution-preflight.js +24 -6
- package/dist/src/core/negative-control-compiled.d.ts +27 -0
- package/dist/src/core/negative-control-compiled.js +73 -0
- package/dist/src/core/negative-control-diff.d.ts +14 -2
- package/dist/src/core/negative-control-diff.js +49 -6
- package/dist/src/core/negative-control.d.ts +12 -0
- package/dist/src/core/negative-control.js +48 -5
- package/dist/src/core/park-store.d.ts +63 -0
- package/dist/src/core/park-store.js +216 -0
- package/dist/src/core/patch-export-update.d.ts +4 -0
- package/dist/src/core/patch-export-update.js +12 -3
- package/dist/src/core/patch-export.d.ts +3 -2
- package/dist/src/core/patch-export.js +21 -14
- package/dist/src/core/patch-files.js +26 -8
- package/dist/src/core/patch-lint-checkjs-any-import.d.ts +6 -4
- package/dist/src/core/patch-lint-checkjs-any-import.js +9 -7
- package/dist/src/core/patch-lint-checkjs-diagnostics.d.ts +17 -0
- package/dist/src/core/patch-lint-checkjs-diagnostics.js +32 -0
- package/dist/src/core/patch-lint-checkjs.d.ts +10 -1
- package/dist/src/core/patch-lint-checkjs.js +13 -3
- package/dist/src/core/patch-lint-cross.d.ts +3 -1
- package/dist/src/core/patch-lint-cross.js +5 -0
- package/dist/src/core/patch-lint-css.js +14 -10
- package/dist/src/core/patch-lint-forward-registration-extended.d.ts +5 -0
- package/dist/src/core/patch-lint-forward-registration-extended.js +1 -1
- package/dist/src/core/patch-lint-ignore.d.ts +56 -12
- package/dist/src/core/patch-lint-ignore.js +173 -43
- package/dist/src/core/patch-lint-ownership.d.ts +10 -0
- package/dist/src/core/patch-lint-ownership.js +26 -0
- package/dist/src/core/patch-lint-size.js +9 -1
- package/dist/src/core/patch-lint-token-scale.d.ts +26 -8
- package/dist/src/core/patch-lint-token-scale.js +206 -29
- package/dist/src/core/patch-lint.js +2 -2
- package/dist/src/core/patch-manifest-io.js +8 -1
- package/dist/src/core/patch-manifest-key-order.d.ts +24 -0
- package/dist/src/core/patch-manifest-key-order.js +61 -0
- package/dist/src/core/patch-manifest-stage.d.ts +46 -0
- package/dist/src/core/patch-manifest-stage.js +96 -0
- package/dist/src/core/patch-manifest-validate.js +27 -1
- package/dist/src/core/patch-order-legal.d.ts +2 -1
- package/dist/src/core/patch-order-legal.js +7 -2
- package/dist/src/core/patch-policy.d.ts +6 -2
- package/dist/src/core/patch-policy.js +14 -7
- package/dist/src/core/patch-retired.d.ts +26 -4
- package/dist/src/core/patch-retired.js +40 -2
- package/dist/src/core/run-log.d.ts +6 -0
- package/dist/src/core/run-log.js +17 -1
- package/dist/src/core/test-harness-crash.d.ts +2 -1
- package/dist/src/core/test-harness-crash.js +5 -8
- package/dist/src/core/test-harness-signature.d.ts +16 -0
- package/dist/src/core/test-harness-signature.js +19 -1
- package/dist/src/core/test-raw-log.d.ts +66 -0
- package/dist/src/core/test-raw-log.js +122 -0
- package/dist/src/core/test-run-completeness.d.ts +36 -3
- package/dist/src/core/test-run-completeness.js +57 -17
- package/dist/src/core/test-xpcshell-retry.d.ts +2 -0
- package/dist/src/core/test-xpcshell-retry.js +2 -1
- package/dist/src/core/token-lint.d.ts +5 -2
- package/dist/src/core/token-lint.js +8 -3
- package/dist/src/core/token-sibling-sheets.d.ts +46 -0
- package/dist/src/core/token-sibling-sheets.js +119 -0
- package/dist/src/core/tree-guard.js +2 -0
- package/dist/src/core/typecheck-shim.d.ts +5 -1
- package/dist/src/core/typecheck-shim.js +115 -2
- package/dist/src/core/typecheck.js +3 -1
- package/dist/src/index.d.ts +9 -3
- package/dist/src/index.js +4 -1
- package/dist/src/types/commands/index.d.ts +2 -2
- package/dist/src/types/commands/options.d.ts +68 -7
- package/dist/src/types/commands/patches.d.ts +43 -0
- package/dist/src/types/config.d.ts +74 -2
- package/dist/src/types/furnace.d.ts +20 -0
- package/dist/src/types/index.d.ts +1 -1
- package/dist/src/utils/options.d.ts +3 -2
- package/dist/src/utils/options.js +9 -4
- package/docs/configuration.md +94 -9
- package/docs/furnace.md +43 -1
- package/docs/lifecycle-invariants.md +13 -11
- package/docs/machine-output.md +64 -18
- package/docs/patch-workflow.md +174 -13
- package/docs/testing.md +73 -4
- package/package.json +3 -3
|
@@ -13,7 +13,7 @@ import type { HarnessRunVerdict } from '../core/test-harness-crash.js';
|
|
|
13
13
|
*/
|
|
14
14
|
export type FireforgeVerdictReason = 'crash' | 'harness-arguments' | 'no-tests' | 'test-failures' | 'preflight' | 'inconclusive' | 'lock-timeout' | 'killed';
|
|
15
15
|
/** Additive keys a run can stamp on its verdict line. */
|
|
16
|
-
export type VerdictRunAttributeKey = 'shuffle' | 'orphans-reaped' | 'host-load' | 'host-cpu-warning' | 'power-source' | 'power-changed' | 'furnace-applied' | 'furnace-apply';
|
|
16
|
+
export type VerdictRunAttributeKey = 'shuffle' | 'orphans-reaped' | 'host-load' | 'host-cpu-warning' | 'power-source' | 'power-changed' | 'furnace-applied' | 'furnace-apply' | 'furnace-skipped' | 'control-build' | 'drift-tolerated' | 'lock' | 'jobs' | 'results';
|
|
17
17
|
/** Records an additive attribute for this run's verdict line. */
|
|
18
18
|
export declare function setVerdictRunAttribute(key: VerdictRunAttributeKey, value: string): void;
|
|
19
19
|
/**
|
|
@@ -46,6 +46,7 @@ export declare function emitAggregateVerdict(body: string): void;
|
|
|
46
46
|
export declare function emitHarnessVerdict(verdict: HarnessRunVerdict, shards?: {
|
|
47
47
|
passed: number;
|
|
48
48
|
total: number;
|
|
49
|
+
failed?: readonly string[];
|
|
49
50
|
}): void;
|
|
50
51
|
/** Emits the pathless `--doctor` PASS verdict. */
|
|
51
52
|
export declare function emitPassVerdict(): void;
|
|
@@ -19,4 +19,4 @@ import type { TestOptions } from '../types/commands/index.js';
|
|
|
19
19
|
* @param options - Test options
|
|
20
20
|
* @param underLock - Runs the rest of the command under the engine session lock
|
|
21
21
|
*/
|
|
22
|
-
export declare function testCommand(projectRoot: string, testPaths: string[], options?: TestOptions, underLock?: (body: () => Promise<void
|
|
22
|
+
export declare function testCommand(projectRoot: string, testPaths: string[], options?: TestOptions, underLock?: (body: () => Promise<void>, mode?: 'exclusive' | 'shared') => Promise<void>): Promise<void>;
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
-
import { join } from 'node:path';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
3
|
import { getProjectPaths, loadConfig } from '../core/config.js';
|
|
4
4
|
import { assertEngineExists } from '../core/engine-precondition.js';
|
|
5
|
-
import { assertEngineGenerationUnchanged, snapshotEngineGeneration, } from '../core/engine-session-lock.js';
|
|
6
5
|
import { hasBuildArtifacts } from '../core/mach.js';
|
|
7
6
|
import { assertBuildArtifacts } from '../core/mach-build-artifacts.js';
|
|
8
7
|
import { ensureLaunchableBrowserNotRunning } from '../core/marionette-port.js';
|
|
@@ -13,23 +12,22 @@ import { analyzeTestPathScopes, formatScopeNotice, } from '../core/test-path-sco
|
|
|
13
12
|
import { cleanupProfileFiles, stageProfileFiles, } from '../core/test-profile-files.js';
|
|
14
13
|
import { assertObjdirMatchesTreeMarker } from '../core/tree-store.js';
|
|
15
14
|
import { FireForgeError, GeneralError, LockContentionError, PreflightRefusalError, } from '../errors/base.js';
|
|
16
|
-
import { BuildError } from '../errors/build.js';
|
|
17
15
|
import { toError } from '../utils/errors.js';
|
|
18
16
|
import { pathExists } from '../utils/fs.js';
|
|
19
|
-
import { info, intro,
|
|
17
|
+
import { info, intro, warn } from '../utils/logger.js';
|
|
20
18
|
import { resolveWaitLockSeconds } from '../utils/options.js';
|
|
21
19
|
import { stripEnginePrefix } from '../utils/paths.js';
|
|
22
20
|
import { appendMarionetteForwardingArgs, ensureTestBrowserEnvironment, runDoctorPreflight, } from './test-browser-preflight.js';
|
|
23
21
|
import { runTestBuildPhase } from './test-build-phase.js';
|
|
24
|
-
import {
|
|
25
|
-
import { buildPerfSampleEnv, mergeHarnessEnv, resolveShuffleSeed
|
|
22
|
+
import { assertParallelTestUsage, dispatchTests } from './test-dispatch.js';
|
|
23
|
+
import { buildPerfSampleEnv, mergeHarnessEnv, resolveShuffleSeed } from './test-harness-env.js';
|
|
26
24
|
import { removePgidFile, setActivePgidFile } from './test-harness-teardown.js';
|
|
27
|
-
import { assertPathlessTestMode, assertTestModeCombinations, canaryTimeoutSeconds, classifyBeforeDispatch,
|
|
25
|
+
import { assertPathlessTestMode, assertTestModeCombinations, canaryTimeoutSeconds, classifyBeforeDispatch, resolveCanaryPath, restartBetweenTestsArgs, } from './test-modes.js';
|
|
28
26
|
import { orchestrateTestRuns } from './test-orchestrate.js';
|
|
29
|
-
import { perfGenerationTolerance } from './test-perf-scope.js';
|
|
30
27
|
import { resolveLaunchablePathForTests, runTestPreLockPreflight, TEST_BUILD_PREFLIGHT, } from './test-prelock.js';
|
|
31
|
-
import {
|
|
32
|
-
import {
|
|
28
|
+
import { resetTestResults, withRawLogDir, writeTestResults } from './test-results.js';
|
|
29
|
+
import { DEFAULT_HARNESS_RETRIES } from './test-run.js';
|
|
30
|
+
import { emitFailVerdict, resetVerdictEmission, setVerdictRunAttribute, verdictEmitted, } from './test-verdict.js';
|
|
33
31
|
async function assertTestPathsExist(engineDir, testPaths) {
|
|
34
32
|
const missingPaths = [];
|
|
35
33
|
for (const testPath of testPaths) {
|
|
@@ -126,12 +124,33 @@ export async function testCommand(projectRoot, testPaths, options = {}, underLoc
|
|
|
126
124
|
staged = await stageProfileFiles(projectRoot, options.profileFile);
|
|
127
125
|
options = { ...options, machArg: [...(options.machArg ?? []), ...staged.args] };
|
|
128
126
|
}
|
|
127
|
+
assertParallelTestUsage(testPaths, options);
|
|
129
128
|
const lockedOptions = await runTestPreLockPreflight(projectRoot, testPaths, options);
|
|
130
129
|
const profileEnv = staged?.env;
|
|
131
|
-
|
|
130
|
+
// The pre-lock pass resolved `sharedLock` against config and the run's shape.
|
|
131
|
+
const lockMode = lockedOptions.sharedLock === true ? 'shared' : 'exclusive';
|
|
132
|
+
if (lockMode === 'shared')
|
|
133
|
+
setVerdictRunAttribute('lock', 'shared');
|
|
134
|
+
resetTestResults();
|
|
135
|
+
if (options.results !== undefined) {
|
|
136
|
+
setVerdictRunAttribute('results', resolve(projectRoot, options.results));
|
|
137
|
+
}
|
|
138
|
+
const orchestrate = (rawLogDir) => orchestrateTestRuns(projectRoot, testPaths, lockedOptions, (paths, runOptions, hooks) => runTestCommandBody(projectRoot, paths, runOptions, profileEnv, {
|
|
139
|
+
...hooks,
|
|
140
|
+
...(rawLogDir === undefined ? {} : { rawLogDir }),
|
|
141
|
+
}), (error) => {
|
|
132
142
|
renderPreflightRefusal(error);
|
|
133
143
|
emitFailVerdict('preflight', refusalNote(error));
|
|
134
|
-
})
|
|
144
|
+
});
|
|
145
|
+
const collectsResults = options.results !== undefined || options.shardSize !== undefined;
|
|
146
|
+
try {
|
|
147
|
+
await underLock(() => (collectsResults ? withRawLogDir((dir) => orchestrate(dir)) : orchestrate()), lockMode);
|
|
148
|
+
}
|
|
149
|
+
finally {
|
|
150
|
+
if (options.results !== undefined) {
|
|
151
|
+
await writeResultsFile(resolve(projectRoot, options.results));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
135
154
|
}
|
|
136
155
|
catch (error) {
|
|
137
156
|
if (!verdictEmitted()) {
|
|
@@ -164,6 +183,15 @@ export async function testCommand(projectRoot, testPaths, options = {}, underLoc
|
|
|
164
183
|
}
|
|
165
184
|
}
|
|
166
185
|
}
|
|
186
|
+
/** Writes `--results`; a failure warns, since the run's verdict is already out. */
|
|
187
|
+
async function writeResultsFile(path) {
|
|
188
|
+
try {
|
|
189
|
+
await writeTestResults(path);
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
warn(`Could not write --results ${path}: ${toError(error).message}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
167
195
|
/** The refusal class a preflight gate named, when it named one. */
|
|
168
196
|
function refusalNote(error) {
|
|
169
197
|
return error instanceof PreflightRefusalError ? error.note : undefined;
|
|
@@ -198,26 +226,6 @@ function renderPreflightRefusal(error) {
|
|
|
198
226
|
process.stdout.write(block);
|
|
199
227
|
writeToActiveRunLog(block);
|
|
200
228
|
}
|
|
201
|
-
/**
|
|
202
|
-
* Verifies `engine/` did not change under the run before any PASS verdict
|
|
203
|
-
* may print. On failure the run's verdict is `FAIL reason=inconclusive`,
|
|
204
|
-
* because the harness result exists but cannot be trusted (`preflight` would
|
|
205
|
-
* misdescribe a run whose harness already executed). It is emitted here so the
|
|
206
|
-
* guard's throw can never leave a stale or missing verdict line behind.
|
|
207
|
-
*/
|
|
208
|
-
async function verifyEngineGenerationOrEmitInconclusive(engineDir, before, powerChanged = false, perfSamples) {
|
|
209
|
-
try {
|
|
210
|
-
await (perfSamples === undefined
|
|
211
|
-
? assertEngineGenerationUnchanged(engineDir, before)
|
|
212
|
-
: assertEngineGenerationUnchanged(engineDir, before, perfGenerationTolerance(engineDir)));
|
|
213
|
-
if (powerChanged)
|
|
214
|
-
throw new GeneralError('Perf power source changed during the run; repeat this sitting on a stable power source.');
|
|
215
|
-
}
|
|
216
|
-
catch (error) {
|
|
217
|
-
emitFailVerdict('inconclusive');
|
|
218
|
-
throw error;
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
229
|
async function guardBuildBrowser(engineDir, launchablePath, options) {
|
|
222
230
|
if ((options.build || options.buildOnly) && launchablePath) {
|
|
223
231
|
await waitForPreflight(() => ensureLaunchableBrowserNotRunning(join(engineDir, launchablePath), {
|
|
@@ -254,7 +262,12 @@ function prepareDispatchArguments(classification, options, canaryPath, projectCo
|
|
|
254
262
|
? filterRedundantXpcshellFlavorArgs(options.machArg, classification)
|
|
255
263
|
: [];
|
|
256
264
|
const extraArgs = prepareExtraMachArgs(options, shuffleSeed, canaryPath, projectConfig, forwardedMachArgs);
|
|
257
|
-
|
|
265
|
+
// A `--shard-size` chunk runs several files in one browser; mochitest
|
|
266
|
+
// restarts it between them so a chunk judges each file as a shard would.
|
|
267
|
+
const restart = options.shardSize !== undefined && suite === 'mochitest'
|
|
268
|
+
? { ...options, restartBetweenTests: true }
|
|
269
|
+
: options;
|
|
270
|
+
extraArgs.push(...restartBetweenTestsArgs(restart, suite, forwardedMachArgs));
|
|
258
271
|
return { suite, shuffleSeed, extraArgs };
|
|
259
272
|
}
|
|
260
273
|
/**
|
|
@@ -327,6 +340,9 @@ async function runTestCommandBody(projectRoot, testPaths, options = {}, profileE
|
|
|
327
340
|
// documented `--mach-arg --marionette-port=NNNN` route still hits the
|
|
328
341
|
// wrapper preflight refusing on 2828 before the forwarded arg reaches
|
|
329
342
|
// mach.
|
|
343
|
+
// A negative control on compiled source goes in here, so the build
|
|
344
|
+
// compiles it (see negative-control-compiled.ts).
|
|
345
|
+
await hooks.beforeBuild?.();
|
|
330
346
|
if (await runTestBuildPhase(projectRoot, paths, projectConfig, harnessRetries, options, {
|
|
331
347
|
classification,
|
|
332
348
|
normalizedPaths: hooks.coveragePaths ?? normalizedPaths,
|
|
@@ -364,6 +380,8 @@ async function runTestCommandBody(projectRoot, testPaths, options = {}, profileE
|
|
|
364
380
|
...(options.fullOutput === true ? { fullOutput: true } : {}),
|
|
365
381
|
...(harnessEnv ? { env: harnessEnv } : {}),
|
|
366
382
|
pgidFile: options.pgidFile,
|
|
383
|
+
...(hooks.rawLogDir === undefined ? {} : { rawLogDir: hooks.rawLogDir }),
|
|
384
|
+
...(hooks.repetition === undefined ? {} : { repetition: hooks.repetition }),
|
|
367
385
|
};
|
|
368
386
|
const postRebuildContext = options.build
|
|
369
387
|
? createPostRebuildFailureContext('fireforge test --build', normalizedPaths)
|
|
@@ -376,46 +394,18 @@ async function runTestCommandBody(projectRoot, testPaths, options = {}, profileE
|
|
|
376
394
|
// into later files. Sharding is per path argument: a directory argument
|
|
377
395
|
// keeps its enumerated files together in one invocation, preserving the
|
|
378
396
|
// one-browser-instance semantics of a directory run. `--no-shard` restores
|
|
379
|
-
// the combined invocation
|
|
380
|
-
//
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
}
|
|
394
|
-
finally {
|
|
395
|
-
// Runs before the aggregate verdict below: a mutated engine/ emits
|
|
396
|
-
// `FAIL reason=inconclusive` and throws, so an invalidated run can
|
|
397
|
-
// never print `PASS shards=N/N` first. (A throw here masks an
|
|
398
|
-
// in-flight shard error, as the plain assert always did.)
|
|
399
|
-
await verifyEngineGenerationOrEmitInconclusive(paths.engine, generationBefore, runCtx.powerChanged, options.perfSamples);
|
|
400
|
-
}
|
|
401
|
-
finalizeShardedOutcome(summary);
|
|
402
|
-
return;
|
|
403
|
-
}
|
|
404
|
-
const combinedDispatchPaths = dispatchGroups.flatMap((group) => group.paths);
|
|
405
|
-
let outcome;
|
|
406
|
-
const generationBefore = await snapshotEngineGeneration(paths.engine);
|
|
407
|
-
try {
|
|
408
|
-
outcome = await runTestsWithRetries(runCtx, combinedDispatchPaths);
|
|
409
|
-
}
|
|
410
|
-
catch (error) {
|
|
411
|
-
throw new BuildError('Test process failed to start', 'mach test', error instanceof Error ? error : undefined);
|
|
412
|
-
}
|
|
413
|
-
finally {
|
|
414
|
-
await verifyEngineGenerationOrEmitInconclusive(paths.engine, generationBefore, runCtx.powerChanged, options.perfSamples);
|
|
415
|
-
}
|
|
416
|
-
if (canaryPath !== undefined) {
|
|
417
|
-
reportCanaryOutcome(outcome);
|
|
418
|
-
return;
|
|
419
|
-
}
|
|
420
|
-
finalizeSingleRunOutcome(outcome, normalizedPaths, projectConfig.binaryName, postRebuildContext, options.headless === true);
|
|
397
|
+
// the combined invocation; `--shard-size` and `--jobs` reshape it (see
|
|
398
|
+
// test-dispatch.ts).
|
|
399
|
+
await dispatchTests({
|
|
400
|
+
engineDir: paths.engine,
|
|
401
|
+
runCtx,
|
|
402
|
+
dispatchGroups,
|
|
403
|
+
options,
|
|
404
|
+
projectConfig,
|
|
405
|
+
canaryPath,
|
|
406
|
+
shuffleSeed,
|
|
407
|
+
normalizedPaths,
|
|
408
|
+
xpcshellOnly,
|
|
409
|
+
postRebuildContext,
|
|
410
|
+
});
|
|
421
411
|
}
|
|
@@ -8,15 +8,18 @@
|
|
|
8
8
|
import { join } from 'node:path';
|
|
9
9
|
import { getProjectPaths, loadConfig } from '../core/config.js';
|
|
10
10
|
import { furnaceConfigExists, loadFurnaceConfig } from '../core/furnace-config.js';
|
|
11
|
+
import { buildPatchQueueContext } from '../core/patch-lint.js';
|
|
12
|
+
import { buildJarTargetIndex } from '../core/patch-lint-forward-registration-extended.js';
|
|
11
13
|
import { loadPatchesManifest } from '../core/patch-manifest.js';
|
|
12
14
|
import { collectDeclaredTokenNames, lintTokenLockstep, lintUndeclaredTokenReferences, } from '../core/token-lint.js';
|
|
13
15
|
import { getTokensCssPath } from '../core/token-manager.js';
|
|
16
|
+
import { buildSiblingSheetMap } from '../core/token-sibling-sheets.js';
|
|
14
17
|
import { GeneralError } from '../errors/base.js';
|
|
15
18
|
import { toError } from '../utils/errors.js';
|
|
16
19
|
import { pathExists, readText } from '../utils/fs.js';
|
|
17
20
|
import { info, intro, outro, setMachineOutputMode, setStdoutSealed, success, warn, } from '../utils/logger.js';
|
|
18
21
|
import { emitMachineError, MACHINE_OUTPUT_SCHEMA_VERSION } from '../utils/machine-output.js';
|
|
19
|
-
/** Token prefix
|
|
22
|
+
/** Token prefix, exempt names and sibling sheets from furnace.json, with the init default. */
|
|
20
23
|
async function loadTokenSettings(projectRoot, binaryName) {
|
|
21
24
|
const fallback = { tokenPrefix: `--${binaryName}-`, exempt: new Set() };
|
|
22
25
|
if (!(await furnaceConfigExists(projectRoot)))
|
|
@@ -26,6 +29,7 @@ async function loadTokenSettings(projectRoot, binaryName) {
|
|
|
26
29
|
return {
|
|
27
30
|
tokenPrefix: furnace.tokenPrefix ?? fallback.tokenPrefix,
|
|
28
31
|
exempt: new Set([...(furnace.tokenAllowlist ?? []), ...(furnace.runtimeVariables ?? [])]),
|
|
32
|
+
siblingSheets: furnace.tokenSiblingSheets,
|
|
29
33
|
};
|
|
30
34
|
}
|
|
31
35
|
catch (error) {
|
|
@@ -41,7 +45,7 @@ async function collectTokenLint(projectRoot) {
|
|
|
41
45
|
if (!(await pathExists(tokensFile))) {
|
|
42
46
|
throw new GeneralError(`Token CSS file not found: ${tokensCssPath}. Run "fireforge furnace init" to seed it.`);
|
|
43
47
|
}
|
|
44
|
-
const { tokenPrefix, exempt } = await loadTokenSettings(projectRoot, config.binaryName);
|
|
48
|
+
const { tokenPrefix, exempt, siblingSheets } = await loadTokenSettings(projectRoot, config.binaryName);
|
|
45
49
|
const tokensCss = await readText(tokensFile);
|
|
46
50
|
const lines = tokensCss.split('\n');
|
|
47
51
|
const declared = collectDeclaredTokenNames(lines);
|
|
@@ -52,19 +56,55 @@ async function collectTokenLint(projectRoot) {
|
|
|
52
56
|
// Every CSS file the patch queue touches: the fork's own stylesheets and
|
|
53
57
|
// the upstream ones it edits.
|
|
54
58
|
const manifest = await loadPatchesManifest(paths.patches);
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
59
|
+
const touched = [...new Set((manifest?.patches ?? []).flatMap((patch) => patch.filesAffected))];
|
|
60
|
+
const cssFiles = touched.filter((file) => file.endsWith('.css') && file !== tokensCssPath).sort();
|
|
61
|
+
const contents = await readEngineFiles(paths.engine, touched.filter((file) => LINKING_FILE.test(file)));
|
|
62
|
+
const siblings = buildSiblingSheetMap({
|
|
63
|
+
documents: contents,
|
|
64
|
+
jarTargets: buildJarTargetIndex((await buildPatchQueueContext(paths.patches)).entries),
|
|
65
|
+
sheets: cssFiles,
|
|
66
|
+
explicit: siblingSheets,
|
|
67
|
+
});
|
|
58
68
|
let filesScanned = 1;
|
|
59
69
|
for (const file of cssFiles) {
|
|
60
|
-
const
|
|
61
|
-
if (
|
|
70
|
+
const css = contents.get(file);
|
|
71
|
+
if (css === undefined)
|
|
62
72
|
continue;
|
|
63
73
|
filesScanned++;
|
|
64
|
-
findings.push(...lintUndeclaredTokenReferences(file, await
|
|
74
|
+
findings.push(...lintUndeclaredTokenReferences(file, css, tokenPrefix, await withSiblingDeclarations(paths.engine, declared, siblings.get(file), contents), exempt));
|
|
65
75
|
}
|
|
66
76
|
return { tokensCssPath, tokenPrefix, filesScanned, findings };
|
|
67
77
|
}
|
|
78
|
+
/** Files whose content can link a stylesheet: markup, scripts, and sheets (`@import`). */
|
|
79
|
+
const LINKING_FILE = /\.(?:css|xhtml|html|xml|mjs|js)$/;
|
|
80
|
+
/** Reads the engine files that exist, keyed by engine-relative path. */
|
|
81
|
+
async function readEngineFiles(engineDir, files) {
|
|
82
|
+
const contents = new Map();
|
|
83
|
+
for (const file of files) {
|
|
84
|
+
const absolute = join(engineDir, file);
|
|
85
|
+
if (await pathExists(absolute))
|
|
86
|
+
contents.set(file, await readText(absolute));
|
|
87
|
+
}
|
|
88
|
+
return contents;
|
|
89
|
+
}
|
|
90
|
+
/** The tokens CSS declarations plus every declaration of a sheet's siblings. */
|
|
91
|
+
async function withSiblingDeclarations(engineDir, declared, siblings, contents) {
|
|
92
|
+
if (siblings === undefined || siblings.size === 0)
|
|
93
|
+
return declared;
|
|
94
|
+
const merged = new Set(declared);
|
|
95
|
+
for (const sibling of siblings) {
|
|
96
|
+
if (!contents.has(sibling)) {
|
|
97
|
+
const absolute = join(engineDir, sibling);
|
|
98
|
+
if (!(await pathExists(absolute)))
|
|
99
|
+
continue;
|
|
100
|
+
contents.set(sibling, await readText(absolute));
|
|
101
|
+
}
|
|
102
|
+
const css = contents.get(sibling) ?? '';
|
|
103
|
+
for (const name of collectDeclaredTokenNames(css.split('\n')))
|
|
104
|
+
merged.add(name);
|
|
105
|
+
}
|
|
106
|
+
return merged;
|
|
107
|
+
}
|
|
68
108
|
function failureMessage(count) {
|
|
69
109
|
return `token lint found ${String(count)} finding(s).`;
|
|
70
110
|
}
|
|
@@ -21,7 +21,7 @@ import { collectFurnaceManagedPrefixes } from '../core/furnace-config.js';
|
|
|
21
21
|
import { isGitRepository } from '../core/git.js';
|
|
22
22
|
import { withPrivateGitIndex } from '../core/git-readonly-index.js';
|
|
23
23
|
import { expandUntrackedDirectoryEntries, getWorkingTreeStatus } from '../core/git-status.js';
|
|
24
|
-
import { findUnresolvedSystemModuleImports, formatUnresolvedSystemModuleImports, } from '../core/module-resolution-preflight.js';
|
|
24
|
+
import { collectWorktreeModuleFiles, findUnresolvedSystemModuleImports, formatUnresolvedSystemModuleImports, } from '../core/module-resolution-preflight.js';
|
|
25
25
|
import { buildPatchQueueContext, formatPatchLintIssue, lintPatchQueue, } from '../core/patch-lint.js';
|
|
26
26
|
import { loadPatchesManifest, validatePatchesManifestConsistency } from '../core/patch-manifest.js';
|
|
27
27
|
import { evaluatePatchPolicy } from '../core/patch-policy.js';
|
|
@@ -206,8 +206,15 @@ export async function collectPatchQueueHealth(projectRoot, scope) {
|
|
|
206
206
|
// CREATES. This asks the same question of the engine, so an import ADDED
|
|
207
207
|
// to an existing module is named as text instead of surfacing later as a
|
|
208
208
|
// bare `xpcshell return code: -11` with zero output.
|
|
209
|
+
// Unscoped, the engine worktree joins in: an unexported chrome script
|
|
210
|
+
// importing an unexported, unregistered module crashes every objdir
|
|
211
|
+
// start before either file is in the queue. A scoped run never judges
|
|
212
|
+
// unmanaged files.
|
|
209
213
|
if (await pathExists(paths.engine)) {
|
|
210
|
-
const
|
|
214
|
+
const worktree = scope === undefined && (await isGitRepository(paths.engine))
|
|
215
|
+
? await collectWorktreeModuleFiles(paths.engine)
|
|
216
|
+
: undefined;
|
|
217
|
+
const unresolved = await findUnresolvedSystemModuleImports(paths.engine, scopePatches(manifest.patches, scope).flatMap((patch) => patch.filesAffected), worktree);
|
|
211
218
|
if (unresolved.length > 0) {
|
|
212
219
|
groups.push({
|
|
213
220
|
title: `Unresolvable system-module imports (${unresolved.length})`,
|
|
@@ -42,6 +42,12 @@ export interface PrepareBuildOptions {
|
|
|
42
42
|
* (`fireforge test --build --no-furnace-apply`).
|
|
43
43
|
*/
|
|
44
44
|
skipFurnaceApply?: boolean | undefined;
|
|
45
|
+
/**
|
|
46
|
+
* Apply only these components (`fireforge test --build --furnace-apply
|
|
47
|
+
* reached`). Undefined applies every component with undeployed edits; an
|
|
48
|
+
* empty list applies none.
|
|
49
|
+
*/
|
|
50
|
+
furnaceComponents?: readonly string[] | undefined;
|
|
45
51
|
}
|
|
46
52
|
/**
|
|
47
53
|
* Describes an exit code in the shell's 128+N signal convention. Truncated
|
|
@@ -170,14 +170,35 @@ async function reportUnexportedDriftBeforeOverwrite(projectRoot, config, options
|
|
|
170
170
|
}
|
|
171
171
|
notice(message);
|
|
172
172
|
}
|
|
173
|
+
/**
|
|
174
|
+
* Applies the named components one at a time, each under its own Furnace
|
|
175
|
+
* mutation (one rollback journal per apply), and merges the results.
|
|
176
|
+
*/
|
|
177
|
+
async function applySelectedComponents(projectRoot, names) {
|
|
178
|
+
const merged = {
|
|
179
|
+
applied: [],
|
|
180
|
+
skipped: [],
|
|
181
|
+
errors: [],
|
|
182
|
+
warnings: [],
|
|
183
|
+
};
|
|
184
|
+
for (const componentName of names) {
|
|
185
|
+
const result = await runFurnaceMutation(projectRoot, 'apply-rollback', (ctx) => applyAllComponents(projectRoot, false, { operationContext: ctx, componentName }));
|
|
186
|
+
merged.applied.push(...result.applied);
|
|
187
|
+
merged.skipped.push(...result.skipped);
|
|
188
|
+
merged.errors.push(...result.errors);
|
|
189
|
+
merged.warnings.push(...(result.warnings ?? []));
|
|
190
|
+
}
|
|
191
|
+
return merged;
|
|
192
|
+
}
|
|
173
193
|
/**
|
|
174
194
|
* Applies Furnace components before the build when `furnace.json` exists.
|
|
175
195
|
* Prints every warning the apply computed, then the loud sync banner.
|
|
176
196
|
*
|
|
177
197
|
* @param projectRoot - Project root
|
|
198
|
+
* @param only - Apply just these components; every component when undefined
|
|
178
199
|
* @returns Names of the components written to engine/
|
|
179
200
|
*/
|
|
180
|
-
async function applyFurnaceBeforeBuild(projectRoot) {
|
|
201
|
+
async function applyFurnaceBeforeBuild(projectRoot, only) {
|
|
181
202
|
if (!(await furnaceConfigExists(projectRoot)))
|
|
182
203
|
return [];
|
|
183
204
|
const furnaceConfig = await loadFurnaceConfig(projectRoot);
|
|
@@ -187,7 +208,10 @@ async function applyFurnaceBeforeBuild(projectRoot) {
|
|
|
187
208
|
const furnaceSpinner = spinner('Applying Furnace components...');
|
|
188
209
|
let result;
|
|
189
210
|
try {
|
|
190
|
-
result =
|
|
211
|
+
result =
|
|
212
|
+
only === undefined
|
|
213
|
+
? await runFurnaceMutation(projectRoot, 'apply-rollback', (ctx) => applyAllComponents(projectRoot, false, { operationContext: ctx }))
|
|
214
|
+
: await applySelectedComponents(projectRoot, only);
|
|
191
215
|
}
|
|
192
216
|
catch (error) {
|
|
193
217
|
furnaceSpinner.error('Failed to apply Furnace components');
|
|
@@ -349,7 +373,9 @@ export async function prepareBuildEnvironment(projectRoot, paths, config, option
|
|
|
349
373
|
throw error;
|
|
350
374
|
}
|
|
351
375
|
}
|
|
352
|
-
const furnaceAppliedNames = options.skipFurnaceApply === true
|
|
376
|
+
const furnaceAppliedNames = options.skipFurnaceApply === true || options.furnaceComponents?.length === 0
|
|
377
|
+
? []
|
|
378
|
+
: await applyFurnaceBeforeBuild(projectRoot, options.furnaceComponents);
|
|
353
379
|
// Generate mozconfig
|
|
354
380
|
const mozconfigSpinner = spinner('Generating mozconfig...');
|
|
355
381
|
let mozconfigWritten;
|
|
@@ -7,7 +7,7 @@ export declare const FIREFORGE_DIR = ".fireforge";
|
|
|
7
7
|
/** Supported top-level fireforge.json keys backed by the current schema. */
|
|
8
8
|
export declare const SUPPORTED_CONFIG_ROOT_KEYS: readonly ["name", "vendor", "appId", "binaryName", "firefox", "build", "buildAudit", "test", "history", "externalToolchains", "license", "wire", "patchLint", "patchPolicy", "typecheck", "markerComment", "check"];
|
|
9
9
|
/** Supported config paths that can be read or set without --force. */
|
|
10
|
-
export declare const SUPPORTED_CONFIG_PATHS: readonly ["name", "vendor", "appId", "binaryName", "license", "firefox", "firefox.version", "firefox.product", "firefox.sha256", "firefox.candidate", "firefox.allowUnverifiedDownload", "build", "build.jobs", "test", "test.canaryPath", "test.canaryTimeoutSeconds", "test.reapOrphans", "test.mochitestPorts", "history", "history.path", "externalToolchains", "wire", "wire.subscriptDir", "patchLint", "patchLint.checkJs", "patchLint.checkJsStrict", "patchLint.checkJsCompilerOptions", "patchLint.checkJsExtraShim", "patchLint.checkJsTestFiles", "patchLint.checkJsTestShim", "patchLint.rawColorAllowlist", "patchLint.jsdocClassMethods", "patchLint.testAssertionFloor", "patchLint.chromeScriptJsDoc", "patchLint.forwardRegistration", "patchLint.fileSizeMetric", "patchLint.patchSizeMetric", "patchPolicy", "typecheck", "typecheck.projects", "typecheck.extraShim", "markerComment", "check"];
|
|
10
|
+
export declare const SUPPORTED_CONFIG_PATHS: readonly ["name", "vendor", "appId", "binaryName", "license", "firefox", "firefox.version", "firefox.product", "firefox.sha256", "firefox.candidate", "firefox.allowUnverifiedDownload", "build", "build.jobs", "test", "test.canaryPath", "test.canaryTimeoutSeconds", "test.reapOrphans", "test.mochitestPorts", "test.lockMode", "test.furnaceApply", "history", "history.path", "externalToolchains", "wire", "wire.subscriptDir", "patchLint", "patchLint.checkJs", "patchLint.checkJsStrict", "patchLint.checkJsCompilerOptions", "patchLint.checkJsExtraShim", "patchLint.checkJsTestFiles", "patchLint.checkJsTestShim", "patchLint.rawColorAllowlist", "patchLint.jsdocClassMethods", "patchLint.testAssertionFloor", "patchLint.chromeScriptJsDoc", "patchLint.forwardRegistration", "patchLint.fileSizeMetric", "patchLint.patchSizeMetric", "patchPolicy", "typecheck", "typecheck.projects", "typecheck.extraShim", "markerComment", "check"];
|
|
11
11
|
/**
|
|
12
12
|
* Gets all project paths based on a root directory.
|
|
13
13
|
* @param root - Root directory of the project
|
|
@@ -78,7 +78,7 @@ function parseExtraStep(raw, field) {
|
|
|
78
78
|
if (!isObject(raw)) {
|
|
79
79
|
throw new ConfigError(`Config field "${field}" must be a plain object with "name" and "command"`);
|
|
80
80
|
}
|
|
81
|
-
assertKnownKeys(raw, field, ['name', 'command', 'cwd', 'timeout']);
|
|
81
|
+
assertKnownKeys(raw, field, ['name', 'command', 'cwd', 'timeout', 'after']);
|
|
82
82
|
const name = raw['name'];
|
|
83
83
|
if (typeof name !== 'string' || !STEP_NAME_PATTERN.test(name)) {
|
|
84
84
|
throw new ConfigError(`Config field "${field}.name" must be lowercase letters, digits and dashes (e.g. "policy-audit")`);
|
|
@@ -99,14 +99,63 @@ function parseExtraStep(raw, field) {
|
|
|
99
99
|
step.cwd = cwd;
|
|
100
100
|
}
|
|
101
101
|
if (raw['timeout'] !== undefined) {
|
|
102
|
-
|
|
103
|
-
if (timeout === 0) {
|
|
104
|
-
throw new ConfigError(`Config field "${field}.timeout" must be a positive number of seconds`);
|
|
105
|
-
}
|
|
106
|
-
step.timeout = timeout;
|
|
102
|
+
step.timeout = parsePositiveSeconds(raw['timeout'], `${field}.timeout`);
|
|
107
103
|
}
|
|
104
|
+
if (raw['after'] !== undefined)
|
|
105
|
+
step.after = parseStringList(raw['after'], `${field}.after`);
|
|
108
106
|
return step;
|
|
109
107
|
}
|
|
108
|
+
function parsePositiveSeconds(raw, field) {
|
|
109
|
+
const seconds = parseNonNegativeInteger(raw, field);
|
|
110
|
+
if (seconds === 0) {
|
|
111
|
+
throw new ConfigError(`Config field "${field}" must be a positive number of seconds`);
|
|
112
|
+
}
|
|
113
|
+
return seconds;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Checks every `after` names a known step and the extra steps' ordering
|
|
117
|
+
* has no cycle: a cycle would leave its steps waiting forever.
|
|
118
|
+
*/
|
|
119
|
+
function assertExtraStepOrdering(steps) {
|
|
120
|
+
const extras = new Map(steps.map((step) => [step.name, step]));
|
|
121
|
+
for (const [index, step] of steps.entries()) {
|
|
122
|
+
for (const dependency of step.after ?? []) {
|
|
123
|
+
if (dependency === step.name) {
|
|
124
|
+
throw new ConfigError(`Config field "check.extraSteps[${String(index)}].after" names the step itself`);
|
|
125
|
+
}
|
|
126
|
+
if (!isBuiltinStep(dependency) && !extras.has(dependency)) {
|
|
127
|
+
throw new ConfigError(`Config field "check.extraSteps[${String(index)}].after" names unknown step "${dependency}"`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const state = new Map();
|
|
132
|
+
const visit = (name, path) => {
|
|
133
|
+
if (state.get(name) === 'done')
|
|
134
|
+
return;
|
|
135
|
+
if (state.get(name) === 'visiting') {
|
|
136
|
+
throw new ConfigError(`Config field "check.extraSteps" orders its steps in a cycle: ${[...path, name].join(' -> ')}`);
|
|
137
|
+
}
|
|
138
|
+
state.set(name, 'visiting');
|
|
139
|
+
for (const dependency of extras.get(name)?.after ?? []) {
|
|
140
|
+
if (extras.has(dependency))
|
|
141
|
+
visit(dependency, [...path, name]);
|
|
142
|
+
}
|
|
143
|
+
state.set(name, 'done');
|
|
144
|
+
};
|
|
145
|
+
for (const step of steps)
|
|
146
|
+
visit(step.name, []);
|
|
147
|
+
}
|
|
148
|
+
function parseTimeouts(raw) {
|
|
149
|
+
if (!isObject(raw))
|
|
150
|
+
throw new ConfigError('Config field "check.timeouts" must be a plain object');
|
|
151
|
+
assertKnownKeys(raw, 'check.timeouts', [...CHECK_BUILTIN_STEPS]);
|
|
152
|
+
const out = {};
|
|
153
|
+
for (const [name, value] of Object.entries(raw)) {
|
|
154
|
+
if (isBuiltinStep(name))
|
|
155
|
+
out[name] = parsePositiveSeconds(value, `check.timeouts.${name}`);
|
|
156
|
+
}
|
|
157
|
+
return out;
|
|
158
|
+
}
|
|
110
159
|
function parseExtraSteps(raw) {
|
|
111
160
|
if (!Array.isArray(raw))
|
|
112
161
|
throw new ConfigError('Config field "check.extraSteps" must be an array');
|
|
@@ -118,6 +167,7 @@ function parseExtraSteps(raw) {
|
|
|
118
167
|
}
|
|
119
168
|
seen.add(step.name);
|
|
120
169
|
}
|
|
170
|
+
assertExtraStepOrdering(steps);
|
|
121
171
|
return steps;
|
|
122
172
|
}
|
|
123
173
|
/**
|
|
@@ -129,7 +179,16 @@ function parseExtraSteps(raw) {
|
|
|
129
179
|
export function parseCheckConfig(raw) {
|
|
130
180
|
if (!isObject(raw))
|
|
131
181
|
throw new ConfigError('Config field "check" must be a plain object');
|
|
132
|
-
assertKnownKeys(raw, 'check', [
|
|
182
|
+
assertKnownKeys(raw, 'check', [
|
|
183
|
+
'skip',
|
|
184
|
+
'status',
|
|
185
|
+
'maxWarnings',
|
|
186
|
+
'extraSteps',
|
|
187
|
+
'timeout',
|
|
188
|
+
'timeouts',
|
|
189
|
+
'failOnSkipped',
|
|
190
|
+
'lintReport',
|
|
191
|
+
]);
|
|
133
192
|
const out = {};
|
|
134
193
|
if (raw['skip'] !== undefined)
|
|
135
194
|
out.skip = parseSkip(raw['skip']);
|
|
@@ -139,5 +198,22 @@ export function parseCheckConfig(raw) {
|
|
|
139
198
|
out.maxWarnings = parseMaxWarnings(raw['maxWarnings']);
|
|
140
199
|
if (raw['extraSteps'] !== undefined)
|
|
141
200
|
out.extraSteps = parseExtraSteps(raw['extraSteps']);
|
|
201
|
+
if (raw['timeout'] !== undefined)
|
|
202
|
+
out.timeout = parsePositiveSeconds(raw['timeout'], 'check.timeout');
|
|
203
|
+
if (raw['timeouts'] !== undefined)
|
|
204
|
+
out.timeouts = parseTimeouts(raw['timeouts']);
|
|
205
|
+
if (raw['failOnSkipped'] !== undefined) {
|
|
206
|
+
if (typeof raw['failOnSkipped'] !== 'boolean') {
|
|
207
|
+
throw new ConfigError('Config field "check.failOnSkipped" must be a boolean');
|
|
208
|
+
}
|
|
209
|
+
out.failOnSkipped = raw['failOnSkipped'];
|
|
210
|
+
}
|
|
211
|
+
if (raw['lintReport'] !== undefined) {
|
|
212
|
+
const path = raw['lintReport'];
|
|
213
|
+
if (typeof path !== 'string' || path.trim() === '' || !isContainedRelativePath(path)) {
|
|
214
|
+
throw new ConfigError('Config field "check.lintReport" must be a project-relative path');
|
|
215
|
+
}
|
|
216
|
+
out.lintReport = path;
|
|
217
|
+
}
|
|
142
218
|
return out;
|
|
143
219
|
}
|
|
@@ -59,6 +59,20 @@ export function parseTestBlock(rec, config) {
|
|
|
59
59
|
}
|
|
60
60
|
out.mochitestPorts = mochitestPorts;
|
|
61
61
|
}
|
|
62
|
+
const lockMode = testRec.raw('lockMode');
|
|
63
|
+
if (lockMode !== undefined) {
|
|
64
|
+
if (lockMode !== 'exclusive' && lockMode !== 'shared') {
|
|
65
|
+
throw new ConfigError('Config field "test.lockMode" must be "exclusive" or "shared"');
|
|
66
|
+
}
|
|
67
|
+
out.lockMode = lockMode;
|
|
68
|
+
}
|
|
69
|
+
const furnaceApply = testRec.raw('furnaceApply');
|
|
70
|
+
if (furnaceApply !== undefined) {
|
|
71
|
+
if (furnaceApply !== 'all' && furnaceApply !== 'reached' && furnaceApply !== 'none') {
|
|
72
|
+
throw new ConfigError('Config field "test.furnaceApply" must be "all", "reached" or "none"');
|
|
73
|
+
}
|
|
74
|
+
out.furnaceApply = furnaceApply;
|
|
75
|
+
}
|
|
62
76
|
config.test = out;
|
|
63
77
|
}
|
|
64
78
|
/** Parses optional project-specific external toolchain doctor probes. */
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Validation for `fireforge.json#patchLint.tokenScales`.
|
|
3
|
+
*
|
|
4
|
+
* Unknown keys are refused: a misspelt `allowInCalc` or `minFiles` would
|
|
5
|
+
* otherwise loosen or drop a scale without a word.
|
|
3
6
|
*/
|
|
4
7
|
import type { PatchLintTokenScale } from '../types/config.js';
|
|
5
8
|
/** Parses and validates `patchLint.tokenScales`. */
|