@hominis/fireforge 0.49.2 → 0.50.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 +40 -1
- package/README.md +1 -1
- package/dist/bin/fireforge.js +6 -0
- package/dist/build-info.json +4 -4
- package/dist/src/commands/build-prelock.d.ts +20 -0
- package/dist/src/commands/build-prelock.js +50 -0
- package/dist/src/commands/build.d.ts +3 -1
- package/dist/src/commands/build.js +13 -6
- package/dist/src/commands/check-report.d.ts +28 -0
- package/dist/src/commands/check-report.js +78 -0
- package/dist/src/commands/check-run.d.ts +28 -0
- package/dist/src/commands/check-run.js +104 -0
- package/dist/src/commands/check-steps.d.ts +67 -0
- package/dist/src/commands/check-steps.js +175 -0
- package/dist/src/commands/check.d.ts +32 -0
- package/dist/src/commands/check.js +160 -0
- package/dist/src/commands/config.js +5 -6
- package/dist/src/commands/doctor-engine-eslint.js +6 -1
- package/dist/src/commands/doctor-source-pin.js +4 -1
- package/dist/src/commands/export-adopt-check.d.ts +38 -0
- package/dist/src/commands/export-adopt-check.js +98 -0
- package/dist/src/commands/export-flow.js +29 -14
- package/dist/src/commands/export-placement-gate.d.ts +6 -2
- package/dist/src/commands/export-shared.d.ts +2 -1
- package/dist/src/commands/export-staged-registration.d.ts +46 -0
- package/dist/src/commands/export-staged-registration.js +91 -0
- package/dist/src/commands/forward-import-remedy.d.ts +32 -0
- package/dist/src/commands/forward-import-remedy.js +63 -0
- package/dist/src/commands/furnace/apply.js +3 -3
- package/dist/src/commands/furnace/chrome-doc-remove.d.ts +2 -1
- package/dist/src/commands/furnace/chrome-doc-remove.js +2 -2
- package/dist/src/commands/furnace/chrome-doc.d.ts +2 -1
- package/dist/src/commands/furnace/chrome-doc.js +2 -2
- package/dist/src/commands/furnace/create.js +2 -2
- package/dist/src/commands/furnace/deploy-named.d.ts +48 -0
- package/dist/src/commands/furnace/deploy-named.js +107 -0
- package/dist/src/commands/furnace/deploy.d.ts +3 -2
- package/dist/src/commands/furnace/deploy.js +50 -86
- package/dist/src/commands/furnace/diff.js +26 -3
- package/dist/src/commands/furnace/index.js +66 -41
- package/dist/src/commands/furnace/init.js +13 -6
- package/dist/src/commands/furnace/override.js +4 -2
- package/dist/src/commands/furnace/preview.js +4 -3
- package/dist/src/commands/furnace/refresh.js +2 -2
- package/dist/src/commands/furnace/remove.js +4 -3
- package/dist/src/commands/furnace/rename-dry-run.d.ts +29 -0
- package/dist/src/commands/furnace/rename-dry-run.js +127 -0
- package/dist/src/commands/furnace/rename-helpers.d.ts +7 -0
- package/dist/src/commands/furnace/rename-helpers.js +15 -1
- package/dist/src/commands/furnace/rename-registrations.d.ts +41 -0
- package/dist/src/commands/furnace/rename-registrations.js +93 -0
- package/dist/src/commands/furnace/rename.d.ts +2 -1
- package/dist/src/commands/furnace/rename.js +28 -66
- package/dist/src/commands/furnace/scan.d.ts +9 -4
- package/dist/src/commands/furnace/scan.js +11 -9
- package/dist/src/commands/furnace/sync.js +3 -2
- package/dist/src/commands/furnace/validate-report.d.ts +45 -0
- package/dist/src/commands/furnace/validate-report.js +91 -0
- package/dist/src/commands/furnace/validate.d.ts +1 -1
- package/dist/src/commands/furnace/validate.js +127 -114
- package/dist/src/commands/furnace/validation-output.js +14 -3
- package/dist/src/commands/lint-per-patch.js +31 -60
- package/dist/src/commands/lint-report.d.ts +8 -4
- package/dist/src/commands/lint-report.js +15 -5
- package/dist/src/commands/lint.js +15 -34
- package/dist/src/commands/manifest.js +2 -0
- package/dist/src/commands/patch/delete-gap-notice.js +3 -1
- package/dist/src/commands/patch/delete.d.ts +1 -1
- package/dist/src/commands/patch/delete.js +13 -98
- package/dist/src/commands/patch/index.d.ts +1 -1
- package/dist/src/commands/patch/index.js +4 -2
- package/dist/src/commands/patch/lint-ignore.d.ts +5 -1
- package/dist/src/commands/patch/lint-ignore.js +65 -28
- package/dist/src/commands/patch/move-files-create.js +26 -17
- package/dist/src/commands/patch/move-files-into.js +5 -3
- package/dist/src/commands/patch/move-files.js +3 -1
- package/dist/src/commands/patch/removal-dependents.d.ts +63 -0
- package/dist/src/commands/patch/removal-dependents.js +133 -0
- package/dist/src/commands/patch/rename.js +7 -0
- package/dist/src/commands/patch/retire.d.ts +34 -0
- package/dist/src/commands/patch/retire.js +128 -0
- package/dist/src/commands/patch/split-plan.d.ts +7 -7
- package/dist/src/commands/patch/split-plan.js +45 -28
- package/dist/src/commands/patch/split-staged.d.ts +108 -0
- package/dist/src/commands/patch/split-staged.js +202 -0
- package/dist/src/commands/patch/split.d.ts +5 -0
- package/dist/src/commands/patch/split.js +51 -15
- package/dist/src/commands/patch/staged-dependency-batch.d.ts +48 -0
- package/dist/src/commands/patch/staged-dependency-batch.js +176 -0
- package/dist/src/commands/patch/staged-dependency-entries.d.ts +36 -0
- package/dist/src/commands/patch/staged-dependency-entries.js +52 -0
- package/dist/src/commands/patch/staged-dependency-from.d.ts +14 -0
- package/dist/src/commands/patch/staged-dependency-from.js +108 -0
- package/dist/src/commands/patch/staged-dependency.js +14 -41
- package/dist/src/commands/re-export-adjacent-registrations.d.ts +20 -0
- package/dist/src/commands/re-export-adjacent-registrations.js +75 -0
- package/dist/src/commands/re-export-adjacent.d.ts +45 -2
- package/dist/src/commands/re-export-adjacent.js +156 -37
- package/dist/src/commands/re-export-drift.d.ts +36 -8
- package/dist/src/commands/re-export-drift.js +73 -49
- package/dist/src/commands/re-export-files.js +11 -8
- package/dist/src/commands/re-export-lint.d.ts +2 -1
- package/dist/src/commands/re-export-lint.js +23 -10
- package/dist/src/commands/re-export-options.js +45 -4
- package/dist/src/commands/re-export-preflight.d.ts +23 -0
- package/dist/src/commands/re-export-preflight.js +160 -0
- package/dist/src/commands/re-export-register.js +42 -17
- package/dist/src/commands/re-export-removed.d.ts +12 -1
- package/dist/src/commands/re-export-removed.js +41 -11
- package/dist/src/commands/re-export-scan-owner.d.ts +44 -0
- package/dist/src/commands/re-export-scan-owner.js +116 -0
- package/dist/src/commands/re-export-scan.d.ts +17 -9
- package/dist/src/commands/re-export-scan.js +56 -23
- package/dist/src/commands/re-export-single.js +22 -6
- package/dist/src/commands/re-export-summary.d.ts +28 -0
- package/dist/src/commands/re-export-summary.js +36 -0
- package/dist/src/commands/re-export.js +37 -24
- package/dist/src/commands/rebase/index.js +17 -17
- package/dist/src/commands/run.js +16 -2
- package/dist/src/commands/status-check.d.ts +55 -16
- package/dist/src/commands/status-check.js +92 -37
- package/dist/src/commands/status-json.d.ts +22 -9
- package/dist/src/commands/status-json.js +47 -32
- package/dist/src/commands/status-output.js +3 -1
- package/dist/src/commands/status-scope.d.ts +37 -0
- package/dist/src/commands/status-scope.js +54 -0
- package/dist/src/commands/status.js +66 -23
- package/dist/src/commands/test-browser-preflight.js +19 -3
- package/dist/src/commands/test-build-furnace.d.ts +21 -0
- package/dist/src/commands/test-build-furnace.js +48 -0
- package/dist/src/commands/test-build-phase.js +47 -41
- package/dist/src/commands/test-checkjs.js +7 -0
- package/dist/src/commands/test-diagnose.js +7 -0
- package/dist/src/commands/test-modes.d.ts +16 -1
- package/dist/src/commands/test-modes.js +24 -1
- package/dist/src/commands/test-multi-verdict.d.ts +61 -0
- package/dist/src/commands/test-multi-verdict.js +111 -0
- package/dist/src/commands/test-orchestrate.d.ts +43 -0
- package/dist/src/commands/test-orchestrate.js +222 -0
- package/dist/src/commands/test-perf-scope.d.ts +29 -0
- package/dist/src/commands/test-perf-scope.js +83 -0
- package/dist/src/commands/test-prelock.d.ts +51 -0
- package/dist/src/commands/test-prelock.js +177 -0
- package/dist/src/commands/test-register.js +54 -10
- package/dist/src/commands/test-run.js +4 -1
- package/dist/src/commands/test-stale-gate.js +57 -27
- package/dist/src/commands/test-verdict.d.ts +10 -1
- package/dist/src/commands/test-verdict.js +28 -1
- package/dist/src/commands/test.d.ts +6 -1
- package/dist/src/commands/test.js +66 -60
- package/dist/src/commands/token-lint.d.ts +14 -0
- package/dist/src/commands/token-lint.js +111 -0
- package/dist/src/commands/token.js +10 -0
- package/dist/src/commands/verify-scope.d.ts +28 -0
- package/dist/src/commands/verify-scope.js +67 -0
- package/dist/src/commands/verify.d.ts +10 -2
- package/dist/src/commands/verify.js +59 -35
- package/dist/src/core/build-baseline-types.d.ts +46 -0
- package/dist/src/core/build-baseline-validate.js +19 -0
- package/dist/src/core/build-baseline.d.ts +5 -1
- package/dist/src/core/build-baseline.js +3 -1
- package/dist/src/core/build-overwrite-guard.d.ts +50 -0
- package/dist/src/core/build-overwrite-guard.js +84 -4
- package/dist/src/core/build-prepare.d.ts +7 -0
- package/dist/src/core/build-prepare.js +14 -8
- package/dist/src/core/build-run-record.d.ts +74 -0
- package/dist/src/core/build-run-record.js +136 -0
- package/dist/src/core/checkjs-support-heads.d.ts +41 -0
- package/dist/src/core/checkjs-support-heads.js +104 -0
- package/dist/src/core/config-paths.d.ts +2 -2
- package/dist/src/core/config-paths.js +8 -0
- package/dist/src/core/config-validate-check.d.ts +8 -0
- package/dist/src/core/config-validate-check.js +143 -0
- package/dist/src/core/config-validate-file-size.d.ts +9 -6
- package/dist/src/core/config-validate-file-size.js +91 -2
- package/dist/src/core/config-validate-history.d.ts +11 -0
- package/dist/src/core/config-validate-history.js +38 -0
- package/dist/src/core/config-validate-patch-policy.js +100 -0
- package/dist/src/core/config-validate-test-toolchains.js +7 -0
- package/dist/src/core/config-validate-token-scales.d.ts +6 -0
- package/dist/src/core/config-validate-token-scales.js +50 -0
- package/dist/src/core/config-validate.js +14 -5
- package/dist/src/core/config.d.ts +5 -0
- package/dist/src/core/config.js +16 -0
- package/dist/src/core/coverage-dispatch.d.ts +24 -0
- package/dist/src/core/coverage-dispatch.js +44 -0
- package/dist/src/core/coverage-extend.d.ts +70 -45
- package/dist/src/core/coverage-extend.js +199 -144
- package/dist/src/core/coverage-records.d.ts +97 -0
- package/dist/src/core/coverage-records.js +350 -0
- package/dist/src/core/destructive.d.ts +7 -4
- package/dist/src/core/destructive.js +10 -10
- package/dist/src/core/engine-session-lock.d.ts +10 -1
- package/dist/src/core/engine-session-lock.js +28 -8
- package/dist/src/core/export-moved-code-shape.d.ts +49 -11
- package/dist/src/core/export-moved-code-shape.js +135 -44
- package/dist/src/core/file-lock-queue.d.ts +67 -0
- package/dist/src/core/file-lock-queue.js +221 -0
- package/dist/src/core/file-lock.d.ts +4 -25
- package/dist/src/core/file-lock.js +105 -138
- package/dist/src/core/furnace-apply-output.d.ts +10 -0
- package/dist/src/core/furnace-apply-output.js +31 -1
- package/dist/src/core/furnace-apply.js +12 -2
- package/dist/src/core/furnace-composed-source.d.ts +76 -0
- package/dist/src/core/furnace-composed-source.js +167 -0
- package/dist/src/core/furnace-config-validate-exceptions.d.ts +9 -0
- package/dist/src/core/furnace-config-validate-exceptions.js +71 -0
- package/dist/src/core/furnace-config-validate.js +8 -0
- package/dist/src/core/furnace-css-fragments.d.ts +10 -0
- package/dist/src/core/furnace-css-fragments.js +26 -0
- package/dist/src/core/furnace-operation.d.ts +3 -3
- package/dist/src/core/furnace-registration-remove.d.ts +22 -1
- package/dist/src/core/furnace-registration-remove.js +53 -17
- package/dist/src/core/furnace-registration-rename.d.ts +26 -0
- package/dist/src/core/furnace-registration-rename.js +77 -0
- package/dist/src/core/furnace-registration.d.ts +14 -4
- package/dist/src/core/furnace-registration.js +44 -10
- package/dist/src/core/furnace-staleness.d.ts +7 -0
- package/dist/src/core/furnace-staleness.js +34 -24
- package/dist/src/core/furnace-validate-accessibility.d.ts +4 -2
- package/dist/src/core/furnace-validate-accessibility.js +10 -1
- package/dist/src/core/furnace-validate-exceptions.d.ts +40 -0
- package/dist/src/core/furnace-validate-exceptions.js +43 -0
- package/dist/src/core/furnace-validate-icon-button.d.ts +7 -0
- package/dist/src/core/furnace-validate-icon-button.js +49 -0
- package/dist/src/core/furnace-validate.js +1 -1
- package/dist/src/core/harness-port-fallback.d.ts +60 -0
- package/dist/src/core/harness-port-fallback.js +96 -0
- package/dist/src/core/history-log-file.d.ts +61 -0
- package/dist/src/core/history-log-file.js +157 -0
- package/dist/src/core/history-log.d.ts +2 -1
- package/dist/src/core/history-log.js +2 -1
- package/dist/src/core/json-preserving-edit.d.ts +22 -0
- package/dist/src/core/json-preserving-edit.js +199 -0
- package/dist/src/core/mach.js +8 -1
- package/dist/src/core/negative-control-diff.d.ts +39 -0
- package/dist/src/core/negative-control-diff.js +136 -0
- package/dist/src/core/negative-control.d.ts +138 -0
- package/dist/src/core/negative-control.js +356 -0
- package/dist/src/core/objdir-install-manifests.d.ts +73 -0
- package/dist/src/core/objdir-install-manifests.js +226 -0
- package/dist/src/core/patch-export-update.d.ts +2 -0
- package/dist/src/core/patch-export-update.js +1 -0
- package/dist/src/core/patch-export.js +3 -4
- package/dist/src/core/patch-lint-checkjs-any-import.d.ts +37 -0
- package/dist/src/core/patch-lint-checkjs-any-import.js +61 -0
- package/dist/src/core/patch-lint-checkjs.js +8 -11
- package/dist/src/core/patch-lint-code-lines.d.ts +59 -0
- package/dist/src/core/patch-lint-code-lines.js +105 -0
- package/dist/src/core/patch-lint-cross.js +12 -4
- package/dist/src/core/patch-lint-css.d.ts +8 -2
- package/dist/src/core/patch-lint-css.js +10 -4
- package/dist/src/core/patch-lint-file-size.d.ts +12 -5
- package/dist/src/core/patch-lint-file-size.js +22 -11
- package/dist/src/core/patch-lint-forward-registration-extended.d.ts +18 -0
- package/dist/src/core/patch-lint-forward-registration-extended.js +35 -10
- package/dist/src/core/patch-lint-forward-registration.js +10 -0
- package/dist/src/core/patch-lint-ignore.d.ts +152 -0
- package/dist/src/core/patch-lint-ignore.js +299 -0
- package/dist/src/core/patch-lint-size-thresholds.d.ts +52 -0
- package/dist/src/core/patch-lint-size-thresholds.js +110 -0
- package/dist/src/core/patch-lint-size.d.ts +117 -0
- package/dist/src/core/patch-lint-size.js +323 -0
- package/dist/src/core/patch-lint-staged-registration.d.ts +18 -1
- package/dist/src/core/patch-lint-staged-registration.js +20 -0
- package/dist/src/core/patch-lint-token-scale.d.ts +39 -0
- package/dist/src/core/patch-lint-token-scale.js +109 -0
- package/dist/src/core/patch-lint.d.ts +11 -83
- package/dist/src/core/patch-lint.js +64 -286
- package/dist/src/core/patch-lock.js +7 -6
- package/dist/src/core/patch-manifest-consistency.js +7 -6
- package/dist/src/core/patch-manifest-io.d.ts +6 -3
- package/dist/src/core/patch-manifest-io.js +19 -8
- package/dist/src/core/patch-manifest-validate.js +34 -2
- package/dist/src/core/patch-order-legal.d.ts +25 -0
- package/dist/src/core/patch-order-legal.js +83 -0
- package/dist/src/core/patch-policy-content.d.ts +40 -0
- package/dist/src/core/patch-policy-content.js +216 -0
- package/dist/src/core/patch-policy.d.ts +16 -3
- package/dist/src/core/patch-policy.js +16 -9
- package/dist/src/core/patch-retired.d.ts +35 -0
- package/dist/src/core/patch-retired.js +54 -0
- package/dist/src/core/patch-subset.d.ts +16 -0
- package/dist/src/core/patch-subset.js +66 -0
- package/dist/src/core/preflight-wait.d.ts +20 -0
- package/dist/src/core/preflight-wait.js +38 -0
- package/dist/src/core/rebase-from-version.d.ts +46 -0
- package/dist/src/core/rebase-from-version.js +71 -0
- package/dist/src/core/run-log.d.ts +20 -0
- package/dist/src/core/run-log.js +89 -3
- package/dist/src/core/run-profile.d.ts +7 -0
- package/dist/src/core/run-profile.js +7 -1
- package/dist/src/core/smoke-patterns.d.ts +5 -0
- package/dist/src/core/smoke-patterns.js +13 -0
- package/dist/src/core/test-harness-crash.d.ts +18 -1
- package/dist/src/core/test-harness-crash.js +6 -4
- package/dist/src/core/test-harness-signature.d.ts +17 -0
- package/dist/src/core/test-harness-signature.js +4 -0
- package/dist/src/core/test-run-completeness.d.ts +57 -0
- package/dist/src/core/test-run-completeness.js +144 -0
- package/dist/src/core/test-stale-check.d.ts +27 -1
- package/dist/src/core/test-stale-check.js +39 -49
- package/dist/src/core/test-stale-exempt.d.ts +40 -0
- package/dist/src/core/test-stale-exempt.js +70 -0
- package/dist/src/core/token-lint.d.ts +50 -0
- package/dist/src/core/token-lint.js +126 -0
- package/dist/src/core/tree-guard.js +3 -0
- package/dist/src/core/typecheck-shim.d.ts +1 -1
- package/dist/src/core/typecheck-shim.js +90 -2
- package/dist/src/index.d.ts +8 -3
- package/dist/src/index.js +3 -1
- package/dist/src/types/commands/index.d.ts +2 -2
- package/dist/src/types/commands/options.d.ts +148 -21
- package/dist/src/types/commands/patches.d.ts +75 -1
- package/dist/src/types/config.d.ts +192 -0
- package/dist/src/types/config.js +9 -0
- package/dist/src/types/furnace.d.ts +43 -1
- package/dist/src/types/furnace.js +7 -0
- package/dist/src/types/index.d.ts +1 -1
- package/dist/src/utils/options.d.ts +34 -6
- package/dist/src/utils/options.js +86 -5
- package/dist/src/utils/path-glob.d.ts +15 -0
- package/dist/src/utils/path-glob.js +47 -0
- package/dist/src/utils/regex.d.ts +15 -3
- package/dist/src/utils/regex.js +37 -16
- package/docs/configuration.md +282 -9
- package/docs/exit-codes.md +18 -1
- package/docs/furnace.md +97 -3
- package/docs/lifecycle-invariants.md +52 -19
- package/docs/machine-output.md +74 -12
- package/docs/patch-workflow.md +297 -22
- package/docs/run-logs.md +20 -1
- package/docs/testing.md +276 -20
- package/package.json +1 -1
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* The `fireforge test` checks that run BEFORE the engine session lock.
|
|
4
|
+
*
|
|
5
|
+
* Everything here reads, never writes, so none of it needs the lock, and
|
|
6
|
+
* each check used to cost a full place in the lock queue before it could
|
|
7
|
+
* refuse:
|
|
8
|
+
*
|
|
9
|
+
* - a missing launchable binary (a peer's interrupted build) refused only
|
|
10
|
+
* after the wait, in every session, with nothing naming the cause;
|
|
11
|
+
* - the named-file checkJs pass refused an `Element | null` after an
|
|
12
|
+
* eighteen-minute queue;
|
|
13
|
+
* - `--wait-browser` and `--wait-port` spent their budget holding the
|
|
14
|
+
* lock, so every peer queued behind a run that was doing nothing.
|
|
15
|
+
*
|
|
16
|
+
* The in-lock ladder in `test.ts` still runs in full afterwards. That is
|
|
17
|
+
* the "re-check once under the lock": the waits this pass spent are
|
|
18
|
+
* stripped from the options it returns, so the in-lock browser and port
|
|
19
|
+
* checks probe once instead of waiting again.
|
|
20
|
+
*/
|
|
21
|
+
import { join } from 'node:path';
|
|
22
|
+
import { describeBuildRunState, readBuildRunState } from '../core/build-run-record.js';
|
|
23
|
+
import { getProjectPaths, loadConfig } from '../core/config.js';
|
|
24
|
+
import { assertEngineExists } from '../core/engine-precondition.js';
|
|
25
|
+
import { hasBuildArtifacts, hasRunnableBundle } from '../core/mach.js';
|
|
26
|
+
import { assertBuildArtifacts } from '../core/mach-build-artifacts.js';
|
|
27
|
+
import { assertMarionettePortAvailable, ensureLaunchableBrowserNotRunning, extractForwardedMarionettePort, } from '../core/marionette-port.js';
|
|
28
|
+
import { ensureMochitestServerPortAvailable } from '../core/mochitest-server-port.js';
|
|
29
|
+
import { waitBeforeLock } from '../core/preflight-wait.js';
|
|
30
|
+
import { PreflightRefusalError } from '../errors/base.js';
|
|
31
|
+
import { info } from '../utils/logger.js';
|
|
32
|
+
import { resolveWaitLockSeconds } from '../utils/options.js';
|
|
33
|
+
import { stripEnginePrefix } from '../utils/paths.js';
|
|
34
|
+
import { checkRequestedTestTypes } from './test-checkjs.js';
|
|
35
|
+
import { assertPathlessTestMode, classifyBeforeDispatch, resolveCanaryPath } from './test-modes.js';
|
|
36
|
+
/**
|
|
37
|
+
* Resolves the launchable binary a test run needs, or refuses naming why
|
|
38
|
+
* it is missing. The cause comes from the build run record, which any
|
|
39
|
+
* session's build writes, so a session that never ran the build can still
|
|
40
|
+
* say "a peer's build was interrupted" rather than only "the binary is
|
|
41
|
+
* missing".
|
|
42
|
+
*
|
|
43
|
+
* @param projectRoot - Project root
|
|
44
|
+
* @param engineDir - Absolute engine directory
|
|
45
|
+
* @param binaryName - Configured binary name
|
|
46
|
+
* @param objDir - Active objdir, when one exists
|
|
47
|
+
* @returns The engine-relative launchable path, or undefined without an objdir
|
|
48
|
+
*/
|
|
49
|
+
export async function resolveLaunchablePathForTests(projectRoot, engineDir, binaryName, objDir) {
|
|
50
|
+
if (!objDir)
|
|
51
|
+
return undefined;
|
|
52
|
+
const bundleCheck = await hasRunnableBundle(engineDir, binaryName, objDir);
|
|
53
|
+
if (bundleCheck.runnable)
|
|
54
|
+
return bundleCheck.expectedPath;
|
|
55
|
+
const expectedSuffix = bundleCheck.expectedPath
|
|
56
|
+
? ` (expected at engine/${bundleCheck.expectedPath})`
|
|
57
|
+
: '';
|
|
58
|
+
const cause = describeBuildRunState(await readBuildRunState(projectRoot));
|
|
59
|
+
throw new PreflightRefusalError(`Tests require a complete launchable build${expectedSuffix}. ` +
|
|
60
|
+
'The obj-*/dist/ tree exists but the launchable binary is missing' +
|
|
61
|
+
(cause !== undefined
|
|
62
|
+
? `.\n\n${cause}\n\n`
|
|
63
|
+
: ' — typically the result of an interrupted or partially failed `fireforge build`.\n\n') +
|
|
64
|
+
'Run "fireforge build" again and let it finish before retrying "fireforge test".', 'no-launchable-binary');
|
|
65
|
+
}
|
|
66
|
+
/** Build-artifact preflight wording for `fireforge test`. */
|
|
67
|
+
export const TEST_BUILD_PREFLIGHT = {
|
|
68
|
+
label: 'Tests',
|
|
69
|
+
requirement: 'Tests require a completed build.',
|
|
70
|
+
remediation: "Run 'fireforge build' first, then run 'fireforge test'.",
|
|
71
|
+
requireExisting: true,
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* The head of the in-lock ladder, run early: argument shape, engine, build
|
|
75
|
+
* artifacts and config. Each refuses with exactly the message it gives
|
|
76
|
+
* under the lock. Returns undefined when there is no objdir to check
|
|
77
|
+
* further.
|
|
78
|
+
*/
|
|
79
|
+
async function loadPreLockContext(projectRoot, testPaths, options) {
|
|
80
|
+
assertPathlessTestMode(testPaths, options);
|
|
81
|
+
const paths = getProjectPaths(projectRoot);
|
|
82
|
+
await assertEngineExists(paths.engine);
|
|
83
|
+
const buildCheck = await hasBuildArtifacts(paths.engine);
|
|
84
|
+
assertBuildArtifacts(paths.engine, buildCheck, TEST_BUILD_PREFLIGHT);
|
|
85
|
+
const config = await loadConfig(projectRoot);
|
|
86
|
+
return buildCheck.objDir === undefined ? undefined : { paths, config, objDir: buildCheck.objDir };
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Runs the lock-free checks and waits, and returns the options the in-lock
|
|
90
|
+
* run should use: with `waitBrowser`/`waitPort` removed when their budget
|
|
91
|
+
* was already spent here.
|
|
92
|
+
*
|
|
93
|
+
* @param projectRoot - Project root
|
|
94
|
+
* @param testPaths - Requested test paths
|
|
95
|
+
* @param options - Parsed test options
|
|
96
|
+
*/
|
|
97
|
+
export async function runTestPreLockPreflight(projectRoot, testPaths, options) {
|
|
98
|
+
const context = await loadPreLockContext(projectRoot, testPaths, options);
|
|
99
|
+
if (context === undefined)
|
|
100
|
+
return options;
|
|
101
|
+
const { paths, config, objDir } = context;
|
|
102
|
+
let launchablePath;
|
|
103
|
+
try {
|
|
104
|
+
launchablePath = await resolveLaunchablePathForTests(projectRoot, paths.engine, config.binaryName, objDir);
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
// A build in progress holds the engine lock and relinks the bundle
|
|
108
|
+
// before it lets go, so this run queues behind it rather than refusing.
|
|
109
|
+
if ((await readBuildRunState(projectRoot)).kind !== 'running')
|
|
110
|
+
throw error;
|
|
111
|
+
info('The launchable binary is missing while a build runs; queueing behind it.');
|
|
112
|
+
}
|
|
113
|
+
const canaryPath = resolveCanaryPath(options, config);
|
|
114
|
+
const normalizedPaths = (canaryPath !== undefined ? [canaryPath] : testPaths).map((path) => stripEnginePrefix(path).trim());
|
|
115
|
+
if (options.build === true || options.buildOnly === true) {
|
|
116
|
+
await checkRequestedTestTypes(paths, config, normalizedPaths);
|
|
117
|
+
}
|
|
118
|
+
return waitForResourcesBeforeLock(paths.engine, config, options, {
|
|
119
|
+
launchablePath,
|
|
120
|
+
normalizedPaths,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
/** `options` without one wait budget, which this pass already spent. */
|
|
124
|
+
function withoutOption(options, key) {
|
|
125
|
+
const rest = { ...options };
|
|
126
|
+
if (key === 'waitBrowser')
|
|
127
|
+
delete rest.waitBrowser;
|
|
128
|
+
else
|
|
129
|
+
delete rest.waitPort;
|
|
130
|
+
return rest;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Spends `--wait-browser` and `--wait-port` before the lock, mirroring
|
|
134
|
+
* which in-lock checks would apply to this run, and strips each budget it
|
|
135
|
+
* spent from the returned options.
|
|
136
|
+
*/
|
|
137
|
+
async function waitForResourcesBeforeLock(engineDir, config, options, request) {
|
|
138
|
+
if (options.waitBrowser === undefined && options.waitPort === undefined)
|
|
139
|
+
return options;
|
|
140
|
+
let xpcshellOnly;
|
|
141
|
+
try {
|
|
142
|
+
({ xpcshellOnly } = await classifyBeforeDispatch(engineDir, request.normalizedPaths, {
|
|
143
|
+
allowMixed: true,
|
|
144
|
+
}));
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return options;
|
|
148
|
+
}
|
|
149
|
+
const kill = options.killStaleMarionette === true;
|
|
150
|
+
let result = options;
|
|
151
|
+
const building = options.build === true || options.buildOnly === true;
|
|
152
|
+
if (options.waitBrowser !== undefined &&
|
|
153
|
+
request.launchablePath !== undefined &&
|
|
154
|
+
(building || !xpcshellOnly)) {
|
|
155
|
+
const binary = join(engineDir, request.launchablePath);
|
|
156
|
+
await waitBeforeLock(() => ensureLaunchableBrowserNotRunning(binary), resolveWaitLockSeconds(options.waitBrowser),
|
|
157
|
+
// A stale browser the run may reap is not worth waiting out; the
|
|
158
|
+
// in-lock check terminates it.
|
|
159
|
+
kill ? ['stale-browser'] : []);
|
|
160
|
+
result = withoutOption(result, 'waitBrowser');
|
|
161
|
+
}
|
|
162
|
+
// Port waits stay under the lock with --kill-stale-marionette: a stale
|
|
163
|
+
// listener reads as busy until the in-lock pass reaps it, so waiting it
|
|
164
|
+
// out here would only burn the budget.
|
|
165
|
+
if (options.waitPort !== undefined && options.buildOnly !== true && !kill) {
|
|
166
|
+
const seconds = resolveWaitLockSeconds(options.waitPort);
|
|
167
|
+
if (!xpcshellOnly) {
|
|
168
|
+
await waitBeforeLock(() => ensureMochitestServerPortAvailable(undefined, { engineDir }), seconds);
|
|
169
|
+
}
|
|
170
|
+
if (!xpcshellOnly || options.doctor === true) {
|
|
171
|
+
const port = options.marionettePort ?? extractForwardedMarionettePort(options.machArg ?? []);
|
|
172
|
+
await waitBeforeLock(() => assertMarionettePortAvailable(port, { binaryName: config.binaryName }), seconds);
|
|
173
|
+
}
|
|
174
|
+
result = withoutOption(result, 'waitPort');
|
|
175
|
+
}
|
|
176
|
+
return result;
|
|
177
|
+
}
|
|
@@ -26,12 +26,33 @@ const TEST_HELP_TEXT = [
|
|
|
26
26
|
'not exercise cross-argument state; --no-shard restores the',
|
|
27
27
|
'combined single-instance invocation.',
|
|
28
28
|
'',
|
|
29
|
+
'Mixed xpcshell and browser/mochitest paths run as two consecutive',
|
|
30
|
+
'mach invocations (xpcshell first) under one lock hold and one',
|
|
31
|
+
'verdict; with --build, one pre-test build packages both halves.',
|
|
32
|
+
'--repeat <n> and --negative-control <diff> run under one lock hold',
|
|
33
|
+
'too. Each sub-run prints a FIREFORGE-RUN: line (its own verdict',
|
|
34
|
+
'plus run=<i>/<n>) before the single aggregate verdict.',
|
|
35
|
+
'',
|
|
36
|
+
'A dev objdir symlinks chrome JS/CSS and widget files into engine/,',
|
|
37
|
+
'so the browser reads engine/ sources when it loads them, after the',
|
|
38
|
+
'build step: an edit made while a suite runs reaches that suite, and',
|
|
39
|
+
'a hand-made neuter must stay until the verdict line, not until',
|
|
40
|
+
'"Build complete", since one restored early is gone for later',
|
|
41
|
+
'shards. --negative-control applies its diff after the build and',
|
|
42
|
+
'restores it byte-exact only after the run, before the lock is',
|
|
43
|
+
'released.',
|
|
44
|
+
'',
|
|
29
45
|
'Machine-readable verdict: every run (single, sharded, --canary,',
|
|
30
46
|
'--doctor) ends with exactly one raw stdout line',
|
|
31
47
|
' FIREFORGE-VERDICT: PASS|FAIL [reason=crash|harness-arguments|test-failures|',
|
|
32
48
|
' no-tests|preflight|inconclusive|lock-timeout|killed] [checks=<n>]',
|
|
33
|
-
' [unexpected=<n>] [
|
|
34
|
-
' [
|
|
49
|
+
' [unexpected=<n>] [started=<n>/<m> first-unstarted=<path>]',
|
|
50
|
+
' [shards=<p>/<t>] [(<note>)] [shuffle=<seed>]',
|
|
51
|
+
' [runs=<p>/<n>] [control=caught|missed|inconclusive]',
|
|
52
|
+
' [furnace-applied=<names>|furnace-apply=skipped] [log=<path>]',
|
|
53
|
+
"control= is a negative control's outcome: caught (went red on test",
|
|
54
|
+
'failures, as a control should; exit 0), missed (stayed green; exit',
|
|
55
|
+
'5) or inconclusive.',
|
|
35
56
|
'shuffle=<seed> is the seed a --shuffle run exported to the harness.',
|
|
36
57
|
"log=<path> names this run's complete output, written to",
|
|
37
58
|
'.fireforge/logs/ as the run streams, so a piped or truncated run',
|
|
@@ -39,7 +60,11 @@ const TEST_HELP_TEXT = [
|
|
|
39
60
|
'The status follows the harness classifier, not the raw exit code:',
|
|
40
61
|
'a crash-classified run says FAIL reason=crash even at exit 0, and',
|
|
41
62
|
'a green-summary-override pass says PASS despite a non-zero mach',
|
|
42
|
-
'exit.
|
|
63
|
+
'exit. An exit-0 run is still FAIL reason=test-failures when its',
|
|
64
|
+
'summary reports unexpected results, the browser shut down mid-test,',
|
|
65
|
+
'or a requested file never started; started=/first-unstarted= then',
|
|
66
|
+
'name how far it got and where a re-run has to pick up.',
|
|
67
|
+
'reason=preflight covers any failure before the harness ran',
|
|
43
68
|
'(missing/stale build, invalid paths, port conflicts, config',
|
|
44
69
|
'errors); reason=inconclusive means engine/ or perf power source',
|
|
45
70
|
'changed while tests ran, so the result cannot be trusted;',
|
|
@@ -56,13 +81,33 @@ const TEST_HELP_TEXT = [
|
|
|
56
81
|
'"(headed run stalled with the display asleep)" marks a no-output',
|
|
57
82
|
'stall whose display was measured asleep. Key on this line',
|
|
58
83
|
'instead of mach internals.',
|
|
84
|
+
'',
|
|
85
|
+
'Lock-free checks run before the engine lock is requested: a missing',
|
|
86
|
+
'launchable binary (with the build that left it missing, when one is',
|
|
87
|
+
'recorded), the named-file checkJs pass of --build, and the',
|
|
88
|
+
'--wait-browser/--wait-port budgets. Each is re-checked once under the',
|
|
89
|
+
'lock.',
|
|
59
90
|
].join('\n');
|
|
91
|
+
function registerTestMultiRunOptions(test) {
|
|
92
|
+
return test
|
|
93
|
+
.option('--repeat <n>', 'Run the same paths n times under one engine-lock hold (a --build builds once); prints each run and aggregates runs=<passed>/<n> on the verdict', commanderArgParser((raw) => parsePositiveIntegerFlag('--repeat', raw)))
|
|
94
|
+
.option('--negative-control <diff>', 'Apply this reviewed git diff (engine/ or components/ sources; project-relative path) after the pre-test build and right before dispatch, restore it byte-exact after the run (also on failure, SIGINT and SIGTERM) before the lock is released, and report control=caught|missed|inconclusive on the verdict')
|
|
95
|
+
.option('--auto-ports', "Let a browser run proceed when the mochitest server or Marionette port is held by something that is not this checkout's own: the engine's harness falls back to a free port itself. Same as test.mochitestPorts: \"auto\". This checkout's own leftovers are still refused.");
|
|
96
|
+
}
|
|
60
97
|
function registerTestSharingOptions(test) {
|
|
61
98
|
return test
|
|
62
99
|
.option('--wait-browser [seconds]', 'Wait for this objdir browser before any deployment/build (default 60)', commanderArgParser((raw) => resolveWaitLockSeconds(raw)))
|
|
63
100
|
.option('--wait-port [seconds]', 'Wait for the mochitest and Marionette ports (default 60)', commanderArgParser((raw) => resolveWaitLockSeconds(raw)))
|
|
101
|
+
.option('--restart-between-tests', "Forward mochitest's --restart-between-tests: restart the browser between test files to expose undocumented cross-file dependencies (the shape a manifest's standalone key asks for). Mochitest dispatch only.")
|
|
102
|
+
.option('--keep-log <path>', "Also write this run's log to <path> (a directory receives it under its own name), outside .fireforge/logs/ retention. FIREFORGE_LOG_KEEP sets a session default.")
|
|
64
103
|
.option('--profile-file <source=destination>', 'Merge a file into the harness profile, e.g. sheet.css=chrome/userChrome.css. Repeatable.', ...stringListOption());
|
|
65
104
|
}
|
|
105
|
+
/** Pre-test build options that shape the coverage claim and the Furnace step. */
|
|
106
|
+
function registerTestBuildOptions(test) {
|
|
107
|
+
return test
|
|
108
|
+
.option('--extend-coverage', 'With --build/--build-only and explicit paths, refuse instead of dropping any earlier packaging claim. Scoped builds keep each earlier path by default while its own staging inputs (manifest directory and the fixtures its manifest names) and engine/mozconfig are unchanged; this flag turns a drop into a refusal before the build.')
|
|
109
|
+
.option('--no-furnace-apply', 'With --build/--build-only, skip the Furnace apply so the build packages engine/ as it stands; names the components whose undeployed components/ edits it leaves out');
|
|
110
|
+
}
|
|
66
111
|
/** Registers the test command on the CLI program. */
|
|
67
112
|
export function registerTest(program, { getProjectRoot, withErrorHandling }) {
|
|
68
113
|
const test = program
|
|
@@ -72,7 +117,6 @@ export function registerTest(program, { getProjectRoot, withErrorHandling }) {
|
|
|
72
117
|
.option('--full-output', "Pass mozbuild's full verbosity through for the TEST phase. mozbuild quiets terminal output to warnings and errors when it detects a coding agent, which removes TEST_START and console INFO — the lines a hang or stall diagnosis needs. This unsets that detection for test dispatches only; the build phase stays quiet.")
|
|
73
118
|
.option('--build', 'Run incremental UI build before testing')
|
|
74
119
|
.option('--build-only', 'Run the scoped pre-test build (packaging exactly [paths...], mixed harnesses allowed) and exit without dispatching tests — one build covers both harness halves, then run each half without --build')
|
|
75
|
-
.option('--extend-coverage', 'With --build/--build-only and explicit paths, union those paths into the recorded test-packaging coverage instead of replacing it, so an earlier scoped build stays covered. Refused when the build anchor moved (engine HEAD, mozconfig, or a previously fingerprinted packageable file changed).')
|
|
76
120
|
.option('--refuse-unexported-drift', 'With --build/--build-only, refuse (instead of warning) when the pre-test build would overwrite engine content recorded in neither a patch body nor the pristine baseline — the same belt as "fireforge build --refuse-unexported-drift", armable from the scripted "test --build" shape the hazard actually shows up in')
|
|
77
121
|
.option('--auto', 'Forward mach test --auto. Valid only when no explicit paths are provided.')
|
|
78
122
|
.option('--allow-stale-build', 'Allow tests to run even when packageable engine files changed since the last successful FireForge build')
|
|
@@ -83,7 +127,7 @@ export function registerTest(program, { getProjectRoot, withErrorHandling }) {
|
|
|
83
127
|
.option('--canary [path]', 'Run one short browser-chrome harness canary. Uses test.canaryPath from fireforge.json when no path is supplied.')
|
|
84
128
|
.option('--doctor', 'Run a marionette handshake preflight before tests (exit 1 on FAIL). With no paths, runs the preflight only.')
|
|
85
129
|
.option('--mach-arg <arg>', 'Forward this argument verbatim to `mach test` (repeatable). Escape valve for upstream xpcshell/mochitest flags FireForge does not model.', ...stringListOption())
|
|
86
|
-
.option('--shuffle [seed]', 'Shuffle isolated mochitest path arguments in a repeatable seeded order (fresh seed when omitted). Native mach --shuffle within each invocation remains unseeded. Export FIREFORGE_SHUFFLE_SEED for custom task code and stamp shuffle=<seed> on the verdict. --no-shard disables the seeded argument permutation.
|
|
130
|
+
.option('--shuffle [seed]', 'Shuffle isolated mochitest path arguments in a repeatable seeded order (fresh seed when omitted). Native mach --shuffle within each invocation remains unseeded. Export FIREFORGE_SHUFFLE_SEED for custom task code and stamp shuffle=<seed> on the verdict. --no-shard disables the seeded argument permutation. A path-shaped argument right after the flag (browser_x.js, dir/) is a test path, not a seed.', commanderArgParser((raw) => parsePositiveIntegerFlag('--shuffle', raw)))
|
|
87
131
|
.option('--harness-retries <n>', `Retry budget for recognized harness crashes (resource-monitor tracebacks, pre-test hangs, post-green shutdown re-entry). 0 disables retries. Default: ${DEFAULT_HARNESS_RETRIES}.`, commanderArgParser((raw) => {
|
|
88
132
|
const n = Number.parseInt(raw, 10);
|
|
89
133
|
if (!Number.isFinite(n) || n < 0 || n > 10) {
|
|
@@ -102,14 +146,14 @@ export function registerTest(program, { getProjectRoot, withErrorHandling }) {
|
|
|
102
146
|
return n;
|
|
103
147
|
}))
|
|
104
148
|
.addHelpText('after', TEST_HELP_TEXT);
|
|
105
|
-
addWaitLockOption(registerTestSharingOptions(test)).action(withErrorHandling(async (paths, options) => {
|
|
149
|
+
addWaitLockOption(registerTestBuildOptions(registerTestMultiRunOptions(registerTestSharingOptions(test)))).action(withErrorHandling(async (paths, options) => {
|
|
106
150
|
const projectRoot = getProjectRoot();
|
|
107
|
-
//
|
|
108
|
-
// exactly-one-verdict guarantee, so a lock failure must emit here or
|
|
109
|
-
// the run ends with no FIREFORGE-VERDICT line at all.
|
|
151
|
+
// Fallback for a verdict testCommand did not emit (it takes the lock).
|
|
110
152
|
resetVerdictEmission();
|
|
111
153
|
try {
|
|
112
|
-
await
|
|
154
|
+
await testCommand(projectRoot, paths, pickDefined(options), (body) => withEngineSessionLock(projectRoot, 'test', body, {
|
|
155
|
+
waitLockSeconds: resolveWaitLockSeconds(options.waitLock),
|
|
156
|
+
}));
|
|
113
157
|
}
|
|
114
158
|
catch (error) {
|
|
115
159
|
if (!verdictEmitted()) {
|
|
@@ -18,6 +18,7 @@ import { getActiveRunLogPath } from '../core/run-log.js';
|
|
|
18
18
|
import { changedPrefNoiseVerdictNote } from '../core/test-changed-prefs.js';
|
|
19
19
|
import { buildHarnessCrashMessage, classifyHarnessRun, headedDisplayAsleepVerdictNote, headedNoOutputTimeoutHint, } from '../core/test-harness-crash.js';
|
|
20
20
|
import { reportTestHost, sampleTestHost, stampPerfHost } from '../core/test-host-state.js';
|
|
21
|
+
import { vetoIncompletePass } from '../core/test-run-completeness.js';
|
|
21
22
|
import { retryAfterXpcshellSymlinkRepair } from '../core/test-xpcshell-retry.js';
|
|
22
23
|
import { withXpcshellProfileDir } from '../core/xpcshell-profile-dir.js';
|
|
23
24
|
import { TestFailureError } from '../errors/build.js';
|
|
@@ -85,7 +86,9 @@ async function runTestsWithRetriesInner(ctx, paths, env) {
|
|
|
85
86
|
fullOutput: ctx.fullOutput,
|
|
86
87
|
});
|
|
87
88
|
const combined = `${result.stdout}\n${result.stderr}`;
|
|
88
|
-
|
|
89
|
+
// An exit-0 run is still not a pass when it stopped partway or
|
|
90
|
+
// reported unexpected results (see test-run-completeness.ts).
|
|
91
|
+
verdict = vetoIncompletePass(classifyHarnessRun(result.exitCode, combined, paths), combined, paths);
|
|
89
92
|
if (verdict.kind !== 'harness-crash' || attempts >= maxAttempts)
|
|
90
93
|
break;
|
|
91
94
|
warn(`Harness crash detected (${verdict.signature?.reason ?? 'unknown shape'}): ` +
|
|
@@ -7,9 +7,12 @@
|
|
|
7
7
|
* stays wiring. The probes and refusal copy live in
|
|
8
8
|
* `src/core/test-stale-check.ts`.
|
|
9
9
|
*/
|
|
10
|
-
import {
|
|
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';
|
|
12
|
+
import { formatStaleExemptions } from '../core/test-stale-exempt.js';
|
|
11
13
|
import { PreflightRefusalError } from '../errors/base.js';
|
|
12
|
-
import { warn } from '../utils/logger.js';
|
|
14
|
+
import { info, warn } from '../utils/logger.js';
|
|
15
|
+
import { describePerfStaleImpact } from './test-perf-scope.js';
|
|
13
16
|
/**
|
|
14
17
|
* Stale-build preflight: when `--build` was not requested, detect packageable
|
|
15
18
|
* engine edits since the last successful build and fail up front unless the
|
|
@@ -34,34 +37,61 @@ export async function enforceStaleBuildGate(projectRoot, engineDir, options, nor
|
|
|
34
37
|
const stale = await checkStaleBuildForTest(projectRoot, engineDir);
|
|
35
38
|
const dispatchesNoTests = options.doctor === true && normalizedPaths.length === 0;
|
|
36
39
|
if (!dispatchesNoTests) {
|
|
37
|
-
|
|
38
|
-
const uncovered = findUncoveredRequestPaths(recordedCoverage, normalizedPaths);
|
|
39
|
-
if (uncovered.length > 0) {
|
|
40
|
-
// The refusal is correct on its own. Naming the manifest that gained
|
|
41
|
-
// an entry is the triage step it otherwise leaves to the reader.
|
|
42
|
-
const changedManifests = await findChangedTestManifestsForPaths(engineDir, stale.baseline, uncovered);
|
|
43
|
-
throw new PreflightRefusalError(formatTestCoverageRefusal(uncovered, Array.isArray(recordedCoverage) ? recordedCoverage : [], changedManifests), 'coverage-replaced');
|
|
44
|
-
}
|
|
40
|
+
await enforceCoverageGate(engineDir, stale.baseline, normalizedPaths);
|
|
45
41
|
}
|
|
46
42
|
await enforceStaticComponentsGate(engineDir, stale.baseline, options);
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
'always the right move, because the usual cause is your own edit landing between the ' +
|
|
51
|
-
'last build and this run.\n\n' +
|
|
52
|
-
'`--allow-stale-build` is NOT the general escape hatch: it accepts a packaging that ' +
|
|
53
|
-
'predates the edit under test, so the run tests the OLD code. Pass it only when you ' +
|
|
54
|
-
'rebuilt out-of-band (outside FireForge) and know the packaged runtime already contains ' +
|
|
55
|
-
'the change.'
|
|
56
|
-
: undefined;
|
|
57
|
-
if (staleMessage !== undefined) {
|
|
58
|
-
if (options.allowStaleBuild === true) {
|
|
59
|
-
warn(staleMessage);
|
|
60
|
-
}
|
|
61
|
-
else {
|
|
62
|
-
throw new PreflightRefusalError(staleMessage, 'stale-build');
|
|
63
|
-
}
|
|
43
|
+
const exempt = stale.exempt ?? [];
|
|
44
|
+
if (!stale.stale && exempt.length > 0) {
|
|
45
|
+
info(`Stale-build preflight: ${exempt.length} edit(s) since the last build need no rebuild: ${formatStaleExemptions(exempt)}.`);
|
|
64
46
|
}
|
|
47
|
+
if (!stale.stale)
|
|
48
|
+
return;
|
|
49
|
+
// The one-line repackaging command, so a build-less sitting (a perf
|
|
50
|
+
// series) can repackage and continue without dispatching anything.
|
|
51
|
+
const repackage = normalizedPaths.length > 0
|
|
52
|
+
? `fireforge test --build-only ${normalizedPaths.join(' ')}`
|
|
53
|
+
: 'fireforge build --ui';
|
|
54
|
+
const perfImpact = options.perfSamples === undefined
|
|
55
|
+
? ''
|
|
56
|
+
: `\n\n${await describePerfStaleImpact(engineDir, stale.changedPaths, normalizedPaths)}`;
|
|
57
|
+
const staleMessage = `${formatStaleBuildWarning(stale)}\n\n` +
|
|
58
|
+
'Run `fireforge test --build` to refresh the packaged runtime first — that is almost ' +
|
|
59
|
+
'always the right move, because the usual cause is your own edit landing between the ' +
|
|
60
|
+
'last build and this run. To repackage without running anything: ' +
|
|
61
|
+
`\`${repackage}\`.${perfImpact}\n\n` +
|
|
62
|
+
'`--allow-stale-build` is NOT the general escape hatch: it accepts a packaging that ' +
|
|
63
|
+
'predates the edit under test, so the run tests the OLD code. Pass it only when you ' +
|
|
64
|
+
'rebuilt out-of-band (outside FireForge) and know the packaged runtime already contains ' +
|
|
65
|
+
'the change.';
|
|
66
|
+
if (options.allowStaleBuild === true) {
|
|
67
|
+
warn(staleMessage);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
throw new PreflightRefusalError(staleMessage, 'stale-build');
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Packaging-coverage refusal. A path outside the recorded scoped list is
|
|
75
|
+
* still covered while its own staging inputs are unchanged against a
|
|
76
|
+
* carried full-build anchor. The refusal names, per uncovered path, the
|
|
77
|
+
* inputs that changed.
|
|
78
|
+
*/
|
|
79
|
+
async function enforceCoverageGate(engineDir, baseline, normalizedPaths) {
|
|
80
|
+
const recordedCoverage = baseline?.testPackagingCoverage;
|
|
81
|
+
const outsideList = findUncoveredRequestPaths(recordedCoverage, normalizedPaths);
|
|
82
|
+
if (outsideList.length === 0)
|
|
83
|
+
return;
|
|
84
|
+
const remaining = await filterByCarriedFullCoverage(engineDir, baseline, outsideList, FULL_SUITE_REQUEST);
|
|
85
|
+
if (remaining.length === 0)
|
|
86
|
+
return;
|
|
87
|
+
const uncovered = remaining.map((request) => request.path);
|
|
88
|
+
// The refusal is correct on its own. Naming the manifest that gained
|
|
89
|
+
// an entry is the triage step it otherwise leaves to the reader.
|
|
90
|
+
const changedManifests = await findChangedTestManifestsForPaths(engineDir, baseline, uncovered);
|
|
91
|
+
throw new PreflightRefusalError(formatTestCoverageRefusal(uncovered, Array.isArray(recordedCoverage) ? recordedCoverage : [], changedManifests, {
|
|
92
|
+
changedInputs: Object.fromEntries(remaining.map((request) => [request.path, request.changed])),
|
|
93
|
+
carriesFullAnchor: baseline?.fullCoverageAnchor !== undefined,
|
|
94
|
+
}), 'coverage-replaced');
|
|
65
95
|
}
|
|
66
96
|
/**
|
|
67
97
|
* Compiled-StaticComponents gate: refuses runs whose child process would
|
|
@@ -13,7 +13,7 @@ import type { HarnessRunVerdict } from '../core/test-harness-crash.js';
|
|
|
13
13
|
*/
|
|
14
14
|
export type FireforgeVerdictReason = 'crash' | 'harness-arguments' | 'no-tests' | 'test-failures' | 'preflight' | 'inconclusive' | 'lock-timeout' | 'killed';
|
|
15
15
|
/** Additive keys a run can stamp on its verdict line. */
|
|
16
|
-
export type VerdictRunAttributeKey = 'shuffle' | 'orphans-reaped' | 'host-load' | 'host-cpu-warning' | 'power-source' | 'power-changed';
|
|
16
|
+
export type VerdictRunAttributeKey = 'shuffle' | 'orphans-reaped' | 'host-load' | 'host-cpu-warning' | 'power-source' | 'power-changed' | 'furnace-applied' | 'furnace-apply';
|
|
17
17
|
/** Records an additive attribute for this run's verdict line. */
|
|
18
18
|
export declare function setVerdictRunAttribute(key: VerdictRunAttributeKey, value: string): void;
|
|
19
19
|
/**
|
|
@@ -30,6 +30,15 @@ export declare function addVerdictRunCount(key: 'orphans-reaped', count: number)
|
|
|
30
30
|
export declare function resetVerdictEmission(): void;
|
|
31
31
|
/** True once this run has written its `FIREFORGE-VERDICT:` line. */
|
|
32
32
|
export declare function verdictEmitted(): boolean;
|
|
33
|
+
/** Installs (or, with `undefined`, removes) the sub-run capture slot. */
|
|
34
|
+
export declare function setVerdictCapture(slot: {
|
|
35
|
+
line?: string;
|
|
36
|
+
} | undefined): void;
|
|
37
|
+
/**
|
|
38
|
+
* Emits a multi-run aggregate verdict. `body` is everything after the
|
|
39
|
+
* `FIREFORGE-VERDICT: ` prefix.
|
|
40
|
+
*/
|
|
41
|
+
export declare function emitAggregateVerdict(body: string): void;
|
|
33
42
|
/**
|
|
34
43
|
* Emits the classifier-derived verdict for a completed harness run, with
|
|
35
44
|
* the aggregate `shards=<passed>/<total>` suffix for sharded runs.
|
|
@@ -51,11 +51,31 @@ export function resetVerdictEmission() {
|
|
|
51
51
|
emitted = false;
|
|
52
52
|
armed = true;
|
|
53
53
|
runAttributes.clear();
|
|
54
|
+
capture = undefined;
|
|
54
55
|
setStdoutSealed(false);
|
|
55
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* The sub-run capture slot of a multi-run invocation (`--repeat`, mixed
|
|
59
|
+
* harnesses, `--negative-control`; see `test-orchestrate.ts`). While one is
|
|
60
|
+
* installed, a sub-run's verdict line is recorded here, first write wins as
|
|
61
|
+
* on stdout, and nothing is printed or sealed, so the invocation can still
|
|
62
|
+
* end with its own single aggregate line.
|
|
63
|
+
*/
|
|
64
|
+
let capture;
|
|
56
65
|
/** True once this run has written its `FIREFORGE-VERDICT:` line. */
|
|
57
66
|
export function verdictEmitted() {
|
|
58
|
-
return emitted;
|
|
67
|
+
return capture === undefined ? emitted : capture.line !== undefined;
|
|
68
|
+
}
|
|
69
|
+
/** Installs (or, with `undefined`, removes) the sub-run capture slot. */
|
|
70
|
+
export function setVerdictCapture(slot) {
|
|
71
|
+
capture = slot;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Emits a multi-run aggregate verdict. `body` is everything after the
|
|
75
|
+
* `FIREFORGE-VERDICT: ` prefix.
|
|
76
|
+
*/
|
|
77
|
+
export function emitAggregateVerdict(body) {
|
|
78
|
+
writeVerdictLine(`FIREFORGE-VERDICT: ${body}`);
|
|
59
79
|
}
|
|
60
80
|
/**
|
|
61
81
|
* Writes the run's single verdict line to raw stdout (clack's renderer can
|
|
@@ -68,6 +88,10 @@ export function verdictEmitted() {
|
|
|
68
88
|
* rendering after an emit-then-throw path, routes to stderr.
|
|
69
89
|
*/
|
|
70
90
|
function writeVerdictLine(line) {
|
|
91
|
+
if (capture !== undefined) {
|
|
92
|
+
capture.line ??= line;
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
71
95
|
if (emitted)
|
|
72
96
|
return;
|
|
73
97
|
emitted = true;
|
|
@@ -130,6 +154,9 @@ export function emitFailVerdict(reason, note) {
|
|
|
130
154
|
export function emitKilledVerdict(signal) {
|
|
131
155
|
if (!armed || emitted)
|
|
132
156
|
return false;
|
|
157
|
+
// A kill ends the whole invocation, so a sub-run capture must not
|
|
158
|
+
// swallow the one line that says so.
|
|
159
|
+
capture = undefined;
|
|
133
160
|
writeVerdictLine(`FIREFORGE-VERDICT: FAIL reason=killed signal=${signal}`);
|
|
134
161
|
return true;
|
|
135
162
|
}
|
|
@@ -10,8 +10,13 @@ import type { TestOptions } from '../types/commands/index.js';
|
|
|
10
10
|
* since no harness classification exists for such a run. Writers closer to
|
|
11
11
|
* the harness (doctor, canary, single, sharded, the engine-generation
|
|
12
12
|
* guard) emit first and win.
|
|
13
|
+
* The lock-free checks (launchable binary, named-file checkJs, the
|
|
14
|
+
* `--wait-browser`/`--wait-port` budgets) run first, outside `underLock`,
|
|
15
|
+
* so a run that would be refused or would only wait never holds a place in
|
|
16
|
+
* the engine lock queue. See `test-prelock.ts`.
|
|
13
17
|
* @param projectRoot - Root directory of the project
|
|
14
18
|
* @param testPaths - Test file or directory paths
|
|
15
19
|
* @param options - Test options
|
|
20
|
+
* @param underLock - Runs the rest of the command under the engine session lock
|
|
16
21
|
*/
|
|
17
|
-
export declare function testCommand(projectRoot: string, testPaths: string[], options?: TestOptions): Promise<void>;
|
|
22
|
+
export declare function testCommand(projectRoot: string, testPaths: string[], options?: TestOptions, underLock?: (body: () => Promise<void>) => Promise<void>): Promise<void>;
|