@hominis/fireforge 0.52.0 → 0.54.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 +45 -0
- package/dist/build-info.json +4 -4
- package/dist/src/commands/bootstrap.js +2 -3
- package/dist/src/commands/build-prelock.d.ts +3 -3
- package/dist/src/commands/build-prelock.js +3 -3
- package/dist/src/commands/build.js +4 -0
- package/dist/src/commands/declarations-report.d.ts +9 -3
- package/dist/src/commands/declarations-report.js +18 -11
- package/dist/src/commands/discard.js +3 -3
- package/dist/src/commands/doctor-furnace-config-sync.js +4 -4
- package/dist/src/commands/doctor-furnace.js +5 -5
- package/dist/src/commands/export-adopt-check.d.ts +5 -6
- package/dist/src/commands/export-adopt-check.js +5 -6
- package/dist/src/commands/export-flow.js +6 -9
- package/dist/src/commands/export-staged-registration.d.ts +4 -4
- package/dist/src/commands/export-staged-registration.js +4 -4
- package/dist/src/commands/forward-import-remedy.d.ts +2 -3
- package/dist/src/commands/forward-import-remedy.js +2 -3
- package/dist/src/commands/furnace/create-dry-run.d.ts +2 -2
- package/dist/src/commands/furnace/deploy-fragment-includers.d.ts +2 -2
- package/dist/src/commands/furnace/deploy-fragment-includers.js +2 -2
- package/dist/src/commands/furnace/index.js +2 -2
- package/dist/src/commands/lint.js +4 -4
- package/dist/src/commands/patch/compact.js +1 -1
- package/dist/src/commands/patch/lint-ignore-from.d.ts +3 -4
- package/dist/src/commands/patch/lint-ignore-from.js +3 -4
- package/dist/src/commands/patch/move-files-adopt.d.ts +33 -0
- package/dist/src/commands/patch/move-files-adopt.js +65 -0
- package/dist/src/commands/patch/move-files-create.js +41 -20
- package/dist/src/commands/patch/move-files-into.d.ts +18 -0
- package/dist/src/commands/patch/move-files-into.js +125 -60
- package/dist/src/commands/patch/move-files-retire.d.ts +39 -0
- package/dist/src/commands/patch/move-files-retire.js +91 -0
- package/dist/src/commands/patch/move-files.d.ts +3 -0
- package/dist/src/commands/patch/move-files.js +42 -12
- package/dist/src/commands/patch/move-size-tier.d.ts +27 -0
- package/dist/src/commands/patch/move-size-tier.js +66 -0
- package/dist/src/commands/patch/rename.js +1 -1
- package/dist/src/commands/patch/reorder-projection.d.ts +17 -0
- package/dist/src/commands/patch/reorder-projection.js +49 -0
- package/dist/src/commands/patch/reorder-shift.d.ts +30 -0
- package/dist/src/commands/patch/reorder-shift.js +307 -0
- package/dist/src/commands/patch/reorder.d.ts +9 -15
- package/dist/src/commands/patch/reorder.js +52 -50
- package/dist/src/commands/patch/split-plan.d.ts +31 -3
- package/dist/src/commands/patch/split-plan.js +36 -12
- package/dist/src/commands/patch/split-staged.d.ts +2 -2
- package/dist/src/commands/patch/split-staged.js +2 -2
- package/dist/src/commands/patch/split.js +18 -5
- package/dist/src/commands/patch/staged-dependency-entries.d.ts +9 -1
- package/dist/src/commands/patch/staged-dependency-entries.js +32 -4
- package/dist/src/commands/patch/staged-dependency-from.js +15 -9
- package/dist/src/commands/patch/staged-dependency-remove.d.ts +26 -4
- package/dist/src/commands/patch/staged-dependency-remove.js +60 -23
- package/dist/src/commands/patch/staged-dependency.js +35 -24
- package/dist/src/commands/patch/tier.js +2 -2
- package/dist/src/commands/patch-rename-map.d.ts +14 -3
- package/dist/src/commands/patch-rename-map.js +21 -3
- package/dist/src/commands/re-export-adjacent.d.ts +4 -5
- package/dist/src/commands/re-export-lint.d.ts +4 -3
- package/dist/src/commands/re-export-lint.js +4 -3
- package/dist/src/commands/re-export-options.js +8 -2
- package/dist/src/commands/re-export-preflight.js +1 -2
- package/dist/src/commands/re-export-purity-scope.d.ts +1 -1
- package/dist/src/commands/re-export-purity-scope.js +13 -7
- package/dist/src/commands/re-export-retired-declarations.d.ts +22 -0
- package/dist/src/commands/re-export-retired-declarations.js +113 -0
- package/dist/src/commands/re-export-single.js +2 -2
- package/dist/src/commands/re-export-stale-registrations.d.ts +27 -6
- package/dist/src/commands/re-export-stale-registrations.js +65 -21
- package/dist/src/commands/re-export.js +4 -0
- package/dist/src/commands/rebase/continue.js +3 -3
- package/dist/src/commands/register.js +5 -4
- package/dist/src/commands/run.js +11 -4
- package/dist/src/commands/status.js +1 -1
- package/dist/src/commands/test-build-furnace.d.ts +1 -2
- package/dist/src/commands/test-build-furnace.js +1 -2
- package/dist/src/commands/test-build-phase.d.ts +8 -0
- package/dist/src/commands/test-build-phase.js +42 -7
- package/dist/src/commands/test-control-restore.d.ts +40 -0
- package/dist/src/commands/test-control-restore.js +86 -0
- package/dist/src/commands/test-dispatch.js +68 -7
- package/dist/src/commands/test-multi-verdict.js +1 -0
- package/dist/src/commands/test-orchestrate.d.ts +6 -0
- package/dist/src/commands/test-orchestrate.js +32 -1
- package/dist/src/commands/test-perf-scope.d.ts +1 -1
- package/dist/src/commands/test-perf-scope.js +1 -1
- package/dist/src/commands/test-prelock.d.ts +18 -8
- package/dist/src/commands/test-prelock.js +42 -14
- package/dist/src/commands/test-register.js +4 -3
- package/dist/src/commands/test-results.d.ts +10 -1
- package/dist/src/commands/test-results.js +20 -3
- package/dist/src/commands/test-run.d.ts +6 -0
- package/dist/src/commands/test-run.js +23 -4
- package/dist/src/commands/test-shards.d.ts +7 -3
- package/dist/src/commands/test-shards.js +24 -7
- package/dist/src/commands/test-stale-gate.d.ts +6 -1
- package/dist/src/commands/test-stale-gate.js +40 -2
- package/dist/src/commands/test-verdict.d.ts +5 -3
- package/dist/src/commands/test.js +14 -2
- package/dist/src/commands/token.js +2 -2
- package/dist/src/commands/verify.js +1 -1
- package/dist/src/commands/watch.js +1 -1
- package/dist/src/commands/wire.js +1 -1
- package/dist/src/core/build-audit-resolve.d.ts +4 -5
- package/dist/src/core/build-audit-resolve.js +4 -5
- package/dist/src/core/build-audit-skip.d.ts +2 -2
- package/dist/src/core/build-audit.d.ts +3 -4
- package/dist/src/core/build-baseline-types.d.ts +24 -3
- package/dist/src/core/build-baseline-validate.d.ts +30 -1
- package/dist/src/core/build-baseline-validate.js +127 -35
- package/dist/src/core/build-baseline.d.ts +8 -2
- package/dist/src/core/build-baseline.js +25 -16
- package/dist/src/core/build-jar-escalation.d.ts +4 -4
- package/dist/src/core/build-jar-escalation.js +4 -4
- package/dist/src/core/build-prepare.js +4 -4
- package/dist/src/core/config-validate.js +4 -4
- package/dist/src/core/coverage-extend.d.ts +5 -3
- package/dist/src/core/coverage-extend.js +8 -3
- package/dist/src/core/coverage-records.d.ts +4 -5
- package/dist/src/core/coverage-records.js +4 -5
- package/dist/src/core/destructive.d.ts +2 -2
- package/dist/src/core/destructive.js +2 -2
- package/dist/src/core/discard-baseline.d.ts +2 -2
- package/dist/src/core/discard-baseline.js +3 -3
- package/dist/src/core/engine-session-lock.d.ts +1 -1
- package/dist/src/core/engine-session-lock.js +1 -1
- package/dist/src/core/file-lock-owner.d.ts +5 -5
- package/dist/src/core/file-lock-owner.js +5 -5
- package/dist/src/core/file-lock-queue.js +6 -8
- package/dist/src/core/file-lock-shared.d.ts +4 -4
- package/dist/src/core/file-lock-shared.js +4 -4
- package/dist/src/core/file-lock.js +7 -10
- package/dist/src/core/firefox.js +2 -2
- package/dist/src/core/furnace-apply-overwrite-warn.d.ts +1 -1
- package/dist/src/core/furnace-apply-overwrite-warn.js +1 -1
- package/dist/src/core/furnace-apply.js +3 -3
- package/dist/src/core/furnace-operation.js +5 -5
- package/dist/src/core/furnace-registration-jar-inplace.js +5 -6
- package/dist/src/core/furnace-registration-rename.d.ts +5 -5
- package/dist/src/core/furnace-registration-rename.js +5 -5
- package/dist/src/core/furnace-registration-validate.d.ts +2 -1
- package/dist/src/core/furnace-registration-validate.js +2 -1
- package/dist/src/core/furnace-registration.js +2 -2
- package/dist/src/core/git-base.js +4 -3
- package/dist/src/core/git-diff.js +1 -1
- package/dist/src/core/git.js +1 -1
- package/dist/src/core/harness-orphans.d.ts +6 -6
- package/dist/src/core/harness-orphans.js +6 -6
- package/dist/src/core/harness-port-reservation.d.ts +60 -0
- package/dist/src/core/harness-port-reservation.js +122 -0
- package/dist/src/core/history-log-file.d.ts +9 -9
- package/dist/src/core/history-log-file.js +15 -15
- package/dist/src/core/history-log.d.ts +3 -3
- package/dist/src/core/history-log.js +3 -3
- package/dist/src/core/launchable-build.d.ts +39 -0
- package/dist/src/core/launchable-build.js +85 -0
- package/dist/src/core/marionette-port.js +1 -3
- package/dist/src/core/moz-manifest-rules.js +3 -2
- package/dist/src/core/negative-control-compiled.d.ts +13 -3
- package/dist/src/core/negative-control-compiled.js +27 -5
- package/dist/src/core/negative-control-diff.js +31 -6
- package/dist/src/core/parser-fallback.d.ts +5 -5
- package/dist/src/core/parser-fallback.js +5 -5
- package/dist/src/core/patch-identifier-suggest.d.ts +6 -0
- package/dist/src/core/patch-identifier-suggest.js +11 -4
- package/dist/src/core/patch-lint-checkjs.d.ts +1 -1
- package/dist/src/core/patch-lint-checkjs.js +1 -1
- package/dist/src/core/patch-lint-css.js +2 -2
- package/dist/src/core/patch-lint-file-size.d.ts +5 -7
- package/dist/src/core/patch-lint-file-size.js +5 -7
- package/dist/src/core/patch-lint-ignore.d.ts +3 -3
- package/dist/src/core/patch-lint-ignore.js +3 -3
- package/dist/src/core/patch-lint-prettier.d.ts +4 -6
- package/dist/src/core/patch-lint-prettier.js +4 -6
- package/dist/src/core/patch-lint-projection.d.ts +1 -1
- package/dist/src/core/patch-lint-projection.js +1 -1
- package/dist/src/core/patch-lint-queue-types.d.ts +2 -2
- package/dist/src/core/patch-lint-queue-types.js +2 -2
- package/dist/src/core/patch-manifest-consistency.d.ts +2 -2
- package/dist/src/core/patch-manifest-consistency.js +2 -2
- package/dist/src/core/patch-manifest-io.d.ts +5 -1
- package/dist/src/core/patch-manifest-io.js +12 -10
- package/dist/src/core/patch-manifest-key-order.d.ts +3 -4
- package/dist/src/core/patch-manifest-key-order.js +3 -4
- package/dist/src/core/patch-parse.d.ts +3 -3
- package/dist/src/core/patch-parse.js +3 -3
- package/dist/src/core/patch-policy.d.ts +1 -1
- package/dist/src/core/patch-policy.js +1 -1
- package/dist/src/core/patch-source-metadata.d.ts +1 -1
- package/dist/src/core/patch-source-metadata.js +1 -1
- package/dist/src/core/patch-subset.d.ts +5 -3
- package/dist/src/core/patch-subset.js +8 -7
- package/dist/src/core/patch-transform.d.ts +2 -2
- package/dist/src/core/patch-transform.js +2 -2
- package/dist/src/core/rebase-session.js +4 -6
- package/dist/src/core/register-xpcshell-test.d.ts +28 -7
- package/dist/src/core/register-xpcshell-test.js +76 -27
- package/dist/src/core/run-log.d.ts +8 -8
- package/dist/src/core/run-log.js +8 -8
- package/dist/src/core/status-classify.d.ts +1 -2
- package/dist/src/core/status-classify.js +1 -2
- package/dist/src/core/test-harness-crash.d.ts +5 -3
- package/dist/src/core/test-harness-crash.js +13 -14
- package/dist/src/core/test-harness-infra.d.ts +32 -0
- package/dist/src/core/test-harness-infra.js +69 -0
- package/dist/src/core/test-harness-signature.d.ts +6 -0
- package/dist/src/core/test-stale-check.d.ts +38 -0
- package/dist/src/core/test-stale-check.js +82 -1
- package/dist/src/core/test-stall-triage.js +5 -7
- package/dist/src/core/token-category.js +2 -2
- package/dist/src/core/token-inventory.js +3 -3
- package/dist/src/core/token-manager.d.ts +2 -2
- package/dist/src/core/token-manager.js +5 -7
- package/dist/src/core/token-variant.d.ts +6 -6
- package/dist/src/core/token-variant.js +7 -7
- package/dist/src/core/typecheck-shim.js +1 -1
- package/dist/src/core/wire-destroy.d.ts +1 -1
- package/dist/src/core/wire-destroy.js +1 -1
- package/dist/src/core/wire-dom-fragment.js +1 -1
- package/dist/src/core/wire-init.d.ts +1 -1
- package/dist/src/core/wire-init.js +1 -1
- package/dist/src/core/wire-subscript.d.ts +1 -1
- package/dist/src/core/wire-subscript.js +1 -1
- package/dist/src/core/wire-utils.d.ts +2 -2
- package/dist/src/core/wire-utils.js +3 -3
- package/dist/src/errors/base.d.ts +1 -1
- package/dist/src/errors/base.js +1 -1
- package/dist/src/errors/rebase.d.ts +1 -1
- package/dist/src/errors/rebase.js +1 -1
- package/dist/src/types/commands/options.d.ts +38 -3
- package/dist/src/types/config.d.ts +5 -7
- package/dist/src/utils/options.js +2 -2
- package/dist/src/utils/process.js +2 -2
- package/dist/src/utils/regex.d.ts +2 -2
- package/dist/src/utils/regex.js +3 -3
- package/dist/src/utils/sleep.d.ts +5 -6
- package/dist/src/utils/sleep.js +5 -6
- package/dist/src/utils/validation.js +2 -2
- package/docs/configuration.md +28 -23
- package/docs/exit-codes.md +8 -11
- package/docs/furnace.md +3 -3
- package/docs/lifecycle-invariants.md +36 -45
- package/docs/machine-output.md +41 -24
- package/docs/patch-workflow.md +140 -29
- package/docs/run-logs.md +8 -9
- package/docs/testing.md +114 -48
- package/package.json +1 -1
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* `FIREFORGE-VERDICT:` line. A request that needs none of this runs the
|
|
15
15
|
* body exactly as before.
|
|
16
16
|
*/
|
|
17
|
+
import type { ControlBuildStamp } from '../core/build-baseline-types.js';
|
|
17
18
|
import type { TestOptions } from '../types/commands/index.js';
|
|
18
19
|
/** What the orchestrator asks of one sub-run beyond its paths and options. */
|
|
19
20
|
export interface TestRunHooks {
|
|
@@ -35,6 +36,11 @@ export interface TestRunHooks {
|
|
|
35
36
|
* compiles nothing, so a compiled control's binary needs the full one.
|
|
36
37
|
*/
|
|
37
38
|
fullBuildReason?: string;
|
|
39
|
+
/**
|
|
40
|
+
* Stamp the pre-test build records on its baseline because it compiles a
|
|
41
|
+
* negative control in (see `BuildBaseline.controlBuild`).
|
|
42
|
+
*/
|
|
43
|
+
controlBuild?: ControlBuildStamp;
|
|
38
44
|
/** `--repeat` run this sub-run is (1-based), for `--results`. */
|
|
39
45
|
repetition?: number;
|
|
40
46
|
/** Where dispatches write their mozlog raw logs (`--results`, `--shard-size`). */
|
|
@@ -24,8 +24,10 @@ import { TestFailureError } from '../errors/build.js';
|
|
|
24
24
|
import { toError } from '../utils/errors.js';
|
|
25
25
|
import { info, notice, success } from '../utils/logger.js';
|
|
26
26
|
import { stripEnginePrefix } from '../utils/paths.js';
|
|
27
|
+
import { controlBuildRecorded, runControlRestoreBuild } from './test-control-restore.js';
|
|
27
28
|
import { classifyTestHarnesses } from './test-modes.js';
|
|
28
29
|
import { deriveControlOutcome, describeControlOutcome, formatAggregateBody, formatRunLine, parseCapturedVerdict, worstRun, } from './test-multi-verdict.js';
|
|
30
|
+
import { rearmSpentWaits } from './test-prelock.js';
|
|
29
31
|
import { emitAggregateVerdict, emitFailVerdict, setVerdictCapture, setVerdictRunAttribute, verdictEmitted, } from './test-verdict.js';
|
|
30
32
|
function assertMultiRunUsage(testPaths, options) {
|
|
31
33
|
const flag = options.negativeControl !== undefined ? '--negative-control' : '--repeat';
|
|
@@ -57,7 +59,9 @@ async function splitHarnesses(engineDir, testPaths, options) {
|
|
|
57
59
|
}
|
|
58
60
|
/** Options for every run after the first: the build and doctor run once. */
|
|
59
61
|
function laterRunOptions(options) {
|
|
60
|
-
|
|
62
|
+
// A later run follows the earlier runs' work under the same lock hold, so
|
|
63
|
+
// its browser and port checks wait out the budgets again.
|
|
64
|
+
const later = { ...rearmSpentWaits(options) };
|
|
61
65
|
delete later.build;
|
|
62
66
|
delete later.extendCoverage;
|
|
63
67
|
delete later.refuseUnexportedDrift;
|
|
@@ -105,6 +109,26 @@ function controlHook(paths, plan, slot) {
|
|
|
105
109
|
'it stays in place until this run has finished.');
|
|
106
110
|
};
|
|
107
111
|
}
|
|
112
|
+
/** The stamp a compiled control's pre-test build records on its baseline. */
|
|
113
|
+
function compiledControlStamp(options, compiled) {
|
|
114
|
+
if (compiled === undefined || options.negativeControl === undefined)
|
|
115
|
+
return undefined;
|
|
116
|
+
return {
|
|
117
|
+
diffFile: options.negativeControl,
|
|
118
|
+
targets: [...compiled],
|
|
119
|
+
appliedAt: new Date().toISOString(),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* After a compiled control was restored, rebuilds the binary that still
|
|
124
|
+
* carries it, when this run's control build recorded its stamp, and
|
|
125
|
+
* stamps `control-restore-build=done|failed` on the verdict.
|
|
126
|
+
*/
|
|
127
|
+
async function rebuildAfterCompiledControl(projectRoot, stamp, options) {
|
|
128
|
+
if (stamp === undefined || !(await controlBuildRecorded(projectRoot, stamp)))
|
|
129
|
+
return;
|
|
130
|
+
setVerdictRunAttribute('control-restore-build', await runControlRestoreBuild(projectRoot, stamp, options.harnessRetries));
|
|
131
|
+
}
|
|
108
132
|
/**
|
|
109
133
|
* Throws the invocation's error after the aggregate verdict, or returns
|
|
110
134
|
* when the invocation succeeded (all runs green, or a control caught).
|
|
@@ -172,6 +196,7 @@ export async function orchestrateTestRuns(projectRoot, testPaths, options, body,
|
|
|
172
196
|
}
|
|
173
197
|
setVerdictRunAttribute('control-build', 'compiled');
|
|
174
198
|
}
|
|
199
|
+
const controlStamp = compiledControlStamp(options, compiled);
|
|
175
200
|
const results = [];
|
|
176
201
|
let restoreError;
|
|
177
202
|
for (const [index, run] of runs.entries()) {
|
|
@@ -190,6 +215,7 @@ export async function orchestrateTestRuns(projectRoot, testPaths, options, body,
|
|
|
190
215
|
? {
|
|
191
216
|
beforeBuild: controlHook(controlPaths, plan, slot),
|
|
192
217
|
fullBuildReason: formatCompiledControlBuildNotice(compiled),
|
|
218
|
+
...(controlStamp === undefined ? {} : { controlBuild: controlStamp }),
|
|
193
219
|
}
|
|
194
220
|
: { beforeDispatch: controlHook(controlPaths, plan, slot) }),
|
|
195
221
|
});
|
|
@@ -234,6 +260,11 @@ export async function orchestrateTestRuns(projectRoot, testPaths, options, body,
|
|
|
234
260
|
emitFailVerdict('inconclusive', 'negative-control-restore');
|
|
235
261
|
throw toError(restoreError);
|
|
236
262
|
}
|
|
263
|
+
// The source is back byte-exact, but the binary still carries the
|
|
264
|
+
// compiled control: rebuild it under this same hold (see
|
|
265
|
+
// test-control-restore.ts). Only after every run, since a later
|
|
266
|
+
// `--repeat` run tests the same control-built binary.
|
|
267
|
+
await rebuildAfterCompiledControl(projectRoot, controlStamp, options);
|
|
237
268
|
const repetitions = options.repeat ?? 1;
|
|
238
269
|
const control = plan === undefined
|
|
239
270
|
? undefined
|
|
@@ -32,7 +32,7 @@ export declare function perfGenerationTolerance(engineDir: string): (before: str
|
|
|
32
32
|
* (with a warning, and `drift-tolerated=<n>` on the verdict) when HEAD did
|
|
33
33
|
* not move and every changed path is one the running browser cannot see.
|
|
34
34
|
*
|
|
35
|
-
* A peer deleting an unrelated chrome script
|
|
35
|
+
* A peer deleting an unrelated chrome script must not make a whole run
|
|
36
36
|
* inconclusive. What a running browser can see mid-run is narrower than
|
|
37
37
|
* "anything under engine/": a file the objdir links to its source (`live`),
|
|
38
38
|
* one the harness stages at dispatch (`at-dispatch`), and the run's own
|
|
@@ -86,7 +86,7 @@ export function perfGenerationTolerance(engineDir) {
|
|
|
86
86
|
* (with a warning, and `drift-tolerated=<n>` on the verdict) when HEAD did
|
|
87
87
|
* not move and every changed path is one the running browser cannot see.
|
|
88
88
|
*
|
|
89
|
-
* A peer deleting an unrelated chrome script
|
|
89
|
+
* A peer deleting an unrelated chrome script must not make a whole run
|
|
90
90
|
* inconclusive. What a running browser can see mid-run is narrower than
|
|
91
91
|
* "anything under engine/": a file the objdir links to its source (`live`),
|
|
92
92
|
* one the harness stages at dispatch (`at-dispatch`), and the run's own
|
|
@@ -2,15 +2,14 @@
|
|
|
2
2
|
* The `fireforge test` checks that run BEFORE the engine session lock.
|
|
3
3
|
*
|
|
4
4
|
* Everything here reads, never writes, so none of it needs the lock, and
|
|
5
|
-
* each check
|
|
6
|
-
* refuse:
|
|
5
|
+
* running it first keeps each check from costing a full place in the lock
|
|
6
|
+
* queue before it can refuse:
|
|
7
7
|
*
|
|
8
|
-
* - a missing launchable binary (a peer's interrupted build)
|
|
9
|
-
*
|
|
10
|
-
* - the named-file checkJs pass
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* lock, so every peer queued behind a run that was doing nothing.
|
|
8
|
+
* - a missing launchable binary (a peer's interrupted build) refuses
|
|
9
|
+
* before the wait, naming the cause;
|
|
10
|
+
* - the named-file checkJs pass refuses a type error without a queue;
|
|
11
|
+
* - `--wait-browser` and `--wait-port` spend their budget without holding
|
|
12
|
+
* the lock, so no peer queues behind a run that is doing nothing.
|
|
14
13
|
*
|
|
15
14
|
* The in-lock ladder in `test.ts` still runs in full afterwards. That is
|
|
16
15
|
* the "re-check once under the lock": the waits this pass spent are
|
|
@@ -49,3 +48,14 @@ export declare const TEST_BUILD_PREFLIGHT: {
|
|
|
49
48
|
* @param options - Parsed test options
|
|
50
49
|
*/
|
|
51
50
|
export declare function runTestPreLockPreflight(projectRoot: string, testPaths: string[], options: TestOptions): Promise<TestOptions>;
|
|
51
|
+
/**
|
|
52
|
+
* `options` with the wait budgets the pre-lock pass spent put back, for a
|
|
53
|
+
* browser or port check that follows work this invocation already did
|
|
54
|
+
* under the lock: its pre-test build, or an earlier run of a sequence. A
|
|
55
|
+
* busy browser there is a peer that started after the lock was taken, and
|
|
56
|
+
* refusing would throw the build away, so it is waited out like the first
|
|
57
|
+
* check's.
|
|
58
|
+
*
|
|
59
|
+
* @param options - In-lock run options
|
|
60
|
+
*/
|
|
61
|
+
export declare function rearmSpentWaits(options: TestOptions): TestOptions;
|
|
@@ -3,15 +3,14 @@
|
|
|
3
3
|
* The `fireforge test` checks that run BEFORE the engine session lock.
|
|
4
4
|
*
|
|
5
5
|
* Everything here reads, never writes, so none of it needs the lock, and
|
|
6
|
-
* each check
|
|
7
|
-
* refuse:
|
|
6
|
+
* running it first keeps each check from costing a full place in the lock
|
|
7
|
+
* queue before it can refuse:
|
|
8
8
|
*
|
|
9
|
-
* - a missing launchable binary (a peer's interrupted build)
|
|
10
|
-
*
|
|
11
|
-
* - the named-file checkJs pass
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* lock, so every peer queued behind a run that was doing nothing.
|
|
9
|
+
* - a missing launchable binary (a peer's interrupted build) refuses
|
|
10
|
+
* before the wait, naming the cause;
|
|
11
|
+
* - the named-file checkJs pass refuses a type error without a queue;
|
|
12
|
+
* - `--wait-browser` and `--wait-port` spend their budget without holding
|
|
13
|
+
* the lock, so no peer queues behind a run that is doing nothing.
|
|
15
14
|
*
|
|
16
15
|
* The in-lock ladder in `test.ts` still runs in full afterwards. That is
|
|
17
16
|
* the "re-check once under the lock": the waits this pass spent are
|
|
@@ -122,7 +121,10 @@ export async function runTestPreLockPreflight(projectRoot, testPaths, options) {
|
|
|
122
121
|
}
|
|
123
122
|
else {
|
|
124
123
|
await refuseCompiledControlBeforeLock(projectRoot, paths.engine, options);
|
|
125
|
-
await refuseStalePackagingBeforeLock(projectRoot, paths.engine, options,
|
|
124
|
+
await refuseStalePackagingBeforeLock(projectRoot, paths.engine, options, {
|
|
125
|
+
normalizedPaths,
|
|
126
|
+
launchablePath,
|
|
127
|
+
});
|
|
126
128
|
}
|
|
127
129
|
return waitForResourcesBeforeLock(paths.engine, config, options, {
|
|
128
130
|
launchablePath,
|
|
@@ -195,16 +197,17 @@ async function refuseCompiledControlBeforeLock(projectRoot, engineDir, options)
|
|
|
195
197
|
* under `--allow-stale-build`/`--allow-stale-components`, whose warnings
|
|
196
198
|
* belong to the in-lock pass alone. A `--negative-control` refusal says to
|
|
197
199
|
* add `--build`: without it the control has nothing current to run on, and
|
|
198
|
-
* the invocation
|
|
200
|
+
* the invocation would end `control=inconclusive` after a full queue.
|
|
199
201
|
*/
|
|
200
|
-
async function refuseStalePackagingBeforeLock(projectRoot, engineDir, options,
|
|
202
|
+
async function refuseStalePackagingBeforeLock(projectRoot, engineDir, options, request) {
|
|
201
203
|
if (options.allowStaleBuild === true || options.allowStaleComponents === true)
|
|
202
204
|
return;
|
|
203
205
|
if ((await readBuildRunState(projectRoot)).kind === 'running')
|
|
204
206
|
return;
|
|
205
207
|
try {
|
|
206
|
-
await enforceStaleBuildGate(projectRoot, engineDir, options, normalizedPaths, {
|
|
208
|
+
await enforceStaleBuildGate(projectRoot, engineDir, options, request.normalizedPaths, {
|
|
207
209
|
quiet: true,
|
|
210
|
+
launchablePath: request.launchablePath,
|
|
208
211
|
});
|
|
209
212
|
}
|
|
210
213
|
catch (error) {
|
|
@@ -216,15 +219,40 @@ async function refuseStalePackagingBeforeLock(projectRoot, engineDir, options, n
|
|
|
216
219
|
'against a fresh build of the requested paths.', error.note);
|
|
217
220
|
}
|
|
218
221
|
}
|
|
219
|
-
/**
|
|
222
|
+
/**
|
|
223
|
+
* `options` without one wait budget, which this pass already spent. The
|
|
224
|
+
* budget moves to `spentWaits`, for {@link rearmSpentWaits}.
|
|
225
|
+
*/
|
|
220
226
|
function withoutOption(options, key) {
|
|
221
|
-
const rest = { ...options };
|
|
227
|
+
const rest = { ...options, spentWaits: { ...options.spentWaits, [key]: options[key] } };
|
|
222
228
|
if (key === 'waitBrowser')
|
|
223
229
|
delete rest.waitBrowser;
|
|
224
230
|
else
|
|
225
231
|
delete rest.waitPort;
|
|
226
232
|
return rest;
|
|
227
233
|
}
|
|
234
|
+
/**
|
|
235
|
+
* `options` with the wait budgets the pre-lock pass spent put back, for a
|
|
236
|
+
* browser or port check that follows work this invocation already did
|
|
237
|
+
* under the lock: its pre-test build, or an earlier run of a sequence. A
|
|
238
|
+
* busy browser there is a peer that started after the lock was taken, and
|
|
239
|
+
* refusing would throw the build away, so it is waited out like the first
|
|
240
|
+
* check's.
|
|
241
|
+
*
|
|
242
|
+
* @param options - In-lock run options
|
|
243
|
+
*/
|
|
244
|
+
export function rearmSpentWaits(options) {
|
|
245
|
+
const spent = options.spentWaits;
|
|
246
|
+
if (spent === undefined)
|
|
247
|
+
return options;
|
|
248
|
+
const rearmed = { ...options };
|
|
249
|
+
if (spent.waitBrowser !== undefined)
|
|
250
|
+
rearmed.waitBrowser = spent.waitBrowser;
|
|
251
|
+
if (spent.waitPort !== undefined)
|
|
252
|
+
rearmed.waitPort = spent.waitPort;
|
|
253
|
+
delete rearmed.spentWaits;
|
|
254
|
+
return rearmed;
|
|
255
|
+
}
|
|
228
256
|
/**
|
|
229
257
|
* Spends `--wait-browser` and `--wait-port` before the lock, mirroring
|
|
230
258
|
* which in-lock checks would apply to this run, and strips each budget it
|
|
@@ -44,8 +44,8 @@ const TEST_HELP_TEXT = [
|
|
|
44
44
|
'',
|
|
45
45
|
'Machine-readable verdict: every run (single, sharded, --canary,',
|
|
46
46
|
'--doctor) ends with exactly one raw stdout line',
|
|
47
|
-
' FIREFORGE-VERDICT: PASS|FAIL [reason=crash|harness-arguments|
|
|
48
|
-
' no-tests|preflight|inconclusive|lock-timeout|killed] [checks=<n>]',
|
|
47
|
+
' FIREFORGE-VERDICT: PASS|FAIL [reason=crash|server-startup|harness-arguments|',
|
|
48
|
+
' test-failures|no-tests|preflight|inconclusive|lock-timeout|killed] [checks=<n>]',
|
|
49
49
|
' [unexpected=<n>] [started=<n>/<m> first-unstarted=<path>]',
|
|
50
50
|
' [shards=<p>/<t>] [(<note>)] [shuffle=<seed>]',
|
|
51
51
|
' [runs=<p>/<n>] [control=caught|missed|inconclusive]',
|
|
@@ -86,7 +86,8 @@ const TEST_HELP_TEXT = [
|
|
|
86
86
|
'launchable binary (with the build that left it missing, when one is',
|
|
87
87
|
'recorded), the named-file checkJs pass of --build, and the',
|
|
88
88
|
'--wait-browser/--wait-port budgets. Each is re-checked once under the',
|
|
89
|
-
'lock
|
|
89
|
+
'lock; the checks after a pre-test build or an earlier run of the',
|
|
90
|
+
'sequence wait the budgets out again.',
|
|
90
91
|
].join('\n');
|
|
91
92
|
function registerTestMultiRunOptions(test) {
|
|
92
93
|
return test
|
|
@@ -18,6 +18,13 @@ export interface TestFileResult extends RawTestResult {
|
|
|
18
18
|
shard: string;
|
|
19
19
|
/** Line of the run log where this file's dispatch began (1-based). */
|
|
20
20
|
logLine: number;
|
|
21
|
+
/**
|
|
22
|
+
* The shard's dispatched files, on a harness pseudo-record only (see
|
|
23
|
+
* {@link HARNESS_PSEUDO_TEST}).
|
|
24
|
+
*/
|
|
25
|
+
shardFiles?: string[];
|
|
26
|
+
/** The shard's one file, on a harness pseudo-record of a one-file shard. */
|
|
27
|
+
file?: string;
|
|
21
28
|
}
|
|
22
29
|
/** The document `--results` writes. */
|
|
23
30
|
export interface TestResultsDocument {
|
|
@@ -38,12 +45,14 @@ export declare function resetTestResults(): void;
|
|
|
38
45
|
*
|
|
39
46
|
* @param results - The dispatch's finished files
|
|
40
47
|
* @param unfinished - Its started-but-unfinished files
|
|
41
|
-
* @param context - Run, shard and run-log line of the dispatch
|
|
48
|
+
* @param context - Run, shard and run-log line of the dispatch, and the
|
|
49
|
+
* shard's dispatched files for harness pseudo-records
|
|
42
50
|
*/
|
|
43
51
|
export declare function recordTestResults(results: readonly RawTestResult[], unfinished: readonly string[], context: {
|
|
44
52
|
run: number;
|
|
45
53
|
shard: string;
|
|
46
54
|
logLine: number;
|
|
55
|
+
shardFiles?: readonly string[];
|
|
47
56
|
}): void;
|
|
48
57
|
/** The results collected so far (for tests and the shard summary). */
|
|
49
58
|
export declare function collectedTestResults(): TestResultsDocument;
|
|
@@ -14,6 +14,13 @@ import { mkdtemp, rm } from 'node:fs/promises';
|
|
|
14
14
|
import { tmpdir } from 'node:os';
|
|
15
15
|
import { join } from 'node:path';
|
|
16
16
|
import { writeJson } from '../utils/fs.js';
|
|
17
|
+
/**
|
|
18
|
+
* The `test` id mozlog gives a result the harness itself reports, such as
|
|
19
|
+
* the no-output TIMEOUT of a hung browser: a record that names no test
|
|
20
|
+
* file. It keeps that id, and gains the shard's files so a consumer can
|
|
21
|
+
* attribute it without matching `shard` labels back to paths.
|
|
22
|
+
*/
|
|
23
|
+
const HARNESS_PSEUDO_TEST = 'automation.py';
|
|
17
24
|
let collected = { schemaVersion: 1, files: [], unfinished: [] };
|
|
18
25
|
/** Clears the collector at the start of an invocation. */
|
|
19
26
|
export function resetTestResults() {
|
|
@@ -24,15 +31,25 @@ export function resetTestResults() {
|
|
|
24
31
|
*
|
|
25
32
|
* @param results - The dispatch's finished files
|
|
26
33
|
* @param unfinished - Its started-but-unfinished files
|
|
27
|
-
* @param context - Run, shard and run-log line of the dispatch
|
|
34
|
+
* @param context - Run, shard and run-log line of the dispatch, and the
|
|
35
|
+
* shard's dispatched files for harness pseudo-records
|
|
28
36
|
*/
|
|
29
37
|
export function recordTestResults(results, unfinished, context) {
|
|
30
|
-
|
|
31
|
-
|
|
38
|
+
const { shardFiles, ...dispatch } = context;
|
|
39
|
+
for (const result of results) {
|
|
40
|
+
collected.files.push({ ...result, ...dispatch, ...harnessRecordFiles(result, shardFiles) });
|
|
41
|
+
}
|
|
32
42
|
for (const test of unfinished) {
|
|
33
43
|
collected.unfinished.push({ test, run: context.run, shard: context.shard });
|
|
34
44
|
}
|
|
35
45
|
}
|
|
46
|
+
/** The `shardFiles`/`file` keys a harness pseudo-record gains. */
|
|
47
|
+
function harnessRecordFiles(result, shardFiles) {
|
|
48
|
+
if (result.test !== HARNESS_PSEUDO_TEST || shardFiles === undefined)
|
|
49
|
+
return {};
|
|
50
|
+
const only = shardFiles.length === 1 ? shardFiles[0] : undefined;
|
|
51
|
+
return { shardFiles: [...shardFiles], ...(only === undefined ? {} : { file: only }) };
|
|
52
|
+
}
|
|
36
53
|
/** The results collected so far (for tests and the shard summary). */
|
|
37
54
|
export function collectedTestResults() {
|
|
38
55
|
return collected;
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* stays available via `--no-shard`.
|
|
11
11
|
*/
|
|
12
12
|
import { type DisplaySleepState } from '../core/display-state.js';
|
|
13
|
+
import { type HarnessServerPorts } from '../core/harness-port-reservation.js';
|
|
13
14
|
import { type MachCommandResult } from '../core/mach.js';
|
|
14
15
|
import { type HarnessRunVerdict } from '../core/test-harness-crash.js';
|
|
15
16
|
import { type RawTestResult } from '../core/test-raw-log.js';
|
|
@@ -74,6 +75,11 @@ export interface TestRunOutcome {
|
|
|
74
75
|
displayState?: DisplaySleepState | undefined;
|
|
75
76
|
/** Per-file results from the raw log, when the run wrote one. */
|
|
76
77
|
fileResults?: RawTestResult[] | undefined;
|
|
78
|
+
/**
|
|
79
|
+
* The mochitest server ports the last attempt reported (its "falling
|
|
80
|
+
* back to port" lines), for the shard line's `ports=` key.
|
|
81
|
+
*/
|
|
82
|
+
serverPorts?: HarnessServerPorts | undefined;
|
|
77
83
|
}
|
|
78
84
|
/**
|
|
79
85
|
* Runs one mach test invocation for `paths`, retrying recognized harness
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { join } from 'node:path';
|
|
14
14
|
import { probeDisplaySleepState } from '../core/display-state.js';
|
|
15
|
+
import { createHarnessPortTracker, } from '../core/harness-port-reservation.js';
|
|
15
16
|
import { runMachTestSuite } from '../core/mach.js';
|
|
16
17
|
import { parentExited } from '../core/parent-exit-watchdog.js';
|
|
17
18
|
import { activeRunLogLine, getActiveRunLogPath } from '../core/run-log.js';
|
|
@@ -68,12 +69,17 @@ async function runTestsWithRetriesInner(ctx, paths, env, label) {
|
|
|
68
69
|
let verdict;
|
|
69
70
|
let rawLog;
|
|
70
71
|
let logLine;
|
|
72
|
+
let portTracker;
|
|
71
73
|
for (;;) {
|
|
72
74
|
attempts += 1;
|
|
73
75
|
let tracker = createStartMarkerTracker();
|
|
76
|
+
portTracker = createHarnessPortTracker();
|
|
74
77
|
const raw = rawLogDispatchArgs(ctx, extraArgs);
|
|
75
78
|
logLine = activeRunLogLine();
|
|
76
|
-
const dispatched = await runTrackedMachTestSuite(ctx, kind, paths, raw.args, env,
|
|
79
|
+
const dispatched = await runTrackedMachTestSuite(ctx, kind, paths, raw.args, env, [
|
|
80
|
+
tracker,
|
|
81
|
+
portTracker,
|
|
82
|
+
]);
|
|
77
83
|
result = dispatched;
|
|
78
84
|
// The parent that was reading this run is gone and the watchdog has
|
|
79
85
|
// already killed the harness. Classifying the corpse would call it a
|
|
@@ -108,9 +114,14 @@ async function runTestsWithRetriesInner(ctx, paths, env, label) {
|
|
|
108
114
|
verdict = vetoIncompletePass(classifyHarnessRun(result.exitCode, combined, paths), combined, paths, rawLog !== undefined && rawLog.markers.started.length > 0
|
|
109
115
|
? { markers: rawLog.markers, source: 'raw' }
|
|
110
116
|
: { markers: tracker.markers(), source: 'stream' });
|
|
117
|
+
// A mochitest server that never came up is retried like a crash: no
|
|
118
|
+
// test ran, and a parallel instance's port race is not a result.
|
|
111
119
|
if (verdict.kind !== 'harness-crash' || attempts >= maxAttempts)
|
|
112
120
|
break;
|
|
113
|
-
|
|
121
|
+
const shape = verdict.signature?.verdictReason === 'server-startup'
|
|
122
|
+
? 'Harness server startup failed'
|
|
123
|
+
: 'Harness crash detected';
|
|
124
|
+
warn(`${shape} (${verdict.signature?.reason ?? 'unknown shape'}): ` +
|
|
114
125
|
`${verdict.signature?.line ?? ''}\n` +
|
|
115
126
|
`Retrying (attempt ${attempts + 1} of ${maxAttempts})...`);
|
|
116
127
|
}
|
|
@@ -137,6 +148,7 @@ async function runTestsWithRetriesInner(ctx, paths, env, label) {
|
|
|
137
148
|
run: ctx.repetition ?? 1,
|
|
138
149
|
shard: label,
|
|
139
150
|
logLine,
|
|
151
|
+
shardFiles: paths,
|
|
140
152
|
});
|
|
141
153
|
}
|
|
142
154
|
return {
|
|
@@ -146,8 +158,14 @@ async function runTestsWithRetriesInner(ctx, paths, env, label) {
|
|
|
146
158
|
appdirInjectionAttempted,
|
|
147
159
|
displayState,
|
|
148
160
|
...(rawLog === undefined ? {} : { fileResults: rawLog.results }),
|
|
161
|
+
...serverPortsOf(portTracker),
|
|
149
162
|
};
|
|
150
163
|
}
|
|
164
|
+
/** The `serverPorts` field of an outcome, present only when a port was seen. */
|
|
165
|
+
function serverPortsOf(tracker) {
|
|
166
|
+
const ports = tracker.ports();
|
|
167
|
+
return Object.keys(ports).length > 0 ? { serverPorts: ports } : {};
|
|
168
|
+
}
|
|
151
169
|
/** Sequence for raw-log file names, unique within the process. */
|
|
152
170
|
let rawLogSequence = 0;
|
|
153
171
|
/**
|
|
@@ -170,7 +188,7 @@ function rawLogDispatchArgs(ctx, extraArgs) {
|
|
|
170
188
|
* post-close reaping, the process-group announcement, and the parent-exit
|
|
171
189
|
* watchdog for exactly as long as mach runs.
|
|
172
190
|
*/
|
|
173
|
-
async function runTrackedMachTestSuite(ctx, kind, paths, extraArgs, env,
|
|
191
|
+
async function runTrackedMachTestSuite(ctx, kind, paths, extraArgs, env, trackers) {
|
|
174
192
|
const teardown = createHarnessTeardown({
|
|
175
193
|
objDir: ctx.objDir === undefined ? undefined : join(ctx.engineDir, ctx.objDir),
|
|
176
194
|
pgidFile: ctx.pgidFile,
|
|
@@ -196,7 +214,8 @@ async function runTrackedMachTestSuite(ctx, kind, paths, extraArgs, env, tracker
|
|
|
196
214
|
teardown: {
|
|
197
215
|
...teardown.hooks,
|
|
198
216
|
onOutputChunk: (stream, chunk) => {
|
|
199
|
-
|
|
217
|
+
for (const tracker of trackers)
|
|
218
|
+
tracker.onChunk(stream, chunk);
|
|
200
219
|
teardown.hooks.onOutputChunk?.(stream, chunk);
|
|
201
220
|
},
|
|
202
221
|
},
|
|
@@ -62,13 +62,17 @@ export interface ShardRunOptions {
|
|
|
62
62
|
* `tests-ran-ok`. Otherwise the most structural failure wins. A crashed
|
|
63
63
|
* shard makes the whole run `harness-crash`, then `no-tests`, then plain
|
|
64
64
|
* `test-failures`, so the aggregate `reason=` keeps the classifier-driven
|
|
65
|
-
* contract instead of collapsing every failure to `test-failures`.
|
|
65
|
+
* contract instead of collapsing every failure to `test-failures`. When
|
|
66
|
+
* every crashed shard is a server that never started, the aggregate keeps
|
|
67
|
+
* that signature, so its line says `reason=server-startup` too.
|
|
66
68
|
*/
|
|
67
69
|
export declare function deriveAggregateShardVerdict(shards: ShardOutcome[]): HarnessRunVerdict;
|
|
68
70
|
/**
|
|
69
71
|
* The `FIREFORGE-SHARD:` line for one finished shard: the verdict it would
|
|
70
|
-
* have printed alone, plus `shard=<i>/<n>` and `path=<label
|
|
71
|
-
* `
|
|
72
|
+
* have printed alone, plus `shard=<i>/<n>` and `path=<label>`, and
|
|
73
|
+
* `ports=http:N,ssl:N,ws:N` when the harness reported the mochitest server
|
|
74
|
+
* ports it took. Never a `FIREFORGE-VERDICT:` line, so the run keeps
|
|
75
|
+
* exactly one.
|
|
72
76
|
*/
|
|
73
77
|
export declare function formatShardLine(shard: ShardOutcome, index: number, total: number, failedFiles?: readonly string[]): string;
|
|
74
78
|
/**
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* judged per file from each dispatch's raw log: `shards=` then counts files
|
|
14
14
|
* and `failed-shards=` names the files that failed.
|
|
15
15
|
*/
|
|
16
|
+
import { formatHarnessServerPorts } from '../core/harness-port-reservation.js';
|
|
16
17
|
import { getActiveRunLogPath, writeToActiveRunLog } from '../core/run-log.js';
|
|
17
18
|
import { buildHarnessCrashMessage, formatFireforgeVerdictLine, headedNoOutputTimeoutHint, } from '../core/test-harness-crash.js';
|
|
18
19
|
import { TestFailureError } from '../errors/build.js';
|
|
@@ -27,19 +28,32 @@ const SHARD_STATUS_LABEL = {
|
|
|
27
28
|
'no-tests': 'NO-TESTS',
|
|
28
29
|
'harness-arguments': 'ARGUMENTS',
|
|
29
30
|
};
|
|
31
|
+
/** The summary box's status word; a server that never started is not a crash. */
|
|
32
|
+
function shardStatusLabel(verdict) {
|
|
33
|
+
return verdict.signature?.verdictReason === 'server-startup'
|
|
34
|
+
? 'SERVER'
|
|
35
|
+
: SHARD_STATUS_LABEL[verdict.kind];
|
|
36
|
+
}
|
|
30
37
|
/**
|
|
31
38
|
* Derives the aggregate classification for a sharded run: all-green is
|
|
32
39
|
* `tests-ran-ok`. Otherwise the most structural failure wins. A crashed
|
|
33
40
|
* shard makes the whole run `harness-crash`, then `no-tests`, then plain
|
|
34
41
|
* `test-failures`, so the aggregate `reason=` keeps the classifier-driven
|
|
35
|
-
* contract instead of collapsing every failure to `test-failures`.
|
|
42
|
+
* contract instead of collapsing every failure to `test-failures`. When
|
|
43
|
+
* every crashed shard is a server that never started, the aggregate keeps
|
|
44
|
+
* that signature, so its line says `reason=server-startup` too.
|
|
36
45
|
*/
|
|
37
46
|
export function deriveAggregateShardVerdict(shards) {
|
|
38
47
|
const kinds = new Set(shards.map(({ outcome }) => outcome.verdict.kind));
|
|
39
48
|
if (kinds.has('harness-arguments'))
|
|
40
49
|
return { kind: 'harness-arguments' };
|
|
41
|
-
if (kinds.has('harness-crash'))
|
|
42
|
-
|
|
50
|
+
if (kinds.has('harness-crash')) {
|
|
51
|
+
const crashes = shards.filter(({ outcome }) => outcome.verdict.kind === 'harness-crash');
|
|
52
|
+
const signature = crashes[0]?.outcome.verdict.signature;
|
|
53
|
+
return crashes.every(({ outcome }) => outcome.verdict.signature?.verdictReason === 'server-startup') && signature !== undefined
|
|
54
|
+
? { kind: 'harness-crash', signature }
|
|
55
|
+
: { kind: 'harness-crash' };
|
|
56
|
+
}
|
|
43
57
|
if (kinds.has('no-tests'))
|
|
44
58
|
return { kind: 'no-tests' };
|
|
45
59
|
if (kinds.has('test-failures'))
|
|
@@ -48,15 +62,18 @@ export function deriveAggregateShardVerdict(shards) {
|
|
|
48
62
|
}
|
|
49
63
|
/**
|
|
50
64
|
* The `FIREFORGE-SHARD:` line for one finished shard: the verdict it would
|
|
51
|
-
* have printed alone, plus `shard=<i>/<n>` and `path=<label
|
|
52
|
-
* `
|
|
65
|
+
* have printed alone, plus `shard=<i>/<n>` and `path=<label>`, and
|
|
66
|
+
* `ports=http:N,ssl:N,ws:N` when the harness reported the mochitest server
|
|
67
|
+
* ports it took. Never a `FIREFORGE-VERDICT:` line, so the run keeps
|
|
68
|
+
* exactly one.
|
|
53
69
|
*/
|
|
54
70
|
export function formatShardLine(shard, index, total, failedFiles) {
|
|
55
71
|
const body = formatFireforgeVerdictLine(shard.outcome.verdict).slice('FIREFORGE-VERDICT: '.length);
|
|
56
72
|
const files = failedFiles !== undefined && failedFiles.length > 0
|
|
57
73
|
? ` failed-files=${failedFiles.join(',')}`
|
|
58
74
|
: '';
|
|
59
|
-
|
|
75
|
+
const ports = formatHarnessServerPorts(shard.outcome.serverPorts);
|
|
76
|
+
return `FIREFORGE-SHARD: ${body} shard=${index + 1}/${total} path=${shard.label.replace(/\s+/g, '_')}${files}${ports === undefined ? '' : ` ports=${ports}`}`;
|
|
60
77
|
}
|
|
61
78
|
function writeShardLine(line) {
|
|
62
79
|
process.stdout.write(`${line}\n`);
|
|
@@ -163,7 +180,7 @@ function reportProgress(finished, total, jobs) {
|
|
|
163
180
|
* counts as a whole.
|
|
164
181
|
*/
|
|
165
182
|
function summarizeShards(shards, perFile) {
|
|
166
|
-
const lines = shards.map(({ label, outcome }) => `${
|
|
183
|
+
const lines = shards.map(({ label, outcome }) => `${shardStatusLabel(outcome.verdict).padEnd(8)} ${label}` +
|
|
167
184
|
(outcome.attempts > 1 ? ` (${outcome.attempts} attempts)` : ''));
|
|
168
185
|
const failingShards = shards.filter(({ outcome }) => outcome.verdict.kind !== 'tests-ran-ok');
|
|
169
186
|
let passed = shards.length - failingShards.length;
|
|
@@ -29,9 +29,14 @@ import type { TestOptions } from '../types/commands/index.js';
|
|
|
29
29
|
* stale-content refusal still applies to it unchanged.
|
|
30
30
|
*/
|
|
31
31
|
export declare function enforceStaleBuildGate(projectRoot: string, engineDir: string, options: TestOptions, normalizedPaths: readonly string[],
|
|
32
|
-
/**
|
|
32
|
+
/**
|
|
33
|
+
* `quiet`: refuse only, without the exemption note (the pre-lock pass).
|
|
34
|
+
* `launchablePath`: the engine-relative binary the run launches, which
|
|
35
|
+
* anchors the stale-compiled check (skipped without one).
|
|
36
|
+
*/
|
|
33
37
|
gate?: {
|
|
34
38
|
quiet?: boolean;
|
|
39
|
+
launchablePath?: string | undefined;
|
|
35
40
|
}): Promise<void>;
|
|
36
41
|
/**
|
|
37
42
|
* Compiled-StaticComponents gate: refuses runs whose child process would
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* `src/core/test-stale-check.ts`.
|
|
9
9
|
*/
|
|
10
10
|
import { filterByCarriedFullCoverage } from '../core/coverage-dispatch.js';
|
|
11
|
-
import { checkStaleBuildForTest, checkStaticComponentsStale, findChangedTestManifestsForPaths, findUncoveredRequestPaths, formatStaleBuildWarning, formatStaticComponentsRefusal, formatTestCoverageRefusal, FULL_SUITE_REQUEST, } from '../core/test-stale-check.js';
|
|
11
|
+
import { checkStaleBuildForTest, checkStaticComponentsStale, findChangedTestManifestsForPaths, findCompiledSourcesNewerThanBinary, findUncoveredRequestPaths, formatStaleBuildWarning, formatStaleCompiledRefusal, 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
14
|
import { isProcessAlive } from '../utils/errors.js';
|
|
@@ -35,14 +35,20 @@ import { describePerfStaleImpact } from './test-perf-scope.js';
|
|
|
35
35
|
* stale-content refusal still applies to it unchanged.
|
|
36
36
|
*/
|
|
37
37
|
export async function enforceStaleBuildGate(projectRoot, engineDir, options, normalizedPaths,
|
|
38
|
-
/**
|
|
38
|
+
/**
|
|
39
|
+
* `quiet`: refuse only, without the exemption note (the pre-lock pass).
|
|
40
|
+
* `launchablePath`: the engine-relative binary the run launches, which
|
|
41
|
+
* anchors the stale-compiled check (skipped without one).
|
|
42
|
+
*/
|
|
39
43
|
gate = {}) {
|
|
40
44
|
const stale = await checkStaleBuildForTest(projectRoot, engineDir);
|
|
45
|
+
enforceControlBuiltGate(stale.baseline);
|
|
41
46
|
const dispatchesNoTests = options.doctor === true && normalizedPaths.length === 0;
|
|
42
47
|
if (!dispatchesNoTests) {
|
|
43
48
|
await enforceCoverageGate(engineDir, stale.baseline, normalizedPaths);
|
|
44
49
|
}
|
|
45
50
|
await enforceStaticComponentsGate(engineDir, stale.baseline, options);
|
|
51
|
+
await enforceStaleCompiledGate(engineDir, stale.baseline, options, gate.launchablePath);
|
|
46
52
|
const exempt = stale.exempt ?? [];
|
|
47
53
|
if (!stale.stale && exempt.length > 0 && gate.quiet !== true) {
|
|
48
54
|
info(`Stale-build preflight: ${exempt.length} edit(s) since the last build need no rebuild: ${formatStaleExemptions(exempt)}.`);
|
|
@@ -73,6 +79,38 @@ gate = {}) {
|
|
|
73
79
|
throw new PreflightRefusalError(staleMessage, 'stale-build');
|
|
74
80
|
}
|
|
75
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* Control-built refusal: the recorded build compiled a negative control
|
|
84
|
+
* into the binary, and no build since has recompiled the restored source
|
|
85
|
+
* (the run was killed, or its restore build failed). Every test against
|
|
86
|
+
* that binary tests the mutation. Not bypassable: `--allow-stale-build`
|
|
87
|
+
* accepts old content, not a deliberately broken binary.
|
|
88
|
+
*/
|
|
89
|
+
function enforceControlBuiltGate(baseline) {
|
|
90
|
+
const stamp = baseline?.controlBuild;
|
|
91
|
+
if (stamp === undefined)
|
|
92
|
+
return;
|
|
93
|
+
throw new PreflightRefusalError(`The binary under test was built with the negative control ${stamp.diffFile} applied ` +
|
|
94
|
+
`(${stamp.targets.map((target) => `engine/${target}`).join(', ')}, applied ${stamp.appliedAt}), ` +
|
|
95
|
+
'and no build has recompiled the restored source since. Run "fireforge build" to ' +
|
|
96
|
+
'rebuild the binary without the control, then rerun.', 'control-built');
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Stale-compiled refusal: compiled sources modified after the binary under
|
|
100
|
+
* test was linked. `--allow-stale-build` downgrades it to a warning, as it
|
|
101
|
+
* does the stale-build refusal.
|
|
102
|
+
*/
|
|
103
|
+
async function enforceStaleCompiledGate(engineDir, baseline, options, launchablePath) {
|
|
104
|
+
const result = await findCompiledSourcesNewerThanBinary(engineDir, baseline, launchablePath);
|
|
105
|
+
if (result === undefined || result.paths.length === 0)
|
|
106
|
+
return;
|
|
107
|
+
const message = formatStaleCompiledRefusal(result);
|
|
108
|
+
if (options.allowStaleBuild === true) {
|
|
109
|
+
warn(message);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
throw new PreflightRefusalError(message, 'stale-compiled');
|
|
113
|
+
}
|
|
76
114
|
/**
|
|
77
115
|
* Packaging-coverage refusal. A path outside the recorded scoped list is
|
|
78
116
|
* still covered while its own staging inputs are unchanged against a
|