@hominis/fireforge 0.51.0 → 0.53.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 +46 -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.js +13 -9
- 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-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/status-output.js +5 -1
- 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 +10 -0
- package/dist/src/commands/test-build-phase.js +51 -16
- 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 +83 -13
- package/dist/src/commands/test-multi-verdict.js +1 -0
- package/dist/src/commands/test-orchestrate.d.ts +11 -0
- package/dist/src/commands/test-orchestrate.js +33 -2
- 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 +7 -8
- package/dist/src/commands/test-prelock.js +15 -12
- package/dist/src/commands/test-register.js +2 -2
- 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 +47 -2
- package/dist/src/commands/test-verdict.d.ts +5 -3
- package/dist/src/commands/test.js +14 -1
- 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 +37 -3
- package/dist/src/core/build-baseline-validate.d.ts +30 -1
- package/dist/src/core/build-baseline-validate.js +134 -33
- package/dist/src/core/build-baseline.d.ts +8 -2
- package/dist/src/core/build-baseline.js +32 -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-config-validate-exceptions.d.ts +4 -3
- package/dist/src/core/furnace-config-validate-exceptions.js +26 -6
- 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/furnace-validate-accessibility.d.ts +1 -1
- package/dist/src/core/furnace-validate-accessibility.js +22 -11
- package/dist/src/core/furnace-validate-exceptions.d.ts +2 -1
- package/dist/src/core/furnace-validate-exceptions.js +31 -13
- 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 +22 -3
- package/dist/src/core/negative-control-compiled.js +41 -5
- 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-run-completeness.d.ts +12 -5
- package/dist/src/core/test-run-completeness.js +15 -8
- package/dist/src/core/test-stale-check.d.ts +48 -1
- package/dist/src/core/test-stale-check.js +97 -2
- 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.d.ts +2 -1
- package/dist/src/core/typecheck-shim.js +29 -5
- 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 +26 -3
- package/dist/src/types/config.d.ts +5 -7
- package/dist/src/types/furnace.d.ts +9 -0
- 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 +40 -26
- package/docs/exit-codes.md +8 -11
- package/docs/furnace.md +21 -3
- package/docs/lifecycle-invariants.md +36 -45
- package/docs/machine-output.md +43 -22
- package/docs/patch-workflow.md +129 -24
- package/docs/run-logs.md +8 -9
- package/docs/testing.md +118 -44
- package/package.json +1 -1
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* The restore build after a compiled negative control.
|
|
4
|
+
*
|
|
5
|
+
* A control on compiled source is applied before a full pre-test build, so
|
|
6
|
+
* the binary under test carries it. Restoring the source byte-exact after
|
|
7
|
+
* the run does not unbuild it: the objdir binary keeps the mutation until a
|
|
8
|
+
* build recompiles the original. Every later `fireforge test` would then
|
|
9
|
+
* test the control. The orchestrator therefore runs one more full
|
|
10
|
+
* `mach build` right after a successful restore, still under the same
|
|
11
|
+
* engine-session hold, and its baseline drops the `controlBuild` stamp the
|
|
12
|
+
* control's build recorded. When that build fails, or the run never gets
|
|
13
|
+
* here (killed), the stamp stays and the next `fireforge test` refuses with
|
|
14
|
+
* `note=control-built` until `fireforge build` replaces it.
|
|
15
|
+
*/
|
|
16
|
+
import { readBuildBaseline, writeBuildBaseline } from '../core/build-baseline.js';
|
|
17
|
+
import { getProjectPaths, loadConfig } from '../core/config.js';
|
|
18
|
+
import { runProtectedMachBuild, withBuildLock } from '../core/mach.js';
|
|
19
|
+
import { toError } from '../utils/errors.js';
|
|
20
|
+
import { info, notice, verbose, warn } from '../utils/logger.js';
|
|
21
|
+
/**
|
|
22
|
+
* Whether the recorded baseline is the one this run's control build wrote:
|
|
23
|
+
* only then did a build compile the control in (a failed control build
|
|
24
|
+
* writes no baseline, and its restored source's fresh mtime trips the
|
|
25
|
+
* stale-compiled check instead).
|
|
26
|
+
*
|
|
27
|
+
* @param projectRoot - Project root
|
|
28
|
+
* @param stamp - The stamp this run's control build recorded
|
|
29
|
+
*/
|
|
30
|
+
export async function controlBuildRecorded(projectRoot, stamp) {
|
|
31
|
+
const baseline = await readBuildBaseline(projectRoot);
|
|
32
|
+
return (baseline?.controlBuild?.appliedAt === stamp.appliedAt &&
|
|
33
|
+
baseline.controlBuild.diffFile === stamp.diffFile);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Rebuilds the binary after a compiled control was restored: a full
|
|
37
|
+
* protected `mach build` under the build lock, then a baseline without the
|
|
38
|
+
* stamp. The previous baseline's packaging-coverage claim is carried
|
|
39
|
+
* forward unchanged, since recompiling the restored source stages no tests.
|
|
40
|
+
* Never throws: a failure leaves the stamp in place, which is the refusal
|
|
41
|
+
* the next run needs.
|
|
42
|
+
*
|
|
43
|
+
* @param projectRoot - Project root
|
|
44
|
+
* @param stamp - The stamp this run's control build recorded
|
|
45
|
+
* @param harnessRetries - Recognized-crash retry budget for the build
|
|
46
|
+
*/
|
|
47
|
+
export async function runControlRestoreBuild(projectRoot, stamp, harnessRetries) {
|
|
48
|
+
const paths = getProjectPaths(projectRoot);
|
|
49
|
+
notice(`Rebuilding after the compiled negative control (${stamp.targets.map((target) => `engine/${target}`).join(', ')}): ` +
|
|
50
|
+
'the binary still carries the mutation until a full mach build recompiles the restored source.');
|
|
51
|
+
try {
|
|
52
|
+
const config = await loadConfig(projectRoot);
|
|
53
|
+
return await withBuildLock(projectRoot, async () => {
|
|
54
|
+
const previousBaseline = await readBuildBaseline(projectRoot);
|
|
55
|
+
const result = await runProtectedMachBuild('full', paths.engine, {
|
|
56
|
+
...(harnessRetries === undefined ? {} : { retries: harnessRetries }),
|
|
57
|
+
});
|
|
58
|
+
if (result.exitCode !== 0) {
|
|
59
|
+
warn('The restore build after the negative control failed, so the binary still carries ' +
|
|
60
|
+
'the control. The next "fireforge test" refuses (note=control-built) until ' +
|
|
61
|
+
'"fireforge build" succeeds.');
|
|
62
|
+
return 'failed';
|
|
63
|
+
}
|
|
64
|
+
await writeBuildBaseline({
|
|
65
|
+
projectRoot,
|
|
66
|
+
engineDir: paths.engine,
|
|
67
|
+
binaryName: config.binaryName,
|
|
68
|
+
testPackagingCoverage: previousBaseline?.testPackagingCoverage,
|
|
69
|
+
testCoverageRecords: previousBaseline?.testCoverageRecords,
|
|
70
|
+
fullCoverageAnchor: previousBaseline?.fullCoverageAnchor,
|
|
71
|
+
previousBaseline,
|
|
72
|
+
recordedBy: 'fireforge test --negative-control (restore build)',
|
|
73
|
+
staticComponentsHandling: 'refresh',
|
|
74
|
+
buildKind: 'full',
|
|
75
|
+
});
|
|
76
|
+
info('Restore build complete: the binary no longer carries the negative control.');
|
|
77
|
+
return 'done';
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
verbose(`Restore build after the negative control failed: ${toError(error).message}`);
|
|
82
|
+
warn(`The restore build after the negative control could not run (${toError(error).message}). ` +
|
|
83
|
+
'The next "fireforge test" refuses (note=control-built) until "fireforge build" succeeds.');
|
|
84
|
+
return 'failed';
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -6,11 +6,14 @@
|
|
|
6
6
|
* `--jobs` parallel harness instances.
|
|
7
7
|
*/
|
|
8
8
|
import { createServer } from 'node:net';
|
|
9
|
+
import { join } from 'node:path';
|
|
9
10
|
import { assertEngineGenerationUnchanged, snapshotEngineGeneration, } from '../core/engine-session-lock.js';
|
|
10
|
-
import { autoPortsRequested } from '../core/harness-port-fallback.js';
|
|
11
|
+
import { autoPortsRequested, detectHarnessPortFallback } from '../core/harness-port-fallback.js';
|
|
12
|
+
import { reserveHarnessServerPorts, } from '../core/harness-port-reservation.js';
|
|
11
13
|
import { extractForwardedMarionettePort } from '../core/marionette-port.js';
|
|
12
14
|
import { GeneralError, InvalidArgumentError } from '../errors/base.js';
|
|
13
15
|
import { BuildError } from '../errors/build.js';
|
|
16
|
+
import { ensureDir } from '../utils/fs.js';
|
|
14
17
|
import { notice } from '../utils/logger.js';
|
|
15
18
|
import { diagnoseShardOutcome, finalizeSingleRunOutcome } from './test-diagnose.js';
|
|
16
19
|
import { shuffleTestGroups } from './test-harness-env.js';
|
|
@@ -85,27 +88,92 @@ async function allocateLoopbackPort() {
|
|
|
85
88
|
});
|
|
86
89
|
});
|
|
87
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* The mochitest harness writes the dispatched test list to
|
|
93
|
+
* `_tests/testing/mochitest/tests.json` and the browser fetches it from
|
|
94
|
+
* there, so instances sharing the default name read each other's list (and
|
|
95
|
+
* one instance's cleanup removes it under the other). Each instance names
|
|
96
|
+
* its own file.
|
|
97
|
+
*/
|
|
98
|
+
function testRunManifestArg(index) {
|
|
99
|
+
return `--testrun-manifest-file=fireforge-tests-${process.pid}-job${index + 1}.json`;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The `MOZ_UPLOAD_DIR` the caller gave this run (its own env first, then
|
|
103
|
+
* the process environment), or undefined when there is none.
|
|
104
|
+
*/
|
|
105
|
+
function uploadDirBase(base) {
|
|
106
|
+
const dir = base.env?.['MOZ_UPLOAD_DIR'] ?? process.env['MOZ_UPLOAD_DIR'];
|
|
107
|
+
return dir === undefined || dir === '' ? undefined : dir;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* `--jobs` instance `index`'s own upload directory, `<base>/job-<n>`. The
|
|
111
|
+
* harness writes per-run artifacts there under fixed names (the
|
|
112
|
+
* `modifiedPrefs.json` pref census among them), so instances sharing the
|
|
113
|
+
* caller's directory replace each other's files.
|
|
114
|
+
*/
|
|
115
|
+
function instanceUploadDir(uploadBase, index) {
|
|
116
|
+
return join(uploadBase, `job-${index + 1}`);
|
|
117
|
+
}
|
|
88
118
|
/**
|
|
89
119
|
* The context for `--jobs` instance `index` (0-based): its own output
|
|
90
|
-
* prefix,
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
120
|
+
* prefix, upload directory (when the caller set `MOZ_UPLOAD_DIR`) and
|
|
121
|
+
* test-list manifest, and from the second instance on its own Marionette
|
|
122
|
+
* port, so browsers started at the same moment never race for 2828. The
|
|
123
|
+
* mochitest server ports are moved by the engine harness's busy-port
|
|
124
|
+
* fallback, which `--jobs` requires (auto ports), while
|
|
125
|
+
* {@link reserveHarnessServerPorts} holds the defaults.
|
|
94
126
|
*/
|
|
95
127
|
async function instanceContext(base, index, xpcshellOnly) {
|
|
96
|
-
|
|
97
|
-
|
|
128
|
+
let ctx = { ...base, outputPrefix: `[job ${index + 1}] ` };
|
|
129
|
+
const uploadBase = uploadDirBase(base);
|
|
130
|
+
if (uploadBase !== undefined) {
|
|
131
|
+
const uploadDir = instanceUploadDir(uploadBase, index);
|
|
132
|
+
await ensureDir(uploadDir);
|
|
133
|
+
ctx = { ...ctx, env: { ...base.env, MOZ_UPLOAD_DIR: uploadDir } };
|
|
134
|
+
}
|
|
135
|
+
if (xpcshellOnly)
|
|
98
136
|
return ctx;
|
|
137
|
+
const own = [...base.baseExtraArgs, testRunManifestArg(index)];
|
|
138
|
+
if (index === 0)
|
|
139
|
+
return { ...ctx, baseExtraArgs: own };
|
|
99
140
|
const port = await allocateLoopbackPort();
|
|
100
141
|
return {
|
|
101
142
|
...ctx,
|
|
102
|
-
baseExtraArgs: [
|
|
103
|
-
...base.baseExtraArgs,
|
|
104
|
-
`--setpref=marionette.port=${port}`,
|
|
105
|
-
`--marionette=127.0.0.1:${port}`,
|
|
106
|
-
],
|
|
143
|
+
baseExtraArgs: [...own, `--setpref=marionette.port=${port}`, `--marionette=127.0.0.1:${port}`],
|
|
107
144
|
};
|
|
108
145
|
}
|
|
146
|
+
/**
|
|
147
|
+
* Stamps a `--jobs` run's verdict attributes: `jobs=<n>`, and
|
|
148
|
+
* `upload-dirs=<dir>,...` naming each instance's upload directory, so a
|
|
149
|
+
* caller can merge per-instance artifacts.
|
|
150
|
+
*/
|
|
151
|
+
function stampParallelAttributes(runCtx, jobs, shards) {
|
|
152
|
+
setVerdictRunAttribute('jobs', String(jobs));
|
|
153
|
+
const uploadBase = uploadDirBase(runCtx);
|
|
154
|
+
if (uploadBase === undefined)
|
|
155
|
+
return;
|
|
156
|
+
const instances = Math.max(1, Math.min(jobs, shards));
|
|
157
|
+
setVerdictRunAttribute('upload-dirs', Array.from({ length: instances }, (_, index) => instanceUploadDir(uploadBase, index)).join(','));
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Holds the mochitest server ports for a browser run on more than one
|
|
161
|
+
* instance (see `harness-port-reservation.ts`). Only on an engine whose
|
|
162
|
+
* harness falls back from a busy server port: without that fallback a
|
|
163
|
+
* held port would fail every instance instead of moving it.
|
|
164
|
+
*/
|
|
165
|
+
async function reserveParallelServerPorts(engineDir, instances, xpcshellOnly) {
|
|
166
|
+
if (instances < 2 || xpcshellOnly)
|
|
167
|
+
return undefined;
|
|
168
|
+
if (!(await detectHarnessPortFallback(engineDir)).serverPorts)
|
|
169
|
+
return undefined;
|
|
170
|
+
const reservation = await reserveHarnessServerPorts();
|
|
171
|
+
if (reservation.held.length > 0) {
|
|
172
|
+
notice(`Holding mochitest server port(s) ${reservation.held.join(', ')} for the parallel run, so ` +
|
|
173
|
+
"each instance's harness moves to server ports of its own.");
|
|
174
|
+
}
|
|
175
|
+
return reservation;
|
|
176
|
+
}
|
|
109
177
|
/** The shards to run, or undefined for one combined invocation. */
|
|
110
178
|
function planShards(inputs) {
|
|
111
179
|
const { dispatchGroups, options, canaryPath, shuffleSeed } = inputs;
|
|
@@ -143,9 +211,10 @@ export async function dispatchTests(inputs) {
|
|
|
143
211
|
notice(describeSharding(shards.length, options, jobs));
|
|
144
212
|
const base = jobs > 1 ? await instanceContext(runCtx, 0, inputs.xpcshellOnly) : runCtx;
|
|
145
213
|
if (jobs > 1)
|
|
146
|
-
|
|
214
|
+
stampParallelAttributes(runCtx, jobs, shards.length);
|
|
147
215
|
const generationBefore = await snapshotEngineGeneration(engineDir);
|
|
148
216
|
let summary;
|
|
217
|
+
const reservation = await reserveParallelServerPorts(engineDir, Math.min(jobs, shards.length), inputs.xpcshellOnly);
|
|
149
218
|
try {
|
|
150
219
|
summary = await runShardedTests(base, shards, (outcome, label) => diagnoseShardOutcome(outcome, label, projectConfig.binaryName, inputs.postRebuildContext), {
|
|
151
220
|
jobs,
|
|
@@ -154,6 +223,7 @@ export async function dispatchTests(inputs) {
|
|
|
154
223
|
});
|
|
155
224
|
}
|
|
156
225
|
finally {
|
|
226
|
+
await reservation?.release();
|
|
157
227
|
// Runs before the aggregate verdict below: a mutated engine/ emits
|
|
158
228
|
// `FAIL reason=inconclusive` and throws, so an invalidated run can
|
|
159
229
|
// never print `PASS shards=N/N` first. (A throw here masks an
|
|
@@ -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 {
|
|
@@ -30,6 +31,16 @@ export interface TestRunHooks {
|
|
|
30
31
|
* source, which only reaches the binary when the build compiles it.
|
|
31
32
|
*/
|
|
32
33
|
beforeBuild?: () => Promise<void>;
|
|
34
|
+
/**
|
|
35
|
+
* Why the pre-test build must be a full `mach build`: `mach build faster`
|
|
36
|
+
* compiles nothing, so a compiled control's binary needs the full one.
|
|
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;
|
|
33
44
|
/** `--repeat` run this sub-run is (1-based), for `--results`. */
|
|
34
45
|
repetition?: number;
|
|
35
46
|
/** Where dispatches write their mozlog raw logs (`--results`, `--shard-size`). */
|
|
@@ -17,13 +17,14 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import { getProjectPaths } from '../core/config.js';
|
|
19
19
|
import { applyNegativeControl, planNegativeControl, recoverPendingNegativeControl, restoreNegativeControl, } from '../core/negative-control.js';
|
|
20
|
-
import { formatCompiledControlRefusal } from '../core/negative-control-compiled.js';
|
|
20
|
+
import { formatCompiledControlBuildNotice, formatCompiledControlRefusal, } from '../core/negative-control-compiled.js';
|
|
21
21
|
import { getActiveRunLogPath, writeToActiveRunLog } from '../core/run-log.js';
|
|
22
22
|
import { GeneralError, InvalidArgumentError, PreflightRefusalError } from '../errors/base.js';
|
|
23
23
|
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';
|
|
29
30
|
import { emitAggregateVerdict, emitFailVerdict, setVerdictCapture, setVerdictRunAttribute, verdictEmitted, } from './test-verdict.js';
|
|
@@ -105,6 +106,26 @@ function controlHook(paths, plan, slot) {
|
|
|
105
106
|
'it stays in place until this run has finished.');
|
|
106
107
|
};
|
|
107
108
|
}
|
|
109
|
+
/** The stamp a compiled control's pre-test build records on its baseline. */
|
|
110
|
+
function compiledControlStamp(options, compiled) {
|
|
111
|
+
if (compiled === undefined || options.negativeControl === undefined)
|
|
112
|
+
return undefined;
|
|
113
|
+
return {
|
|
114
|
+
diffFile: options.negativeControl,
|
|
115
|
+
targets: [...compiled],
|
|
116
|
+
appliedAt: new Date().toISOString(),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* After a compiled control was restored, rebuilds the binary that still
|
|
121
|
+
* carries it, when this run's control build recorded its stamp, and
|
|
122
|
+
* stamps `control-restore-build=done|failed` on the verdict.
|
|
123
|
+
*/
|
|
124
|
+
async function rebuildAfterCompiledControl(projectRoot, stamp, options) {
|
|
125
|
+
if (stamp === undefined || !(await controlBuildRecorded(projectRoot, stamp)))
|
|
126
|
+
return;
|
|
127
|
+
setVerdictRunAttribute('control-restore-build', await runControlRestoreBuild(projectRoot, stamp, options.harnessRetries));
|
|
128
|
+
}
|
|
108
129
|
/**
|
|
109
130
|
* Throws the invocation's error after the aggregate verdict, or returns
|
|
110
131
|
* when the invocation succeeded (all runs green, or a control caught).
|
|
@@ -172,6 +193,7 @@ export async function orchestrateTestRuns(projectRoot, testPaths, options, body,
|
|
|
172
193
|
}
|
|
173
194
|
setVerdictRunAttribute('control-build', 'compiled');
|
|
174
195
|
}
|
|
196
|
+
const controlStamp = compiledControlStamp(options, compiled);
|
|
175
197
|
const results = [];
|
|
176
198
|
let restoreError;
|
|
177
199
|
for (const [index, run] of runs.entries()) {
|
|
@@ -187,7 +209,11 @@ export async function orchestrateTestRuns(projectRoot, testPaths, options, body,
|
|
|
187
209
|
...(plan === undefined
|
|
188
210
|
? {}
|
|
189
211
|
: compiledControl && index === 0
|
|
190
|
-
? {
|
|
212
|
+
? {
|
|
213
|
+
beforeBuild: controlHook(controlPaths, plan, slot),
|
|
214
|
+
fullBuildReason: formatCompiledControlBuildNotice(compiled),
|
|
215
|
+
...(controlStamp === undefined ? {} : { controlBuild: controlStamp }),
|
|
216
|
+
}
|
|
191
217
|
: { beforeDispatch: controlHook(controlPaths, plan, slot) }),
|
|
192
218
|
});
|
|
193
219
|
}
|
|
@@ -231,6 +257,11 @@ export async function orchestrateTestRuns(projectRoot, testPaths, options, body,
|
|
|
231
257
|
emitFailVerdict('inconclusive', 'negative-control-restore');
|
|
232
258
|
throw toError(restoreError);
|
|
233
259
|
}
|
|
260
|
+
// The source is back byte-exact, but the binary still carries the
|
|
261
|
+
// compiled control: rebuild it under this same hold (see
|
|
262
|
+
// test-control-restore.ts). Only after every run, since a later
|
|
263
|
+
// `--repeat` run tests the same control-built binary.
|
|
264
|
+
await rebuildAfterCompiledControl(projectRoot, controlStamp, options);
|
|
234
265
|
const repetitions = options.repeat ?? 1;
|
|
235
266
|
const control = plan === undefined
|
|
236
267
|
? 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
|
|
@@ -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) {
|
|
@@ -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]',
|
|
@@ -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
|
/**
|