@hominis/fireforge 0.50.0 → 0.51.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 +47 -0
- package/dist/build-info.json +4 -4
- package/dist/src/commands/check-report.d.ts +17 -4
- package/dist/src/commands/check-report.js +19 -6
- package/dist/src/commands/check-run.d.ts +5 -0
- package/dist/src/commands/check-run.js +9 -1
- package/dist/src/commands/check-steps.d.ts +8 -0
- package/dist/src/commands/check-steps.js +31 -6
- package/dist/src/commands/check.js +1 -1
- package/dist/src/commands/declarations-report.d.ts +43 -0
- package/dist/src/commands/declarations-report.js +76 -0
- package/dist/src/commands/export-placement-gate.js +8 -0
- package/dist/src/commands/furnace/rename-registrations.js +7 -2
- package/dist/src/commands/furnace/validate.js +26 -1
- package/dist/src/commands/lint-per-patch.js +6 -2
- package/dist/src/commands/lint-per-run-checkjs.js +2 -2
- package/dist/src/commands/lint-report.js +7 -2
- package/dist/src/commands/manifest.js +2 -0
- package/dist/src/commands/park.d.ts +22 -0
- package/dist/src/commands/park.js +232 -0
- package/dist/src/commands/patch/delete.js +11 -6
- package/dist/src/commands/patch/index.d.ts +3 -3
- package/dist/src/commands/patch/index.js +8 -4
- package/dist/src/commands/patch/lint-ignore-from.d.ts +44 -0
- package/dist/src/commands/patch/lint-ignore-from.js +142 -0
- package/dist/src/commands/patch/lint-ignore.js +39 -29
- package/dist/src/commands/patch/move-files-create.js +11 -4
- package/dist/src/commands/patch/move-files-into.js +10 -4
- package/dist/src/commands/patch/move-files.js +3 -1
- package/dist/src/commands/patch/removal-created-files.d.ts +48 -0
- package/dist/src/commands/patch/removal-created-files.js +83 -0
- package/dist/src/commands/patch/rename.js +15 -3
- package/dist/src/commands/patch/reserve.d.ts +33 -0
- package/dist/src/commands/patch/reserve.js +152 -0
- package/dist/src/commands/patch/retire.js +11 -6
- package/dist/src/commands/patch/split-plan.d.ts +10 -1
- package/dist/src/commands/patch/split-plan.js +16 -1
- package/dist/src/commands/patch/split.js +14 -5
- package/dist/src/commands/patch/stage.d.ts +34 -0
- package/dist/src/commands/patch/stage.js +153 -0
- package/dist/src/commands/patch/staged-dependency-remove.d.ts +31 -0
- package/dist/src/commands/patch/staged-dependency-remove.js +80 -0
- package/dist/src/commands/patch/staged-dependency.js +40 -98
- package/dist/src/commands/re-export-adjacent.d.ts +5 -0
- package/dist/src/commands/re-export-adjacent.js +4 -3
- package/dist/src/commands/re-export-bulk-scan.d.ts +2 -1
- package/dist/src/commands/re-export-bulk-scan.js +22 -7
- package/dist/src/commands/re-export-hunks.d.ts +35 -0
- package/dist/src/commands/re-export-hunks.js +220 -0
- package/dist/src/commands/re-export-options.js +29 -2
- package/dist/src/commands/re-export-purity-scope.d.ts +18 -0
- package/dist/src/commands/re-export-purity-scope.js +131 -0
- package/dist/src/commands/re-export-register.js +15 -1
- package/dist/src/commands/re-export-removed.d.ts +1 -1
- package/dist/src/commands/re-export-removed.js +17 -2
- package/dist/src/commands/re-export-scan.d.ts +2 -0
- package/dist/src/commands/re-export-scan.js +4 -1
- package/dist/src/commands/re-export-single.js +102 -42
- package/dist/src/commands/re-export-stale-registrations.d.ts +18 -0
- package/dist/src/commands/re-export-stale-registrations.js +62 -0
- package/dist/src/commands/re-export.js +35 -34
- package/dist/src/commands/test-build-furnace.d.ts +3 -2
- package/dist/src/commands/test-build-furnace.js +8 -2
- package/dist/src/commands/test-build-phase.js +24 -7
- package/dist/src/commands/test-checkjs.js +3 -2
- package/dist/src/commands/test-dispatch.d.ts +39 -0
- package/dist/src/commands/test-dispatch.js +211 -0
- package/dist/src/commands/test-furnace-reach.d.ts +37 -0
- package/dist/src/commands/test-furnace-reach.js +132 -0
- package/dist/src/commands/test-orchestrate.d.ts +9 -0
- package/dist/src/commands/test-orchestrate.js +22 -4
- package/dist/src/commands/test-perf-scope.d.ts +21 -0
- package/dist/src/commands/test-perf-scope.js +55 -0
- package/dist/src/commands/test-prelock.js +100 -4
- package/dist/src/commands/test-register.js +18 -3
- package/dist/src/commands/test-results.d.ts +62 -0
- package/dist/src/commands/test-results.js +62 -0
- package/dist/src/commands/test-run.d.ts +13 -55
- package/dist/src/commands/test-run.js +73 -101
- package/dist/src/commands/test-shards.d.ts +110 -0
- package/dist/src/commands/test-shards.js +255 -0
- package/dist/src/commands/test-stale-gate.d.ts +5 -1
- package/dist/src/commands/test-stale-gate.js +4 -2
- package/dist/src/commands/test-verdict.d.ts +2 -1
- package/dist/src/commands/test.d.ts +1 -1
- package/dist/src/commands/test.js +65 -75
- package/dist/src/commands/token-lint.js +48 -8
- package/dist/src/commands/verify.js +9 -2
- package/dist/src/core/build-prepare.d.ts +6 -0
- package/dist/src/core/build-prepare.js +29 -3
- package/dist/src/core/config-paths.d.ts +1 -1
- package/dist/src/core/config-paths.js +2 -0
- package/dist/src/core/config-validate-check.js +83 -7
- package/dist/src/core/config-validate-test-toolchains.js +14 -0
- package/dist/src/core/config-validate-token-scales.d.ts +3 -0
- package/dist/src/core/config-validate-token-scales.js +47 -5
- package/dist/src/core/config-validate.js +6 -0
- package/dist/src/core/coverage-extend.js +2 -2
- package/dist/src/core/coverage-records.d.ts +11 -0
- package/dist/src/core/coverage-records.js +34 -0
- package/dist/src/core/engine-session-lock.d.ts +6 -0
- package/dist/src/core/engine-session-lock.js +10 -2
- package/dist/src/core/file-lock-shared.d.ts +57 -0
- package/dist/src/core/file-lock-shared.js +117 -0
- package/dist/src/core/file-lock.d.ts +9 -0
- package/dist/src/core/file-lock.js +58 -2
- package/dist/src/core/furnace-apply.js +10 -10
- package/dist/src/core/furnace-config-validate-exceptions.d.ts +10 -3
- package/dist/src/core/furnace-config-validate-exceptions.js +39 -8
- package/dist/src/core/furnace-config-validate.js +7 -1
- package/dist/src/core/furnace-registration-jar-inplace.d.ts +12 -0
- package/dist/src/core/furnace-registration-jar-inplace.js +80 -0
- package/dist/src/core/furnace-validate-accessibility.d.ts +4 -2
- package/dist/src/core/furnace-validate-accessibility.js +61 -27
- package/dist/src/core/furnace-validate-exceptions.d.ts +12 -4
- package/dist/src/core/furnace-validate-exceptions.js +41 -4
- package/dist/src/core/furnace-validate-icon-button.js +30 -1
- package/dist/src/core/history-log-file.js +8 -1
- package/dist/src/core/line-prefixer.d.ts +18 -0
- package/dist/src/core/line-prefixer.js +34 -0
- package/dist/src/core/mach.d.ts +7 -0
- package/dist/src/core/mach.js +24 -9
- package/dist/src/core/module-resolution-preflight.d.ts +19 -3
- package/dist/src/core/module-resolution-preflight.js +24 -6
- package/dist/src/core/negative-control-compiled.d.ts +27 -0
- package/dist/src/core/negative-control-compiled.js +73 -0
- package/dist/src/core/negative-control-diff.d.ts +14 -2
- package/dist/src/core/negative-control-diff.js +49 -6
- package/dist/src/core/negative-control.d.ts +12 -0
- package/dist/src/core/negative-control.js +48 -5
- package/dist/src/core/park-store.d.ts +63 -0
- package/dist/src/core/park-store.js +216 -0
- package/dist/src/core/patch-export-update.d.ts +4 -0
- package/dist/src/core/patch-export-update.js +12 -3
- package/dist/src/core/patch-export.d.ts +3 -2
- package/dist/src/core/patch-export.js +21 -14
- package/dist/src/core/patch-files.js +26 -8
- package/dist/src/core/patch-lint-checkjs-any-import.d.ts +6 -4
- package/dist/src/core/patch-lint-checkjs-any-import.js +9 -7
- package/dist/src/core/patch-lint-checkjs-diagnostics.d.ts +17 -0
- package/dist/src/core/patch-lint-checkjs-diagnostics.js +32 -0
- package/dist/src/core/patch-lint-checkjs.d.ts +10 -1
- package/dist/src/core/patch-lint-checkjs.js +13 -3
- package/dist/src/core/patch-lint-cross.d.ts +3 -1
- package/dist/src/core/patch-lint-cross.js +5 -0
- package/dist/src/core/patch-lint-css.js +14 -10
- package/dist/src/core/patch-lint-forward-registration-extended.d.ts +5 -0
- package/dist/src/core/patch-lint-forward-registration-extended.js +1 -1
- package/dist/src/core/patch-lint-ignore.d.ts +56 -12
- package/dist/src/core/patch-lint-ignore.js +173 -43
- package/dist/src/core/patch-lint-ownership.d.ts +10 -0
- package/dist/src/core/patch-lint-ownership.js +26 -0
- package/dist/src/core/patch-lint-size.js +9 -1
- package/dist/src/core/patch-lint-token-scale.d.ts +26 -8
- package/dist/src/core/patch-lint-token-scale.js +206 -29
- package/dist/src/core/patch-lint.js +2 -2
- package/dist/src/core/patch-manifest-io.js +8 -1
- package/dist/src/core/patch-manifest-key-order.d.ts +24 -0
- package/dist/src/core/patch-manifest-key-order.js +61 -0
- package/dist/src/core/patch-manifest-stage.d.ts +46 -0
- package/dist/src/core/patch-manifest-stage.js +96 -0
- package/dist/src/core/patch-manifest-validate.js +27 -1
- package/dist/src/core/patch-order-legal.d.ts +2 -1
- package/dist/src/core/patch-order-legal.js +7 -2
- package/dist/src/core/patch-policy.d.ts +6 -2
- package/dist/src/core/patch-policy.js +14 -7
- package/dist/src/core/patch-retired.d.ts +26 -4
- package/dist/src/core/patch-retired.js +40 -2
- package/dist/src/core/run-log.d.ts +6 -0
- package/dist/src/core/run-log.js +17 -1
- package/dist/src/core/test-harness-crash.d.ts +2 -1
- package/dist/src/core/test-harness-crash.js +5 -8
- package/dist/src/core/test-harness-signature.d.ts +16 -0
- package/dist/src/core/test-harness-signature.js +19 -1
- package/dist/src/core/test-raw-log.d.ts +66 -0
- package/dist/src/core/test-raw-log.js +122 -0
- package/dist/src/core/test-run-completeness.d.ts +36 -3
- package/dist/src/core/test-run-completeness.js +57 -17
- package/dist/src/core/test-xpcshell-retry.d.ts +2 -0
- package/dist/src/core/test-xpcshell-retry.js +2 -1
- package/dist/src/core/token-lint.d.ts +5 -2
- package/dist/src/core/token-lint.js +8 -3
- package/dist/src/core/token-sibling-sheets.d.ts +46 -0
- package/dist/src/core/token-sibling-sheets.js +119 -0
- package/dist/src/core/tree-guard.js +2 -0
- package/dist/src/core/typecheck-shim.d.ts +5 -1
- package/dist/src/core/typecheck-shim.js +115 -2
- package/dist/src/core/typecheck.js +3 -1
- package/dist/src/index.d.ts +9 -3
- package/dist/src/index.js +4 -1
- package/dist/src/types/commands/index.d.ts +2 -2
- package/dist/src/types/commands/options.d.ts +68 -7
- package/dist/src/types/commands/patches.d.ts +43 -0
- package/dist/src/types/config.d.ts +74 -2
- package/dist/src/types/furnace.d.ts +20 -0
- package/dist/src/types/index.d.ts +1 -1
- package/dist/src/utils/options.d.ts +3 -2
- package/dist/src/utils/options.js +9 -4
- package/docs/configuration.md +94 -9
- package/docs/furnace.md +43 -1
- package/docs/lifecycle-invariants.md +13 -11
- package/docs/machine-output.md +64 -18
- package/docs/patch-workflow.md +174 -13
- package/docs/testing.md +73 -4
- package/package.json +3 -3
|
@@ -19,7 +19,7 @@ import { applyReExportFilesPositionalFolding, validateReExportOptionCombinations
|
|
|
19
19
|
import { validateExpectedRemovals } from './re-export-removed.js';
|
|
20
20
|
import { normalizeEngineRelativeInput, normalizeScanFiles } from './re-export-scan.js';
|
|
21
21
|
import { assignScanFileOwners, scanOptionsForPatch } from './re-export-scan-owner.js';
|
|
22
|
-
import { reExportSinglePatchWithIndexLockRetry } from './re-export-single.js';
|
|
22
|
+
import { reExportSinglePatchWithIndexLockRetry, } from './re-export-single.js';
|
|
23
23
|
import { formatReExportRunSummary } from './re-export-summary.js';
|
|
24
24
|
async function resolveSelectedPatches(patches, options, manifest) {
|
|
25
25
|
if (options.all) {
|
|
@@ -59,6 +59,27 @@ async function resolveSelectedPatches(patches, options, manifest) {
|
|
|
59
59
|
const selectedFilenames = selected;
|
|
60
60
|
return manifest.patches.filter((p) => selectedFilenames.includes(p.filename));
|
|
61
61
|
}
|
|
62
|
+
/** Refreshes each selected patch in turn, collecting which wrote and which failed. */
|
|
63
|
+
async function reExportEach(args) {
|
|
64
|
+
const { selectedPatches, options, scanAssignments, progress, startedAt, shared } = args;
|
|
65
|
+
const written = [];
|
|
66
|
+
const failed = [];
|
|
67
|
+
for (const [index, patch] of selectedPatches.entries()) {
|
|
68
|
+
const patchOptions = scanOptionsForPatch(options, patch.filename, scanAssignments);
|
|
69
|
+
progress.message(`Re-exporting ${index + 1}/${selectedPatches.length}: ${patch.filename} (${patch.filesAffected.length} file(s), ${elapsedSince(startedAt)} elapsed)...`);
|
|
70
|
+
try {
|
|
71
|
+
if (await reExportSinglePatchWithIndexLockRetry({ ...shared, patch, options: patchOptions })) {
|
|
72
|
+
written.push(patch.filename);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
failed.push(patch.filename);
|
|
77
|
+
warn(`Failed to re-export ${patch.filename}`);
|
|
78
|
+
warn(toError(error).message);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return { written, failed };
|
|
82
|
+
}
|
|
62
83
|
/**
|
|
63
84
|
* Runs the re-export command to regenerate existing patches from current engine state.
|
|
64
85
|
* @param projectRoot - Root directory of the project
|
|
@@ -105,7 +126,7 @@ export async function reExportCommand(projectRoot, patches, options) {
|
|
|
105
126
|
outro('Nothing to re-export');
|
|
106
127
|
return;
|
|
107
128
|
}
|
|
108
|
-
const expectedRemovals = await validateExpectedRemovals(paths, selectedPatches, options);
|
|
129
|
+
const expectedRemovals = await validateExpectedRemovals(paths, selectedPatches, options, manifest.patches);
|
|
109
130
|
if (expectedRemovals.length > 0)
|
|
110
131
|
options = { ...options, expectRemoved: expectedRemovals };
|
|
111
132
|
if (scanFilesByPatch !== undefined) {
|
|
@@ -125,7 +146,7 @@ export async function reExportCommand(projectRoot, patches, options) {
|
|
|
125
146
|
// we write anything.
|
|
126
147
|
if (options.files !== undefined) {
|
|
127
148
|
const filesConfig = await loadConfig(projectRoot);
|
|
128
|
-
await withDryRunReExportLock(paths.fireforgeDir, isDryRun, () => withDryRunPurityGuard(paths.engine, paths.patches, isDryRun, () => reExportFilesInPlace(paths, selectedPatches, options, filesConfig)));
|
|
149
|
+
await withDryRunReExportLock(paths.fireforgeDir, isDryRun, () => withDryRunPurityGuard(paths.engine, paths.patches, isDryRun, () => reExportFilesInPlace(paths, selectedPatches, options, filesConfig), { patches: selectedPatches }));
|
|
129
150
|
return;
|
|
130
151
|
}
|
|
131
152
|
const config = await loadConfig(projectRoot);
|
|
@@ -139,38 +160,18 @@ export async function reExportCommand(projectRoot, patches, options) {
|
|
|
139
160
|
// reports includes it.
|
|
140
161
|
const startedAt = Date.now();
|
|
141
162
|
const lintCtx = await buildReExportLintContext(projectRoot, paths, config, options.noCache === true, new Set(selectedPatches.map((patch) => patch.filename)));
|
|
142
|
-
let reExported = 0;
|
|
143
|
-
const reExportedFilenames = [];
|
|
144
|
-
const failedFilenames = [];
|
|
145
163
|
const progress = spinner('Preparing re-export...');
|
|
146
|
-
await withDryRunReExportLock(paths.fireforgeDir, isDryRun,
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
config,
|
|
158
|
-
adjacentCtx,
|
|
159
|
-
driftCtx,
|
|
160
|
-
lintCtx,
|
|
161
|
-
});
|
|
162
|
-
if (exported) {
|
|
163
|
-
reExported++;
|
|
164
|
-
reExportedFilenames.push(patch.filename);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
catch (error) {
|
|
168
|
-
failedFilenames.push(patch.filename);
|
|
169
|
-
warn(`Failed to re-export ${patch.filename}`);
|
|
170
|
-
warn(toError(error).message);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
}));
|
|
164
|
+
const outcome = await withDryRunReExportLock(paths.fireforgeDir, isDryRun, () => withDryRunPurityGuard(paths.engine, paths.patches, isDryRun, () => reExportEach({
|
|
165
|
+
selectedPatches,
|
|
166
|
+
options,
|
|
167
|
+
scanAssignments,
|
|
168
|
+
progress,
|
|
169
|
+
startedAt,
|
|
170
|
+
shared: { paths, manifest, isDryRun, config, adjacentCtx, driftCtx, lintCtx },
|
|
171
|
+
}), { patches: selectedPatches }));
|
|
172
|
+
const reExportedFilenames = outcome.written;
|
|
173
|
+
const failedFilenames = outcome.failed;
|
|
174
|
+
const reExported = reExportedFilenames.length;
|
|
174
175
|
await finishReExportLint(lintCtx);
|
|
175
176
|
warnUnseenExpectedDrift(driftCtx);
|
|
176
177
|
warnUnseenApprovedUnmanaged(adjacentCtx);
|
|
@@ -16,6 +16,7 @@ import type { BuildPreparation } from '../core/build-prepare.js';
|
|
|
16
16
|
*
|
|
17
17
|
* @param projectRoot - Root directory of the project
|
|
18
18
|
* @param preparation - Result of `prepareBuildEnvironment`
|
|
19
|
-
* @param skipped - True under `--no-furnace-apply`
|
|
19
|
+
* @param skipped - True under `--no-furnace-apply` / `--furnace-apply none`
|
|
20
|
+
* @param unreached - Components `--furnace-apply reached` left out
|
|
20
21
|
*/
|
|
21
|
-
export declare function reportPreTestFurnace(projectRoot: string, preparation: BuildPreparation, skipped: boolean): Promise<void>;
|
|
22
|
+
export declare function reportPreTestFurnace(projectRoot: string, preparation: BuildPreparation, skipped: boolean, unreached?: readonly string[]): Promise<void>;
|
|
@@ -20,9 +20,10 @@ import { setVerdictRunAttribute } from './test-verdict.js';
|
|
|
20
20
|
*
|
|
21
21
|
* @param projectRoot - Root directory of the project
|
|
22
22
|
* @param preparation - Result of `prepareBuildEnvironment`
|
|
23
|
-
* @param skipped - True under `--no-furnace-apply`
|
|
23
|
+
* @param skipped - True under `--no-furnace-apply` / `--furnace-apply none`
|
|
24
|
+
* @param unreached - Components `--furnace-apply reached` left out
|
|
24
25
|
*/
|
|
25
|
-
export async function reportPreTestFurnace(projectRoot, preparation, skipped) {
|
|
26
|
+
export async function reportPreTestFurnace(projectRoot, preparation, skipped, unreached) {
|
|
26
27
|
if (skipped) {
|
|
27
28
|
setVerdictRunAttribute('furnace-apply', 'skipped');
|
|
28
29
|
let pending = [];
|
|
@@ -37,6 +38,11 @@ export async function reportPreTestFurnace(projectRoot, preparation, skipped) {
|
|
|
37
38
|
: 'Furnace apply skipped (--no-furnace-apply): engine/ is packaged as it stands.');
|
|
38
39
|
return;
|
|
39
40
|
}
|
|
41
|
+
if (unreached !== undefined && unreached.length > 0) {
|
|
42
|
+
setVerdictRunAttribute('furnace-skipped', unreached.join(','));
|
|
43
|
+
notice(`--furnace-apply reached: ${unreached.length} component(s) with undeployed components/ ` +
|
|
44
|
+
`edits that no requested test reaches are NOT in this build: ${unreached.join(', ')}.`);
|
|
45
|
+
}
|
|
40
46
|
const applied = preparation.furnaceAppliedNames ?? [];
|
|
41
47
|
if (applied.length === 0)
|
|
42
48
|
return;
|
|
@@ -16,6 +16,7 @@ import { BuildError } from '../errors/build.js';
|
|
|
16
16
|
import { toError } from '../utils/errors.js';
|
|
17
17
|
import { info, notice, spinner, verbose } from '../utils/logger.js';
|
|
18
18
|
import { reportPreTestFurnace } from './test-build-furnace.js';
|
|
19
|
+
import { partitionReachedComponents, resolveFurnaceApplyMode } from './test-furnace-reach.js';
|
|
19
20
|
import { reportBuildOnlyCompletion } from './test-modes.js';
|
|
20
21
|
import { enforceStaleBuildGate, enforceStaticComponentsGate } from './test-stale-gate.js';
|
|
21
22
|
import { emitPassVerdict } from './test-verdict.js';
|
|
@@ -26,7 +27,7 @@ import { emitPassVerdict } from './test-verdict.js';
|
|
|
26
27
|
* @param options - See {@link RunPreTestBuildOptions}
|
|
27
28
|
*/
|
|
28
29
|
async function runPreTestBuild(options) {
|
|
29
|
-
const { projectRoot, paths, projectConfig, harnessRetries, testPackagingCoverage, refuseUnexportedDrift, extend,
|
|
30
|
+
const { projectRoot, paths, projectConfig, harnessRetries, testPackagingCoverage, refuseUnexportedDrift, extend, furnace, } = options;
|
|
30
31
|
await withBuildLock(projectRoot, async () => {
|
|
31
32
|
// Pass the previous baseline exactly like `fireforge build` does, so
|
|
32
33
|
// auto-configure runs under the same conditions on both paths. The
|
|
@@ -46,9 +47,10 @@ async function runPreTestBuild(options) {
|
|
|
46
47
|
const preparation = await prepareBuildEnvironment(projectRoot, paths, projectConfig, {
|
|
47
48
|
previousBaseline,
|
|
48
49
|
refuseUnexportedDrift,
|
|
49
|
-
skipFurnaceApply,
|
|
50
|
+
skipFurnaceApply: furnace.skip,
|
|
51
|
+
furnaceComponents: furnace.only,
|
|
50
52
|
});
|
|
51
|
-
await reportPreTestFurnace(projectRoot, preparation,
|
|
53
|
+
await reportPreTestFurnace(projectRoot, preparation, furnace.skip, furnace.unreached);
|
|
52
54
|
// Which earlier claims this scoped build keeps, per staged path. The
|
|
53
55
|
// mozconfig half can only be checked once prepareBuildEnvironment has
|
|
54
56
|
// regenerated engine/mozconfig (the file this build will configure
|
|
@@ -161,13 +163,28 @@ function assertRefuseUnexportedDriftUsage(options) {
|
|
|
161
163
|
return;
|
|
162
164
|
throw new InvalidArgumentError('--refuse-unexported-drift requires --build or --build-only: it guards the pre-test build, and this run dispatches none.', '--refuse-unexported-drift');
|
|
163
165
|
}
|
|
164
|
-
/** Refuses `--no-furnace-apply` on a run that dispatches no build, for the same reason. */
|
|
166
|
+
/** Refuses `--no-furnace-apply`/`--furnace-apply` on a run that dispatches no build, for the same reason. */
|
|
165
167
|
function assertNoFurnaceApplyUsage(options) {
|
|
166
|
-
if (options.furnaceApply
|
|
168
|
+
if (options.furnaceApply === undefined || options.furnaceApply === true)
|
|
167
169
|
return;
|
|
168
170
|
if (options.build === true || options.buildOnly === true)
|
|
169
171
|
return;
|
|
170
|
-
|
|
172
|
+
const flag = options.furnaceApply === false ? '--no-furnace-apply' : '--furnace-apply';
|
|
173
|
+
throw new InvalidArgumentError(`${flag} requires --build or --build-only: it shapes the pre-test build's Furnace apply, and this run dispatches no build.`, flag);
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* The Furnace part of a pre-test build: skip the apply, apply everything,
|
|
177
|
+
* or apply only the components the request reaches (and name the rest).
|
|
178
|
+
*/
|
|
179
|
+
async function planPreTestFurnace(projectRoot, paths, projectConfig, options, normalizedPaths) {
|
|
180
|
+
const mode = resolveFurnaceApplyMode(options, projectConfig);
|
|
181
|
+
if (mode === 'none')
|
|
182
|
+
return { skip: true };
|
|
183
|
+
// A path-less build packages the whole suite, which reaches everything.
|
|
184
|
+
if (mode === 'all' || normalizedPaths.length === 0)
|
|
185
|
+
return { skip: false };
|
|
186
|
+
const { reached, skipped } = await partitionReachedComponents(projectRoot, paths.engine, normalizedPaths);
|
|
187
|
+
return { skip: false, only: reached, unreached: skipped };
|
|
171
188
|
}
|
|
172
189
|
/** Renders the `recordedBy` invocation string surfaced by `status --test-coverage`. */
|
|
173
190
|
function describeBuildInvocation(extending, coverage) {
|
|
@@ -218,7 +235,7 @@ export async function runTestBuildPhase(projectRoot, paths, projectConfig, harne
|
|
|
218
235
|
testPackagingCoverage: coverage,
|
|
219
236
|
refuseUnexportedDrift: options.refuseUnexportedDrift === true,
|
|
220
237
|
extend: extending ? { requestedPaths: normalizedPaths } : undefined,
|
|
221
|
-
|
|
238
|
+
furnace: await planPreTestFurnace(projectRoot, paths, projectConfig, options, normalizedPaths),
|
|
222
239
|
});
|
|
223
240
|
info('');
|
|
224
241
|
if (options.buildOnly) {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
2
|
import { findUnownedSupportHeads, formatUnownedSupportHeadNotice, } from '../core/checkjs-support-heads.js';
|
|
3
|
+
import { buildPatchQueueContext } from '../core/patch-lint.js';
|
|
3
4
|
import { runCheckJsTestFilesGrouped } from '../core/patch-lint-checkjs.js';
|
|
4
|
-
import { isTestScriptFile } from '../core/patch-lint-ownership.js';
|
|
5
|
+
import { isTestScriptFile, resolvableModulesForTests } from '../core/patch-lint-ownership.js';
|
|
5
6
|
import { loadPatchesManifest } from '../core/patch-manifest.js';
|
|
6
7
|
import { analyzeTestPathScopes } from '../core/test-path-scope.js';
|
|
7
8
|
import { GeneralError } from '../errors/base.js';
|
|
@@ -24,7 +25,7 @@ export async function checkRequestedTestTypes(paths, config, requested) {
|
|
|
24
25
|
owned.add(head.file);
|
|
25
26
|
notice(`Pre-build checkJs: ${formatUnownedSupportHeadNotice(head)}`);
|
|
26
27
|
}
|
|
27
|
-
const result = await runCheckJsTestFilesGrouped(paths.engine, owned, config.patchLint, paths.root, selected);
|
|
28
|
+
const result = await runCheckJsTestFilesGrouped(paths.engine, owned, config.patchLint, paths.root, selected, resolvableModulesForTests(await buildPatchQueueContext(paths.patches)));
|
|
28
29
|
const issues = [...result.global, ...[...result.byFile.values()].flat()];
|
|
29
30
|
for (const issue of issues)
|
|
30
31
|
notice(`Pre-build checkJs: ${issue.file}: ${issue.message}`);
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The dispatch half of one `fireforge test` run, split out of `test.ts`:
|
|
3
|
+
* the engine-generation guard around the harness, and the choice between
|
|
4
|
+
* one combined invocation, per-argument shards, `--shard-size` chunks and
|
|
5
|
+
* `--jobs` parallel harness instances.
|
|
6
|
+
*/
|
|
7
|
+
import type { PostRebuildFailureContext } from '../core/test-harness-output.js';
|
|
8
|
+
import type { TestOptions } from '../types/commands/index.js';
|
|
9
|
+
import type { FireForgeConfig } from '../types/config.js';
|
|
10
|
+
import { type TestRunContext } from './test-run.js';
|
|
11
|
+
import { type ShardGroup } from './test-shards.js';
|
|
12
|
+
/** Everything the dispatch half needs from the run body. */
|
|
13
|
+
export interface TestDispatchInputs {
|
|
14
|
+
engineDir: string;
|
|
15
|
+
runCtx: TestRunContext;
|
|
16
|
+
dispatchGroups: ShardGroup[];
|
|
17
|
+
options: TestOptions;
|
|
18
|
+
projectConfig: FireForgeConfig;
|
|
19
|
+
canaryPath: string | undefined;
|
|
20
|
+
shuffleSeed: number | undefined;
|
|
21
|
+
normalizedPaths: string[];
|
|
22
|
+
xpcshellOnly: boolean;
|
|
23
|
+
postRebuildContext: PostRebuildFailureContext | undefined;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Refuses `--jobs`/`--shard-size` shapes that cannot work, before any lock
|
|
27
|
+
* or build: both need explicit paths and a real dispatch, and parallel
|
|
28
|
+
* instances cannot share one operator-pinned Marionette port.
|
|
29
|
+
*
|
|
30
|
+
* @param testPaths - Requested paths
|
|
31
|
+
* @param options - Parsed options
|
|
32
|
+
*/
|
|
33
|
+
export declare function assertParallelTestUsage(testPaths: readonly string[], options: TestOptions): void;
|
|
34
|
+
/**
|
|
35
|
+
* Runs the planned dispatch and finalizes its verdict.
|
|
36
|
+
*
|
|
37
|
+
* @param inputs - See {@link TestDispatchInputs}
|
|
38
|
+
*/
|
|
39
|
+
export declare function dispatchTests(inputs: TestDispatchInputs): Promise<void>;
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* The dispatch half of one `fireforge test` run, split out of `test.ts`:
|
|
4
|
+
* the engine-generation guard around the harness, and the choice between
|
|
5
|
+
* one combined invocation, per-argument shards, `--shard-size` chunks and
|
|
6
|
+
* `--jobs` parallel harness instances.
|
|
7
|
+
*/
|
|
8
|
+
import { createServer } from 'node:net';
|
|
9
|
+
import { assertEngineGenerationUnchanged, snapshotEngineGeneration, } from '../core/engine-session-lock.js';
|
|
10
|
+
import { autoPortsRequested } from '../core/harness-port-fallback.js';
|
|
11
|
+
import { extractForwardedMarionettePort } from '../core/marionette-port.js';
|
|
12
|
+
import { GeneralError, InvalidArgumentError } from '../errors/base.js';
|
|
13
|
+
import { BuildError } from '../errors/build.js';
|
|
14
|
+
import { notice } from '../utils/logger.js';
|
|
15
|
+
import { diagnoseShardOutcome, finalizeSingleRunOutcome } from './test-diagnose.js';
|
|
16
|
+
import { shuffleTestGroups } from './test-harness-env.js';
|
|
17
|
+
import { reportCanaryOutcome } from './test-modes.js';
|
|
18
|
+
import { perfGenerationTolerance, runGenerationTolerance } from './test-perf-scope.js';
|
|
19
|
+
import { runTestsWithRetries } from './test-run.js';
|
|
20
|
+
import { chunkShardGroups, finalizeShardedOutcome, runShardedTests, splitContiguous, } from './test-shards.js';
|
|
21
|
+
import { emitFailVerdict, setVerdictRunAttribute } from './test-verdict.js';
|
|
22
|
+
/**
|
|
23
|
+
* Refuses `--jobs`/`--shard-size` shapes that cannot work, before any lock
|
|
24
|
+
* or build: both need explicit paths and a real dispatch, and parallel
|
|
25
|
+
* instances cannot share one operator-pinned Marionette port.
|
|
26
|
+
*
|
|
27
|
+
* @param testPaths - Requested paths
|
|
28
|
+
* @param options - Parsed options
|
|
29
|
+
*/
|
|
30
|
+
export function assertParallelTestUsage(testPaths, options) {
|
|
31
|
+
const parallel = (options.jobs ?? 1) > 1;
|
|
32
|
+
const flags = [
|
|
33
|
+
...(parallel ? ['--jobs'] : []),
|
|
34
|
+
...(options.shardSize !== undefined ? ['--shard-size'] : []),
|
|
35
|
+
];
|
|
36
|
+
if (flags.length === 0)
|
|
37
|
+
return;
|
|
38
|
+
const flag = flags.join('/');
|
|
39
|
+
if (testPaths.length === 0 || options.canary !== undefined || options.auto === true) {
|
|
40
|
+
throw new InvalidArgumentError(`${flag} splits explicit test paths, so it needs paths and cannot be combined with --canary or --auto.`, flags[0]);
|
|
41
|
+
}
|
|
42
|
+
if (options.buildOnly === true) {
|
|
43
|
+
throw new InvalidArgumentError(`${flag} shapes a dispatch; --build-only runs none.`, flags[0]);
|
|
44
|
+
}
|
|
45
|
+
if (parallel &&
|
|
46
|
+
(options.marionettePort !== undefined ||
|
|
47
|
+
extractForwardedMarionettePort(options.machArg ?? []) !== undefined)) {
|
|
48
|
+
throw new InvalidArgumentError('--jobs gives every harness instance its own Marionette port, so it cannot be combined with a pinned --marionette-port (or a forwarded one).', '--jobs');
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Verifies `engine/` did not change under the run in a way the run could
|
|
53
|
+
* see before any PASS verdict may print. On failure the run's verdict is
|
|
54
|
+
* `FAIL reason=inconclusive`, because the harness result exists but cannot
|
|
55
|
+
* be trusted (`preflight` would misdescribe a run whose harness already
|
|
56
|
+
* executed). It is emitted here so the guard's throw can never leave a
|
|
57
|
+
* stale or missing verdict line behind.
|
|
58
|
+
*/
|
|
59
|
+
async function verifyEngineGenerationOrEmitInconclusive(engineDir, before, inputs) {
|
|
60
|
+
try {
|
|
61
|
+
await assertEngineGenerationUnchanged(engineDir, before, inputs.perfSamples === undefined
|
|
62
|
+
? runGenerationTolerance(engineDir, inputs.requested, (count) => {
|
|
63
|
+
setVerdictRunAttribute('drift-tolerated', String(count));
|
|
64
|
+
})
|
|
65
|
+
: perfGenerationTolerance(engineDir));
|
|
66
|
+
if (inputs.powerChanged === true)
|
|
67
|
+
throw new GeneralError('Perf power source changed during the run; repeat this sitting on a stable power source.');
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
emitFailVerdict('inconclusive');
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/** A free TCP port on the loopback interface, for one harness instance. */
|
|
75
|
+
async function allocateLoopbackPort() {
|
|
76
|
+
return new Promise((resolve, reject) => {
|
|
77
|
+
const server = createServer();
|
|
78
|
+
server.once('error', reject);
|
|
79
|
+
server.listen(0, '127.0.0.1', () => {
|
|
80
|
+
const address = server.address();
|
|
81
|
+
const port = typeof address === 'object' && address !== null ? address.port : 0;
|
|
82
|
+
server.close(() => {
|
|
83
|
+
resolve(port);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The context for `--jobs` instance `index` (0-based): its own output
|
|
90
|
+
* prefix, and from the second instance on its own Marionette port, so
|
|
91
|
+
* browsers started at the same moment never race for 2828. The mochitest
|
|
92
|
+
* server ports are left to the engine harness's busy-port fallback, which
|
|
93
|
+
* `--jobs` requires (auto ports).
|
|
94
|
+
*/
|
|
95
|
+
async function instanceContext(base, index, xpcshellOnly) {
|
|
96
|
+
const ctx = { ...base, outputPrefix: `[job ${index + 1}] ` };
|
|
97
|
+
if (index === 0 || xpcshellOnly)
|
|
98
|
+
return ctx;
|
|
99
|
+
const port = await allocateLoopbackPort();
|
|
100
|
+
return {
|
|
101
|
+
...ctx,
|
|
102
|
+
baseExtraArgs: [
|
|
103
|
+
...base.baseExtraArgs,
|
|
104
|
+
`--setpref=marionette.port=${port}`,
|
|
105
|
+
`--marionette=127.0.0.1:${port}`,
|
|
106
|
+
],
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/** The shards to run, or undefined for one combined invocation. */
|
|
110
|
+
function planShards(inputs) {
|
|
111
|
+
const { dispatchGroups, options, canaryPath, shuffleSeed } = inputs;
|
|
112
|
+
if (canaryPath !== undefined)
|
|
113
|
+
return undefined;
|
|
114
|
+
const ordered = shuffleSeed === undefined ? dispatchGroups : shuffleTestGroups(dispatchGroups, shuffleSeed);
|
|
115
|
+
if (options.shardSize !== undefined)
|
|
116
|
+
return chunkShardGroups(ordered, options.shardSize);
|
|
117
|
+
const jobs = options.jobs ?? 1;
|
|
118
|
+
if (jobs > 1 && (options.shard === false || ordered.length === 1)) {
|
|
119
|
+
return splitContiguous(ordered.flatMap((group) => group.paths), jobs);
|
|
120
|
+
}
|
|
121
|
+
if (ordered.length > 1 && options.shard !== false)
|
|
122
|
+
return ordered;
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Runs the planned dispatch and finalizes its verdict.
|
|
127
|
+
*
|
|
128
|
+
* @param inputs - See {@link TestDispatchInputs}
|
|
129
|
+
*/
|
|
130
|
+
export async function dispatchTests(inputs) {
|
|
131
|
+
const { engineDir, runCtx, options, projectConfig, normalizedPaths } = inputs;
|
|
132
|
+
const jobs = options.jobs ?? 1;
|
|
133
|
+
if (jobs > 1 && !inputs.xpcshellOnly && !autoPortsRequested(options, projectConfig)) {
|
|
134
|
+
throw new InvalidArgumentError('--jobs runs several browsers at once, which needs auto ports (--auto-ports or ' +
|
|
135
|
+
'test.mochitestPorts: "auto") so the harness can move each instance to free server ports.', '--jobs');
|
|
136
|
+
}
|
|
137
|
+
const guard = {
|
|
138
|
+
perfSamples: options.perfSamples,
|
|
139
|
+
requested: normalizedPaths,
|
|
140
|
+
};
|
|
141
|
+
const shards = planShards(inputs);
|
|
142
|
+
if (shards !== undefined) {
|
|
143
|
+
notice(describeSharding(shards.length, options, jobs));
|
|
144
|
+
const base = jobs > 1 ? await instanceContext(runCtx, 0, inputs.xpcshellOnly) : runCtx;
|
|
145
|
+
if (jobs > 1)
|
|
146
|
+
setVerdictRunAttribute('jobs', String(jobs));
|
|
147
|
+
const generationBefore = await snapshotEngineGeneration(engineDir);
|
|
148
|
+
let summary;
|
|
149
|
+
try {
|
|
150
|
+
summary = await runShardedTests(base, shards, (outcome, label) => diagnoseShardOutcome(outcome, label, projectConfig.binaryName, inputs.postRebuildContext), {
|
|
151
|
+
jobs,
|
|
152
|
+
contextFor: (index) => instanceContext(runCtx, index, inputs.xpcshellOnly),
|
|
153
|
+
perFile: options.shardSize !== undefined,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
finally {
|
|
157
|
+
// Runs before the aggregate verdict below: a mutated engine/ emits
|
|
158
|
+
// `FAIL reason=inconclusive` and throws, so an invalidated run can
|
|
159
|
+
// never print `PASS shards=N/N` first. (A throw here masks an
|
|
160
|
+
// in-flight shard error, as the plain assert always did.)
|
|
161
|
+
await verifyEngineGenerationOrEmitInconclusive(engineDir, generationBefore, {
|
|
162
|
+
...guard,
|
|
163
|
+
...(base.powerChanged === true || runCtx.powerChanged === true
|
|
164
|
+
? { powerChanged: true }
|
|
165
|
+
: {}),
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
finalizeShardedOutcome(summary);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const combinedDispatchPaths = inputs.dispatchGroups.flatMap((group) => group.paths);
|
|
172
|
+
let outcome;
|
|
173
|
+
const generationBefore = await snapshotEngineGeneration(engineDir);
|
|
174
|
+
try {
|
|
175
|
+
outcome = await runTestsWithRetries(runCtx, combinedDispatchPaths);
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
throw new BuildError('Test process failed to start', 'mach test', error instanceof Error ? error : undefined);
|
|
179
|
+
}
|
|
180
|
+
finally {
|
|
181
|
+
await verifyEngineGenerationOrEmitInconclusive(engineDir, generationBefore, {
|
|
182
|
+
...guard,
|
|
183
|
+
...(runCtx.powerChanged === true ? { powerChanged: true } : {}),
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
if (inputs.canaryPath !== undefined) {
|
|
187
|
+
reportCanaryOutcome(outcome);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
finalizeSingleRunOutcome(outcome, normalizedPaths, projectConfig.binaryName, inputs.postRebuildContext, options.headless === true);
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* The sharding notice. The default must not be silent: a cross-file
|
|
194
|
+
* pollution repro otherwise "passes" because the comparison run was
|
|
195
|
+
* sharded without saying so, misattributing a suite-context bug upstream.
|
|
196
|
+
*/
|
|
197
|
+
function describeSharding(count, options, jobs) {
|
|
198
|
+
const instances = jobs > 1 ? ` on ${jobs} harness instances at once (each with its own ports and profile)` : '';
|
|
199
|
+
if (options.shardSize !== undefined) {
|
|
200
|
+
return (`Sharding: running ${count} chunk(s) of up to ${options.shardSize} file(s)${instances}, ` +
|
|
201
|
+
'each chunk one mach invocation with a browser restart between files; results are judged ' +
|
|
202
|
+
'per file from the structured log.');
|
|
203
|
+
}
|
|
204
|
+
if (jobs > 1 && options.shard === false) {
|
|
205
|
+
return (`Sharding: --jobs splits the combined request into ${count} contiguous chunk(s) in order` +
|
|
206
|
+
`${instances}. Cross-chunk state is NOT exercised.`);
|
|
207
|
+
}
|
|
208
|
+
return (`Sharding: running ${count} test path arguments in isolated browser instances${instances} ` +
|
|
209
|
+
'(one mach invocation per argument; a directory argument keeps its files in one instance). ' +
|
|
210
|
+
'Cross-argument state is NOT exercised — pass --no-shard for a combined single-instance run.');
|
|
211
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `fireforge test --build --furnace-apply reached`: deploy only the Furnace
|
|
3
|
+
* components a requested test reaches.
|
|
4
|
+
*
|
|
5
|
+
* The pre-test build's Furnace apply runs over the whole `components/`
|
|
6
|
+
* tree, so on a shared engine a peer's `test --build` deployed every
|
|
7
|
+
* session's undeployed component edits into the shared objdir, including
|
|
8
|
+
* edits another session was holding back for a before-capture. `reached`
|
|
9
|
+
* keeps the apply to the components the run can observe and names the rest.
|
|
10
|
+
*
|
|
11
|
+
* "Reaches" is textual, and errs towards applying: a component is reached
|
|
12
|
+
* when a requested test file, a `head*.js` beside it, or any test file
|
|
13
|
+
* under a requested directory names the component's tag, or when the test
|
|
14
|
+
* path itself names it. A component no test names cannot change what the
|
|
15
|
+
* run observes through its own code, so leaving its undeployed edit out
|
|
16
|
+
* keeps the run honest about what it packaged.
|
|
17
|
+
*/
|
|
18
|
+
import type { TestOptions } from '../types/commands/index.js';
|
|
19
|
+
import type { FireForgeConfig } from '../types/config.js';
|
|
20
|
+
/** The effective Furnace apply mode for a pre-test build. */
|
|
21
|
+
export type FurnaceApplyMode = 'all' | 'reached' | 'none';
|
|
22
|
+
/**
|
|
23
|
+
* Resolves the mode from the flag and `test.furnaceApply`. `false`
|
|
24
|
+
* (`--no-furnace-apply`) is `none`.
|
|
25
|
+
*/
|
|
26
|
+
export declare function resolveFurnaceApplyMode(options: TestOptions, config: FireForgeConfig): FurnaceApplyMode;
|
|
27
|
+
/**
|
|
28
|
+
* The stale components a request reaches, and the stale ones it does not.
|
|
29
|
+
*
|
|
30
|
+
* @param projectRoot - Project root
|
|
31
|
+
* @param engineDir - Absolute engine directory
|
|
32
|
+
* @param testPaths - Engine-relative requested paths
|
|
33
|
+
*/
|
|
34
|
+
export declare function partitionReachedComponents(projectRoot: string, engineDir: string, testPaths: readonly string[]): Promise<{
|
|
35
|
+
reached: string[];
|
|
36
|
+
skipped: string[];
|
|
37
|
+
}>;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* `fireforge test --build --furnace-apply reached`: deploy only the Furnace
|
|
4
|
+
* components a requested test reaches.
|
|
5
|
+
*
|
|
6
|
+
* The pre-test build's Furnace apply runs over the whole `components/`
|
|
7
|
+
* tree, so on a shared engine a peer's `test --build` deployed every
|
|
8
|
+
* session's undeployed component edits into the shared objdir, including
|
|
9
|
+
* edits another session was holding back for a before-capture. `reached`
|
|
10
|
+
* keeps the apply to the components the run can observe and names the rest.
|
|
11
|
+
*
|
|
12
|
+
* "Reaches" is textual, and errs towards applying: a component is reached
|
|
13
|
+
* when a requested test file, a `head*.js` beside it, or any test file
|
|
14
|
+
* under a requested directory names the component's tag, or when the test
|
|
15
|
+
* path itself names it. A component no test names cannot change what the
|
|
16
|
+
* run observes through its own code, so leaving its undeployed edit out
|
|
17
|
+
* keeps the run honest about what it packaged.
|
|
18
|
+
*/
|
|
19
|
+
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
20
|
+
import { basename, dirname, join } from 'node:path';
|
|
21
|
+
import { listStaleFurnaceComponents } from '../core/furnace-staleness.js';
|
|
22
|
+
import { toError } from '../utils/errors.js';
|
|
23
|
+
import { verbose } from '../utils/logger.js';
|
|
24
|
+
/** Files read per run, so a huge directory request cannot stall the build. */
|
|
25
|
+
const MAX_SCANNED_FILES = 4000;
|
|
26
|
+
const SCANNED_EXTENSIONS = /\.(?:m?js|html|xhtml|toml|ini)$/;
|
|
27
|
+
/**
|
|
28
|
+
* Resolves the mode from the flag and `test.furnaceApply`. `false`
|
|
29
|
+
* (`--no-furnace-apply`) is `none`.
|
|
30
|
+
*/
|
|
31
|
+
export function resolveFurnaceApplyMode(options, config) {
|
|
32
|
+
if (options.furnaceApply === false)
|
|
33
|
+
return 'none';
|
|
34
|
+
if (typeof options.furnaceApply === 'string')
|
|
35
|
+
return options.furnaceApply;
|
|
36
|
+
return config.test?.furnaceApply ?? 'all';
|
|
37
|
+
}
|
|
38
|
+
async function collectFiles(path, out) {
|
|
39
|
+
if (out.length >= MAX_SCANNED_FILES)
|
|
40
|
+
return;
|
|
41
|
+
let info;
|
|
42
|
+
try {
|
|
43
|
+
info = await stat(path);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (info.isFile()) {
|
|
49
|
+
if (SCANNED_EXTENSIONS.test(path))
|
|
50
|
+
out.push(path);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (!info.isDirectory())
|
|
54
|
+
return;
|
|
55
|
+
let entries;
|
|
56
|
+
try {
|
|
57
|
+
entries = await readdir(path);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
for (const entry of entries.sort()) {
|
|
63
|
+
if (out.length >= MAX_SCANNED_FILES)
|
|
64
|
+
return;
|
|
65
|
+
await collectFiles(join(path, entry), out);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/** The files whose text decides reach: the request plus `head*.js` beside each file. */
|
|
69
|
+
async function reachFiles(engineDir, testPaths) {
|
|
70
|
+
const files = [];
|
|
71
|
+
for (const path of testPaths) {
|
|
72
|
+
const absolute = join(engineDir, path);
|
|
73
|
+
await collectFiles(absolute, files);
|
|
74
|
+
try {
|
|
75
|
+
if ((await stat(absolute)).isFile()) {
|
|
76
|
+
const dir = dirname(absolute);
|
|
77
|
+
for (const entry of await readdir(dir)) {
|
|
78
|
+
if (/^head.*\.js$/.test(entry))
|
|
79
|
+
files.push(join(dir, entry));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
// A missing path is refused later by the path-existence check.
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return [...new Set(files)];
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* The stale components a request reaches, and the stale ones it does not.
|
|
91
|
+
*
|
|
92
|
+
* @param projectRoot - Project root
|
|
93
|
+
* @param engineDir - Absolute engine directory
|
|
94
|
+
* @param testPaths - Engine-relative requested paths
|
|
95
|
+
*/
|
|
96
|
+
export async function partitionReachedComponents(projectRoot, engineDir, testPaths) {
|
|
97
|
+
let stale;
|
|
98
|
+
try {
|
|
99
|
+
stale = await listStaleFurnaceComponents(projectRoot);
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
verbose(`Furnace pending-edit probe failed: ${toError(error).message}`);
|
|
103
|
+
return { reached: [], skipped: [] };
|
|
104
|
+
}
|
|
105
|
+
if (stale.length === 0)
|
|
106
|
+
return { reached: [], skipped: [] };
|
|
107
|
+
const names = new Set();
|
|
108
|
+
for (const path of testPaths) {
|
|
109
|
+
for (const name of stale)
|
|
110
|
+
if (path.includes(name))
|
|
111
|
+
names.add(name);
|
|
112
|
+
}
|
|
113
|
+
for (const file of await reachFiles(engineDir, testPaths)) {
|
|
114
|
+
if (names.size === stale.length)
|
|
115
|
+
break;
|
|
116
|
+
let text;
|
|
117
|
+
try {
|
|
118
|
+
text = await readFile(file, 'utf8');
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
for (const name of stale) {
|
|
124
|
+
if (text.includes(name) || basename(file).includes(name))
|
|
125
|
+
names.add(name);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
reached: stale.filter((name) => names.has(name)),
|
|
130
|
+
skipped: stale.filter((name) => !names.has(name)),
|
|
131
|
+
};
|
|
132
|
+
}
|