@hominis/fireforge 0.51.0 → 0.52.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 +20 -0
- package/dist/build-info.json +4 -4
- package/dist/src/commands/status-output.js +5 -1
- package/dist/src/commands/test-build-phase.d.ts +2 -0
- package/dist/src/commands/test-build-phase.js +11 -11
- package/dist/src/commands/test-dispatch.js +19 -10
- package/dist/src/commands/test-orchestrate.d.ts +5 -0
- package/dist/src/commands/test-orchestrate.js +5 -2
- package/dist/src/commands/test-stale-gate.js +7 -0
- package/dist/src/commands/test.js +4 -1
- package/dist/src/core/build-baseline-types.d.ts +13 -0
- package/dist/src/core/build-baseline-validate.js +9 -0
- package/dist/src/core/build-baseline.js +7 -0
- package/dist/src/core/furnace-config-validate-exceptions.d.ts +4 -3
- package/dist/src/core/furnace-config-validate-exceptions.js +26 -6
- package/dist/src/core/furnace-validate-accessibility.d.ts +1 -1
- package/dist/src/core/furnace-validate-accessibility.js +22 -11
- package/dist/src/core/furnace-validate-exceptions.d.ts +2 -1
- package/dist/src/core/furnace-validate-exceptions.js +31 -13
- package/dist/src/core/negative-control-compiled.d.ts +10 -1
- package/dist/src/core/negative-control-compiled.js +15 -1
- package/dist/src/core/test-run-completeness.d.ts +12 -5
- package/dist/src/core/test-run-completeness.js +15 -8
- package/dist/src/core/test-stale-check.d.ts +10 -1
- package/dist/src/core/test-stale-check.js +15 -1
- package/dist/src/core/typecheck-shim.d.ts +2 -1
- package/dist/src/core/typecheck-shim.js +28 -4
- package/dist/src/types/furnace.d.ts +9 -0
- package/docs/configuration.md +20 -4
- package/docs/furnace.md +19 -1
- package/docs/machine-output.md +5 -1
- package/docs/testing.md +21 -6
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.52.0
|
|
4
|
+
|
|
5
|
+
### Test harness
|
|
6
|
+
|
|
7
|
+
- `test --jobs` gives each harness instance its own mochitest test list (`--testrun-manifest-file`). On the shared default `tests.json` the second instance ran the first shard's file and the verdict passed, or it found no file at all. A `--jobs` run now always reads each dispatch's mozlog raw log, and a dispatch whose raw log starts none of its requested files fails with `started=0/<m>` instead of passing on another shard's results.
|
|
8
|
+
- A negative control on compiled source escalates its pre-test build to a full `mach build`, with a notice that names the control's files. `mach build faster` compiles nothing, so under `--build` such a control still tested the previous binary and reported `control=missed`.
|
|
9
|
+
- A `coverage-replaced` refusal names who wrote the coverage record: its invocation, when, and the writing process's pid (marked `still running` while it is), `FIREFORGE_SESSION_ID` and invocation id. Every build baseline now records that `recorder`, and `status --test-coverage` shows it.
|
|
10
|
+
|
|
11
|
+
### Furnace
|
|
12
|
+
|
|
13
|
+
- A `validationExceptions` entry can name a `file` of the component, as one can name a `tag`. `hardcoded-text` judges each module on its own and lists the modules in `files`, so a vendored bundle whose bytes are pinned can be accepted while the authored modules beside it keep the check. A `furnace-ignore: hardcoded-text` comment now covers only the file it is in.
|
|
14
|
+
|
|
15
|
+
### Typecheck
|
|
16
|
+
|
|
17
|
+
- `typecheck.litTypes` declares the global type `MozLitElementWith<Extra>`. A widget casts its `MozLitElement` base to it to type query getters and members declared only in `static properties`. Without `litTypes`, and under per-patch checkJs, the type is `any`, so the same source checks in both.
|
|
18
|
+
|
|
19
|
+
### Other
|
|
20
|
+
|
|
21
|
+
- CI: `github/codeql-action` 4.39.0.
|
|
22
|
+
|
|
3
23
|
## 0.51.0
|
|
4
24
|
|
|
5
25
|
### Test harness
|
package/dist/build-info.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"version": "0.
|
|
4
|
-
"commit": "
|
|
5
|
-
"shortCommit": "
|
|
3
|
+
"version": "0.52.0",
|
|
4
|
+
"commit": "8926d4e4fe3a6ce4c52f3cf1530776d09352b130",
|
|
5
|
+
"shortCommit": "8926d4e4fe3a",
|
|
6
6
|
"dirty": false,
|
|
7
7
|
"dirtyHash": null,
|
|
8
|
-
"buildTime": "2026-10-
|
|
8
|
+
"buildTime": "2026-10-19T23:32:47.811Z"
|
|
9
9
|
}
|
|
@@ -206,7 +206,11 @@ export function renderTestCoverageStatus(baseline) {
|
|
|
206
206
|
}
|
|
207
207
|
info(`Last build: ${baseline.builtAt} (binary: ${baseline.binaryName}, ` +
|
|
208
208
|
`engine HEAD: ${baseline.engineHeadSha || '(unborn)'})`);
|
|
209
|
-
|
|
209
|
+
const recorder = baseline.recorder;
|
|
210
|
+
info(`Recorded by: ${baseline.recordedBy ?? 'unknown'}` +
|
|
211
|
+
(recorder === undefined
|
|
212
|
+
? ''
|
|
213
|
+
: ` (pid ${recorder.pid}${recorder.sessionId !== undefined ? `, session ${recorder.sessionId}` : ''})`));
|
|
210
214
|
const coverage = baseline.testPackagingCoverage;
|
|
211
215
|
if (coverage === undefined) {
|
|
212
216
|
info('Test packaging coverage: full (implicit — baseline predates coverage recording)');
|
|
@@ -16,4 +16,6 @@ import type { HarnessClassification } from './test-modes.js';
|
|
|
16
16
|
export declare function runTestBuildPhase(projectRoot: string, paths: ProjectPaths, projectConfig: FireForgeConfig, harnessRetries: number, options: TestOptions, request: {
|
|
17
17
|
classification: HarnessClassification;
|
|
18
18
|
normalizedPaths: string[];
|
|
19
|
+
/** Forces a full pre-test build (a compiled negative control). */
|
|
20
|
+
fullBuildReason?: string;
|
|
19
21
|
}): Promise<boolean>;
|
|
@@ -27,7 +27,7 @@ import { emitPassVerdict } from './test-verdict.js';
|
|
|
27
27
|
* @param options - See {@link RunPreTestBuildOptions}
|
|
28
28
|
*/
|
|
29
29
|
async function runPreTestBuild(options) {
|
|
30
|
-
const { projectRoot, paths, projectConfig, harnessRetries, testPackagingCoverage, refuseUnexportedDrift, extend, furnace, } = options;
|
|
30
|
+
const { projectRoot, paths, projectConfig, harnessRetries, testPackagingCoverage, refuseUnexportedDrift, extend, furnace, fullBuildReason, } = options;
|
|
31
31
|
await withBuildLock(projectRoot, async () => {
|
|
32
32
|
// Pass the previous baseline exactly like `fireforge build` does, so
|
|
33
33
|
// auto-configure runs under the same conditions on both paths. The
|
|
@@ -66,17 +66,16 @@ async function runPreTestBuild(options) {
|
|
|
66
66
|
if (retentionNotice !== undefined)
|
|
67
67
|
notice(retentionNotice);
|
|
68
68
|
}
|
|
69
|
-
const
|
|
70
|
-
|
|
69
|
+
const fullBuild = preparation.fullBuildRequired || fullBuildReason !== undefined;
|
|
70
|
+
const buildKind = fullBuild ? 'full' : 'faster';
|
|
71
|
+
if (fullBuild) {
|
|
71
72
|
// Warning severity rather than info: agent-facing output filters
|
|
72
73
|
// keep warnings and errors only, and this is the line that explains an
|
|
73
74
|
// otherwise unexplained multi-minute build.
|
|
74
|
-
notice(preparation.fullBuildReason ??
|
|
75
|
+
notice((preparation.fullBuildRequired ? preparation.fullBuildReason : fullBuildReason) ??
|
|
75
76
|
'A jar.mn registration changed since the last successful build; escalating this pre-test build to a full mach build so new install-manifest destinations are created.');
|
|
76
77
|
}
|
|
77
|
-
const s = spinner(
|
|
78
|
-
? 'Running required full build...'
|
|
79
|
-
: 'Running incremental build...');
|
|
78
|
+
const s = spinner(fullBuild ? 'Running required full build...' : 'Running incremental build...');
|
|
80
79
|
// The pre-test build routes through the same protected mach dispatch as
|
|
81
80
|
// `fireforge build` / `build --ui`: in-venv resource-monitor guard plus
|
|
82
81
|
// the uniform recognized-crash retry budget, with a fresh mach process
|
|
@@ -116,7 +115,7 @@ async function runPreTestBuild(options) {
|
|
|
116
115
|
...recorded,
|
|
117
116
|
previousBaseline,
|
|
118
117
|
recordedBy: describeBuildInvocation(extend !== undefined, testPackagingCoverage),
|
|
119
|
-
staticComponentsHandling:
|
|
118
|
+
staticComponentsHandling: fullBuild
|
|
120
119
|
? 'refresh'
|
|
121
120
|
: plan !== undefined && (plan.keepFull || Object.keys(plan.retained).length > 0)
|
|
122
121
|
? 'carry-forward'
|
|
@@ -131,8 +130,8 @@ async function runPreTestBuild(options) {
|
|
|
131
130
|
}
|
|
132
131
|
s.error('Pre-test build failed');
|
|
133
132
|
throw new BuildError(result.crashSignature
|
|
134
|
-
? buildHarnessCrashMessage(result.crashSignature, result.attempts, 'mach build faster')
|
|
135
|
-
: 'Pre-test build failed',
|
|
133
|
+
? buildHarnessCrashMessage(result.crashSignature, result.attempts, fullBuild ? 'mach build' : 'mach build faster')
|
|
134
|
+
: 'Pre-test build failed', fullBuild ? 'mach build' : 'mach build faster');
|
|
136
135
|
});
|
|
137
136
|
}
|
|
138
137
|
/**
|
|
@@ -201,7 +200,7 @@ function describeBuildInvocation(extending, coverage) {
|
|
|
201
200
|
* next steps, and emits the run's single `PASS` verdict.
|
|
202
201
|
*/
|
|
203
202
|
export async function runTestBuildPhase(projectRoot, paths, projectConfig, harnessRetries, options, request) {
|
|
204
|
-
const { classification, normalizedPaths } = request;
|
|
203
|
+
const { classification, normalizedPaths, fullBuildReason } = request;
|
|
205
204
|
const extending = options.extendCoverage === true;
|
|
206
205
|
if (extending) {
|
|
207
206
|
assertExtendCoverageUsage(options, normalizedPaths);
|
|
@@ -235,6 +234,7 @@ export async function runTestBuildPhase(projectRoot, paths, projectConfig, harne
|
|
|
235
234
|
testPackagingCoverage: coverage,
|
|
236
235
|
refuseUnexportedDrift: options.refuseUnexportedDrift === true,
|
|
237
236
|
extend: extending ? { requestedPaths: normalizedPaths } : undefined,
|
|
237
|
+
fullBuildReason,
|
|
238
238
|
furnace: await planPreTestFurnace(projectRoot, paths, projectConfig, options, normalizedPaths),
|
|
239
239
|
});
|
|
240
240
|
info('');
|
|
@@ -85,25 +85,34 @@ async function allocateLoopbackPort() {
|
|
|
85
85
|
});
|
|
86
86
|
});
|
|
87
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* The mochitest harness writes the dispatched test list to
|
|
90
|
+
* `_tests/testing/mochitest/tests.json` and the browser fetches it from
|
|
91
|
+
* there, so instances sharing the default name read each other's list (and
|
|
92
|
+
* one instance's cleanup removes it under the other). Each instance names
|
|
93
|
+
* its own file.
|
|
94
|
+
*/
|
|
95
|
+
function testRunManifestArg(index) {
|
|
96
|
+
return `--testrun-manifest-file=fireforge-tests-${process.pid}-job${index + 1}.json`;
|
|
97
|
+
}
|
|
88
98
|
/**
|
|
89
99
|
* The context for `--jobs` instance `index` (0-based): its own output
|
|
90
|
-
* prefix, and from the second instance on its own
|
|
91
|
-
* browsers started at the same moment never race for
|
|
92
|
-
* server ports are left to the engine harness's
|
|
93
|
-
* `--jobs` requires (auto ports).
|
|
100
|
+
* prefix and test-list manifest, and from the second instance on its own
|
|
101
|
+
* Marionette port, so browsers started at the same moment never race for
|
|
102
|
+
* 2828. The mochitest server ports are left to the engine harness's
|
|
103
|
+
* busy-port fallback, which `--jobs` requires (auto ports).
|
|
94
104
|
*/
|
|
95
105
|
async function instanceContext(base, index, xpcshellOnly) {
|
|
96
106
|
const ctx = { ...base, outputPrefix: `[job ${index + 1}] ` };
|
|
97
|
-
if (
|
|
107
|
+
if (xpcshellOnly)
|
|
98
108
|
return ctx;
|
|
109
|
+
const own = [...base.baseExtraArgs, testRunManifestArg(index)];
|
|
110
|
+
if (index === 0)
|
|
111
|
+
return { ...ctx, baseExtraArgs: own };
|
|
99
112
|
const port = await allocateLoopbackPort();
|
|
100
113
|
return {
|
|
101
114
|
...ctx,
|
|
102
|
-
baseExtraArgs: [
|
|
103
|
-
...base.baseExtraArgs,
|
|
104
|
-
`--setpref=marionette.port=${port}`,
|
|
105
|
-
`--marionette=127.0.0.1:${port}`,
|
|
106
|
-
],
|
|
115
|
+
baseExtraArgs: [...own, `--setpref=marionette.port=${port}`, `--marionette=127.0.0.1:${port}`],
|
|
107
116
|
};
|
|
108
117
|
}
|
|
109
118
|
/** The shards to run, or undefined for one combined invocation. */
|
|
@@ -30,6 +30,11 @@ export interface TestRunHooks {
|
|
|
30
30
|
* source, which only reaches the binary when the build compiles it.
|
|
31
31
|
*/
|
|
32
32
|
beforeBuild?: () => Promise<void>;
|
|
33
|
+
/**
|
|
34
|
+
* Why the pre-test build must be a full `mach build`: `mach build faster`
|
|
35
|
+
* compiles nothing, so a compiled control's binary needs the full one.
|
|
36
|
+
*/
|
|
37
|
+
fullBuildReason?: string;
|
|
33
38
|
/** `--repeat` run this sub-run is (1-based), for `--results`. */
|
|
34
39
|
repetition?: number;
|
|
35
40
|
/** Where dispatches write their mozlog raw logs (`--results`, `--shard-size`). */
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import { getProjectPaths } from '../core/config.js';
|
|
19
19
|
import { applyNegativeControl, planNegativeControl, recoverPendingNegativeControl, restoreNegativeControl, } from '../core/negative-control.js';
|
|
20
|
-
import { formatCompiledControlRefusal } from '../core/negative-control-compiled.js';
|
|
20
|
+
import { formatCompiledControlBuildNotice, formatCompiledControlRefusal, } from '../core/negative-control-compiled.js';
|
|
21
21
|
import { getActiveRunLogPath, writeToActiveRunLog } from '../core/run-log.js';
|
|
22
22
|
import { GeneralError, InvalidArgumentError, PreflightRefusalError } from '../errors/base.js';
|
|
23
23
|
import { TestFailureError } from '../errors/build.js';
|
|
@@ -187,7 +187,10 @@ export async function orchestrateTestRuns(projectRoot, testPaths, options, body,
|
|
|
187
187
|
...(plan === undefined
|
|
188
188
|
? {}
|
|
189
189
|
: compiledControl && index === 0
|
|
190
|
-
? {
|
|
190
|
+
? {
|
|
191
|
+
beforeBuild: controlHook(controlPaths, plan, slot),
|
|
192
|
+
fullBuildReason: formatCompiledControlBuildNotice(compiled),
|
|
193
|
+
}
|
|
191
194
|
: { beforeDispatch: controlHook(controlPaths, plan, slot) }),
|
|
192
195
|
});
|
|
193
196
|
}
|
|
@@ -11,6 +11,7 @@ import { filterByCarriedFullCoverage } from '../core/coverage-dispatch.js';
|
|
|
11
11
|
import { checkStaleBuildForTest, checkStaticComponentsStale, findChangedTestManifestsForPaths, findUncoveredRequestPaths, formatStaleBuildWarning, formatStaticComponentsRefusal, formatTestCoverageRefusal, FULL_SUITE_REQUEST, } from '../core/test-stale-check.js';
|
|
12
12
|
import { formatStaleExemptions } from '../core/test-stale-exempt.js';
|
|
13
13
|
import { PreflightRefusalError } from '../errors/base.js';
|
|
14
|
+
import { isProcessAlive } from '../utils/errors.js';
|
|
14
15
|
import { info, warn } from '../utils/logger.js';
|
|
15
16
|
import { describePerfStaleImpact } from './test-perf-scope.js';
|
|
16
17
|
/**
|
|
@@ -93,6 +94,12 @@ async function enforceCoverageGate(engineDir, baseline, normalizedPaths) {
|
|
|
93
94
|
throw new PreflightRefusalError(formatTestCoverageRefusal(uncovered, Array.isArray(recordedCoverage) ? recordedCoverage : [], changedManifests, {
|
|
94
95
|
changedInputs: Object.fromEntries(remaining.map((request) => [request.path, request.changed])),
|
|
95
96
|
carriesFullAnchor: baseline?.fullCoverageAnchor !== undefined,
|
|
97
|
+
recordedBy: baseline?.recordedBy,
|
|
98
|
+
builtAt: baseline?.builtAt,
|
|
99
|
+
recorder: baseline?.recorder,
|
|
100
|
+
recorderAlive: baseline?.recorder !== undefined &&
|
|
101
|
+
baseline.recorder.pid !== process.pid &&
|
|
102
|
+
isProcessAlive(baseline.recorder.pid),
|
|
96
103
|
}), 'coverage-replaced');
|
|
97
104
|
}
|
|
98
105
|
/**
|
|
@@ -142,7 +142,9 @@ export async function testCommand(projectRoot, testPaths, options = {}, underLoc
|
|
|
142
142
|
renderPreflightRefusal(error);
|
|
143
143
|
emitFailVerdict('preflight', refusalNote(error));
|
|
144
144
|
});
|
|
145
|
-
|
|
145
|
+
// `--jobs` always reads each dispatch's raw log, so a shard that ran
|
|
146
|
+
// files other than its own fails instead of passing on their results.
|
|
147
|
+
const collectsResults = options.results !== undefined || options.shardSize !== undefined || (options.jobs ?? 1) > 1;
|
|
146
148
|
try {
|
|
147
149
|
await underLock(() => (collectsResults ? withRawLogDir((dir) => orchestrate(dir)) : orchestrate()), lockMode);
|
|
148
150
|
}
|
|
@@ -346,6 +348,7 @@ async function runTestCommandBody(projectRoot, testPaths, options = {}, profileE
|
|
|
346
348
|
if (await runTestBuildPhase(projectRoot, paths, projectConfig, harnessRetries, options, {
|
|
347
349
|
classification,
|
|
348
350
|
normalizedPaths: hooks.coveragePaths ?? normalizedPaths,
|
|
351
|
+
...(hooks.fullBuildReason === undefined ? {} : { fullBuildReason: hooks.fullBuildReason }),
|
|
349
352
|
})) {
|
|
350
353
|
return;
|
|
351
354
|
}
|
|
@@ -144,6 +144,19 @@ export interface BuildBaseline {
|
|
|
144
144
|
* as "unknown" when absent.
|
|
145
145
|
*/
|
|
146
146
|
recordedBy?: string;
|
|
147
|
+
/**
|
|
148
|
+
* The process that wrote this baseline: its pid, its invocation id (the
|
|
149
|
+
* one its history records carry) and the caller's `FIREFORGE_SESSION_ID`
|
|
150
|
+
* when set. A coverage refusal names it, so a run whose claim another
|
|
151
|
+
* session's scoped build replaced can tell whose build that was.
|
|
152
|
+
*/
|
|
153
|
+
recorder?: BaselineRecorder;
|
|
154
|
+
}
|
|
155
|
+
/** See {@link BuildBaseline.recorder}. */
|
|
156
|
+
export interface BaselineRecorder {
|
|
157
|
+
pid: number;
|
|
158
|
+
invocationId: string;
|
|
159
|
+
sessionId?: string;
|
|
147
160
|
}
|
|
148
161
|
/**
|
|
149
162
|
* State of the engine at the last full build, as far as compiled-in XPCOM
|
|
@@ -32,6 +32,13 @@ function isCoverageRecordMap(value) {
|
|
|
32
32
|
Array.isArray(record['inputs']) &&
|
|
33
33
|
record['inputs'].every(isBaselinePath)));
|
|
34
34
|
}
|
|
35
|
+
/** A `recorder` stamp: an integer pid, an invocation id and an optional session id. */
|
|
36
|
+
function isBaselineRecorder(value) {
|
|
37
|
+
return (isObject(value) &&
|
|
38
|
+
Number.isInteger(value['pid']) &&
|
|
39
|
+
typeof value['invocationId'] === 'string' &&
|
|
40
|
+
(value['sessionId'] === undefined || typeof value['sessionId'] === 'string'));
|
|
41
|
+
}
|
|
35
42
|
/** Validates persisted fields without converting malformed coverage into a full claim. */
|
|
36
43
|
export function isBuildBaseline(value) {
|
|
37
44
|
if (!isObject(value))
|
|
@@ -59,6 +66,8 @@ export function isBuildBaseline(value) {
|
|
|
59
66
|
return false;
|
|
60
67
|
if (value['recordedBy'] !== undefined && typeof value['recordedBy'] !== 'string')
|
|
61
68
|
return false;
|
|
69
|
+
if (value['recorder'] !== undefined && !isBaselineRecorder(value['recorder']))
|
|
70
|
+
return false;
|
|
62
71
|
if (value['testCoverageRecords'] !== undefined &&
|
|
63
72
|
!isCoverageRecordMap(value['testCoverageRecords']))
|
|
64
73
|
return false;
|
|
@@ -32,6 +32,7 @@ import { hashEngineFile } from './coverage-extend.js';
|
|
|
32
32
|
import { getHead, hasChanges, isMissingHeadError } from './git.js';
|
|
33
33
|
import { git } from './git-base.js';
|
|
34
34
|
import { getUntrackedFiles } from './git-status.js';
|
|
35
|
+
import { historyRecordIdentity } from './history-log-file.js';
|
|
35
36
|
/*
|
|
36
37
|
* Keep the IO fan-out bounded: a patched Firefox checkout can carry hundreds
|
|
37
38
|
* of dirty packageable paths and serial reads add directly to every build.
|
|
@@ -72,6 +73,11 @@ export async function readBuildBaseline(projectRoot, invalid = 'ignore') {
|
|
|
72
73
|
return undefined;
|
|
73
74
|
}
|
|
74
75
|
}
|
|
76
|
+
/** This process's {@link BuildBaseline.recorder} stamp. */
|
|
77
|
+
function baselineRecorder() {
|
|
78
|
+
const { pid, invocationId, sessionId } = historyRecordIdentity();
|
|
79
|
+
return { pid, invocationId, ...(sessionId !== undefined ? { sessionId } : {}) };
|
|
80
|
+
}
|
|
75
81
|
/**
|
|
76
82
|
* Records a successful build by writing a fresh baseline marker. Captures
|
|
77
83
|
* engine HEAD SHA (or an empty string when the engine has no HEAD yet) and
|
|
@@ -121,6 +127,7 @@ export async function writeBuildBaseline(options) {
|
|
|
121
127
|
...(mozconfigHash !== undefined ? { mozconfigHash } : {}),
|
|
122
128
|
...(staticComponentsBaseline !== undefined ? { staticComponentsBaseline } : {}),
|
|
123
129
|
...(recordedBy !== undefined ? { recordedBy } : {}),
|
|
130
|
+
recorder: baselineRecorder(),
|
|
124
131
|
};
|
|
125
132
|
await writeJson(getBuildBaselinePath(projectRoot), baseline);
|
|
126
133
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { type FurnaceOptInCheck, type FurnaceValidationException } from '../types/furnace.js';
|
|
2
2
|
/**
|
|
3
|
-
* Parses `validationExceptions[]`: `{ component, check, tag?, reason }`
|
|
4
|
-
* objects, no other keys, every field a non-empty string,
|
|
5
|
-
*
|
|
3
|
+
* Parses `validationExceptions[]`: `{ component, check, tag?, file?, reason }`
|
|
4
|
+
* objects, no other keys, every field a non-empty string, at most one of
|
|
5
|
+
* `tag` and `file` (a bare file name of the component), and no two entries
|
|
6
|
+
* for the same component, check and scope.
|
|
6
7
|
*/
|
|
7
8
|
export declare function parseValidationExceptions(raw: unknown): FurnaceValidationException[];
|
|
8
9
|
/**
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import { FurnaceError } from '../errors/furnace.js';
|
|
12
12
|
import { FURNACE_OPT_IN_CHECKS, } from '../types/furnace.js';
|
|
13
13
|
import { isArray, isObject, isString } from '../utils/validation.js';
|
|
14
|
-
const EXCEPTION_KEYS = new Set(['component', 'check', 'tag', 'reason']);
|
|
14
|
+
const EXCEPTION_KEYS = new Set(['component', 'check', 'tag', 'file', 'reason']);
|
|
15
15
|
function requireNonEmptyString(entry, key, field) {
|
|
16
16
|
const value = entry[key];
|
|
17
17
|
if (!isString(value) || value.trim().length === 0) {
|
|
@@ -20,9 +20,10 @@ function requireNonEmptyString(entry, key, field) {
|
|
|
20
20
|
return value;
|
|
21
21
|
}
|
|
22
22
|
/**
|
|
23
|
-
* Parses `validationExceptions[]`: `{ component, check, tag?, reason }`
|
|
24
|
-
* objects, no other keys, every field a non-empty string,
|
|
25
|
-
*
|
|
23
|
+
* Parses `validationExceptions[]`: `{ component, check, tag?, file?, reason }`
|
|
24
|
+
* objects, no other keys, every field a non-empty string, at most one of
|
|
25
|
+
* `tag` and `file` (a bare file name of the component), and no two entries
|
|
26
|
+
* for the same component, check and scope.
|
|
26
27
|
*/
|
|
27
28
|
export function parseValidationExceptions(raw) {
|
|
28
29
|
if (!isArray(raw)) {
|
|
@@ -46,9 +47,28 @@ export function parseValidationExceptions(raw) {
|
|
|
46
47
|
};
|
|
47
48
|
if (entry['tag'] !== undefined)
|
|
48
49
|
exception.tag = requireNonEmptyString(entry, 'tag', field);
|
|
49
|
-
|
|
50
|
+
if (entry['file'] !== undefined) {
|
|
51
|
+
if (exception.tag !== undefined) {
|
|
52
|
+
throw new FurnaceError(`Furnace config: "${field}" may name a "tag" or a "file", not both`);
|
|
53
|
+
}
|
|
54
|
+
const file = requireNonEmptyString(entry, 'file', field);
|
|
55
|
+
if (/[\\/]/.test(file) || file === '.' || file === '..') {
|
|
56
|
+
throw new FurnaceError(`Furnace config: "${field}.file" must be a file name in the component's directory, not a path`);
|
|
57
|
+
}
|
|
58
|
+
exception.file = file;
|
|
59
|
+
}
|
|
60
|
+
const scope = exception.tag !== undefined
|
|
61
|
+
? `tag:${exception.tag}`
|
|
62
|
+
: exception.file !== undefined
|
|
63
|
+
? `file:${exception.file}`
|
|
64
|
+
: '';
|
|
65
|
+
const identity = `${exception.component}\u0000${exception.check}\u0000${scope}`;
|
|
50
66
|
if (seen.has(identity)) {
|
|
51
|
-
const on = exception.tag !== undefined
|
|
67
|
+
const on = exception.tag !== undefined
|
|
68
|
+
? ` on <${exception.tag}>`
|
|
69
|
+
: exception.file !== undefined
|
|
70
|
+
? ` in ${exception.file}`
|
|
71
|
+
: '';
|
|
52
72
|
throw new FurnaceError(`Furnace config: "${field}" repeats the exception for ${exception.component} [${exception.check}]${on}`);
|
|
53
73
|
}
|
|
54
74
|
seen.add(identity);
|
|
@@ -34,6 +34,6 @@ export declare function validateAccessibility(componentDir: string, tagName: str
|
|
|
34
34
|
* `title=`, or `tooltiptext=` attribute when constructing DOM in JS.
|
|
35
35
|
*
|
|
36
36
|
* A file-wide `// furnace-ignore: hardcoded-text` comment suppresses all
|
|
37
|
-
* findings (matches the pre-existing escape hatch).
|
|
37
|
+
* findings in that file (matches the pre-existing escape hatch).
|
|
38
38
|
*/
|
|
39
39
|
export declare function containsHardcodedTemplateText(content: string): boolean;
|
|
@@ -11,8 +11,9 @@ import { collectLitTemplateRegions, parseTemplateAttributes } from './lit-templa
|
|
|
11
11
|
* findings do not depend on which of the component's modules it lives in.
|
|
12
12
|
* Returns undefined when the host module is missing.
|
|
13
13
|
*/
|
|
14
|
-
async function
|
|
15
|
-
const
|
|
14
|
+
async function readComposedComponentModules(componentDir, tagName) {
|
|
15
|
+
const hostName = `${tagName}.mjs`;
|
|
16
|
+
const hostPath = join(componentDir, hostName);
|
|
16
17
|
if (!(await pathExists(hostPath)))
|
|
17
18
|
return undefined;
|
|
18
19
|
const entries = await readdir(componentDir).catch(() => []);
|
|
@@ -21,10 +22,11 @@ async function readComposedComponentSource(componentDir, tagName) {
|
|
|
21
22
|
name.endsWith('.mjs') &&
|
|
22
23
|
!/\.(test|spec|stories)\./.test(name))
|
|
23
24
|
.sort();
|
|
24
|
-
const
|
|
25
|
-
for (const name of companions)
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
const modules = [{ name: hostName, content: await readText(hostPath) }];
|
|
26
|
+
for (const name of companions) {
|
|
27
|
+
modules.push({ name, content: await readText(join(componentDir, name)) });
|
|
28
|
+
}
|
|
29
|
+
return modules;
|
|
28
30
|
}
|
|
29
31
|
/**
|
|
30
32
|
* Validates accessibility patterns in a component's composed source: its
|
|
@@ -44,9 +46,10 @@ async function readComposedComponentSource(componentDir, tagName) {
|
|
|
44
46
|
* adds the icon-only `<moz-button>` naming check.
|
|
45
47
|
*/
|
|
46
48
|
export async function validateAccessibility(componentDir, tagName, customConfig, optInChecks = []) {
|
|
47
|
-
const
|
|
48
|
-
if (
|
|
49
|
+
const modules = await readComposedComponentModules(componentDir, tagName);
|
|
50
|
+
if (modules === undefined)
|
|
49
51
|
return [];
|
|
52
|
+
const content = modules.map((module) => module.content).join('\n');
|
|
50
53
|
const issues = [];
|
|
51
54
|
if (!hasAriaRole(content) && hasGenericInteractiveElement(content)) {
|
|
52
55
|
issues.push(createIssue(tagName, 'warning', 'no-aria-role', 'Generic interactive markup has no native semantics. Prefer native elements, or add role= when native markup cannot provide the semantics.'));
|
|
@@ -72,8 +75,16 @@ export async function validateAccessibility(componentDir, tagName, customConfig,
|
|
|
72
75
|
tags: syntheticTags,
|
|
73
76
|
});
|
|
74
77
|
}
|
|
75
|
-
|
|
76
|
-
|
|
78
|
+
// Judged per module, so the finding names its files and an exception can
|
|
79
|
+
// accept one module (a vendored bundle) without muting the others.
|
|
80
|
+
const textFiles = modules
|
|
81
|
+
.filter((module) => containsHardcodedTemplateText(module.content))
|
|
82
|
+
.map((module) => module.name);
|
|
83
|
+
if (textFiles.length > 0) {
|
|
84
|
+
issues.push({
|
|
85
|
+
...createIssue(tagName, 'warning', 'hardcoded-text', `Possible hardcoded string found in ${textFiles.join(', ')}. Use data-l10n-id for localization.`),
|
|
86
|
+
files: textFiles,
|
|
87
|
+
});
|
|
77
88
|
}
|
|
78
89
|
const isInteractive = hasClick || hasKeyboardHandler;
|
|
79
90
|
if (isInteractive && !hasDelegatesFocusEnabled(content)) {
|
|
@@ -337,7 +348,7 @@ function isWithinLocalizedElement(content, matchIndex) {
|
|
|
337
348
|
* `title=`, or `tooltiptext=` attribute when constructing DOM in JS.
|
|
338
349
|
*
|
|
339
350
|
* A file-wide `// furnace-ignore: hardcoded-text` comment suppresses all
|
|
340
|
-
* findings (matches the pre-existing escape hatch).
|
|
351
|
+
* findings in that file (matches the pre-existing escape hatch).
|
|
341
352
|
*/
|
|
342
353
|
export function containsHardcodedTemplateText(content) {
|
|
343
354
|
if (/furnace-ignore:\s*hardcoded-text/.test(content)) {
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* Applies `furnace.json#validationExceptions[]` to a `furnace validate`
|
|
3
3
|
* result. An exception suppresses every finding of its check on its
|
|
4
4
|
* component, or with `tag`, a finding whose element tags it (and its
|
|
5
|
-
* sibling tag entries) cover
|
|
5
|
+
* sibling tag entries) cover, or with `file`, a finding whose module files
|
|
6
|
+
* it (and its sibling file entries) cover. One that suppresses nothing is stale: the
|
|
6
7
|
* finding was fixed (or renamed) and the review record outlived it, so it
|
|
7
8
|
* is reported as a warning naming the entry to delete, which keeps the
|
|
8
9
|
* list honest under `--max-warnings 0`.
|
|
@@ -6,19 +6,34 @@ export const EXCEPTION_ON_ERROR_CHECK = 'validation-exception-on-error';
|
|
|
6
6
|
function matches(exception, issue) {
|
|
7
7
|
return exception.component === issue.component && exception.check === issue.check;
|
|
8
8
|
}
|
|
9
|
+
/** An exception's narrowing: the finding field it reads and its value. */
|
|
10
|
+
function scopeOf(entry) {
|
|
11
|
+
if (entry.tag !== undefined)
|
|
12
|
+
return { key: 'tags', value: entry.tag };
|
|
13
|
+
if (entry.file !== undefined)
|
|
14
|
+
return { key: 'files', value: entry.file };
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
9
17
|
/**
|
|
10
|
-
* The exceptions that suppress a warning: an
|
|
11
|
-
* ones that together cover every tag the finding names.
|
|
18
|
+
* The exceptions that suppress a warning: an unscoped one alone, or scoped
|
|
19
|
+
* ones that together cover every tag (or every file) the finding names.
|
|
12
20
|
*/
|
|
13
21
|
function coveringExceptions(issue, candidates) {
|
|
14
|
-
const
|
|
15
|
-
if (
|
|
16
|
-
return [
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
+
const unscoped = candidates.find((entry) => scopeOf(entry) === undefined);
|
|
23
|
+
if (unscoped !== undefined)
|
|
24
|
+
return [unscoped];
|
|
25
|
+
for (const key of ['tags', 'files']) {
|
|
26
|
+
const named = issue[key] ?? [];
|
|
27
|
+
if (named.length === 0)
|
|
28
|
+
continue;
|
|
29
|
+
const covering = named.map((value) => candidates.find((entry) => {
|
|
30
|
+
const scope = scopeOf(entry);
|
|
31
|
+
return scope?.key === key && scope.value === value;
|
|
32
|
+
}));
|
|
33
|
+
if (covering.every((entry) => entry !== undefined))
|
|
34
|
+
return [...new Set(covering)];
|
|
35
|
+
}
|
|
36
|
+
return [];
|
|
22
37
|
}
|
|
23
38
|
/**
|
|
24
39
|
* Splits findings into counted and excepted, and reports stale exceptions.
|
|
@@ -47,11 +62,14 @@ export function applyValidationExceptions(issues, exceptions, judged) {
|
|
|
47
62
|
}
|
|
48
63
|
const covering = coveringExceptions(issue, candidates);
|
|
49
64
|
if (covering.length === 0) {
|
|
50
|
-
// A
|
|
51
|
-
// element still carries the finding, beside one it does
|
|
65
|
+
// A scoped exception that covers some of the tags (or files) is not
|
|
66
|
+
// stale: its element still carries the finding, beside one it does
|
|
67
|
+
// not cover.
|
|
52
68
|
for (const entry of candidates) {
|
|
53
|
-
|
|
69
|
+
const scope = scopeOf(entry);
|
|
70
|
+
if (scope !== undefined && issue[scope.key]?.includes(scope.value) === true) {
|
|
54
71
|
used.add(entry);
|
|
72
|
+
}
|
|
55
73
|
}
|
|
56
74
|
outcome.issues.push(issue);
|
|
57
75
|
continue;
|
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
* never compiled: the run tests the unmutated binary and reports
|
|
8
8
|
* `control=missed`, which reads as a test that cannot fail. Such a target
|
|
9
9
|
* is now refused without `--build`, and with `--build` the control is
|
|
10
|
-
* applied before the build so the binary under test carries it.
|
|
10
|
+
* applied before the build so the binary under test carries it. That build
|
|
11
|
+
* is a full `mach build`: the pre-test build is otherwise `mach build
|
|
12
|
+
* faster`, which compiles nothing, so the control still missed.
|
|
11
13
|
*/
|
|
12
14
|
/**
|
|
13
15
|
* The engine-relative targets that reach the runtime only through a
|
|
@@ -18,6 +20,13 @@
|
|
|
18
20
|
* @param enginePaths - The control's engine-relative targets
|
|
19
21
|
*/
|
|
20
22
|
export declare function findCompiledControlTargets(engineDir: string, enginePaths: readonly string[]): Promise<string[]>;
|
|
23
|
+
/**
|
|
24
|
+
* The notice that explains why a compiled control's pre-test build is a
|
|
25
|
+
* full `mach build` instead of `mach build faster`.
|
|
26
|
+
*
|
|
27
|
+
* @param compiled - Engine-relative compiled targets
|
|
28
|
+
*/
|
|
29
|
+
export declare function formatCompiledControlBuildNotice(compiled: readonly string[]): string;
|
|
21
30
|
/**
|
|
22
31
|
* The refusal for a compiled control without `--build`, naming the manual
|
|
23
32
|
* procedure for a run that wants to keep its build out of FireForge.
|
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
* never compiled: the run tests the unmutated binary and reports
|
|
9
9
|
* `control=missed`, which reads as a test that cannot fail. Such a target
|
|
10
10
|
* is now refused without `--build`, and with `--build` the control is
|
|
11
|
-
* applied before the build so the binary under test carries it.
|
|
11
|
+
* applied before the build so the binary under test carries it. That build
|
|
12
|
+
* is a full `mach build`: the pre-test build is otherwise `mach build
|
|
13
|
+
* faster`, which compiles nothing, so the control still missed.
|
|
12
14
|
*/
|
|
13
15
|
import { extname } from 'node:path';
|
|
14
16
|
import { classifyRuntimeDelivery } from './objdir-install-manifests.js';
|
|
@@ -55,6 +57,18 @@ export async function findCompiledControlTargets(engineDir, enginePaths) {
|
|
|
55
57
|
}
|
|
56
58
|
return compiled;
|
|
57
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* The notice that explains why a compiled control's pre-test build is a
|
|
62
|
+
* full `mach build` instead of `mach build faster`.
|
|
63
|
+
*
|
|
64
|
+
* @param compiled - Engine-relative compiled targets
|
|
65
|
+
*/
|
|
66
|
+
export function formatCompiledControlBuildNotice(compiled) {
|
|
67
|
+
const files = compiled.map((path) => `engine/${path}`).join(', ');
|
|
68
|
+
return (`The negative control edits compiled source (${files}); escalating this pre-test build to ` +
|
|
69
|
+
'a full mach build, since mach build faster compiles nothing and the binary under test ' +
|
|
70
|
+
'would not carry the control.');
|
|
71
|
+
}
|
|
58
72
|
/**
|
|
59
73
|
* The refusal for a compiled control without `--build`, naming the manual
|
|
60
74
|
* procedure for a run that wants to keep its build out of FireForge.
|
|
@@ -17,10 +17,14 @@
|
|
|
17
17
|
* `FAIL reason=test-failures`, and a short start count is stamped on the
|
|
18
18
|
* verdict line as `started=<n>/<m> first-unstarted=<path>`.
|
|
19
19
|
*
|
|
20
|
-
*
|
|
21
|
-
* requested file. The suite-specific xpcshell
|
|
22
|
-
* with a result-summary block alone and print
|
|
23
|
-
* run that never names its files cannot be
|
|
20
|
+
* From streamed or captured text, the start-count check only runs when at
|
|
21
|
+
* least one start marker names a requested file. The suite-specific xpcshell
|
|
22
|
+
* dispatch can prove execution with a result-summary block alone and print
|
|
23
|
+
* no `TEST_START` line, and a run that never names its files cannot be
|
|
24
|
+
* counted against them. The structured raw log names every test by its
|
|
25
|
+
* manifest id, so a raw log whose starts name none of the requested files
|
|
26
|
+
* ran something else: a `--jobs` instance that read another instance's test
|
|
27
|
+
* list once passed on that instance's file.
|
|
24
28
|
*/
|
|
25
29
|
import { type HarnessRunVerdict } from './test-harness-crash.js';
|
|
26
30
|
import type { IncompleteRunEvidence } from './test-harness-signature.js';
|
|
@@ -62,8 +66,11 @@ export declare function createStartMarkerTracker(): StartMarkerTracker;
|
|
|
62
66
|
* @param evidence - Captured mach stdout and stderr, or the markers
|
|
63
67
|
* collected from the complete stream
|
|
64
68
|
* @param requestedPaths - Dispatched paths, in dispatch order
|
|
69
|
+
* @param authoritative - The markers come from the raw log, so a run whose
|
|
70
|
+
* starts name no requested file is counted (as none started) instead of
|
|
71
|
+
* being left uncounted
|
|
65
72
|
*/
|
|
66
|
-
export declare function countStartedFiles(evidence: string | StartMarkers, requestedPaths: readonly string[]): {
|
|
73
|
+
export declare function countStartedFiles(evidence: string | StartMarkers, requestedPaths: readonly string[], authoritative?: boolean): {
|
|
67
74
|
started: number;
|
|
68
75
|
requested: number;
|
|
69
76
|
firstUnstarted?: string;
|
|
@@ -18,10 +18,14 @@
|
|
|
18
18
|
* `FAIL reason=test-failures`, and a short start count is stamped on the
|
|
19
19
|
* verdict line as `started=<n>/<m> first-unstarted=<path>`.
|
|
20
20
|
*
|
|
21
|
-
*
|
|
22
|
-
* requested file. The suite-specific xpcshell
|
|
23
|
-
* with a result-summary block alone and print
|
|
24
|
-
* run that never names its files cannot be
|
|
21
|
+
* From streamed or captured text, the start-count check only runs when at
|
|
22
|
+
* least one start marker names a requested file. The suite-specific xpcshell
|
|
23
|
+
* dispatch can prove execution with a result-summary block alone and print
|
|
24
|
+
* no `TEST_START` line, and a run that never names its files cannot be
|
|
25
|
+
* counted against them. The structured raw log names every test by its
|
|
26
|
+
* manifest id, so a raw log whose starts name none of the requested files
|
|
27
|
+
* ran something else: a `--jobs` instance that read another instance's test
|
|
28
|
+
* list once passed on that instance's file.
|
|
25
29
|
*/
|
|
26
30
|
import { SHUTDOWN_REENTRY_PATTERN, START_MARKER_PATTERN, TEST_FILE_BASENAME_PATTERN, } from './test-harness-crash.js';
|
|
27
31
|
/**
|
|
@@ -80,8 +84,11 @@ export function createStartMarkerTracker() {
|
|
|
80
84
|
* @param evidence - Captured mach stdout and stderr, or the markers
|
|
81
85
|
* collected from the complete stream
|
|
82
86
|
* @param requestedPaths - Dispatched paths, in dispatch order
|
|
87
|
+
* @param authoritative - The markers come from the raw log, so a run whose
|
|
88
|
+
* starts name no requested file is counted (as none started) instead of
|
|
89
|
+
* being left uncounted
|
|
83
90
|
*/
|
|
84
|
-
export function countStartedFiles(evidence, requestedPaths) {
|
|
91
|
+
export function countStartedFiles(evidence, requestedPaths, authoritative = false) {
|
|
85
92
|
const requested = requestedPaths.filter((path) => TEST_FILE_BASENAME_PATTERN.test(basenameOf(path)));
|
|
86
93
|
if (requested.length === 0)
|
|
87
94
|
return undefined;
|
|
@@ -92,8 +99,8 @@ export function countStartedFiles(evidence, requestedPaths) {
|
|
|
92
99
|
};
|
|
93
100
|
// Counting needs a dialect that names the requested files on its start
|
|
94
101
|
// lines. When no start line names any of them, the output cannot be
|
|
95
|
-
// counted against the request at all.
|
|
96
|
-
if (!requested.some((path) => names(path, started)))
|
|
102
|
+
// counted against the request at all, unless it is the raw log.
|
|
103
|
+
if (!authoritative && !requested.some((path) => names(path, started)))
|
|
97
104
|
return undefined;
|
|
98
105
|
const unstarted = requested.filter((path) => !names(path, started) && !names(path, skipped));
|
|
99
106
|
const firstUnstarted = unstarted[0];
|
|
@@ -125,7 +132,7 @@ export function vetoIncompletePass(verdict, output, requestedPaths, tracked) {
|
|
|
125
132
|
// tracker means nothing was streamed through it, and the capture is then
|
|
126
133
|
// the only evidence there is.
|
|
127
134
|
const useTracked = tracked !== undefined && tracked.markers.started.length > 0;
|
|
128
|
-
const counted = countStartedFiles(useTracked ? tracked.markers : output, requestedPaths);
|
|
135
|
+
const counted = countStartedFiles(useTracked ? tracked.markers : output, requestedPaths, useTracked && tracked.source === 'raw');
|
|
129
136
|
const unexpected = verdict.unexpected ?? 0;
|
|
130
137
|
const truncated = counted?.firstUnstarted !== undefined;
|
|
131
138
|
if (unexpected === 0 && shutdownLine === undefined && !truncated)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BuildBaseline, TestPackagingCoverage } from './build-baseline-types.js';
|
|
1
|
+
import type { BaselineRecorder, BuildBaseline, TestPackagingCoverage } from './build-baseline-types.js';
|
|
2
2
|
import { type StaleExemption } from './test-stale-exempt.js';
|
|
3
3
|
/** Result of the stale-build preflight probe. */
|
|
4
4
|
export interface StaleBuildResult {
|
|
@@ -97,6 +97,15 @@ export interface CoverageRefusalDetail {
|
|
|
97
97
|
changedInputs?: Readonly<Record<string, readonly string[]>>;
|
|
98
98
|
/** The record carries a full build's anchor forward for unchanged paths. */
|
|
99
99
|
carriesFullAnchor?: boolean;
|
|
100
|
+
/**
|
|
101
|
+
* Who wrote the record and when, so a run whose coverage a peer's scoped
|
|
102
|
+
* build replaced can tell whose build that was.
|
|
103
|
+
*/
|
|
104
|
+
recordedBy?: string | undefined;
|
|
105
|
+
builtAt?: string | undefined;
|
|
106
|
+
recorder?: BaselineRecorder | undefined;
|
|
107
|
+
/** Whether the recorder's process is still running (it may still be using the packaging). */
|
|
108
|
+
recorderAlive?: boolean | undefined;
|
|
100
109
|
}
|
|
101
110
|
/**
|
|
102
111
|
* Formats the refusal shown when a non-`--build` run requests paths the
|
|
@@ -163,6 +163,20 @@ export async function findChangedTestManifestsForPaths(engineDir, baseline, unco
|
|
|
163
163
|
});
|
|
164
164
|
return hits.slice(0, STALE_PATHS_LIMIT);
|
|
165
165
|
}
|
|
166
|
+
/** The refusal's sentence naming who wrote the coverage record. */
|
|
167
|
+
function formatCoverageHolder(detail) {
|
|
168
|
+
if (detail.recorder === undefined && detail.recordedBy === undefined)
|
|
169
|
+
return '';
|
|
170
|
+
const what = detail.recordedBy !== undefined ? `"${detail.recordedBy}"` : 'a build';
|
|
171
|
+
const when = detail.builtAt !== undefined ? ` at ${detail.builtAt}` : '';
|
|
172
|
+
const recorder = detail.recorder;
|
|
173
|
+
const who = recorder === undefined
|
|
174
|
+
? ''
|
|
175
|
+
: ` (pid ${recorder.pid}${detail.recorderAlive === true ? ', still running' : ''}` +
|
|
176
|
+
(recorder.sessionId !== undefined ? `, session ${recorder.sessionId}` : '') +
|
|
177
|
+
`, invocation ${recorder.invocationId})`;
|
|
178
|
+
return ` The current record was written by ${what}${when}${who}.`;
|
|
179
|
+
}
|
|
166
180
|
/**
|
|
167
181
|
* Formats the refusal shown when a non-`--build` run requests paths the
|
|
168
182
|
* packaged runtime's scoped `test --build` coverage does not include. It is
|
|
@@ -210,7 +224,7 @@ export function formatTestCoverageRefusal(uncovered, coverage, changedManifests
|
|
|
210
224
|
return (`The packaged test runtime was produced by a scoped "fireforge test --build" covering only: ${scope}.\n` +
|
|
211
225
|
`The requested run needs ${cap(uncovered)}, which that packaging does not cover — support ` +
|
|
212
226
|
'fixtures for those manifests may be missing from obj-*/_tests/, and the run can hang ' +
|
|
213
|
-
`rather than fail.${affectedLine}${manifestLine} ${rebuildHint} ` +
|
|
227
|
+
`rather than fail.${formatCoverageHolder(detail)}${affectedLine}${manifestLine} ${rebuildHint} ` +
|
|
214
228
|
'(--allow-stale-build does not bypass this check — it accepts stale content, not missing coverage.)');
|
|
215
229
|
}
|
|
216
230
|
/**
|
|
@@ -82,7 +82,8 @@ export interface ComposedShim {
|
|
|
82
82
|
* shim path
|
|
83
83
|
* @param extraShimPath - Optional project-relative path to an extra `.d.ts`
|
|
84
84
|
* @param options - `litTypes` appends {@link LIT_TYPES_SHIM} after the
|
|
85
|
-
* built-in shim (before the extra shim, which can still merge into it)
|
|
85
|
+
* built-in shim (before the extra shim, which can still merge into it),
|
|
86
|
+
* and {@link LOOSE_LIT_SHIM} otherwise
|
|
86
87
|
*/
|
|
87
88
|
export declare function composeShimSource(projectRoot: string, extraShimPath: string | undefined, options?: {
|
|
88
89
|
litTypes?: boolean | undefined;
|
|
@@ -295,8 +295,13 @@ export const UNDEFINED_IDENTIFIER_CODES = new Set([
|
|
|
295
295
|
* `chrome:*` wildcard as `any`: `LitElement` (with `ReactiveElement`'s
|
|
296
296
|
* lifecycle) and `MozLitElement` carry closed member lists, so a misspelt
|
|
297
297
|
* member of a widget host is TS2339, as `Cu` is in the built-in shim.
|
|
298
|
-
* Template helpers and directives stay loose.
|
|
299
|
-
*
|
|
298
|
+
* Template helpers and directives stay loose. The global
|
|
299
|
+
* `MozLitElementWith<Extra>` types a widget's query getters and
|
|
300
|
+
* `static properties` members: the widget casts its `MozLitElement` import
|
|
301
|
+
* to it before extending ({@link LOOSE_LIT_SHIM} keeps it `any` without
|
|
302
|
+
* typed Lit). A project
|
|
303
|
+
* shim can merge members into `LitElementShimMembers`; a project whose
|
|
304
|
+
* jsconfig `paths`
|
|
300
305
|
* maps these URLs to the real sources gets those instead, since a resolved
|
|
301
306
|
* file wins over an ambient module.
|
|
302
307
|
*/
|
|
@@ -399,6 +404,24 @@ declare module 'chrome://global/content/lit-utils.mjs' {
|
|
|
399
404
|
export const SelectControlBaseElement: any;
|
|
400
405
|
export const SelectControlItemMixin: any;
|
|
401
406
|
}
|
|
407
|
+
// MozLitElement with instance members Extra: what a widget casts its base
|
|
408
|
+
// to for the getters static queries installs (and properties it declares
|
|
409
|
+
// only in static properties). A class field would redefine a query getter
|
|
410
|
+
// and throw, and a constructor declaration reads it before renderRoot.
|
|
411
|
+
type MozLitElementWith<Extra> = Pick<
|
|
412
|
+
typeof import('chrome://global/content/lit-utils.mjs').MozLitElement,
|
|
413
|
+
keyof typeof import('chrome://global/content/lit-utils.mjs').MozLitElement
|
|
414
|
+
> &
|
|
415
|
+
(new () => import('chrome://global/content/lit-utils.mjs').MozLitElement & Extra);
|
|
416
|
+
`;
|
|
417
|
+
/**
|
|
418
|
+
* Without typed Lit, `MozLitElementWith<Extra>` is `any`, as the base it
|
|
419
|
+
* casts is: the same widget source checks under per-patch checkJs and a
|
|
420
|
+
* project typecheck that leaves `litTypes` off.
|
|
421
|
+
*/
|
|
422
|
+
const LOOSE_LIT_SHIM = `
|
|
423
|
+
// FireForge loose Lit (typecheck.litTypes off): see typecheck-shim.ts.
|
|
424
|
+
type MozLitElementWith<Extra> = any;
|
|
402
425
|
`;
|
|
403
426
|
/** Hint appended to reported undefined-identifier diagnostics. */
|
|
404
427
|
export const UNDEFINED_IDENTIFIER_HINT = '(undefined identifier — import or declare it, add the global to the extra shim, ' +
|
|
@@ -462,10 +485,11 @@ async function inlineTripleSlashReferences(source, baseDir, seen) {
|
|
|
462
485
|
* shim path
|
|
463
486
|
* @param extraShimPath - Optional project-relative path to an extra `.d.ts`
|
|
464
487
|
* @param options - `litTypes` appends {@link LIT_TYPES_SHIM} after the
|
|
465
|
-
* built-in shim (before the extra shim, which can still merge into it)
|
|
488
|
+
* built-in shim (before the extra shim, which can still merge into it),
|
|
489
|
+
* and {@link LOOSE_LIT_SHIM} otherwise
|
|
466
490
|
*/
|
|
467
491
|
export async function composeShimSource(projectRoot, extraShimPath, options = {}) {
|
|
468
|
-
const base = options.litTypes === true ?
|
|
492
|
+
const base = `${FIREFOX_GLOBALS_SHIM}\n${options.litTypes === true ? LIT_TYPES_SHIM : LOOSE_LIT_SHIM}`;
|
|
469
493
|
if (!extraShimPath) {
|
|
470
494
|
return { source: base, extraShimAppended: false };
|
|
471
495
|
}
|
|
@@ -115,6 +115,13 @@ export interface FurnaceValidationException {
|
|
|
115
115
|
* check for every other element of the component.
|
|
116
116
|
*/
|
|
117
117
|
tag?: string;
|
|
118
|
+
/**
|
|
119
|
+
* Narrows the exception to the findings in this module file of the
|
|
120
|
+
* component (a finding's `files`), by file name: a vendored bundle whose
|
|
121
|
+
* bytes are pinned can be accepted without muting the check for the
|
|
122
|
+
* authored modules beside it.
|
|
123
|
+
*/
|
|
124
|
+
file?: string;
|
|
118
125
|
/** Why the finding is accepted. Required: the entry is the review record. */
|
|
119
126
|
reason: string;
|
|
120
127
|
}
|
|
@@ -379,6 +386,8 @@ export interface ValidationIssue {
|
|
|
379
386
|
message: string;
|
|
380
387
|
/** Element tags the finding is about, when it is about particular elements. */
|
|
381
388
|
tags?: string[];
|
|
389
|
+
/** Component module files the finding is in (`hardcoded-text`), by file name. */
|
|
390
|
+
files?: string[];
|
|
382
391
|
}
|
|
383
392
|
/** Resolved test-harness selection for a `furnace create` run. */
|
|
384
393
|
export type ResolvedTestStyle = 'mochikit' | 'browser-chrome' | 'xpcshell' | 'none';
|
package/docs/configuration.md
CHANGED
|
@@ -306,10 +306,26 @@ shim: `LitElement` and the helpers of
|
|
|
306
306
|
member lists, as `Cu` does, so a misspelt member of a widget host is an
|
|
307
307
|
error rather than `any`. Template helpers and directives stay loose. It is
|
|
308
308
|
off by default because a reactive property declared only in
|
|
309
|
-
`static properties` or `static queries` then needs a
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
309
|
+
`static properties` or `static queries` then needs a declaration to be
|
|
310
|
+
known. Since 0.52.0 the shim declares the global type
|
|
311
|
+
`MozLitElementWith<Extra>`: a widget casts its base to it, and its
|
|
312
|
+
instances carry `Extra` beside MozLitElement's members. A query getter
|
|
313
|
+
cannot be declared any other way: a class field redefines the getter
|
|
314
|
+
MozLitElement installs and throws, and a constructor declaration reads it
|
|
315
|
+
before `renderRoot` exists.
|
|
316
|
+
|
|
317
|
+
```js
|
|
318
|
+
const Base = /** @type {MozLitElementWith<{ input: HTMLInputElement }>} */ (MozLitElement);
|
|
319
|
+
export class MozSearch extends Base {
|
|
320
|
+
static queries = { input: 'input' };
|
|
321
|
+
}
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
Without `litTypes` (and under per-patch checkJs, which does not type Lit)
|
|
325
|
+
`MozLitElementWith<Extra>` is `any`, as the base is, so the same source
|
|
326
|
+
checks in both. A project shim can add members by merging
|
|
327
|
+
`interface LitElementShimMembers`; a jsconfig `paths` entry that maps these
|
|
328
|
+
URLs to real sources takes precedence.
|
|
313
329
|
|
|
314
330
|
Per-patch checkJs gives each test script its own program. Since 0.51.0 a
|
|
315
331
|
test's `resource:///` and `chrome://` imports (including a
|
package/docs/furnace.md
CHANGED
|
@@ -136,12 +136,30 @@ entries cover all of them:
|
|
|
136
136
|
}
|
|
137
137
|
```
|
|
138
138
|
|
|
139
|
+
An optional `file` (since 0.52.0) narrows an entry the same way to findings
|
|
140
|
+
in one module of the component, by file name. `hardcoded-text` names the
|
|
141
|
+
modules it found text in, so a vendored bundle whose bytes are pinned (and
|
|
142
|
+
so cannot carry a `furnace-ignore` comment) can be accepted while the
|
|
143
|
+
authored modules beside it keep the check. An entry names a `tag` or a
|
|
144
|
+
`file`, not both:
|
|
145
|
+
|
|
146
|
+
```json
|
|
147
|
+
{
|
|
148
|
+
"component": "moz-editor",
|
|
149
|
+
"check": "hardcoded-text",
|
|
150
|
+
"file": "moz-editor-codemirror.mjs",
|
|
151
|
+
"reason": "The vendored bundle's width probe is never rendered."
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
139
155
|
Accessibility checks judge the composed component: the host
|
|
140
156
|
`<tag>.mjs` and its companion modules (`<tag>-*.mjs`, tests and stories
|
|
141
157
|
left out) read as one source. Moving a template between the component's
|
|
142
158
|
modules therefore no longer changes its findings: a keyboard handler in a
|
|
143
159
|
companion covers an `@click` in the host, and an `@click` in a companion
|
|
144
|
-
is judged too.
|
|
160
|
+
is judged too. `hardcoded-text` judges each module on its own, so a
|
|
161
|
+
`// furnace-ignore: hardcoded-text` comment covers the file it is in, not
|
|
162
|
+
its siblings. Toolkit's `panel-item` counts as natively
|
|
145
163
|
keyboard-activated, beside `button`, form controls and the `moz-*` buttons.
|
|
146
164
|
|
|
147
165
|
`minComponents` sets the fewest components a full `furnace validate` must
|
package/docs/machine-output.md
CHANGED
|
@@ -92,6 +92,8 @@ Additive keys since 0.51.0 outside `test`:
|
|
|
92
92
|
`@click`). An exception that matched an error adds a
|
|
93
93
|
`validation-exception-on-error` error, and a full run under
|
|
94
94
|
`furnace.json#minComponents` a `component-floor` error on `furnace.json`.
|
|
95
|
+
- `furnace validate --json` (since 0.52.0): a `hardcoded-text` finding
|
|
96
|
+
carries `files`, the component's module file names it was found in.
|
|
95
97
|
|
|
96
98
|
## Versioning
|
|
97
99
|
|
|
@@ -199,7 +201,9 @@ shard did not pass; under `--shard-size` `shards=` counts files and
|
|
|
199
201
|
session lock shared, and `results=<path>` names the `--results` document. A
|
|
200
202
|
run whose engine changed mid-run only in files the running browser cannot
|
|
201
203
|
see keeps its verdict and appends `drift-tolerated=<n>`. A negative control
|
|
202
|
-
on compiled source appends `control-build=compiled`.
|
|
204
|
+
on compiled source appends `control-build=compiled`. Since 0.52.0 a raw log
|
|
205
|
+
that starts none of a dispatch's requested files counts as
|
|
206
|
+
`started=0/<m> ... started-source=raw` instead of leaving the run uncounted.
|
|
203
207
|
|
|
204
208
|
### The additive `shuffle=` key
|
|
205
209
|
|
package/docs/testing.md
CHANGED
|
@@ -112,7 +112,11 @@ install manifest ships) can only reach the binary through a build. Without
|
|
|
112
112
|
`--build` it is refused before the lock. With `--build` it is applied before
|
|
113
113
|
the pre-test build, so the binary under test carries it, stays applied for
|
|
114
114
|
every `--repeat` run, and is restored with a fresh mtime so the next build
|
|
115
|
-
recompiles the original.
|
|
115
|
+
recompiles the original. Since 0.52.0 that pre-test build is a full
|
|
116
|
+
`mach build`, with a notice naming the control's files: the default
|
|
117
|
+
`mach build faster` compiles nothing, so before, the control still tested
|
|
118
|
+
the previous binary and reported `missed`. The verdict adds
|
|
119
|
+
`control-build=compiled`. A
|
|
116
120
|
build-less control whose packaging is not current is refused before the
|
|
117
121
|
lock with the advice to add `--build`, instead of ending `inconclusive`
|
|
118
122
|
after a full queue.
|
|
@@ -219,7 +223,12 @@ A scoped build after a full build keeps the claim full while nothing outside
|
|
|
219
223
|
its scope moved. Otherwise it carries the full build's anchor forward, and a
|
|
220
224
|
later build-less run over any path stays covered while that path's own
|
|
221
225
|
staging inputs are unchanged since the full build. A refusal names the inputs
|
|
222
|
-
that did change.
|
|
226
|
+
that did change, and since 0.52.0 who wrote the coverage record: its
|
|
227
|
+
invocation, when, and the writing process's pid (marked `still running`
|
|
228
|
+
while it is), `FIREFORGE_SESSION_ID` and invocation id, the id its history
|
|
229
|
+
records carry. `status --test-coverage` shows the pid and session too. A
|
|
230
|
+
peer whose run a scoped build or a test-file edit refused can tell whose
|
|
231
|
+
build to wait for.
|
|
223
232
|
|
|
224
233
|
`--extend-coverage` remains accepted. Retention is now the default, so the
|
|
225
234
|
flag only turns a drop into a refusal before the build. Before 0.50.0 a
|
|
@@ -582,10 +591,16 @@ helpers.
|
|
|
582
591
|
`--jobs <n>` runs a request's shards on n harness instances inside one lock
|
|
583
592
|
hold. Each instance has its own profile and its own Marionette port, and
|
|
584
593
|
auto ports (required) move each instance's mochitest server ports; output
|
|
585
|
-
lines carry a `[job <i>]` prefix in the terminal and the run log.
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
594
|
+
lines carry a `[job <i>]` prefix in the terminal and the run log. Since
|
|
595
|
+
0.52.0 each instance also writes its own mochitest test list
|
|
596
|
+
(`--testrun-manifest-file`): on the harness's shared default
|
|
597
|
+
`_tests/testing/mochitest/tests.json`, the second instance ran the first
|
|
598
|
+
shard's file and passed, or found none. A `--jobs` run always reads each
|
|
599
|
+
dispatch's mozlog raw log, and a dispatch whose raw log starts none of its
|
|
600
|
+
requested files fails with `started=0/<m>` instead of passing on another
|
|
601
|
+
shard's results. With `--no-shard`, or a single directory argument, the
|
|
602
|
+
files are split in order into n contiguous chunks. Parallel browsers raise
|
|
603
|
+
host load, so measure before relying on it.
|
|
589
604
|
|
|
590
605
|
`--shard-size <n>` dispatches n files per mach invocation (mochitest restarts
|
|
591
606
|
the browser between files), paying the harness start once per chunk instead
|