@hominis/fireforge 0.50.0 → 0.52.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 +67 -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/status-output.js +5 -1
- 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.d.ts +2 -0
- package/dist/src/commands/test-build-phase.js +34 -17
- 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 +220 -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 +14 -0
- package/dist/src/commands/test-orchestrate.js +25 -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 +11 -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 +68 -75
- package/dist/src/commands/token-lint.js +48 -8
- package/dist/src/commands/verify.js +9 -2
- package/dist/src/core/build-baseline-types.d.ts +13 -0
- package/dist/src/core/build-baseline-validate.js +9 -0
- package/dist/src/core/build-baseline.js +7 -0
- 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 +11 -3
- package/dist/src/core/furnace-config-validate-exceptions.js +59 -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 +5 -3
- package/dist/src/core/furnace-validate-accessibility.js +75 -30
- package/dist/src/core/furnace-validate-exceptions.d.ts +13 -4
- package/dist/src/core/furnace-validate-exceptions.js +59 -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 +36 -0
- package/dist/src/core/negative-control-compiled.js +87 -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 +47 -7
- package/dist/src/core/test-run-completeness.js +70 -23
- package/dist/src/core/test-stale-check.d.ts +10 -1
- package/dist/src/core/test-stale-check.js +15 -1
- 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 +6 -1
- package/dist/src/core/typecheck-shim.js +139 -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 +29 -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 +110 -9
- package/docs/furnace.md +61 -1
- package/docs/lifecycle-invariants.md +13 -11
- package/docs/machine-output.md +68 -18
- package/docs/patch-workflow.md +174 -13
- package/docs/testing.md +89 -5
- package/package.json +3 -3
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Sharded `fireforge test` runs: one harness invocation per shard, run in
|
|
4
|
+
* order on one harness instance, or on a pool of `--jobs <n>` instances.
|
|
5
|
+
*
|
|
6
|
+
* Each finished shard prints a `FIREFORGE-SHARD:` line, and the aggregate
|
|
7
|
+
* verdict names the shards that did not pass as `failed-shards=`. Before,
|
|
8
|
+
* the failing shards were named only in the prose of the aggregate error
|
|
9
|
+
* and the human summary box, so a wrapper that reruns failed shards had to
|
|
10
|
+
* parse a sentence that a reworded message would silently break.
|
|
11
|
+
*
|
|
12
|
+
* Under `--shard-size <n>` a shard is a chunk of n files, and the run is
|
|
13
|
+
* judged per file from each dispatch's raw log: `shards=` then counts files
|
|
14
|
+
* and `failed-shards=` names the files that failed.
|
|
15
|
+
*/
|
|
16
|
+
import { getActiveRunLogPath, writeToActiveRunLog } from '../core/run-log.js';
|
|
17
|
+
import { buildHarnessCrashMessage, formatFireforgeVerdictLine, headedNoOutputTimeoutHint, } from '../core/test-harness-crash.js';
|
|
18
|
+
import { TestFailureError } from '../errors/build.js';
|
|
19
|
+
import { info, note, warn } from '../utils/logger.js';
|
|
20
|
+
import { getPlatform } from '../utils/platform.js';
|
|
21
|
+
import { runTestsWithRetries } from './test-run.js';
|
|
22
|
+
import { emitHarnessVerdict } from './test-verdict.js';
|
|
23
|
+
const SHARD_STATUS_LABEL = {
|
|
24
|
+
'tests-ran-ok': 'PASS',
|
|
25
|
+
'test-failures': 'FAIL',
|
|
26
|
+
'harness-crash': 'CRASH',
|
|
27
|
+
'no-tests': 'NO-TESTS',
|
|
28
|
+
'harness-arguments': 'ARGUMENTS',
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Derives the aggregate classification for a sharded run: all-green is
|
|
32
|
+
* `tests-ran-ok`. Otherwise the most structural failure wins. A crashed
|
|
33
|
+
* shard makes the whole run `harness-crash`, then `no-tests`, then plain
|
|
34
|
+
* `test-failures`, so the aggregate `reason=` keeps the classifier-driven
|
|
35
|
+
* contract instead of collapsing every failure to `test-failures`.
|
|
36
|
+
*/
|
|
37
|
+
export function deriveAggregateShardVerdict(shards) {
|
|
38
|
+
const kinds = new Set(shards.map(({ outcome }) => outcome.verdict.kind));
|
|
39
|
+
if (kinds.has('harness-arguments'))
|
|
40
|
+
return { kind: 'harness-arguments' };
|
|
41
|
+
if (kinds.has('harness-crash'))
|
|
42
|
+
return { kind: 'harness-crash' };
|
|
43
|
+
if (kinds.has('no-tests'))
|
|
44
|
+
return { kind: 'no-tests' };
|
|
45
|
+
if (kinds.has('test-failures'))
|
|
46
|
+
return { kind: 'test-failures' };
|
|
47
|
+
return { kind: 'tests-ran-ok' };
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The `FIREFORGE-SHARD:` line for one finished shard: the verdict it would
|
|
51
|
+
* have printed alone, plus `shard=<i>/<n>` and `path=<label>`. Never a
|
|
52
|
+
* `FIREFORGE-VERDICT:` line, so the run keeps exactly one.
|
|
53
|
+
*/
|
|
54
|
+
export function formatShardLine(shard, index, total, failedFiles) {
|
|
55
|
+
const body = formatFireforgeVerdictLine(shard.outcome.verdict).slice('FIREFORGE-VERDICT: '.length);
|
|
56
|
+
const files = failedFiles !== undefined && failedFiles.length > 0
|
|
57
|
+
? ` failed-files=${failedFiles.join(',')}`
|
|
58
|
+
: '';
|
|
59
|
+
return `FIREFORGE-SHARD: ${body} shard=${index + 1}/${total} path=${shard.label.replace(/\s+/g, '_')}${files}`;
|
|
60
|
+
}
|
|
61
|
+
function writeShardLine(line) {
|
|
62
|
+
process.stdout.write(`${line}\n`);
|
|
63
|
+
writeToActiveRunLog(`${line}\n`);
|
|
64
|
+
}
|
|
65
|
+
/** True when a raw-log test id names the dispatched path. */
|
|
66
|
+
function namesPath(test, path) {
|
|
67
|
+
return test === path || test.endsWith(`/${path}`) || path.endsWith(`/${test}`);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The files of one shard that did not pass, judged from its raw log. A file
|
|
71
|
+
* with no `test_end` failed (it never started, or never finished). Returns
|
|
72
|
+
* undefined when the shard wrote no raw log, so the caller judges the
|
|
73
|
+
* shard as a whole.
|
|
74
|
+
*/
|
|
75
|
+
export function failedFilesOf(shard) {
|
|
76
|
+
const results = shard.outcome.fileResults;
|
|
77
|
+
if (results === undefined || shard.paths === undefined)
|
|
78
|
+
return undefined;
|
|
79
|
+
return shard.paths.filter((path) => {
|
|
80
|
+
const result = results.find((entry) => namesPath(entry.test, path));
|
|
81
|
+
return result === undefined || result.unexpected;
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
function reportShardFailure(ctx, outcome, label, diagnoseShardFailure) {
|
|
85
|
+
if (outcome.verdict.kind === 'harness-crash' && outcome.verdict.signature) {
|
|
86
|
+
const base = buildHarnessCrashMessage(outcome.verdict.signature, outcome.attempts);
|
|
87
|
+
const hint = headedNoOutputTimeoutHint(outcome.verdict.signature, {
|
|
88
|
+
headless: ctx.headless,
|
|
89
|
+
platform: getPlatform(),
|
|
90
|
+
...(outcome.displayState !== undefined ? { displayState: outcome.displayState } : {}),
|
|
91
|
+
});
|
|
92
|
+
warn(hint ? `${base}\n\n${hint}` : base);
|
|
93
|
+
}
|
|
94
|
+
else if (outcome.verdict.kind !== 'tests-ran-ok') {
|
|
95
|
+
const diagnosis = diagnoseShardFailure(outcome, label);
|
|
96
|
+
if (diagnosis)
|
|
97
|
+
warn(diagnosis);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Runs `count` items on up to `jobs` workers at once. Worker `w` handles
|
|
102
|
+
* the items it takes in order, and results keep item order.
|
|
103
|
+
*/
|
|
104
|
+
async function runPool(count, jobs, worker) {
|
|
105
|
+
const results = new Array(count);
|
|
106
|
+
let next = 0;
|
|
107
|
+
const workers = Array.from({ length: Math.max(1, Math.min(jobs, count)) }, async (_, w) => {
|
|
108
|
+
while (next < count) {
|
|
109
|
+
const item = next;
|
|
110
|
+
next += 1;
|
|
111
|
+
results[item] = await worker(item, w);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
await Promise.all(workers);
|
|
115
|
+
return results;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Runs each shard as its own harness invocation and prints an aggregate
|
|
119
|
+
* report. Per-shard failures are diagnosed via `diagnoseShardFailure`
|
|
120
|
+
* (which receives the throwing diagnosis chain from the command layer)
|
|
121
|
+
* but downgraded to warnings so every shard runs. The aggregate verdict
|
|
122
|
+
* line and the aggregate error are not produced here: the caller runs
|
|
123
|
+
* the engine-generation integrity check first and then calls
|
|
124
|
+
* {@link finalizeShardedOutcome}, so a run invalidated by a concurrent
|
|
125
|
+
* `engine/` mutation can never print `PASS` before the check fails.
|
|
126
|
+
*/
|
|
127
|
+
export async function runShardedTests(ctx, groups, diagnoseShardFailure, options = {}) {
|
|
128
|
+
const jobs = options.jobs ?? 1;
|
|
129
|
+
const contexts = new Map([[0, ctx]]);
|
|
130
|
+
const contextFor = async (index) => {
|
|
131
|
+
const known = contexts.get(index);
|
|
132
|
+
if (known !== undefined)
|
|
133
|
+
return known;
|
|
134
|
+
const made = options.contextFor === undefined ? ctx : await options.contextFor(index);
|
|
135
|
+
contexts.set(index, made);
|
|
136
|
+
return made;
|
|
137
|
+
};
|
|
138
|
+
let finished = 0;
|
|
139
|
+
const shards = await runPool(groups.length, jobs, async (index, workerIndex) => {
|
|
140
|
+
const group = groups[index];
|
|
141
|
+
const instance = await contextFor(workerIndex);
|
|
142
|
+
info(`— Shard ${index + 1}/${groups.length}${jobs > 1 ? ` [job ${workerIndex + 1}]` : ''}: ${group.label}`);
|
|
143
|
+
const outcome = await runTestsWithRetries(instance, group.paths, group.label);
|
|
144
|
+
// Perf power-source changes are recorded on the instance's context.
|
|
145
|
+
if (instance.powerChanged === true)
|
|
146
|
+
ctx.powerChanged = true;
|
|
147
|
+
const shard = { label: group.label, outcome, paths: group.paths };
|
|
148
|
+
reportShardFailure(instance, outcome, group.label, diagnoseShardFailure);
|
|
149
|
+
finished += 1;
|
|
150
|
+
writeShardLine(formatShardLine(shard, index, groups.length, options.perFile === true ? failedFilesOf(shard) : undefined));
|
|
151
|
+
reportProgress(finished, groups.length, jobs);
|
|
152
|
+
return shard;
|
|
153
|
+
});
|
|
154
|
+
return summarizeShards(shards, options.perFile === true);
|
|
155
|
+
}
|
|
156
|
+
function reportProgress(finished, total, jobs) {
|
|
157
|
+
if (jobs > 1)
|
|
158
|
+
info(`${finished}/${total} shard(s) finished.`);
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Builds the summary box and the counts. Per-file judging counts every
|
|
162
|
+
* dispatched file of a shard that wrote a raw log; a shard without one
|
|
163
|
+
* counts as a whole.
|
|
164
|
+
*/
|
|
165
|
+
function summarizeShards(shards, perFile) {
|
|
166
|
+
const lines = shards.map(({ label, outcome }) => `${SHARD_STATUS_LABEL[outcome.verdict.kind].padEnd(8)} ${label}` +
|
|
167
|
+
(outcome.attempts > 1 ? ` (${outcome.attempts} attempts)` : ''));
|
|
168
|
+
const failingShards = shards.filter(({ outcome }) => outcome.verdict.kind !== 'tests-ran-ok');
|
|
169
|
+
let passed = shards.length - failingShards.length;
|
|
170
|
+
let total = shards.length;
|
|
171
|
+
let failed = failingShards.map(({ label }) => label);
|
|
172
|
+
if (perFile) {
|
|
173
|
+
passed = 0;
|
|
174
|
+
total = 0;
|
|
175
|
+
failed = [];
|
|
176
|
+
for (const shard of shards) {
|
|
177
|
+
const files = failedFilesOf(shard);
|
|
178
|
+
const ok = shard.outcome.verdict.kind === 'tests-ran-ok';
|
|
179
|
+
if (files === undefined) {
|
|
180
|
+
total += 1;
|
|
181
|
+
if (ok)
|
|
182
|
+
passed += 1;
|
|
183
|
+
else
|
|
184
|
+
failed.push(shard.label);
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
const dispatched = shard.paths ?? [];
|
|
188
|
+
total += dispatched.length;
|
|
189
|
+
// A red shard whose log names no failing file still failed: count
|
|
190
|
+
// every file of it, so a crash cannot read as all-green.
|
|
191
|
+
const red = files.length === 0 && !ok ? dispatched : files;
|
|
192
|
+
passed += dispatched.length - red.length;
|
|
193
|
+
failed.push(...red);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
note(`${lines.join('\n')}\n\n${passed}/${total} ${perFile ? 'file(s)' : 'shard(s)'} passed`, 'Sharded Test Summary');
|
|
197
|
+
return { shards, passed, total, failed, aggregate: deriveAggregateShardVerdict(shards) };
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Emits the aggregate FIREFORGE-VERDICT line for a sharded run, then
|
|
201
|
+
* throws the aggregate error when any shard did not pass, the same
|
|
202
|
+
* emit-then-throw shape as `finalizeSingleRunOutcome`. Called only after
|
|
203
|
+
* the engine-generation check succeeded.
|
|
204
|
+
*/
|
|
205
|
+
export function finalizeShardedOutcome(summary) {
|
|
206
|
+
emitHarnessVerdict(summary.aggregate, {
|
|
207
|
+
passed: summary.passed,
|
|
208
|
+
total: summary.total,
|
|
209
|
+
failed: summary.failed.map((label) => label.replace(/\s+/g, '_')),
|
|
210
|
+
});
|
|
211
|
+
if (summary.passed < summary.total) {
|
|
212
|
+
throw new TestFailureError(`${summary.failed.length} of ${summary.total} sharded test run(s) did not pass: ` +
|
|
213
|
+
`${summary.failed.join(', ')}. ` +
|
|
214
|
+
'See the per-shard diagnosis above. Use --no-shard to reproduce the combined single-invocation behaviour.', 'mach test', undefined, getActiveRunLogPath());
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Splits the dispatch paths of every group into consecutive chunks of at
|
|
219
|
+
* most `size` files (`--shard-size`). Order is kept, and a chunk never
|
|
220
|
+
* mixes groups' order: it is the flattened request, cut every `size`
|
|
221
|
+
* files.
|
|
222
|
+
*/
|
|
223
|
+
export function chunkShardGroups(groups, size) {
|
|
224
|
+
const paths = groups.flatMap((group) => group.paths);
|
|
225
|
+
const chunks = [];
|
|
226
|
+
for (let start = 0; start < paths.length; start += size) {
|
|
227
|
+
const chunk = paths.slice(start, start + size);
|
|
228
|
+
const first = chunk[0] ?? '';
|
|
229
|
+
chunks.push({
|
|
230
|
+
label: chunk.length === 1 ? first : `${first}+${chunk.length - 1}`,
|
|
231
|
+
paths: chunk,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
return chunks;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Splits one combined request into `parts` contiguous chunks of near-equal
|
|
238
|
+
* size, in order (`--jobs` with `--no-shard`).
|
|
239
|
+
*/
|
|
240
|
+
export function splitContiguous(paths, parts) {
|
|
241
|
+
const count = Math.max(1, Math.min(parts, paths.length));
|
|
242
|
+
const groups = [];
|
|
243
|
+
let start = 0;
|
|
244
|
+
for (let index = 0; index < count; index += 1) {
|
|
245
|
+
const size = Math.ceil((paths.length - start) / (count - index));
|
|
246
|
+
const chunk = paths.slice(start, start + size);
|
|
247
|
+
start += size;
|
|
248
|
+
const first = chunk[0] ?? '';
|
|
249
|
+
groups.push({
|
|
250
|
+
label: chunk.length === 1 ? first : `${first}+${chunk.length - 1}`,
|
|
251
|
+
paths: chunk,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
return groups;
|
|
255
|
+
}
|
|
@@ -28,7 +28,11 @@ import type { TestOptions } from '../types/commands/index.js';
|
|
|
28
28
|
* a full-suite request would refuse a probe that touches no fixtures. The
|
|
29
29
|
* stale-content refusal still applies to it unchanged.
|
|
30
30
|
*/
|
|
31
|
-
export declare function enforceStaleBuildGate(projectRoot: string, engineDir: string, options: TestOptions, normalizedPaths: readonly string[]
|
|
31
|
+
export declare function enforceStaleBuildGate(projectRoot: string, engineDir: string, options: TestOptions, normalizedPaths: readonly string[],
|
|
32
|
+
/** `quiet`: refuse only, without the exemption note (the pre-lock pass). */
|
|
33
|
+
gate?: {
|
|
34
|
+
quiet?: boolean;
|
|
35
|
+
}): Promise<void>;
|
|
32
36
|
/**
|
|
33
37
|
* Compiled-StaticComponents gate: refuses runs whose child process would
|
|
34
38
|
* resolve a stale compiled component table. `components.conf` entries bake
|
|
@@ -11,6 +11,7 @@ import { filterByCarriedFullCoverage } from '../core/coverage-dispatch.js';
|
|
|
11
11
|
import { checkStaleBuildForTest, checkStaticComponentsStale, findChangedTestManifestsForPaths, findUncoveredRequestPaths, formatStaleBuildWarning, formatStaticComponentsRefusal, formatTestCoverageRefusal, FULL_SUITE_REQUEST, } from '../core/test-stale-check.js';
|
|
12
12
|
import { formatStaleExemptions } from '../core/test-stale-exempt.js';
|
|
13
13
|
import { PreflightRefusalError } from '../errors/base.js';
|
|
14
|
+
import { isProcessAlive } from '../utils/errors.js';
|
|
14
15
|
import { info, warn } from '../utils/logger.js';
|
|
15
16
|
import { describePerfStaleImpact } from './test-perf-scope.js';
|
|
16
17
|
/**
|
|
@@ -33,7 +34,9 @@ import { describePerfStaleImpact } from './test-perf-scope.js';
|
|
|
33
34
|
* a full-suite request would refuse a probe that touches no fixtures. The
|
|
34
35
|
* stale-content refusal still applies to it unchanged.
|
|
35
36
|
*/
|
|
36
|
-
export async function enforceStaleBuildGate(projectRoot, engineDir, options, normalizedPaths
|
|
37
|
+
export async function enforceStaleBuildGate(projectRoot, engineDir, options, normalizedPaths,
|
|
38
|
+
/** `quiet`: refuse only, without the exemption note (the pre-lock pass). */
|
|
39
|
+
gate = {}) {
|
|
37
40
|
const stale = await checkStaleBuildForTest(projectRoot, engineDir);
|
|
38
41
|
const dispatchesNoTests = options.doctor === true && normalizedPaths.length === 0;
|
|
39
42
|
if (!dispatchesNoTests) {
|
|
@@ -41,7 +44,7 @@ export async function enforceStaleBuildGate(projectRoot, engineDir, options, nor
|
|
|
41
44
|
}
|
|
42
45
|
await enforceStaticComponentsGate(engineDir, stale.baseline, options);
|
|
43
46
|
const exempt = stale.exempt ?? [];
|
|
44
|
-
if (!stale.stale && exempt.length > 0) {
|
|
47
|
+
if (!stale.stale && exempt.length > 0 && gate.quiet !== true) {
|
|
45
48
|
info(`Stale-build preflight: ${exempt.length} edit(s) since the last build need no rebuild: ${formatStaleExemptions(exempt)}.`);
|
|
46
49
|
}
|
|
47
50
|
if (!stale.stale)
|
|
@@ -91,6 +94,12 @@ async function enforceCoverageGate(engineDir, baseline, normalizedPaths) {
|
|
|
91
94
|
throw new PreflightRefusalError(formatTestCoverageRefusal(uncovered, Array.isArray(recordedCoverage) ? recordedCoverage : [], changedManifests, {
|
|
92
95
|
changedInputs: Object.fromEntries(remaining.map((request) => [request.path, request.changed])),
|
|
93
96
|
carriesFullAnchor: baseline?.fullCoverageAnchor !== undefined,
|
|
97
|
+
recordedBy: baseline?.recordedBy,
|
|
98
|
+
builtAt: baseline?.builtAt,
|
|
99
|
+
recorder: baseline?.recorder,
|
|
100
|
+
recorderAlive: baseline?.recorder !== undefined &&
|
|
101
|
+
baseline.recorder.pid !== process.pid &&
|
|
102
|
+
isProcessAlive(baseline.recorder.pid),
|
|
94
103
|
}), 'coverage-replaced');
|
|
95
104
|
}
|
|
96
105
|
/**
|
|
@@ -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,35 @@ 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
|
+
// `--jobs` always reads each dispatch's raw log, so a shard that ran
|
|
146
|
+
// files other than its own fails instead of passing on their results.
|
|
147
|
+
const collectsResults = options.results !== undefined || options.shardSize !== undefined || (options.jobs ?? 1) > 1;
|
|
148
|
+
try {
|
|
149
|
+
await underLock(() => (collectsResults ? withRawLogDir((dir) => orchestrate(dir)) : orchestrate()), lockMode);
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
if (options.results !== undefined) {
|
|
153
|
+
await writeResultsFile(resolve(projectRoot, options.results));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
135
156
|
}
|
|
136
157
|
catch (error) {
|
|
137
158
|
if (!verdictEmitted()) {
|
|
@@ -164,6 +185,15 @@ export async function testCommand(projectRoot, testPaths, options = {}, underLoc
|
|
|
164
185
|
}
|
|
165
186
|
}
|
|
166
187
|
}
|
|
188
|
+
/** Writes `--results`; a failure warns, since the run's verdict is already out. */
|
|
189
|
+
async function writeResultsFile(path) {
|
|
190
|
+
try {
|
|
191
|
+
await writeTestResults(path);
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
warn(`Could not write --results ${path}: ${toError(error).message}`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
167
197
|
/** The refusal class a preflight gate named, when it named one. */
|
|
168
198
|
function refusalNote(error) {
|
|
169
199
|
return error instanceof PreflightRefusalError ? error.note : undefined;
|
|
@@ -198,26 +228,6 @@ function renderPreflightRefusal(error) {
|
|
|
198
228
|
process.stdout.write(block);
|
|
199
229
|
writeToActiveRunLog(block);
|
|
200
230
|
}
|
|
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
231
|
async function guardBuildBrowser(engineDir, launchablePath, options) {
|
|
222
232
|
if ((options.build || options.buildOnly) && launchablePath) {
|
|
223
233
|
await waitForPreflight(() => ensureLaunchableBrowserNotRunning(join(engineDir, launchablePath), {
|
|
@@ -254,7 +264,12 @@ function prepareDispatchArguments(classification, options, canaryPath, projectCo
|
|
|
254
264
|
? filterRedundantXpcshellFlavorArgs(options.machArg, classification)
|
|
255
265
|
: [];
|
|
256
266
|
const extraArgs = prepareExtraMachArgs(options, shuffleSeed, canaryPath, projectConfig, forwardedMachArgs);
|
|
257
|
-
|
|
267
|
+
// A `--shard-size` chunk runs several files in one browser; mochitest
|
|
268
|
+
// restarts it between them so a chunk judges each file as a shard would.
|
|
269
|
+
const restart = options.shardSize !== undefined && suite === 'mochitest'
|
|
270
|
+
? { ...options, restartBetweenTests: true }
|
|
271
|
+
: options;
|
|
272
|
+
extraArgs.push(...restartBetweenTestsArgs(restart, suite, forwardedMachArgs));
|
|
258
273
|
return { suite, shuffleSeed, extraArgs };
|
|
259
274
|
}
|
|
260
275
|
/**
|
|
@@ -327,9 +342,13 @@ async function runTestCommandBody(projectRoot, testPaths, options = {}, profileE
|
|
|
327
342
|
// documented `--mach-arg --marionette-port=NNNN` route still hits the
|
|
328
343
|
// wrapper preflight refusing on 2828 before the forwarded arg reaches
|
|
329
344
|
// mach.
|
|
345
|
+
// A negative control on compiled source goes in here, so the build
|
|
346
|
+
// compiles it (see negative-control-compiled.ts).
|
|
347
|
+
await hooks.beforeBuild?.();
|
|
330
348
|
if (await runTestBuildPhase(projectRoot, paths, projectConfig, harnessRetries, options, {
|
|
331
349
|
classification,
|
|
332
350
|
normalizedPaths: hooks.coveragePaths ?? normalizedPaths,
|
|
351
|
+
...(hooks.fullBuildReason === undefined ? {} : { fullBuildReason: hooks.fullBuildReason }),
|
|
333
352
|
})) {
|
|
334
353
|
return;
|
|
335
354
|
}
|
|
@@ -364,6 +383,8 @@ async function runTestCommandBody(projectRoot, testPaths, options = {}, profileE
|
|
|
364
383
|
...(options.fullOutput === true ? { fullOutput: true } : {}),
|
|
365
384
|
...(harnessEnv ? { env: harnessEnv } : {}),
|
|
366
385
|
pgidFile: options.pgidFile,
|
|
386
|
+
...(hooks.rawLogDir === undefined ? {} : { rawLogDir: hooks.rawLogDir }),
|
|
387
|
+
...(hooks.repetition === undefined ? {} : { repetition: hooks.repetition }),
|
|
367
388
|
};
|
|
368
389
|
const postRebuildContext = options.build
|
|
369
390
|
? createPostRebuildFailureContext('fireforge test --build', normalizedPaths)
|
|
@@ -376,46 +397,18 @@ async function runTestCommandBody(projectRoot, testPaths, options = {}, profileE
|
|
|
376
397
|
// into later files. Sharding is per path argument: a directory argument
|
|
377
398
|
// keeps its enumerated files together in one invocation, preserving the
|
|
378
399
|
// 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);
|
|
400
|
+
// the combined invocation; `--shard-size` and `--jobs` reshape it (see
|
|
401
|
+
// test-dispatch.ts).
|
|
402
|
+
await dispatchTests({
|
|
403
|
+
engineDir: paths.engine,
|
|
404
|
+
runCtx,
|
|
405
|
+
dispatchGroups,
|
|
406
|
+
options,
|
|
407
|
+
projectConfig,
|
|
408
|
+
canaryPath,
|
|
409
|
+
shuffleSeed,
|
|
410
|
+
normalizedPaths,
|
|
411
|
+
xpcshellOnly,
|
|
412
|
+
postRebuildContext,
|
|
413
|
+
});
|
|
421
414
|
}
|
|
@@ -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
|
}
|