@hominis/fireforge 0.48.0 → 0.49.1
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 +29 -0
- package/dist/build-info.json +4 -4
- package/dist/src/cli.js +16 -28
- package/dist/src/commands/build.js +30 -4
- package/dist/src/commands/doctor-engine-eslint.d.ts +3 -0
- package/dist/src/commands/doctor-engine-eslint.js +12 -0
- package/dist/src/commands/doctor-macos-sdk.js +7 -5
- package/dist/src/commands/doctor-source-pin.js +9 -2
- package/dist/src/commands/doctor.js +2 -0
- package/dist/src/commands/furnace/apply.js +10 -4
- package/dist/src/commands/furnace/create-templates.js +0 -3
- package/dist/src/commands/lint.js +4 -0
- package/dist/src/commands/manifest.d.ts +1 -2
- package/dist/src/commands/patch/delete.js +18 -4
- package/dist/src/commands/re-export-options.js +7 -0
- package/dist/src/commands/re-export-register.js +1 -0
- package/dist/src/commands/re-export-removed.d.ts +4 -0
- package/dist/src/commands/re-export-removed.js +49 -0
- package/dist/src/commands/re-export-single.js +4 -1
- package/dist/src/commands/re-export.js +4 -0
- package/dist/src/commands/test-browser-preflight.d.ts +28 -0
- package/dist/src/commands/test-browser-preflight.js +181 -0
- package/dist/src/commands/test-build-phase.js +30 -7
- package/dist/src/commands/test-checkjs.d.ts +3 -0
- package/dist/src/commands/test-checkjs.js +27 -0
- package/dist/src/commands/test-diagnose.js +3 -0
- package/dist/src/commands/test-harness-env.d.ts +5 -6
- package/dist/src/commands/test-harness-env.js +27 -7
- package/dist/src/commands/test-modes.d.ts +2 -4
- package/dist/src/commands/test-register.js +11 -5
- package/dist/src/commands/test-run.d.ts +2 -0
- package/dist/src/commands/test-run.js +21 -1
- package/dist/src/commands/test-verdict.d.ts +2 -2
- package/dist/src/commands/test.js +83 -211
- package/dist/src/core/build-baseline-types.d.ts +12 -14
- package/dist/src/core/build-baseline-validate.d.ts +3 -0
- package/dist/src/core/build-baseline-validate.js +53 -0
- package/dist/src/core/build-baseline.d.ts +4 -3
- package/dist/src/core/build-baseline.js +23 -11
- package/dist/src/core/component-source-guard.d.ts +20 -0
- package/dist/src/core/component-source-guard.js +68 -0
- package/dist/src/core/coverage-extend.d.ts +5 -34
- package/dist/src/core/coverage-extend.js +26 -37
- package/dist/src/core/engine-write-boundary.d.ts +5 -0
- package/dist/src/core/engine-write-boundary.js +41 -0
- package/dist/src/core/furnace-apply-helpers.js +8 -2
- package/dist/src/core/furnace-apply.js +7 -0
- package/dist/src/core/furnace-validate-accessibility.js +9 -12
- package/dist/src/core/furnace-validate.js +0 -43
- package/dist/src/core/gecko-dom-types.d.ts +2 -0
- package/dist/src/core/gecko-dom-types.js +17 -0
- package/dist/src/core/lit-template-regions.d.ts +4 -0
- package/dist/src/core/lit-template-regions.js +50 -0
- package/dist/src/core/mach-build-artifacts.d.ts +6 -6
- package/dist/src/core/mach-build-artifacts.js +13 -7
- package/dist/src/core/mach-resource-shim.d.ts +1 -1
- package/dist/src/core/mach-resource-shim.js +2 -0
- package/dist/src/core/marionette-port.d.ts +2 -0
- package/dist/src/core/marionette-port.js +38 -53
- package/dist/src/core/mochitest-server-port.d.ts +1 -1
- package/dist/src/core/mochitest-server-port.js +20 -17
- package/dist/src/core/objdir-maintenance.d.ts +10 -0
- package/dist/src/core/objdir-maintenance.js +167 -0
- package/dist/src/core/patch-lint-checkjs.js +2 -1
- package/dist/src/core/patch-parse.js +8 -6
- package/dist/src/core/preflight-wait.d.ts +2 -0
- package/dist/src/core/preflight-wait.js +24 -0
- package/dist/src/core/process-owner.d.ts +9 -0
- package/dist/src/core/process-owner.js +43 -0
- package/dist/src/core/test-harness-crash.d.ts +1 -1
- package/dist/src/core/test-harness-crash.js +5 -5
- package/dist/src/core/test-host-state.d.ts +14 -0
- package/dist/src/core/test-host-state.js +80 -0
- package/dist/src/core/test-profile-files.d.ts +17 -0
- package/dist/src/core/test-profile-files.js +75 -0
- package/dist/src/core/test-stale-check.d.ts +6 -7
- package/dist/src/core/test-stale-check.js +9 -32
- package/dist/src/core/token-category.d.ts +7 -26
- package/dist/src/core/token-category.js +91 -117
- package/dist/src/core/token-inventory.js +12 -7
- package/dist/src/core/typecheck.js +6 -6
- package/dist/src/types/commands/options.d.ts +14 -3
- package/dist/src/utils/assert.js +2 -7
- package/dist/src/utils/child-env.d.ts +0 -3
- package/dist/src/utils/child-env.js +0 -3
- package/dist/src/utils/concurrency.d.ts +2 -2
- package/dist/src/utils/concurrency.js +15 -4
- package/dist/src/utils/fs.js +11 -1
- package/dist/src/utils/options.d.ts +16 -1
- package/dist/src/utils/options.js +5 -3
- package/dist/src/utils/paths.js +3 -2
- package/dist/src/utils/process-group.d.ts +1 -2
- package/dist/src/utils/process-group.js +1 -2
- package/dist/src/utils/process-input.js +0 -3
- package/dist/src/utils/process.d.ts +3 -2
- package/dist/src/utils/process.js +19 -7
- package/docs/configuration.md +12 -11
- package/docs/lifecycle-invariants.md +9 -5
- package/docs/machine-output.md +36 -19
- package/docs/patch-workflow.md +15 -0
- package/docs/testing.md +77 -12
- package/package.json +6 -5
|
@@ -3,29 +3,30 @@ import { join } from 'node:path';
|
|
|
3
3
|
import { getProjectPaths, loadConfig } from '../core/config.js';
|
|
4
4
|
import { assertEngineExists } from '../core/engine-precondition.js';
|
|
5
5
|
import { assertEngineGenerationUnchanged, snapshotEngineGeneration, } from '../core/engine-session-lock.js';
|
|
6
|
-
import { reportOrphanedHarnessProcesses } from '../core/harness-orphans.js';
|
|
7
6
|
import { hasBuildArtifacts, hasRunnableBundle } from '../core/mach.js';
|
|
8
7
|
import { assertBuildArtifacts } from '../core/mach-build-artifacts.js';
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import { ensureMochitestServerPortAvailable } from '../core/mochitest-server-port.js';
|
|
8
|
+
import { ensureLaunchableBrowserNotRunning } from '../core/marionette-port.js';
|
|
9
|
+
import { waitForPreflight } from '../core/preflight-wait.js';
|
|
12
10
|
import { closeActiveRunLog, openRunLog, setActiveRunLog, writeToActiveRunLog, } from '../core/run-log.js';
|
|
13
11
|
import { createPostRebuildFailureContext } from '../core/test-harness-output.js';
|
|
14
12
|
import { analyzeTestPathScopes, formatScopeNotice, } from '../core/test-path-scope.js';
|
|
13
|
+
import { cleanupProfileFiles, stageProfileFiles, } from '../core/test-profile-files.js';
|
|
15
14
|
import { assertObjdirMatchesTreeMarker } from '../core/tree-store.js';
|
|
16
15
|
import { FireForgeError, GeneralError, PreflightRefusalError } from '../errors/base.js';
|
|
17
16
|
import { BuildError } from '../errors/build.js';
|
|
18
17
|
import { toError } from '../utils/errors.js';
|
|
19
18
|
import { pathExists } from '../utils/fs.js';
|
|
20
|
-
import { info, intro, notice,
|
|
19
|
+
import { info, intro, notice, warn } from '../utils/logger.js';
|
|
20
|
+
import { resolveWaitLockSeconds } from '../utils/options.js';
|
|
21
21
|
import { stripEnginePrefix } from '../utils/paths.js';
|
|
22
|
+
import { appendMarionetteForwardingArgs, ensureTestBrowserEnvironment, runDoctorPreflight, } from './test-browser-preflight.js';
|
|
22
23
|
import { runTestBuildPhase } from './test-build-phase.js';
|
|
23
24
|
import { diagnoseShardOutcome, finalizeSingleRunOutcome } from './test-diagnose.js';
|
|
24
|
-
import { buildPerfSampleEnv, mergeHarnessEnv, resolveShuffleSeed } from './test-harness-env.js';
|
|
25
|
+
import { buildPerfSampleEnv, mergeHarnessEnv, resolveShuffleSeed, shuffleTestGroups, } from './test-harness-env.js';
|
|
25
26
|
import { removePgidFile, setActivePgidFile } from './test-harness-teardown.js';
|
|
26
27
|
import { assertPathlessTestMode, assertTestModeCombinations, canaryTimeoutSeconds, classifyBeforeDispatch, reportCanaryOutcome, resolveCanaryPath, } from './test-modes.js';
|
|
27
28
|
import { DEFAULT_HARNESS_RETRIES, finalizeShardedOutcome, runShardedTests, runTestsWithRetries, } from './test-run.js';
|
|
28
|
-
import {
|
|
29
|
+
import { emitFailVerdict, resetVerdictEmission, verdictEmitted } from './test-verdict.js';
|
|
29
30
|
async function assertTestPathsExist(engineDir, testPaths) {
|
|
30
31
|
const missingPaths = [];
|
|
31
32
|
for (const testPath of testPaths) {
|
|
@@ -102,171 +103,6 @@ function logTestSelection(scopes) {
|
|
|
102
103
|
}
|
|
103
104
|
info('');
|
|
104
105
|
}
|
|
105
|
-
/**
|
|
106
|
-
* Runs the `--doctor` marionette handshake probe. With no test paths the
|
|
107
|
-
* probe is the entire command (returns `'stop'` after reporting). With
|
|
108
|
-
* paths it gates the mach invocation, where a FAIL throws before mach runs.
|
|
109
|
-
*/
|
|
110
|
-
async function runDoctorPreflight(args) {
|
|
111
|
-
const { engineDir, effectivePort, hasTestPaths, objDir, binaryName, launchablePath } = args;
|
|
112
|
-
// Non-TTY captures need the banner even if clack's renderer defers output
|
|
113
|
-
// in pipe mode. TTY users need the clack framing. Gated rather than
|
|
114
|
-
// written twice: the unconditional pair printed the same line twice on a
|
|
115
|
-
// terminal.
|
|
116
|
-
if (process.stdout.isTTY) {
|
|
117
|
-
info('Running marionette preflight...');
|
|
118
|
-
}
|
|
119
|
-
else {
|
|
120
|
-
process.stdout.write('Running marionette preflight...\n');
|
|
121
|
-
}
|
|
122
|
-
const preflight = effectivePort !== undefined
|
|
123
|
-
? await runMarionettePreflight(engineDir, { port: effectivePort })
|
|
124
|
-
: await runMarionettePreflight(engineDir);
|
|
125
|
-
// The authoritative PASS/FAIL line is written with `process.stdout.write`
|
|
126
|
-
// as the first output after the probe returns, because clack's renderer
|
|
127
|
-
// can drop the summary under non-TTY capture.
|
|
128
|
-
//
|
|
129
|
-
// Gated on non-TTY: unconditional, it stacks with the two clack renderings
|
|
130
|
-
// below and the same line appears three times on a terminal. Captured
|
|
131
|
-
// streams are exactly where this branch still fires.
|
|
132
|
-
const directLine = formatMarionettePreflightLine(preflight);
|
|
133
|
-
if (!process.stdout.isTTY) {
|
|
134
|
-
process.stdout.write(`${directLine}\n`);
|
|
135
|
-
}
|
|
136
|
-
process.stdout.write(`Marionette preflight environment: objdir=${objDir ?? '(none)'}; binary=${binaryName}; app=${launchablePath ? `engine/${launchablePath}` : '(unknown)'}; port=${effectivePort ?? 2828}; elapsed=${preflight.durationMs}ms\n`);
|
|
137
|
-
reportMarionettePreflight(preflight);
|
|
138
|
-
if (!hasTestPaths) {
|
|
139
|
-
if (!preflight.ok) {
|
|
140
|
-
emitFailVerdict('preflight');
|
|
141
|
-
throw new GeneralError('Marionette preflight reported FAIL — see output above.');
|
|
142
|
-
}
|
|
143
|
-
// Doctor-only runs end here, so they carry their own verdict line
|
|
144
|
-
// (reason=preflight on failure). With test paths the verdict comes
|
|
145
|
-
// from the actual harness run downstream.
|
|
146
|
-
emitPassVerdict();
|
|
147
|
-
outro('Test completed');
|
|
148
|
-
return 'stop';
|
|
149
|
-
}
|
|
150
|
-
if (!preflight.ok) {
|
|
151
|
-
emitFailVerdict('preflight');
|
|
152
|
-
throw new GeneralError('Marionette preflight reported FAIL — see output above. Aborting before mach test runs.');
|
|
153
|
-
}
|
|
154
|
-
return 'continue';
|
|
155
|
-
}
|
|
156
|
-
/**
|
|
157
|
-
* Auto-forwards `--marionette-port` to mach (`--setpref=marionette.port`
|
|
158
|
-
* for the listener, `--marionette=127.0.0.1:<n>` for the mochitest
|
|
159
|
-
* client), skipping each piece the operator already forwarded via
|
|
160
|
-
* `--mach-arg` and the xpcshell flavor that ignores the pref entirely.
|
|
161
|
-
* Mutates `extraArgs` in place.
|
|
162
|
-
*/
|
|
163
|
-
function appendMarionetteForwardingArgs(extraArgs, options, forwardedPort, xpcshellOnly = false) {
|
|
164
|
-
// Auto-forward the Marionette port to mach when `--marionette-port` is set.
|
|
165
|
-
// `--setpref=marionette.port=<n>` configures where the browser listener
|
|
166
|
-
// binds. `--marionette=127.0.0.1:<n>` tells the mochitest harness client to
|
|
167
|
-
// connect there (default client is 127.0.0.1:2828). xpcshell ignores both
|
|
168
|
-
// for browser Marionette.
|
|
169
|
-
//
|
|
170
|
-
// Skip setpref forwarding when the operator already supplied an equivalent
|
|
171
|
-
// arg via `--mach-arg`: duplicates would confuse without changing
|
|
172
|
-
// semantics. Skip when mach args explicitly request `--flavor=xpcshell` (or
|
|
173
|
-
// `xpcshell-tests`): the preflight still honours `--marionette-port`, but
|
|
174
|
-
// mach does not use the marionette.port pref on that harness. Any other arg
|
|
175
|
-
// shape still forwards so toolkit widget paths and mixed suites stay
|
|
176
|
-
// aligned with the probe without duplicate `--mach-arg` flags.
|
|
177
|
-
//
|
|
178
|
-
// Skip auto `--marionette=...` when `--mach-arg` already includes a client
|
|
179
|
-
// `--marionette=...` (or two-token `--marionette host:port`).
|
|
180
|
-
if (options.marionettePort === undefined)
|
|
181
|
-
return;
|
|
182
|
-
if (xpcshellOnly) {
|
|
183
|
-
// Manifest classification says every requested path is xpcshell.
|
|
184
|
-
// xpcshell ignores the browser Marionette path entirely, and forwarding
|
|
185
|
-
// the mochitest client flags here makes mach reject the dispatch.
|
|
186
|
-
info(`--marionette-port=${options.marionettePort} applied to the preflight probe only: the requested paths are xpcshell-only, and xpcshell ignores the browser Marionette port. Not forwarding --setpref=marionette.port or --marionette to mach.`);
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
{
|
|
190
|
-
const operatorAlreadyForwarded = forwardedPort !== undefined;
|
|
191
|
-
const machArgs = options.machArg ?? [];
|
|
192
|
-
if (operatorAlreadyForwarded) {
|
|
193
|
-
info(`--marionette-port=${options.marionettePort} set, but the same port is already forwarded via --mach-arg; skipping auto-forward.`);
|
|
194
|
-
}
|
|
195
|
-
else if (shouldAutoForwardMarionettePortToMach(machArgs)) {
|
|
196
|
-
extraArgs.push(`--setpref=marionette.port=${options.marionettePort}`);
|
|
197
|
-
}
|
|
198
|
-
else {
|
|
199
|
-
info(`--marionette-port=${options.marionettePort} applied to the preflight probe, but --flavor=xpcshell is set — mach is not auto-configured with --setpref=marionette.port or --marionette (xpcshell ignores the browser Marionette path). Pass --mach-arg --setpref=marionette.port=${options.marionettePort} explicitly if you still need mach to see the port.`);
|
|
200
|
-
}
|
|
201
|
-
if (shouldAutoForwardMarionettePortToMach(machArgs) &&
|
|
202
|
-
!forwardedMachArgsIncludeMarionetteClient(machArgs)) {
|
|
203
|
-
extraArgs.push(`--marionette=127.0.0.1:${options.marionettePort}`);
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
async function ensureTestMarionettePortAvailable(port, binaryName, options, skip) {
|
|
208
|
-
// Refuse a stale listener before mach surfaces a generic bind failure.
|
|
209
|
-
// This also recognizes a fork-branded browser via binaryName.
|
|
210
|
-
if (skip.xpcshellOnly && !skip.doctor) {
|
|
211
|
-
// xpcshell does not bind the browser Marionette port, so a developer's
|
|
212
|
-
// interactive browser holding 2828 must not kill an xpcshell run.
|
|
213
|
-
// --doctor keeps the preflight: its probe launches a
|
|
214
|
-
// Marionette browser regardless of the requested harness.
|
|
215
|
-
const message = 'Skipping the Marionette stale-port preflight: all requested paths are xpcshell ' +
|
|
216
|
-
'(xpcshell does not bind the browser Marionette port).';
|
|
217
|
-
if (options.marionettePort !== undefined || options.killStaleMarionette === true) {
|
|
218
|
-
info(message);
|
|
219
|
-
}
|
|
220
|
-
else {
|
|
221
|
-
verbose(message);
|
|
222
|
-
}
|
|
223
|
-
return;
|
|
224
|
-
}
|
|
225
|
-
if (options.killStaleMarionette === true) {
|
|
226
|
-
await ensureMarionettePortAvailable(port, { binaryName, killStaleBrowser: true });
|
|
227
|
-
return;
|
|
228
|
-
}
|
|
229
|
-
await assertMarionettePortAvailable(port, { binaryName });
|
|
230
|
-
}
|
|
231
|
-
async function ensureTestBrowserEnvironment(engineDir, launchablePath, xpcshellOnly, projectConfig, options, objDir) {
|
|
232
|
-
// A timed-out mochitest can leave the built app alive after its Marionette
|
|
233
|
-
// listener has disappeared. The port probe cannot see that case, but the
|
|
234
|
-
// survivor can still steal focus and wedge every later headed run.
|
|
235
|
-
if (!xpcshellOnly && launchablePath) {
|
|
236
|
-
await ensureLaunchableBrowserNotRunning(join(engineDir, launchablePath), {
|
|
237
|
-
killStaleBrowser: options.killStaleMarionette === true,
|
|
238
|
-
});
|
|
239
|
-
}
|
|
240
|
-
// A zombie mochitest httpd squatting the server port makes a fresh
|
|
241
|
-
// browser connect to a server that cannot serve this run's manifest,
|
|
242
|
-
// which surfaces as a 370s "Ran 0 checks" stall naming nothing. xpcshell
|
|
243
|
-
// does not use the httpd, so an xpcshell-only run is never blocked by it.
|
|
244
|
-
if (!xpcshellOnly) {
|
|
245
|
-
await ensureMochitestServerPortAvailable(undefined, {
|
|
246
|
-
engineDir,
|
|
247
|
-
killStaleServer: options.killStaleMarionette === true,
|
|
248
|
-
});
|
|
249
|
-
}
|
|
250
|
-
// Helpers that outlived an earlier run (httpd, pywebsocket, ssltunnel,
|
|
251
|
-
// moz-http2) slow every later run without appearing in its output. Both
|
|
252
|
-
// harnesses are affected, so this census is not gated on xpcshellOnly.
|
|
253
|
-
// Reaping is opt-in per invocation (--reap-orphans) or per repo
|
|
254
|
-
// (test.reapOrphans: "reap"); a reap is stamped on the verdict line so a
|
|
255
|
-
// green after one is not mistaken for a green on a quiet machine.
|
|
256
|
-
const census = await reportOrphanedHarnessProcesses(objDir, {
|
|
257
|
-
reap: options.reapOrphans === true || projectConfig.test?.reapOrphans === 'reap',
|
|
258
|
-
});
|
|
259
|
-
addVerdictRunCount('orphans-reaped', census.reaped);
|
|
260
|
-
const forwardedPort = options.machArg
|
|
261
|
-
? extractForwardedMarionettePort(options.machArg)
|
|
262
|
-
: undefined;
|
|
263
|
-
const effectivePort = options.marionettePort ?? forwardedPort;
|
|
264
|
-
await ensureTestMarionettePortAvailable(effectivePort, projectConfig.binaryName, options, {
|
|
265
|
-
xpcshellOnly,
|
|
266
|
-
doctor: options.doctor === true,
|
|
267
|
-
});
|
|
268
|
-
return { forwardedPort, effectivePort };
|
|
269
|
-
}
|
|
270
106
|
/** Build-artifact preflight wording for `fireforge test`. */
|
|
271
107
|
const TEST_BUILD_PREFLIGHT = {
|
|
272
108
|
label: 'Tests',
|
|
@@ -297,8 +133,13 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
|
|
|
297
133
|
// `finally`, after the verdict line has already read the path.
|
|
298
134
|
setActiveRunLog(await openRunLog(projectRoot, 'test'));
|
|
299
135
|
setActivePgidFile(options.pgidFile);
|
|
136
|
+
let staged;
|
|
300
137
|
try {
|
|
301
|
-
|
|
138
|
+
if (options.profileFile?.length) {
|
|
139
|
+
staged = await stageProfileFiles(projectRoot, options.profileFile);
|
|
140
|
+
options = { ...options, machArg: [...(options.machArg ?? []), ...staged.args] };
|
|
141
|
+
}
|
|
142
|
+
await runTestCommandBody(projectRoot, testPaths, options, staged?.env);
|
|
302
143
|
}
|
|
303
144
|
catch (error) {
|
|
304
145
|
if (!verdictEmitted()) {
|
|
@@ -310,8 +151,20 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
|
|
|
310
151
|
finally {
|
|
311
152
|
// A run that ends under FireForge's control owns no harness group any
|
|
312
153
|
// more; the file only outlives FireForge when FireForge was killed.
|
|
313
|
-
|
|
314
|
-
|
|
154
|
+
const stagedRoot = staged?.root;
|
|
155
|
+
const cleanups = [
|
|
156
|
+
...(stagedRoot === undefined ? [] : [() => cleanupProfileFiles(stagedRoot)]),
|
|
157
|
+
() => removePgidFile(options.pgidFile),
|
|
158
|
+
() => closeActiveRunLog(),
|
|
159
|
+
];
|
|
160
|
+
for (const cleanup of cleanups) {
|
|
161
|
+
try {
|
|
162
|
+
await cleanup();
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
warn(`Test cleanup failed: ${toError(error).message}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
315
168
|
}
|
|
316
169
|
}
|
|
317
170
|
/** The refusal class a preflight gate named, when it named one. */
|
|
@@ -355,16 +208,56 @@ function renderPreflightRefusal(error) {
|
|
|
355
208
|
* misdescribe a run whose harness already executed). It is emitted here so the
|
|
356
209
|
* guard's throw can never leave a stale or missing verdict line behind.
|
|
357
210
|
*/
|
|
358
|
-
async function verifyEngineGenerationOrEmitInconclusive(engineDir, before) {
|
|
211
|
+
async function verifyEngineGenerationOrEmitInconclusive(engineDir, before, powerChanged = false) {
|
|
359
212
|
try {
|
|
360
213
|
await assertEngineGenerationUnchanged(engineDir, before);
|
|
214
|
+
if (powerChanged)
|
|
215
|
+
throw new GeneralError('Perf power source changed during the run; repeat this sitting on a stable power source.');
|
|
361
216
|
}
|
|
362
217
|
catch (error) {
|
|
363
218
|
emitFailVerdict('inconclusive');
|
|
364
219
|
throw error;
|
|
365
220
|
}
|
|
366
221
|
}
|
|
367
|
-
async function
|
|
222
|
+
async function guardBuildBrowser(engineDir, launchablePath, options) {
|
|
223
|
+
if ((options.build || options.buildOnly) && launchablePath) {
|
|
224
|
+
await waitForPreflight(() => ensureLaunchableBrowserNotRunning(join(engineDir, launchablePath), {
|
|
225
|
+
killStaleBrowser: options.killStaleMarionette === true,
|
|
226
|
+
}), options.waitBrowser === undefined ? undefined : resolveWaitLockSeconds(options.waitBrowser));
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
function prepareExtraMachArgs(options, shuffleSeed, canaryPath, projectConfig, forwardedMachArgs) {
|
|
230
|
+
const extraArgs = [];
|
|
231
|
+
if (options.headless) {
|
|
232
|
+
extraArgs.push('--headless');
|
|
233
|
+
}
|
|
234
|
+
if (shuffleSeed !== undefined) {
|
|
235
|
+
extraArgs.push('--shuffle');
|
|
236
|
+
}
|
|
237
|
+
if (options.auto === true) {
|
|
238
|
+
extraArgs.push('--auto');
|
|
239
|
+
}
|
|
240
|
+
if (canaryPath !== undefined)
|
|
241
|
+
extraArgs.push(`--timeout=${canaryTimeoutSeconds(projectConfig)}`);
|
|
242
|
+
// --mach-arg is a verbatim passthrough for upstream mach/xpcshell/mochitest
|
|
243
|
+
// flags FireForge does not model directly (see the xpcshell appdir hint
|
|
244
|
+
// above for why). Appended after --headless so mach sees
|
|
245
|
+
// the FireForge-managed flags first and the escape-valve ones last, which
|
|
246
|
+
// keeps the override precedence predictable.
|
|
247
|
+
if (forwardedMachArgs.length > 0)
|
|
248
|
+
extraArgs.push(...forwardedMachArgs);
|
|
249
|
+
return extraArgs;
|
|
250
|
+
}
|
|
251
|
+
function prepareDispatchArguments(classification, options, canaryPath, projectConfig) {
|
|
252
|
+
const suite = resolveTestSuite(classification, options.genericMachTest === true);
|
|
253
|
+
const shuffleSeed = resolveShuffleSeed(options.shuffle, suite);
|
|
254
|
+
const forwardedMachArgs = options.machArg && options.machArg.length > 0
|
|
255
|
+
? filterRedundantXpcshellFlavorArgs(options.machArg, classification)
|
|
256
|
+
: [];
|
|
257
|
+
const extraArgs = prepareExtraMachArgs(options, shuffleSeed, canaryPath, projectConfig, forwardedMachArgs);
|
|
258
|
+
return { suite, shuffleSeed, extraArgs };
|
|
259
|
+
}
|
|
260
|
+
async function runTestCommandBody(projectRoot, testPaths, options = {}, profileEnv) {
|
|
368
261
|
const paths = getProjectPaths(projectRoot);
|
|
369
262
|
// Check if engine exists
|
|
370
263
|
await assertEngineExists(paths.engine);
|
|
@@ -398,12 +291,7 @@ async function runTestCommandBody(projectRoot, testPaths, options = {}) {
|
|
|
398
291
|
const requestedPaths = canaryPath !== undefined ? [canaryPath] : testPaths;
|
|
399
292
|
const normalizedPaths = requestedPaths.map((p) => stripEnginePrefix(p).trim());
|
|
400
293
|
const { classification, xpcshellOnly } = await classifyBeforeDispatch(paths.engine, normalizedPaths, { allowMixed: options.buildOnly === true });
|
|
401
|
-
|
|
402
|
-
classification,
|
|
403
|
-
normalizedPaths,
|
|
404
|
-
})) {
|
|
405
|
-
return;
|
|
406
|
-
}
|
|
294
|
+
await guardBuildBrowser(paths.engine, launchablePath, options);
|
|
407
295
|
// Resolve the effective Marionette port. Operator precedence:
|
|
408
296
|
// 1. `--marionette-port` (first-class option, parsed at the CLI layer)
|
|
409
297
|
// 2. forwarded `--mach-arg --marionette-port=NNNN` /
|
|
@@ -414,6 +302,12 @@ async function runTestCommandBody(projectRoot, testPaths, options = {}) {
|
|
|
414
302
|
// documented `--mach-arg --marionette-port=NNNN` route still hits the
|
|
415
303
|
// wrapper preflight refusing on 2828 before the forwarded arg reaches
|
|
416
304
|
// mach.
|
|
305
|
+
if (await runTestBuildPhase(projectRoot, paths, projectConfig, harnessRetries, options, {
|
|
306
|
+
classification,
|
|
307
|
+
normalizedPaths,
|
|
308
|
+
})) {
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
417
311
|
const { forwardedPort, effectivePort } = await ensureTestBrowserEnvironment(paths.engine, launchablePath, xpcshellOnly, projectConfig, options, buildCheck.objDir);
|
|
418
312
|
if (options.doctor) {
|
|
419
313
|
const doctorOutcome = await runDoctorPreflight({
|
|
@@ -428,30 +322,7 @@ async function runTestCommandBody(projectRoot, testPaths, options = {}) {
|
|
|
428
322
|
return;
|
|
429
323
|
}
|
|
430
324
|
await assertTestPathsExist(paths.engine, normalizedPaths);
|
|
431
|
-
const suite =
|
|
432
|
-
const shuffleSeed = resolveShuffleSeed(options.shuffle, suite);
|
|
433
|
-
const forwardedMachArgs = options.machArg && options.machArg.length > 0
|
|
434
|
-
? filterRedundantXpcshellFlavorArgs(options.machArg, classification)
|
|
435
|
-
: [];
|
|
436
|
-
const extraArgs = [];
|
|
437
|
-
if (options.headless) {
|
|
438
|
-
extraArgs.push('--headless');
|
|
439
|
-
}
|
|
440
|
-
if (shuffleSeed !== undefined) {
|
|
441
|
-
extraArgs.push('--shuffle');
|
|
442
|
-
}
|
|
443
|
-
if (options.auto === true) {
|
|
444
|
-
extraArgs.push('--auto');
|
|
445
|
-
}
|
|
446
|
-
if (canaryPath !== undefined)
|
|
447
|
-
extraArgs.push(`--timeout=${canaryTimeoutSeconds(projectConfig)}`);
|
|
448
|
-
// --mach-arg is a verbatim passthrough for upstream mach/xpcshell/mochitest
|
|
449
|
-
// flags FireForge does not model directly (see the xpcshell appdir hint
|
|
450
|
-
// above for why). Appended after --headless so mach sees
|
|
451
|
-
// the FireForge-managed flags first and the escape-valve ones last, which
|
|
452
|
-
// keeps the override precedence predictable.
|
|
453
|
-
if (forwardedMachArgs.length > 0)
|
|
454
|
-
extraArgs.push(...forwardedMachArgs);
|
|
325
|
+
const { suite, shuffleSeed, extraArgs } = prepareDispatchArguments(classification, options, canaryPath, projectConfig);
|
|
455
326
|
appendMarionetteForwardingArgs(extraArgs, options, forwardedPort, xpcshellOnly);
|
|
456
327
|
// Directory arguments mean exactly that directory: mozbuild's test
|
|
457
328
|
// resolver matches paths by string prefix, so a bare directory arg
|
|
@@ -478,7 +349,7 @@ async function runTestCommandBody(projectRoot, testPaths, options = {}) {
|
|
|
478
349
|
// the manifest for each file individually. See src/core/xpcshell-appdir.ts
|
|
479
350
|
// for the full motivation.
|
|
480
351
|
logTestSelection(scopes);
|
|
481
|
-
const harnessEnv = mergeHarnessEnv(buildPerfSampleEnv(projectRoot, projectConfig.binaryName, options.perfSamples), shuffleSeed === undefined ? undefined : { FIREFORGE_SHUFFLE_SEED: String(shuffleSeed) });
|
|
352
|
+
const harnessEnv = mergeHarnessEnv(buildPerfSampleEnv(projectRoot, projectConfig.binaryName, options.perfSamples), profileEnv, shuffleSeed === undefined ? undefined : { FIREFORGE_SHUFFLE_SEED: String(shuffleSeed) });
|
|
482
353
|
const runCtx = {
|
|
483
354
|
engineDir: paths.engine,
|
|
484
355
|
objDir: buildCheck.objDir,
|
|
@@ -511,14 +382,15 @@ async function runTestCommandBody(projectRoot, testPaths, options = {}) {
|
|
|
511
382
|
const generationBefore = await snapshotEngineGeneration(paths.engine);
|
|
512
383
|
let summary;
|
|
513
384
|
try {
|
|
514
|
-
|
|
385
|
+
const orderedGroups = shuffleSeed === undefined ? dispatchGroups : shuffleTestGroups(dispatchGroups, shuffleSeed);
|
|
386
|
+
summary = await runShardedTests(runCtx, orderedGroups, (outcome, label) => diagnoseShardOutcome(outcome, label, projectConfig.binaryName, postRebuildContext));
|
|
515
387
|
}
|
|
516
388
|
finally {
|
|
517
389
|
// Runs before the aggregate verdict below: a mutated engine/ emits
|
|
518
390
|
// `FAIL reason=inconclusive` and throws, so an invalidated run can
|
|
519
391
|
// never print `PASS shards=N/N` first. (A throw here masks an
|
|
520
392
|
// in-flight shard error, as the plain assert always did.)
|
|
521
|
-
await verifyEngineGenerationOrEmitInconclusive(paths.engine, generationBefore);
|
|
393
|
+
await verifyEngineGenerationOrEmitInconclusive(paths.engine, generationBefore, runCtx.powerChanged);
|
|
522
394
|
}
|
|
523
395
|
finalizeShardedOutcome(summary);
|
|
524
396
|
return;
|
|
@@ -533,7 +405,7 @@ async function runTestCommandBody(projectRoot, testPaths, options = {}) {
|
|
|
533
405
|
throw new BuildError('Test process failed to start', 'mach test', error instanceof Error ? error : undefined);
|
|
534
406
|
}
|
|
535
407
|
finally {
|
|
536
|
-
await verifyEngineGenerationOrEmitInconclusive(paths.engine, generationBefore);
|
|
408
|
+
await verifyEngineGenerationOrEmitInconclusive(paths.engine, generationBefore, runCtx.powerChanged);
|
|
537
409
|
}
|
|
538
410
|
if (canaryPath !== undefined) {
|
|
539
411
|
reportCanaryOutcome(outcome);
|
|
@@ -35,6 +35,12 @@ export interface BuildBaseline {
|
|
|
35
35
|
* a tracked deletion.
|
|
36
36
|
*/
|
|
37
37
|
packageableFingerprints?: Record<string, string>;
|
|
38
|
+
/**
|
|
39
|
+
* All dirty staging inputs, including plain test scripts and support fixtures.
|
|
40
|
+
* Omitted when any input could not be fingerprinted; a partial map cannot
|
|
41
|
+
* anchor retained coverage.
|
|
42
|
+
*/
|
|
43
|
+
testInputFingerprints?: Record<string, string>;
|
|
38
44
|
/**
|
|
39
45
|
* Content hash per build-input manifest (`jar.mn`, `moz.build`,
|
|
40
46
|
* `moz.configure`, `Makefile.in`, `mozbuild.in`) that was dirty
|
|
@@ -83,20 +89,12 @@ export interface BuildBaseline {
|
|
|
83
89
|
* checkouts are refused up-front (`AmbiguousBuildArtifactsError`), so at
|
|
84
90
|
* most one obj dir exists per project.
|
|
85
91
|
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
* The one exception is `test --build --extend-coverage`, which unions only
|
|
93
|
-
* after proving the previous record's anchor still holds: same engine
|
|
94
|
-
* HEAD, same {@link BuildBaseline.mozconfigHash}, and every previously
|
|
95
|
-
* fingerprinted path byte-identical, i.e. the wholesale fingerprint
|
|
96
|
-
* refresh is a no-op for everything the earlier scope's staging depended
|
|
97
|
-
* on. Any divergence refuses fail-closed. See `src/core/coverage-extend.ts`
|
|
98
|
-
* for the one boundary that guard does not cover (dirty non-packageable
|
|
99
|
-
* fixtures).
|
|
92
|
+
* Successive scoped builds retain the previous claim only when HEAD,
|
|
93
|
+
* mozconfig and complete testInputFingerprints outside the rebuilt scope
|
|
94
|
+
* remain anchored. Explicitly rebuilt paths may change. A failed anchor
|
|
95
|
+
* replaces coverage with a dropped-path notice, or refuses when the caller
|
|
96
|
+
* required --extend-coverage. Legacy packageable-only anchors cannot retain
|
|
97
|
+
* coverage because they cannot prove test scripts and fixtures unchanged.
|
|
100
98
|
*/
|
|
101
99
|
testPackagingCoverage?: TestPackagingCoverage;
|
|
102
100
|
/**
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
import { isObject } from '../utils/validation.js';
|
|
3
|
+
import { DELETED_FILE_FINGERPRINT } from './build-baseline-types.js';
|
|
4
|
+
/** Baseline paths are engine-relative POSIX identifiers, never filesystem escapes. */
|
|
5
|
+
function isBaselinePath(value) {
|
|
6
|
+
return (typeof value === 'string' &&
|
|
7
|
+
value.length > 0 &&
|
|
8
|
+
!value.includes('\0') &&
|
|
9
|
+
!value.includes('\\') &&
|
|
10
|
+
!value.includes(':') &&
|
|
11
|
+
value.split('/').every((part) => part.length > 0 && part !== '.' && part !== '..'));
|
|
12
|
+
}
|
|
13
|
+
function isFingerprint(value) {
|
|
14
|
+
return (typeof value === 'string' &&
|
|
15
|
+
(/^[a-f0-9]{64}$/i.test(value) || value === DELETED_FILE_FINGERPRINT));
|
|
16
|
+
}
|
|
17
|
+
function isFingerprintMap(value) {
|
|
18
|
+
return (isObject(value) &&
|
|
19
|
+
Object.entries(value).every(([path, fingerprint]) => isBaselinePath(path) && isFingerprint(fingerprint)));
|
|
20
|
+
}
|
|
21
|
+
/** Validates persisted fields without converting malformed coverage into a full claim. */
|
|
22
|
+
export function isBuildBaseline(value) {
|
|
23
|
+
if (!isObject(value))
|
|
24
|
+
return false;
|
|
25
|
+
if (typeof value['engineHeadSha'] !== 'string' ||
|
|
26
|
+
typeof value['builtAt'] !== 'string' ||
|
|
27
|
+
!Number.isFinite(Date.parse(value['builtAt'])) ||
|
|
28
|
+
typeof value['binaryName'] !== 'string' ||
|
|
29
|
+
!/^[a-z][a-z0-9-]*$/.test(value['binaryName']))
|
|
30
|
+
return false;
|
|
31
|
+
for (const field of [
|
|
32
|
+
'packageableFingerprints',
|
|
33
|
+
'testInputFingerprints',
|
|
34
|
+
'buildInputFingerprints',
|
|
35
|
+
]) {
|
|
36
|
+
if (value[field] !== undefined && !isFingerprintMap(value[field]))
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
const coverage = value['testPackagingCoverage'];
|
|
40
|
+
if (coverage !== undefined &&
|
|
41
|
+
coverage !== 'full' &&
|
|
42
|
+
!(Array.isArray(coverage) && coverage.every(isBaselinePath)))
|
|
43
|
+
return false;
|
|
44
|
+
if (value['mozconfigHash'] !== undefined && !isFingerprint(value['mozconfigHash']))
|
|
45
|
+
return false;
|
|
46
|
+
if (value['recordedBy'] !== undefined && typeof value['recordedBy'] !== 'string')
|
|
47
|
+
return false;
|
|
48
|
+
const components = value['staticComponentsBaseline'];
|
|
49
|
+
return (components === undefined ||
|
|
50
|
+
(isObject(components) &&
|
|
51
|
+
typeof components['engineHeadSha'] === 'string' &&
|
|
52
|
+
isFingerprintMap(components['fingerprints'])));
|
|
53
|
+
}
|
|
@@ -33,11 +33,12 @@ export type BaselineBuildKind = 'full' | 'faster';
|
|
|
33
33
|
export declare function getBuildBaselinePath(projectRoot: string): string;
|
|
34
34
|
/**
|
|
35
35
|
* Reads the last-build baseline if present. Returns undefined when no
|
|
36
|
-
* previous successful build has been recorded.
|
|
37
|
-
*
|
|
36
|
+
* previous successful build has been recorded. Builds can replace invalid
|
|
37
|
+
* markers; dispatch preflights pass `refuse` so invalid coverage never
|
|
38
|
+
* becomes the legacy implicit full claim.
|
|
38
39
|
* @param projectRoot - Root directory of the project
|
|
39
40
|
*/
|
|
40
|
-
export declare function readBuildBaseline(projectRoot: string): Promise<BuildBaseline | undefined>;
|
|
41
|
+
export declare function readBuildBaseline(projectRoot: string, invalid?: 'ignore' | 'refuse'): Promise<BuildBaseline | undefined>;
|
|
41
42
|
export interface WriteBuildBaselineOptions {
|
|
42
43
|
/** Root directory of the project. */
|
|
43
44
|
projectRoot: string;
|
|
@@ -18,13 +18,15 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { readFile } from 'node:fs/promises';
|
|
20
20
|
import { join } from 'node:path';
|
|
21
|
+
import { PreflightRefusalError } from '../errors/base.js';
|
|
21
22
|
import { mapWithConcurrency } from '../utils/concurrency.js';
|
|
22
23
|
import { getNodeErrorCode, toError } from '../utils/errors.js';
|
|
23
|
-
import {
|
|
24
|
+
import { readJson, writeJson } from '../utils/fs.js';
|
|
24
25
|
import { sha256Hex } from '../utils/hash.js';
|
|
25
26
|
import { verbose } from '../utils/logger.js';
|
|
26
27
|
import { isBuildInputPath, isJarManifestPath, isPackageablePath, isXpcomManifestPath, } from './build-audit.js';
|
|
27
28
|
import { DELETED_FILE_FINGERPRINT, } from './build-baseline-types.js';
|
|
29
|
+
import { isBuildBaseline } from './build-baseline-validate.js';
|
|
28
30
|
import { FIREFORGE_DIR } from './config-paths.js';
|
|
29
31
|
import { hashEngineFile } from './coverage-extend.js';
|
|
30
32
|
import { getHead, hasChanges, isMissingHeadError } from './git.js';
|
|
@@ -47,21 +49,26 @@ export function getBuildBaselinePath(projectRoot) {
|
|
|
47
49
|
}
|
|
48
50
|
/**
|
|
49
51
|
* Reads the last-build baseline if present. Returns undefined when no
|
|
50
|
-
* previous successful build has been recorded.
|
|
51
|
-
*
|
|
52
|
+
* previous successful build has been recorded. Builds can replace invalid
|
|
53
|
+
* markers; dispatch preflights pass `refuse` so invalid coverage never
|
|
54
|
+
* becomes the legacy implicit full claim.
|
|
52
55
|
* @param projectRoot - Root directory of the project
|
|
53
56
|
*/
|
|
54
|
-
export async function readBuildBaseline(projectRoot) {
|
|
57
|
+
export async function readBuildBaseline(projectRoot, invalid = 'ignore') {
|
|
55
58
|
const path = getBuildBaselinePath(projectRoot);
|
|
56
|
-
if (!(await pathExists(path))) {
|
|
57
|
-
return undefined;
|
|
58
|
-
}
|
|
59
59
|
try {
|
|
60
|
-
|
|
60
|
+
const value = await readJson(path);
|
|
61
|
+
if (!isBuildBaseline(value))
|
|
62
|
+
throw new Error('invalid baseline fields');
|
|
63
|
+
return value;
|
|
61
64
|
}
|
|
62
|
-
catch {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
+
catch (error) {
|
|
66
|
+
if (getNodeErrorCode(error) === 'ENOENT')
|
|
67
|
+
return undefined;
|
|
68
|
+
if (invalid === 'refuse') {
|
|
69
|
+
throw new PreflightRefusalError(`Build baseline ${path} is invalid: ${toError(error).message}. Run "fireforge build" to record a fresh baseline before dispatching tests.`, 'invalid-build-baseline');
|
|
70
|
+
}
|
|
71
|
+
verbose(`Ignoring invalid build baseline ${path}: ${toError(error).message}`);
|
|
65
72
|
return undefined;
|
|
66
73
|
}
|
|
67
74
|
}
|
|
@@ -89,6 +96,7 @@ export async function writeBuildBaseline(options) {
|
|
|
89
96
|
}
|
|
90
97
|
}
|
|
91
98
|
const packageableFingerprints = await collectPackageableFingerprints(engineDir);
|
|
99
|
+
const testInputFingerprints = await collectDirtyFingerprints(engineDir, () => true, 'test staging fingerprint');
|
|
92
100
|
const buildInputFingerprints = await collectBuildInputFingerprints(engineDir, previousBaseline, buildKind);
|
|
93
101
|
// A full-coverage write (and the legacy no-claim `fireforge build` shape)
|
|
94
102
|
// just recompiled the StaticComponents table, so it anchors the table to
|
|
@@ -105,6 +113,7 @@ export async function writeBuildBaseline(options) {
|
|
|
105
113
|
builtAt: new Date().toISOString(),
|
|
106
114
|
binaryName,
|
|
107
115
|
...(packageableFingerprints !== undefined ? { packageableFingerprints } : {}),
|
|
116
|
+
...(testInputFingerprints !== undefined ? { testInputFingerprints } : {}),
|
|
108
117
|
...(buildInputFingerprints !== undefined ? { buildInputFingerprints } : {}),
|
|
109
118
|
...(testPackagingCoverage !== undefined ? { testPackagingCoverage } : {}),
|
|
110
119
|
...(mozconfigHash !== undefined ? { mozconfigHash } : {}),
|
|
@@ -218,6 +227,9 @@ async function collectDirtyFingerprints(engineDir, includePath, contextLabel) {
|
|
|
218
227
|
}
|
|
219
228
|
});
|
|
220
229
|
const fingerprints = {};
|
|
230
|
+
// Incomplete maps cannot prove retained test inputs unchanged.
|
|
231
|
+
if (entries.some((entry) => entry === undefined))
|
|
232
|
+
return undefined;
|
|
221
233
|
for (const entry of entries) {
|
|
222
234
|
if (entry !== undefined)
|
|
223
235
|
fingerprints[entry[0]] = entry[1];
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { FurnaceConfig } from '../types/furnace.js';
|
|
2
|
+
import type { FurnacePaths } from './furnace-config.js';
|
|
3
|
+
/** Records authored bytes without following directory aliases or writing to source directories. */
|
|
4
|
+
export declare function snapshotComponentSources(roots: readonly string[]): Promise<Map<string, string>>;
|
|
5
|
+
/** Detects reverted, removed or added source files before deployment can be committed. */
|
|
6
|
+
export declare function changedComponentSources(roots: readonly string[], before: ReadonlyMap<string, string>): Promise<string[]>;
|
|
7
|
+
interface ComponentSourceSnapshot {
|
|
8
|
+
roots: string[];
|
|
9
|
+
files: Map<string, string>;
|
|
10
|
+
}
|
|
11
|
+
/** Captures only configured inputs for a real deploy, plus shared CSS fragments. */
|
|
12
|
+
export declare function captureComponentSources(paths: FurnacePaths, config: FurnaceConfig, dryRun: boolean, options?: {
|
|
13
|
+
componentName?: string;
|
|
14
|
+
}): Promise<ComponentSourceSnapshot | undefined>;
|
|
15
|
+
/** Produces a blocking apply error before the journal or ownership state is committed. */
|
|
16
|
+
export declare function componentSourceErrors(snapshot: ComponentSourceSnapshot | undefined): Promise<Array<{
|
|
17
|
+
name: string;
|
|
18
|
+
error: string;
|
|
19
|
+
}>>;
|
|
20
|
+
export {};
|