@hominis/fireforge 0.40.0 → 0.41.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 +79 -0
- package/README.md +16 -10
- package/dist/bin/fireforge.js +25 -4
- package/dist/build-info.json +9 -0
- package/dist/src/cli.js +20 -3
- package/dist/src/commands/bootstrap.js +5 -1
- package/dist/src/commands/discard.js +2 -22
- package/dist/src/commands/doctor/post-rebase-audit.js +27 -4
- package/dist/src/commands/doctor-check-core.d.ts +20 -0
- package/dist/src/commands/doctor-check-core.js +28 -0
- package/dist/src/commands/doctor-external-toolchains.js +2 -0
- package/dist/src/commands/doctor-furnace-manifest-sync.js +144 -46
- package/dist/src/commands/doctor-furnace.js +13 -11
- package/dist/src/commands/doctor-orphaned-harness.js +2 -1
- package/dist/src/commands/doctor.js +2 -2
- package/dist/src/commands/export-all.js +9 -1
- package/dist/src/commands/export-flow.js +13 -3
- package/dist/src/commands/export-placement-conflicts.d.ts +17 -0
- package/dist/src/commands/export-placement-conflicts.js +74 -0
- package/dist/src/commands/export-placement-gate.js +1 -1
- package/dist/src/commands/export-shared.d.ts +8 -1
- package/dist/src/commands/export-shared.js +16 -7
- package/dist/src/commands/export.js +2 -2
- package/dist/src/commands/furnace/apply.js +4 -3
- package/dist/src/commands/furnace/chrome-doc-remove.js +13 -2
- package/dist/src/commands/furnace/chrome-doc.js +13 -2
- package/dist/src/commands/furnace/create-browser-test.d.ts +5 -0
- package/dist/src/commands/furnace/create-browser-test.js +5 -0
- package/dist/src/commands/furnace/create.js +2 -2
- package/dist/src/commands/furnace/diff.js +2 -1
- package/dist/src/commands/furnace/init.js +4 -6
- package/dist/src/commands/furnace/override.js +2 -2
- package/dist/src/commands/furnace/remove-mutations.d.ts +40 -0
- package/dist/src/commands/furnace/remove-mutations.js +171 -0
- package/dist/src/commands/furnace/remove.js +21 -128
- package/dist/src/commands/furnace/rename-browser-test.js +54 -5
- package/dist/src/commands/furnace/scan.js +27 -36
- package/dist/src/commands/furnace/status.js +30 -28
- package/dist/src/commands/furnace/validate.js +25 -18
- package/dist/src/commands/import.js +6 -20
- package/dist/src/commands/lint-per-patch.js +12 -59
- package/dist/src/commands/lint-per-run-checkjs.d.ts +64 -0
- package/dist/src/commands/lint-per-run-checkjs.js +102 -0
- package/dist/src/commands/lint.js +4 -4
- package/dist/src/commands/manifest.js +1 -1
- package/dist/src/commands/package.js +9 -16
- package/dist/src/commands/patch/compact.js +5 -5
- package/dist/src/commands/patch/delete.js +5 -5
- package/dist/src/commands/patch/lint-ignore.js +3 -2
- package/dist/src/commands/patch/move-files-create.js +22 -4
- package/dist/src/commands/patch/move-files-into.js +14 -10
- package/dist/src/commands/patch/move-files.js +6 -5
- package/dist/src/commands/patch/rename.d.ts +2 -2
- package/dist/src/commands/patch/rename.js +151 -50
- package/dist/src/commands/patch/reorder.d.ts +9 -0
- package/dist/src/commands/patch/reorder.js +14 -10
- package/dist/src/commands/patch/split-plan.d.ts +13 -6
- package/dist/src/commands/patch/split-plan.js +22 -12
- package/dist/src/commands/patch/split.d.ts +1 -1
- package/dist/src/commands/patch/split.js +23 -9
- package/dist/src/commands/patch/staged-dependency-validate.d.ts +15 -0
- package/dist/src/commands/patch/staged-dependency-validate.js +49 -0
- package/dist/src/commands/patch/staged-dependency.js +10 -0
- package/dist/src/commands/re-export-adjacent.js +5 -6
- package/dist/src/commands/re-export-bulk-scan.d.ts +13 -0
- package/dist/src/commands/re-export-bulk-scan.js +142 -2
- package/dist/src/commands/re-export-drift.d.ts +74 -0
- package/dist/src/commands/re-export-drift.js +234 -0
- package/dist/src/commands/re-export-files.js +5 -3
- package/dist/src/commands/re-export-lint.d.ts +97 -0
- package/dist/src/commands/re-export-lint.js +210 -0
- package/dist/src/commands/re-export-options.js +8 -0
- package/dist/src/commands/re-export-register.d.ts +18 -0
- package/dist/src/commands/re-export-register.js +61 -0
- package/dist/src/commands/re-export-scan.js +13 -7
- package/dist/src/commands/re-export-single.d.ts +20 -0
- package/dist/src/commands/re-export-single.js +261 -0
- package/dist/src/commands/re-export.d.ts +0 -4
- package/dist/src/commands/re-export.js +113 -268
- package/dist/src/commands/rebase/abort.js +13 -4
- package/dist/src/commands/rebase/continue.js +11 -4
- package/dist/src/commands/rebase/index.js +19 -29
- package/dist/src/commands/rebase/patch-loop.js +12 -0
- package/dist/src/commands/reset.js +4 -23
- package/dist/src/commands/resolve.js +2 -9
- package/dist/src/commands/run.js +11 -21
- package/dist/src/commands/source.js +3 -9
- package/dist/src/commands/status-check.d.ts +13 -0
- package/dist/src/commands/status-check.js +31 -13
- package/dist/src/commands/status-json.d.ts +31 -0
- package/dist/src/commands/status-json.js +76 -0
- package/dist/src/commands/status-output.d.ts +1 -0
- package/dist/src/commands/status-output.js +10 -1
- package/dist/src/commands/status-ownership.d.ts +52 -0
- package/dist/src/commands/status-ownership.js +57 -0
- package/dist/src/commands/status.js +184 -175
- package/dist/src/commands/test-build-phase.d.ts +19 -0
- package/dist/src/commands/test-build-phase.js +168 -0
- package/dist/src/commands/test-diagnose.js +30 -2
- package/dist/src/commands/test-modes.d.ts +9 -1
- package/dist/src/commands/test-modes.js +46 -13
- package/dist/src/commands/test-register.js +59 -27
- package/dist/src/commands/test-run.d.ts +29 -3
- package/dist/src/commands/test-run.js +41 -4
- package/dist/src/commands/test-verdict.d.ts +32 -0
- package/dist/src/commands/test-verdict.js +48 -0
- package/dist/src/commands/test.d.ts +9 -0
- package/dist/src/commands/test.js +100 -123
- package/dist/src/commands/token-coverage.js +4 -9
- package/dist/src/commands/tree.d.ts +3 -0
- package/dist/src/commands/tree.js +171 -25
- package/dist/src/commands/verify.js +2 -2
- package/dist/src/commands/watch.js +22 -21
- package/dist/src/commands/wire.js +1 -1
- package/dist/src/core/ast-utils.d.ts +11 -0
- package/dist/src/core/ast-utils.js +11 -0
- package/dist/src/core/build-audit-platform.js +6 -1
- package/dist/src/core/build-audit-registration.js +2 -0
- package/dist/src/core/build-audit-resolve.d.ts +13 -0
- package/dist/src/core/build-audit-resolve.js +34 -13
- package/dist/src/core/build-audit.js +4 -20
- package/dist/src/core/build-baseline-types.d.ts +29 -8
- package/dist/src/core/build-baseline-types.js +2 -1
- package/dist/src/core/build-baseline.d.ts +10 -2
- package/dist/src/core/build-baseline.js +39 -8
- package/dist/src/core/build-prepare.d.ts +4 -0
- package/dist/src/core/build-prepare.js +7 -1
- package/dist/src/core/config-paths.d.ts +1 -1
- package/dist/src/core/config-paths.js +2 -0
- package/dist/src/core/config-validate-test-toolchains.js +3 -2
- package/dist/src/core/config-validate.js +34 -36
- package/dist/src/core/config.js +16 -3
- package/dist/src/core/coverage-extend.d.ts +89 -0
- package/dist/src/core/coverage-extend.js +188 -0
- package/dist/src/core/destructive.js +3 -1
- package/dist/src/core/discard-baseline.js +18 -4
- package/dist/src/core/engine-precondition.d.ts +35 -0
- package/dist/src/core/engine-precondition.js +50 -0
- package/dist/src/core/engine-session-lock.d.ts +38 -1
- package/dist/src/core/engine-session-lock.js +78 -4
- package/dist/src/core/file-lock.d.ts +7 -0
- package/dist/src/core/file-lock.js +20 -30
- package/dist/src/core/firefox.js +5 -2
- package/dist/src/core/furnace-apply-helpers.d.ts +12 -1
- package/dist/src/core/furnace-apply-helpers.js +12 -3
- package/dist/src/core/furnace-apply-output.js +9 -0
- package/dist/src/core/furnace-apply-overwrite-warn.d.ts +63 -0
- package/dist/src/core/furnace-apply-overwrite-warn.js +123 -0
- package/dist/src/core/furnace-apply-undeploy-actions.d.ts +11 -0
- package/dist/src/core/furnace-apply-undeploy-actions.js +49 -0
- package/dist/src/core/furnace-apply.d.ts +16 -4
- package/dist/src/core/furnace-apply.js +39 -43
- package/dist/src/core/furnace-config.js +2 -0
- package/dist/src/core/furnace-css-fragments.js +2 -0
- package/dist/src/core/furnace-jsconfig.js +14 -7
- package/dist/src/core/furnace-registration-ast.js +8 -8
- package/dist/src/core/furnace-registration-remove.js +6 -3
- package/dist/src/core/furnace-registration.js +9 -12
- package/dist/src/core/furnace-rollback.js +1 -1
- package/dist/src/core/furnace-scanner.js +2 -1
- package/dist/src/core/furnace-stale-export.js +2 -1
- package/dist/src/core/furnace-state-persist.d.ts +4 -6
- package/dist/src/core/furnace-validate-compatibility.js +4 -7
- package/dist/src/core/furnace-validate-helpers.js +33 -1
- package/dist/src/core/furnace-validate-registration.js +16 -0
- package/dist/src/core/furnace-validate.js +4 -2
- package/dist/src/core/git-diff.js +37 -2
- package/dist/src/core/lint-cache.d.ts +1 -1
- package/dist/src/core/lint-cache.js +33 -10
- package/dist/src/core/mach-build-artifacts.d.ts +63 -1
- package/dist/src/core/mach-build-artifacts.js +108 -2
- package/dist/src/core/mach-resource-shim.js +1 -0
- package/dist/src/core/marionette-port.d.ts +16 -0
- package/dist/src/core/marionette-port.js +93 -8
- package/dist/src/core/marionette-preflight.js +6 -5
- package/dist/src/core/patch-apply.d.ts +20 -13
- package/dist/src/core/patch-apply.js +32 -33
- package/dist/src/core/patch-export.js +3 -5
- package/dist/src/core/patch-lint-checkjs.d.ts +25 -3
- package/dist/src/core/patch-lint-checkjs.js +78 -7
- package/dist/src/core/patch-lint-chrome-jsdoc.js +9 -0
- package/dist/src/core/patch-lint-creators.d.ts +30 -0
- package/dist/src/core/patch-lint-creators.js +45 -0
- package/dist/src/core/patch-lint-cross.d.ts +1 -17
- package/dist/src/core/patch-lint-cross.js +16 -32
- package/dist/src/core/patch-lint-jsdoc.d.ts +3 -1
- package/dist/src/core/patch-lint-jsdoc.js +15 -5
- package/dist/src/core/patch-lint-module-registration.d.ts +15 -0
- package/dist/src/core/patch-lint-module-registration.js +88 -0
- package/dist/src/core/patch-lint-mozbuild.js +99 -11
- package/dist/src/core/patch-lint-reexports.d.ts +1 -0
- package/dist/src/core/patch-lint-reexports.js +1 -0
- package/dist/src/core/patch-lint.d.ts +8 -0
- package/dist/src/core/patch-lint.js +84 -6
- package/dist/src/core/patch-lock.d.ts +12 -1
- package/dist/src/core/patch-lock.js +40 -4
- package/dist/src/core/patch-manifest-consistency.js +15 -3
- package/dist/src/core/patch-parse.d.ts +8 -0
- package/dist/src/core/patch-parse.js +10 -0
- package/dist/src/core/patch-policy.d.ts +7 -0
- package/dist/src/core/patch-policy.js +4 -1
- package/dist/src/core/patch-transform.d.ts +10 -0
- package/dist/src/core/patch-transform.js +13 -2
- package/dist/src/core/rebase-session.d.ts +44 -2
- package/dist/src/core/rebase-session.js +114 -17
- package/dist/src/core/register-browser-content.js +2 -1
- package/dist/src/core/register-shared-css.js +2 -1
- package/dist/src/core/smoke-patterns.d.ts +0 -8
- package/dist/src/core/smoke-patterns.js +3 -2
- package/dist/src/core/status-binary.d.ts +55 -0
- package/dist/src/core/status-binary.js +124 -0
- package/dist/src/core/status-classify.d.ts +6 -1
- package/dist/src/core/status-classify.js +101 -73
- package/dist/src/core/test-harness-crash.d.ts +32 -1
- package/dist/src/core/test-harness-crash.js +59 -4
- package/dist/src/core/test-path-scope.js +2 -0
- package/dist/src/core/test-stale-check.js +1 -20
- package/dist/src/core/toolchain-preflight.js +24 -26
- package/dist/src/core/tree-guard.d.ts +1 -1
- package/dist/src/core/tree-guard.js +67 -12
- package/dist/src/core/tree-store.d.ts +86 -10
- package/dist/src/core/tree-store.js +310 -72
- package/dist/src/core/typecheck.js +2 -1
- package/dist/src/core/wire-destroy.js +14 -3
- package/dist/src/core/wire-init.js +37 -40
- package/dist/src/core/wire-subscript.js +11 -3
- package/dist/src/core/wire-utils.js +4 -4
- package/dist/src/core/xpcshell-appdir.js +3 -0
- package/dist/src/errors/base.d.ts +13 -0
- package/dist/src/errors/base.js +13 -0
- package/dist/src/errors/rebase.d.ts +13 -0
- package/dist/src/errors/rebase.js +17 -0
- package/dist/src/types/commands/options.d.ts +86 -1
- package/dist/src/types/commands/patches.d.ts +8 -0
- package/dist/src/types/furnace.d.ts +7 -0
- package/dist/src/utils/build-info.d.ts +31 -0
- package/dist/src/utils/build-info.js +98 -0
- package/dist/src/utils/concurrency.d.ts +8 -0
- package/dist/src/utils/concurrency.js +21 -0
- package/dist/src/utils/errors.d.ts +39 -0
- package/dist/src/utils/errors.js +55 -0
- package/dist/src/utils/fs.js +5 -12
- package/dist/src/utils/logger.d.ts +14 -2
- package/dist/src/utils/logger.js +43 -13
- package/dist/src/utils/options.d.ts +21 -4
- package/dist/src/utils/options.js +26 -6
- package/dist/src/utils/package-root.js +7 -4
- package/dist/src/utils/process-group.js +13 -2
- package/dist/src/utils/stdio-drain.d.ts +28 -0
- package/dist/src/utils/stdio-drain.js +54 -0
- package/dist/src/utils/validation.d.ts +23 -2
- package/dist/src/utils/validation.js +29 -3
- package/package.json +9 -9
|
@@ -1,25 +1,24 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
2
|
import { join, resolve } from 'node:path';
|
|
3
|
-
import { readBuildBaseline, writeBuildBaseline } from '../core/build-baseline.js';
|
|
4
|
-
import { prepareBuildEnvironment } from '../core/build-prepare.js';
|
|
5
3
|
import { getProjectPaths, loadConfig } from '../core/config.js';
|
|
6
4
|
import { assertEngineGenerationUnchanged, snapshotEngineGeneration, } from '../core/engine-session-lock.js';
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
5
|
+
import { hasBuildArtifacts, hasRunnableBundle } from '../core/mach.js';
|
|
6
|
+
import { assertBuildArtifacts } from '../core/mach-build-artifacts.js';
|
|
7
|
+
import { assertMarionettePortAvailable, ensureLaunchableBrowserNotRunning, ensureMarionettePortAvailable, extractForwardedMarionettePort, forwardedMachArgsIncludeMarionetteClient, shouldAutoForwardMarionettePortToMach, } from '../core/marionette-port.js';
|
|
9
8
|
import { formatMarionettePreflightLine, reportMarionettePreflight, runMarionettePreflight, } from '../core/marionette-preflight.js';
|
|
10
|
-
import { buildHarnessCrashMessage } from '../core/test-harness-crash.js';
|
|
11
9
|
import { createPostRebuildFailureContext } from '../core/test-harness-output.js';
|
|
12
10
|
import { analyzeTestPathScopes, formatScopeNotice, } from '../core/test-path-scope.js';
|
|
11
|
+
import { assertObjdirMatchesTreeMarker } from '../core/tree-store.js';
|
|
13
12
|
import { GeneralError } from '../errors/base.js';
|
|
14
|
-
import {
|
|
15
|
-
import { toError } from '../utils/errors.js';
|
|
13
|
+
import { BuildError } from '../errors/build.js';
|
|
16
14
|
import { pathExists } from '../utils/fs.js';
|
|
17
|
-
import { info, intro, outro,
|
|
15
|
+
import { info, intro, outro, success, verbose } from '../utils/logger.js';
|
|
18
16
|
import { stripEnginePrefix } from '../utils/paths.js';
|
|
17
|
+
import { runTestBuildPhase } from './test-build-phase.js';
|
|
19
18
|
import { diagnoseShardOutcome, finalizeSingleRunOutcome } from './test-diagnose.js';
|
|
20
19
|
import { assertPathlessTestMode, assertTestModeCombinations, canaryTimeoutSeconds, classifyBeforeDispatch, reportCanaryOutcome, resolveCanaryPath, } from './test-modes.js';
|
|
21
|
-
import { DEFAULT_HARNESS_RETRIES, runShardedTests, runTestsWithRetries, } from './test-run.js';
|
|
22
|
-
import {
|
|
20
|
+
import { DEFAULT_HARNESS_RETRIES, finalizeShardedOutcome, runShardedTests, runTestsWithRetries, } from './test-run.js';
|
|
21
|
+
import { emitFailVerdict, emitPassVerdict, resetVerdictEmission, verdictEmitted, } from './test-verdict.js';
|
|
23
22
|
async function assertTestPathsExist(engineDir, testPaths) {
|
|
24
23
|
const missingPaths = [];
|
|
25
24
|
for (const testPath of testPaths) {
|
|
@@ -84,57 +83,6 @@ async function resolveLaunchablePathForTests(engineDir, binaryName, objDir) {
|
|
|
84
83
|
}
|
|
85
84
|
return bundleCheck.expectedPath;
|
|
86
85
|
}
|
|
87
|
-
async function runPreTestBuild(projectRoot, paths, projectConfig, harnessRetries, testPackagingCoverage) {
|
|
88
|
-
await withBuildLock(projectRoot, async () => {
|
|
89
|
-
// Pass the previous baseline exactly like `fireforge build` does, so
|
|
90
|
-
// auto-configure runs under the same conditions on both paths. The
|
|
91
|
-
// pre-test build must never invalidate more of the objdir than a plain
|
|
92
|
-
// `mach build faster` would (field incident: a failed pre-test build
|
|
93
|
-
// was followed by a ~64-minute full rebuild where an incremental one
|
|
94
|
-
// should have sufficed).
|
|
95
|
-
const previousBaseline = await readBuildBaseline(projectRoot);
|
|
96
|
-
await prepareBuildEnvironment(projectRoot, paths, projectConfig, { previousBaseline });
|
|
97
|
-
const s = spinner('Running incremental build...');
|
|
98
|
-
// The pre-test build routes through the same protected mach dispatch as
|
|
99
|
-
// `fireforge build` / `build --ui`: in-venv resource-monitor guard plus
|
|
100
|
-
// the uniform recognized-crash retry budget, with a fresh mach process
|
|
101
|
-
// (and fresh guard install) per attempt. prepareBuildEnvironment runs
|
|
102
|
-
// ONCE, outside the retry loop — retries never re-run mach configure.
|
|
103
|
-
const result = await runProtectedMachBuild('faster', paths.engine, {
|
|
104
|
-
retries: harnessRetries,
|
|
105
|
-
onRetry: (signature, nextAttempt, maxAttempts) => {
|
|
106
|
-
s.message(`Pre-test build hit a harness crash (${signature.reason}); ` +
|
|
107
|
-
`retrying (attempt ${nextAttempt} of ${maxAttempts})...`);
|
|
108
|
-
},
|
|
109
|
-
});
|
|
110
|
-
if (result.exitCode === 0) {
|
|
111
|
-
s.stop('Build complete');
|
|
112
|
-
// Same record, same failure tolerance as `fireforge build` /
|
|
113
|
-
// `build --ui` (build.ts): a green pre-test build refreshes the
|
|
114
|
-
// stale-build baseline so a later plain `fireforge test` over the
|
|
115
|
-
// same files — in any invocation shape — is not refused. A failed
|
|
116
|
-
// write never fails the run. The coverage claim is scoped to the
|
|
117
|
-
// requested test paths, since a file-scoped `test --build` only
|
|
118
|
-
// guarantees packaging for those manifests. The previous baseline is
|
|
119
|
-
// passed through so a scoped write carries the static-components
|
|
120
|
-
// anchor forward (a `mach build faster` does not rebake
|
|
121
|
-
// components.conf into the compiled table).
|
|
122
|
-
try {
|
|
123
|
-
await writeBuildBaseline(projectRoot, paths.engine, projectConfig.binaryName, testPackagingCoverage, previousBaseline, testPackagingCoverage === 'full'
|
|
124
|
-
? 'fireforge test --build'
|
|
125
|
-
: `fireforge test --build ${testPackagingCoverage.join(' ')}`);
|
|
126
|
-
}
|
|
127
|
-
catch (baselineError) {
|
|
128
|
-
verbose(`Could not persist build baseline: ${toError(baselineError).message}`);
|
|
129
|
-
}
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
s.error('Pre-test build failed');
|
|
133
|
-
throw new BuildError(result.crashSignature
|
|
134
|
-
? buildHarnessCrashMessage(result.crashSignature, result.attempts, 'mach build faster')
|
|
135
|
-
: 'Pre-test build failed', 'mach build faster');
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
86
|
function logTestSelection(scopes) {
|
|
139
87
|
if (scopes.length > 0) {
|
|
140
88
|
const labels = scopes.map((scope) => scope.isDirectory && scope.testFileCount > 0
|
|
@@ -147,30 +95,6 @@ function logTestSelection(scopes) {
|
|
|
147
95
|
}
|
|
148
96
|
info('');
|
|
149
97
|
}
|
|
150
|
-
/**
|
|
151
|
-
* Validates the build-artifact preconditions for running tests: rejects
|
|
152
|
-
* ambiguous multi-objdir checkouts, platform-mismatched artifacts, and
|
|
153
|
-
* missing/incomplete builds with the actionable message for each. Returns
|
|
154
|
-
* the successful artifact probe for downstream objdir use.
|
|
155
|
-
*/
|
|
156
|
-
async function assertTestBuildArtifacts(engineDir) {
|
|
157
|
-
const buildCheck = await hasBuildArtifacts(engineDir);
|
|
158
|
-
if (buildCheck.ambiguous && buildCheck.objDirs && buildCheck.objDirs.length > 0) {
|
|
159
|
-
throw new AmbiguousBuildArtifactsError(buildCheck.objDirs);
|
|
160
|
-
}
|
|
161
|
-
const mismatchMessage = buildArtifactMismatchMessage(engineDir, buildCheck, 'Tests');
|
|
162
|
-
if (mismatchMessage) {
|
|
163
|
-
throw new GeneralError(mismatchMessage);
|
|
164
|
-
}
|
|
165
|
-
if (!buildCheck.exists) {
|
|
166
|
-
const detail = buildCheck.objDir
|
|
167
|
-
? `Build artifacts incomplete in ${buildCheck.objDir}/`
|
|
168
|
-
: 'No build artifacts found (obj-*/ directory missing)';
|
|
169
|
-
throw new GeneralError(`Tests require a completed build. ${detail}\n\n` +
|
|
170
|
-
"Run 'fireforge build' first, then run 'fireforge test'.");
|
|
171
|
-
}
|
|
172
|
-
return buildCheck;
|
|
173
|
-
}
|
|
174
98
|
/**
|
|
175
99
|
* Runs the `--doctor` marionette handshake probe. With no test paths the
|
|
176
100
|
* probe is the entire command (returns `'stop'` after reporting); with
|
|
@@ -203,13 +127,19 @@ async function runDoctorPreflight(args) {
|
|
|
203
127
|
reportMarionettePreflight(preflight);
|
|
204
128
|
if (!hasTestPaths) {
|
|
205
129
|
if (!preflight.ok) {
|
|
130
|
+
emitFailVerdict('preflight');
|
|
206
131
|
throw new GeneralError('Marionette preflight reported FAIL — see output above.');
|
|
207
132
|
}
|
|
208
133
|
success(directLine);
|
|
134
|
+
// Doctor-only runs end here, so they carry their own verdict line
|
|
135
|
+
// (reason=preflight on failure); with test paths the verdict comes
|
|
136
|
+
// from the actual harness run downstream.
|
|
137
|
+
emitPassVerdict();
|
|
209
138
|
outro('Test completed');
|
|
210
139
|
return 'stop';
|
|
211
140
|
}
|
|
212
141
|
if (!preflight.ok) {
|
|
142
|
+
emitFailVerdict('preflight');
|
|
213
143
|
throw new GeneralError('Marionette preflight reported FAIL — see output above. Aborting before mach test runs.');
|
|
214
144
|
}
|
|
215
145
|
return 'continue';
|
|
@@ -267,6 +197,8 @@ function appendMarionetteForwardingArgs(extraArgs, options, forwardedPort, xpcsh
|
|
|
267
197
|
}
|
|
268
198
|
}
|
|
269
199
|
async function ensureTestMarionettePortAvailable(port, binaryName, options, skip) {
|
|
200
|
+
// Refuse a stale listener before mach surfaces a generic bind failure.
|
|
201
|
+
// This also recognizes a fork-branded browser via binaryName.
|
|
270
202
|
if (skip.xpcshellOnly && !skip.doctor) {
|
|
271
203
|
// xpcshell does not bind the browser Marionette port, so a developer's
|
|
272
204
|
// interactive browser holding 2828 must not kill an xpcshell run
|
|
@@ -288,21 +220,87 @@ async function ensureTestMarionettePortAvailable(port, binaryName, options, skip
|
|
|
288
220
|
}
|
|
289
221
|
await assertMarionettePortAvailable(port, { binaryName });
|
|
290
222
|
}
|
|
223
|
+
async function ensureTestBrowserEnvironment(engineDir, launchablePath, xpcshellOnly, binaryName, options) {
|
|
224
|
+
// A timed-out mochitest can leave the built app alive after its Marionette
|
|
225
|
+
// listener has disappeared. The port probe cannot see that case, but the
|
|
226
|
+
// survivor can still steal focus and wedge every later headed run.
|
|
227
|
+
if (!xpcshellOnly && launchablePath) {
|
|
228
|
+
await ensureLaunchableBrowserNotRunning(join(engineDir, launchablePath), {
|
|
229
|
+
killStaleBrowser: options.killStaleMarionette === true,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
const forwardedPort = options.machArg
|
|
233
|
+
? extractForwardedMarionettePort(options.machArg)
|
|
234
|
+
: undefined;
|
|
235
|
+
const effectivePort = options.marionettePort ?? forwardedPort;
|
|
236
|
+
await ensureTestMarionettePortAvailable(effectivePort, binaryName, options, {
|
|
237
|
+
xpcshellOnly,
|
|
238
|
+
doctor: options.doctor === true,
|
|
239
|
+
});
|
|
240
|
+
return { forwardedPort, effectivePort };
|
|
241
|
+
}
|
|
242
|
+
/** Build-artifact preflight wording for `fireforge test`. */
|
|
243
|
+
const TEST_BUILD_PREFLIGHT = {
|
|
244
|
+
label: 'Tests',
|
|
245
|
+
requirement: 'Tests require a completed build.',
|
|
246
|
+
remediation: "Run 'fireforge build' first, then run 'fireforge test'.",
|
|
247
|
+
requireExisting: true,
|
|
248
|
+
};
|
|
291
249
|
/**
|
|
292
250
|
* Runs the test command to execute mach tests.
|
|
251
|
+
*
|
|
252
|
+
* Owns the run's exactly-one-`FIREFORGE-VERDICT:`-line guarantee: the sink
|
|
253
|
+
* is re-armed at entry, and any failure that reaches this boundary without
|
|
254
|
+
* an inner writer having emitted — the preflight ladder (missing engine or
|
|
255
|
+
* build, config errors, stale-build and port gates, missing paths), and a
|
|
256
|
+
* harness process that failed to start — emits `FAIL reason=preflight`,
|
|
257
|
+
* since no harness classification exists for such a run. Writers closer to
|
|
258
|
+
* the harness (doctor, canary, single, sharded, the engine-generation
|
|
259
|
+
* guard) emit first and win.
|
|
293
260
|
* @param projectRoot - Root directory of the project
|
|
294
261
|
* @param testPaths - Test file or directory paths
|
|
295
262
|
* @param options - Test options
|
|
296
263
|
*/
|
|
297
264
|
export async function testCommand(projectRoot, testPaths, options = {}) {
|
|
298
265
|
intro('FireForge Test');
|
|
266
|
+
resetVerdictEmission();
|
|
267
|
+
try {
|
|
268
|
+
await runTestCommandBody(projectRoot, testPaths, options);
|
|
269
|
+
}
|
|
270
|
+
catch (error) {
|
|
271
|
+
if (!verdictEmitted())
|
|
272
|
+
emitFailVerdict('preflight');
|
|
273
|
+
throw error;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Verifies `engine/` did not change under the run before any PASS verdict
|
|
278
|
+
* may print. On failure the run's verdict is `FAIL reason=inconclusive` —
|
|
279
|
+
* the harness result exists but cannot be trusted (`preflight` would
|
|
280
|
+
* misdescribe a run whose harness already executed) — emitted here so the
|
|
281
|
+
* guard's throw can never leave a stale or missing verdict line behind.
|
|
282
|
+
*/
|
|
283
|
+
async function verifyEngineGenerationOrEmitInconclusive(engineDir, before) {
|
|
284
|
+
try {
|
|
285
|
+
await assertEngineGenerationUnchanged(engineDir, before);
|
|
286
|
+
}
|
|
287
|
+
catch (error) {
|
|
288
|
+
emitFailVerdict('inconclusive');
|
|
289
|
+
throw error;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
async function runTestCommandBody(projectRoot, testPaths, options = {}) {
|
|
299
293
|
const paths = getProjectPaths(projectRoot);
|
|
300
294
|
// Check if engine exists
|
|
301
295
|
if (!(await pathExists(paths.engine))) {
|
|
302
296
|
throw new GeneralError('Firefox source not found. Run "fireforge download" first.');
|
|
303
297
|
}
|
|
304
298
|
assertPathlessTestMode(testPaths, options);
|
|
305
|
-
const buildCheck = await
|
|
299
|
+
const buildCheck = await hasBuildArtifacts(paths.engine);
|
|
300
|
+
assertBuildArtifacts(paths.engine, buildCheck, TEST_BUILD_PREFLIGHT);
|
|
301
|
+
// Inside a verification tree, only the objdir the marker records was
|
|
302
|
+
// proven rewritten-and-reconfigured to the tree; refuse any other.
|
|
303
|
+
await assertObjdirMatchesTreeMarker(projectRoot, buildCheck.objDir);
|
|
306
304
|
// Load the project config once so both the build and the port
|
|
307
305
|
// probe have access to `binaryName` (the port probe uses it to
|
|
308
306
|
// recognise a fork-branded browser holding the Marionette port).
|
|
@@ -326,24 +324,12 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
|
|
|
326
324
|
// stale/coverage refusals keep precedence over missing-path errors.)
|
|
327
325
|
const requestedPaths = canaryPath !== undefined ? [canaryPath] : testPaths;
|
|
328
326
|
const normalizedPaths = requestedPaths.map((p) => stripEnginePrefix(p).trim());
|
|
329
|
-
const { classification, xpcshellOnly } = await classifyBeforeDispatch(paths.engine, normalizedPaths);
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
// so it runs the components.conf gate up-front (the path-less shape
|
|
336
|
-
// refreshes the anchor itself and skips it).
|
|
337
|
-
const coverage = normalizedPaths.length === 0 ? 'full' : normalizedPaths;
|
|
338
|
-
if (normalizedPaths.length > 0) {
|
|
339
|
-
const previousBaseline = await readBuildBaseline(projectRoot);
|
|
340
|
-
await enforceStaticComponentsGate(paths.engine, previousBaseline, options);
|
|
341
|
-
}
|
|
342
|
-
await runPreTestBuild(projectRoot, paths, projectConfig, harnessRetries, coverage);
|
|
343
|
-
info('');
|
|
344
|
-
}
|
|
345
|
-
else {
|
|
346
|
-
await enforceStaleBuildGate(projectRoot, paths.engine, options, normalizedPaths);
|
|
327
|
+
const { classification, xpcshellOnly } = await classifyBeforeDispatch(paths.engine, normalizedPaths, { allowMixed: options.buildOnly === true });
|
|
328
|
+
if (await runTestBuildPhase(projectRoot, paths, projectConfig, harnessRetries, options, {
|
|
329
|
+
classification,
|
|
330
|
+
normalizedPaths,
|
|
331
|
+
})) {
|
|
332
|
+
return;
|
|
347
333
|
}
|
|
348
334
|
// Resolve the effective Marionette port. Operator precedence:
|
|
349
335
|
// 1. `--marionette-port` (first-class option, parsed at the CLI layer)
|
|
@@ -355,22 +341,7 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
|
|
|
355
341
|
// documented `--mach-arg --marionette-port=NNNN` workaround would still
|
|
356
342
|
// hit the wrapper preflight refusing on 2828 before the forwarded arg
|
|
357
343
|
// ever reached mach.
|
|
358
|
-
const forwardedPort = options
|
|
359
|
-
? extractForwardedMarionettePort(options.machArg)
|
|
360
|
-
: undefined;
|
|
361
|
-
const effectivePort = options.marionettePort ?? forwardedPort;
|
|
362
|
-
// Stale-browser probe: an interrupted earlier test run can leave a
|
|
363
|
-
// Firefox/ForgeFresh/fork instance listening on the Marionette
|
|
364
|
-
// control port, which breaks the next mach test launch with a
|
|
365
|
-
// bind error that points nowhere near the real cause. Raise a
|
|
366
|
-
// targeted refusal up front instead of letting mach surface the
|
|
367
|
-
// generic bind failure. 2026-04-21 eval (Finding #20): a stale
|
|
368
|
-
// `-marionette` process from `fresh/` poisoned a later test run in
|
|
369
|
-
// the sibling `mybrowser/` workspace.
|
|
370
|
-
await ensureTestMarionettePortAvailable(effectivePort, projectConfig.binaryName, options, {
|
|
371
|
-
xpcshellOnly,
|
|
372
|
-
doctor: options.doctor === true,
|
|
373
|
-
});
|
|
344
|
+
const { forwardedPort, effectivePort } = await ensureTestBrowserEnvironment(paths.engine, launchablePath, xpcshellOnly, projectConfig.binaryName, options);
|
|
374
345
|
if (options.doctor) {
|
|
375
346
|
const doctorOutcome = await runDoctorPreflight({
|
|
376
347
|
engineDir: paths.engine,
|
|
@@ -464,12 +435,18 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
|
|
|
464
435
|
'(one mach invocation per argument; a directory argument keeps its files in one instance). ' +
|
|
465
436
|
'Cross-argument state is NOT exercised — pass --no-shard for a combined single-instance run.');
|
|
466
437
|
const generationBefore = await snapshotEngineGeneration(paths.engine);
|
|
438
|
+
let summary;
|
|
467
439
|
try {
|
|
468
|
-
await runShardedTests(runCtx, dispatchGroups, (outcome, label) => diagnoseShardOutcome(outcome, label, projectConfig.binaryName, postRebuildContext));
|
|
440
|
+
summary = await runShardedTests(runCtx, dispatchGroups, (outcome, label) => diagnoseShardOutcome(outcome, label, projectConfig.binaryName, postRebuildContext));
|
|
469
441
|
}
|
|
470
442
|
finally {
|
|
471
|
-
|
|
443
|
+
// Runs BEFORE the aggregate verdict below: a mutated engine/ emits
|
|
444
|
+
// `FAIL reason=inconclusive` and throws, so an invalidated run can
|
|
445
|
+
// never print `PASS shards=N/N` first. (A throw here masks an
|
|
446
|
+
// in-flight shard error, as the plain assert always did.)
|
|
447
|
+
await verifyEngineGenerationOrEmitInconclusive(paths.engine, generationBefore);
|
|
472
448
|
}
|
|
449
|
+
finalizeShardedOutcome(summary);
|
|
473
450
|
return;
|
|
474
451
|
}
|
|
475
452
|
const combinedDispatchPaths = dispatchGroups.flatMap((group) => group.paths);
|
|
@@ -482,7 +459,7 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
|
|
|
482
459
|
throw new BuildError('Test process failed to start', 'mach test', error instanceof Error ? error : undefined);
|
|
483
460
|
}
|
|
484
461
|
finally {
|
|
485
|
-
await
|
|
462
|
+
await verifyEngineGenerationOrEmitInconclusive(paths.engine, generationBefore);
|
|
486
463
|
}
|
|
487
464
|
if (canaryPath !== undefined) {
|
|
488
465
|
reportCanaryOutcome(outcome);
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { getProjectPaths, loadConfig } from '../core/config.js';
|
|
4
|
+
import { assertEngineGitReady } from '../core/engine-precondition.js';
|
|
4
5
|
import { furnaceConfigExists, loadFurnaceConfig } from '../core/furnace-config.js';
|
|
5
|
-
import { isGitRepository } from '../core/git.js';
|
|
6
6
|
import { expandUntrackedDirectoryEntries, getWorkingTreeStatus } from '../core/git-status.js';
|
|
7
7
|
import { measureTokenCoverage } from '../core/token-coverage.js';
|
|
8
8
|
import { getTokensCssPath } from '../core/token-manager.js';
|
|
9
|
-
import {
|
|
9
|
+
import { toError } from '../utils/errors.js';
|
|
10
10
|
import { pathExists, readText } from '../utils/fs.js';
|
|
11
11
|
import { info, intro, outro, success, warn } from '../utils/logger.js';
|
|
12
12
|
/**
|
|
@@ -16,12 +16,7 @@ import { info, intro, outro, success, warn } from '../utils/logger.js';
|
|
|
16
16
|
export async function tokenCoverageCommand(projectRoot) {
|
|
17
17
|
intro('Token Coverage');
|
|
18
18
|
const paths = getProjectPaths(projectRoot);
|
|
19
|
-
|
|
20
|
-
throw new GeneralError('Firefox source not found. Run "fireforge download" first.');
|
|
21
|
-
}
|
|
22
|
-
if (!(await isGitRepository(paths.engine))) {
|
|
23
|
-
throw new GeneralError('Engine directory is not a git repository. Run "fireforge download" to initialize.');
|
|
24
|
-
}
|
|
19
|
+
await assertEngineGitReady(paths.engine);
|
|
25
20
|
const config = await loadConfig(projectRoot);
|
|
26
21
|
const tokensCssPath = getTokensCssPath(config.binaryName);
|
|
27
22
|
// Expand collapsed `?? dir/` untracked entries so untracked CSS files
|
|
@@ -153,7 +148,7 @@ async function collectFurnaceCustomCssFiles(projectRoot, engineDir, tokensCssPat
|
|
|
153
148
|
furnaceConfig = await loadFurnaceConfig(projectRoot);
|
|
154
149
|
}
|
|
155
150
|
catch (error) {
|
|
156
|
-
warn(`Could not load furnace.json for token coverage — scanning modified files only (${error.message})`);
|
|
151
|
+
warn(`Could not load furnace.json for token coverage — scanning modified files only (${toError(error).message})`);
|
|
157
152
|
return [];
|
|
158
153
|
}
|
|
159
154
|
const results = [];
|
|
@@ -3,6 +3,8 @@ import type { CommandContext } from '../types/cli.js';
|
|
|
3
3
|
/** Creates a verification tree under `.fireforge/trees/<name>`. */
|
|
4
4
|
export declare function treeCreateCommand(projectRoot: string, name: string, options?: {
|
|
5
5
|
forceCopy?: boolean;
|
|
6
|
+
withObjdir?: boolean;
|
|
7
|
+
waitLockSeconds?: number | undefined;
|
|
6
8
|
}): Promise<void>;
|
|
7
9
|
/** Prints the tree list with staleness against the current primary state. */
|
|
8
10
|
export declare function treeListCommand(projectRoot: string, options?: {
|
|
@@ -11,6 +13,7 @@ export declare function treeListCommand(projectRoot: string, options?: {
|
|
|
11
13
|
/** Removes one tree (or all of them with `--all`). */
|
|
12
14
|
export declare function treeRemoveCommand(projectRoot: string, name: string | undefined, options?: {
|
|
13
15
|
all?: boolean;
|
|
16
|
+
force?: boolean;
|
|
14
17
|
}): Promise<void>;
|
|
15
18
|
/** Registers the tree command group on the CLI program. */
|
|
16
19
|
export declare function registerTree(program: Command, context: CommandContext): void;
|