@hominis/fireforge 0.36.0 → 0.37.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 +13 -0
- package/dist/src/commands/build.js +1 -1
- package/dist/src/commands/doctor-orphaned-harness.d.ts +52 -0
- package/dist/src/commands/doctor-orphaned-harness.js +132 -0
- package/dist/src/commands/doctor.js +2 -0
- package/dist/src/commands/export.js +7 -0
- package/dist/src/commands/patch/staged-dependency.d.ts +1 -1
- package/dist/src/commands/patch/staged-dependency.js +113 -51
- package/dist/src/commands/re-export-files.js +4 -0
- package/dist/src/commands/re-export.js +8 -0
- package/dist/src/commands/test-diagnose.js +20 -1
- package/dist/src/commands/test.js +75 -23
- package/dist/src/core/build-baseline-types.d.ts +24 -0
- package/dist/src/core/build-baseline.d.ts +5 -2
- package/dist/src/core/build-baseline.js +5 -1
- package/dist/src/core/furnace-config-custom.js +10 -0
- package/dist/src/core/furnace-stale-export.d.ts +59 -0
- package/dist/src/core/furnace-stale-export.js +123 -0
- package/dist/src/core/furnace-validate-compatibility.js +9 -2
- package/dist/src/core/furnace-validate-structure.js +3 -2
- package/dist/src/core/mach-known-noise-filter.d.ts +60 -0
- package/dist/src/core/mach-known-noise-filter.js +193 -0
- package/dist/src/core/mach.d.ts +8 -0
- package/dist/src/core/mach.js +36 -3
- package/dist/src/core/patch-lint-cross.js +80 -2
- package/dist/src/core/patch-manifest-io.d.ts +3 -2
- package/dist/src/core/patch-manifest-io.js +27 -16
- package/dist/src/core/patch-manifest-validate.js +24 -0
- package/dist/src/core/test-harness-crash.d.ts +16 -0
- package/dist/src/core/test-harness-crash.js +55 -0
- package/dist/src/core/test-stale-check.d.ts +29 -1
- package/dist/src/core/test-stale-check.js +59 -0
- package/dist/src/types/commands/index.d.ts +1 -1
- package/dist/src/types/commands/options.d.ts +27 -4
- package/dist/src/types/commands/patches.d.ts +29 -0
- package/dist/src/types/furnace.d.ts +13 -0
- package/dist/src/utils/process-group.d.ts +33 -0
- package/dist/src/utils/process-group.js +161 -0
- package/dist/src/utils/process.d.ts +15 -0
- package/dist/src/utils/process.js +88 -44
- package/package.json +2 -2
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Terminal-echo filter for the KNOWN upstream mozsystemmonitor teardown
|
|
4
|
+
* traceback (0.37.0 item 8).
|
|
5
|
+
*
|
|
6
|
+
* Every headless test run against the 153-beta engine ends with an
|
|
7
|
+
* `AttributeError: 'SystemResourceMonitor' object has no attribute
|
|
8
|
+
* 'stop_time'` traceback at harness teardown — upstream noise that sits
|
|
9
|
+
* exactly where a reader looks for the failure summary. 0.36.0 already made
|
|
10
|
+
* real failure lines beat this traceback in CLASSIFICATION; this filter
|
|
11
|
+
* closes the PRESENTATION gap by collapsing the echoed traceback to one
|
|
12
|
+
* labeled line.
|
|
13
|
+
*
|
|
14
|
+
* Scope is deliberately narrow:
|
|
15
|
+
* - Only the terminal ECHO is filtered. The captured stdout/stderr strings
|
|
16
|
+
* stay raw — the classifier (`test-harness-crash.ts`) depends on the
|
|
17
|
+
* raw traceback for its green-summary override and secondary-noise
|
|
18
|
+
* detection.
|
|
19
|
+
* - Only the exact documented incident is collapsed, and every condition
|
|
20
|
+
* must hold: an `AttributeError` on `SystemResourceMonitor` naming one
|
|
21
|
+
* of the two known attributes (`stop_time`, `poll_interval` — the
|
|
22
|
+
* 0.34.0 guard family), AND a `mozsystemmonitor/resourcemonitor.py`
|
|
23
|
+
* stack frame, AND a previously-seen SUITE_END shutdown marker (shared
|
|
24
|
+
* across the run's stdout/stderr filter instances — the marker usually
|
|
25
|
+
* lands on stdout while the traceback lands on stderr). A novel
|
|
26
|
+
* attribute, a novel exception type in resourcemonitor.py, or a
|
|
27
|
+
* pre-shutdown occurrence is echoed verbatim, always.
|
|
28
|
+
* - The hold buffer is bounded; on overflow the block is flushed verbatim
|
|
29
|
+
* and the filter returns to pass-through, so output is never lost.
|
|
30
|
+
*/
|
|
31
|
+
const TRACEBACK_HEADER_PATTERN = /^Traceback \(most recent call last\)/;
|
|
32
|
+
const CHAINED_EXCEPTION_CONNECTOR_PATTERN = /^(?:During handling of the above exception, another exception occurred:|The above exception was the direct cause of the following exception:)\s*$/;
|
|
33
|
+
/**
|
|
34
|
+
* Closed allowlist of the documented teardown family's attributes:
|
|
35
|
+
* `stop_time` (the 0.37.0 item-8 incident) and `poll_interval` (the same
|
|
36
|
+
* mozsystemmonitor init failure the 0.34.0 resource-guard family covers —
|
|
37
|
+
* `test-harness-crash.ts` already classifies it as recognized noise). Any
|
|
38
|
+
* other missing attribute is a NEW upstream defect and must print verbatim.
|
|
39
|
+
*/
|
|
40
|
+
const KNOWN_TEARDOWN_ATTRIBUTE_ERROR_PATTERN = /AttributeError: 'SystemResourceMonitor' object has no attribute '(?:stop_time|poll_interval)'/;
|
|
41
|
+
const RESOURCEMONITOR_FRAME_PATTERN = /mozsystemmonitor[/\\]resourcemonitor\.py/;
|
|
42
|
+
/**
|
|
43
|
+
* Shutdown marker: the harness's SUITE_END line. Matches the shape
|
|
44
|
+
* `test-harness-crash.ts` keys its summary parsing on (module-private
|
|
45
|
+
* there; duplicated here with this cross-reference rather than exported).
|
|
46
|
+
*/
|
|
47
|
+
const SHUTDOWN_MARKER_PATTERN = /\bSUITE_END\b/;
|
|
48
|
+
/** Whole-block recognition — every signal must hold (see module doc). */
|
|
49
|
+
function isRecognizedTeardownNoise(block) {
|
|
50
|
+
return (KNOWN_TEARDOWN_ATTRIBUTE_ERROR_PATTERN.test(block) && RESOURCEMONITOR_FRAME_PATTERN.test(block));
|
|
51
|
+
}
|
|
52
|
+
/** Fresh per-run context — hand the same instance to both stream filters. */
|
|
53
|
+
export function createTeardownNoiseContext() {
|
|
54
|
+
return { shutdownSeen: false };
|
|
55
|
+
}
|
|
56
|
+
/** One line replaces the whole recognized traceback block in the echo. */
|
|
57
|
+
export const KNOWN_TEARDOWN_NOISE_ANNOTATION = '[FireForge] Known upstream mozsystemmonitor teardown noise (SystemResourceMonitor ' +
|
|
58
|
+
'AttributeError at harness shutdown) — not a test failure. See FireForge docs.\n';
|
|
59
|
+
/** Bounds on the held traceback block before flushing verbatim. */
|
|
60
|
+
const HOLD_LINE_LIMIT = 100;
|
|
61
|
+
const HOLD_BYTE_LIMIT = 16 * 1024;
|
|
62
|
+
/**
|
|
63
|
+
* Creates a stateful filter for one output stream. Complete lines outside a
|
|
64
|
+
* traceback pass straight through (only the trailing partial line is held
|
|
65
|
+
* back); a `Traceback (most recent call last)` header switches to hold mode
|
|
66
|
+
* until the block ends, then either the one-line annotation (recognized
|
|
67
|
+
* signature after a seen shutdown marker) or the verbatim block (anything
|
|
68
|
+
* else) is emitted. Pass the run's shared {@link TeardownNoiseContext} so
|
|
69
|
+
* both stream filters see the same shutdown flag.
|
|
70
|
+
*/
|
|
71
|
+
export function createKnownTeardownNoiseFilter(context = createTeardownNoiseContext()) {
|
|
72
|
+
/** Partial (no trailing newline yet) input line. */
|
|
73
|
+
let partial = '';
|
|
74
|
+
/** Held traceback lines (each WITH its newline) while in hold mode. */
|
|
75
|
+
let held = [];
|
|
76
|
+
let heldBytes = 0;
|
|
77
|
+
/**
|
|
78
|
+
* State machine:
|
|
79
|
+
* 'pass' — outside any traceback; lines echo through.
|
|
80
|
+
* 'inside' — between a Traceback header and its closing exception line.
|
|
81
|
+
* 'closed' — saw the closing `SomeError: …` line; still holding in
|
|
82
|
+
* case a chained-exception connector continues the block
|
|
83
|
+
* (the real fixture chains two tracebacks — the whole
|
|
84
|
+
* chain must be evaluated as ONE block or the second half
|
|
85
|
+
* would print raw after the annotation).
|
|
86
|
+
*/
|
|
87
|
+
let state = 'pass';
|
|
88
|
+
const resetHold = () => {
|
|
89
|
+
const block = held.join('');
|
|
90
|
+
held = [];
|
|
91
|
+
heldBytes = 0;
|
|
92
|
+
state = 'pass';
|
|
93
|
+
return block;
|
|
94
|
+
};
|
|
95
|
+
const releaseHeld = () => {
|
|
96
|
+
const block = resetHold();
|
|
97
|
+
if (block.length === 0)
|
|
98
|
+
return '';
|
|
99
|
+
return context.shutdownSeen && isRecognizedTeardownNoise(block)
|
|
100
|
+
? KNOWN_TEARDOWN_NOISE_ANNOTATION
|
|
101
|
+
: block;
|
|
102
|
+
};
|
|
103
|
+
const hold = (line) => {
|
|
104
|
+
held.push(line);
|
|
105
|
+
heldBytes += line.length;
|
|
106
|
+
if (held.length > HOLD_LINE_LIMIT || heldBytes > HOLD_BYTE_LIMIT) {
|
|
107
|
+
// Pathological block: flush verbatim rather than risk withholding
|
|
108
|
+
// output; do not attempt recognition on oversized blocks.
|
|
109
|
+
return { out: resetHold(), overflowed: true };
|
|
110
|
+
}
|
|
111
|
+
return { out: '', overflowed: false };
|
|
112
|
+
};
|
|
113
|
+
const processLine = (line) => {
|
|
114
|
+
const content = line.replace(/\r?\n$/, '');
|
|
115
|
+
if (state === 'pass') {
|
|
116
|
+
if (TRACEBACK_HEADER_PATTERN.test(content)) {
|
|
117
|
+
state = 'inside';
|
|
118
|
+
return hold(line).out;
|
|
119
|
+
}
|
|
120
|
+
if (SHUTDOWN_MARKER_PATTERN.test(content))
|
|
121
|
+
context.shutdownSeen = true;
|
|
122
|
+
return line;
|
|
123
|
+
}
|
|
124
|
+
if (state === 'inside') {
|
|
125
|
+
const isContinuation = content.trim().length === 0 ||
|
|
126
|
+
/^[ \t]/.test(content) ||
|
|
127
|
+
TRACEBACK_HEADER_PATTERN.test(content);
|
|
128
|
+
const { out, overflowed } = hold(line);
|
|
129
|
+
// The first unindented line is the closing `SomeError: …` line; keep
|
|
130
|
+
// holding in case a chained connector extends the block.
|
|
131
|
+
if (!overflowed && !isContinuation) {
|
|
132
|
+
state = 'closed';
|
|
133
|
+
}
|
|
134
|
+
return out;
|
|
135
|
+
}
|
|
136
|
+
// state === 'closed': only a blank line, a chained-exception connector,
|
|
137
|
+
// or another Traceback header continues the block.
|
|
138
|
+
if (content.trim().length === 0 || CHAINED_EXCEPTION_CONNECTOR_PATTERN.test(content)) {
|
|
139
|
+
return hold(line).out;
|
|
140
|
+
}
|
|
141
|
+
if (TRACEBACK_HEADER_PATTERN.test(content)) {
|
|
142
|
+
const { out, overflowed } = hold(line);
|
|
143
|
+
if (!overflowed)
|
|
144
|
+
state = 'inside';
|
|
145
|
+
return out;
|
|
146
|
+
}
|
|
147
|
+
return releaseHeld() + processLineInPass(line);
|
|
148
|
+
};
|
|
149
|
+
// Re-dispatch a line through pass-mode handling after a block release —
|
|
150
|
+
// the line that ended the block may itself start a new traceback.
|
|
151
|
+
const processLineInPass = (line) => {
|
|
152
|
+
const content = line.replace(/\r?\n$/, '');
|
|
153
|
+
if (TRACEBACK_HEADER_PATTERN.test(content)) {
|
|
154
|
+
state = 'inside';
|
|
155
|
+
return hold(line).out;
|
|
156
|
+
}
|
|
157
|
+
if (SHUTDOWN_MARKER_PATTERN.test(content))
|
|
158
|
+
context.shutdownSeen = true;
|
|
159
|
+
return line;
|
|
160
|
+
};
|
|
161
|
+
return {
|
|
162
|
+
transform(chunk) {
|
|
163
|
+
let out = '';
|
|
164
|
+
let buffer = partial + chunk;
|
|
165
|
+
partial = '';
|
|
166
|
+
let newlineIndex = buffer.indexOf('\n');
|
|
167
|
+
while (newlineIndex !== -1) {
|
|
168
|
+
const line = buffer.slice(0, newlineIndex + 1);
|
|
169
|
+
buffer = buffer.slice(newlineIndex + 1);
|
|
170
|
+
out += processLine(line);
|
|
171
|
+
newlineIndex = buffer.indexOf('\n');
|
|
172
|
+
}
|
|
173
|
+
partial = buffer;
|
|
174
|
+
return out;
|
|
175
|
+
},
|
|
176
|
+
flush() {
|
|
177
|
+
let out = '';
|
|
178
|
+
if (state !== 'pass') {
|
|
179
|
+
// A stream that closes mid-block: fold the trailing partial line in
|
|
180
|
+
// and evaluate — the final exception line is often the last thing
|
|
181
|
+
// printed, without a trailing newline.
|
|
182
|
+
if (partial.length > 0) {
|
|
183
|
+
held.push(partial);
|
|
184
|
+
partial = '';
|
|
185
|
+
}
|
|
186
|
+
out += releaseHeld();
|
|
187
|
+
}
|
|
188
|
+
out += partial;
|
|
189
|
+
partial = '';
|
|
190
|
+
return out;
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
}
|
package/dist/src/core/mach.d.ts
CHANGED
|
@@ -17,6 +17,14 @@ export interface MachOptions {
|
|
|
17
17
|
env?: Record<string, string>;
|
|
18
18
|
/** Whether to inherit stdio (show output directly) */
|
|
19
19
|
inherit?: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Collapse the KNOWN mozsystemmonitor teardown traceback to one labeled
|
|
22
|
+
* line in the terminal ECHO (capture-only option). The captured
|
|
23
|
+
* stdout/stderr stay raw — the harness classifier depends on the raw
|
|
24
|
+
* traceback. Unrecognized tracebacks always echo verbatim. Opted into by
|
|
25
|
+
* the test dispatchers only.
|
|
26
|
+
*/
|
|
27
|
+
annotateKnownTeardownNoise?: boolean;
|
|
20
28
|
}
|
|
21
29
|
/**
|
|
22
30
|
* Result of running a mach command while capturing streamed output.
|
package/dist/src/core/mach.js
CHANGED
|
@@ -7,6 +7,7 @@ import { exec, execInherit, execInheritCapture, execSmokeRun, execStream, } from
|
|
|
7
7
|
import { createSiblingLockPath, withFileLock } from './file-lock.js';
|
|
8
8
|
import { ensureFirefoxIgnorefileCompatibility } from './firefox-ignorefile.js';
|
|
9
9
|
import { explainMachError } from './mach-error-hints.js';
|
|
10
|
+
import { createKnownTeardownNoiseFilter, createTeardownNoiseContext, } from './mach-known-noise-filter.js';
|
|
10
11
|
import { getPython } from './mach-python.js';
|
|
11
12
|
import { installMachResourceGuard } from './mach-resource-shim.js';
|
|
12
13
|
import { detectHarnessCrashSignature } from './test-harness-crash.js';
|
|
@@ -42,7 +43,12 @@ export async function runMach(args, engineDir, options = {}) {
|
|
|
42
43
|
...(options.env ? { env: options.env } : {}),
|
|
43
44
|
};
|
|
44
45
|
if (options.inherit) {
|
|
45
|
-
|
|
46
|
+
// Group-reap every long-lived inherited mach dispatch (bootstrap, run,
|
|
47
|
+
// watch): a mach dying at startup must not strand multiprocessing
|
|
48
|
+
// workers. Short-lived metadata queries below stay on plain exec() —
|
|
49
|
+
// adding groups there would touch every non-mach exec consumer's path
|
|
50
|
+
// for no reaping benefit.
|
|
51
|
+
return execInherit(python, [machPath, ...args], { ...execOptions, processGroup: true });
|
|
46
52
|
}
|
|
47
53
|
const result = await exec(python, [machPath, ...args], execOptions);
|
|
48
54
|
return result.exitCode;
|
|
@@ -67,24 +73,47 @@ export async function runMachCapture(args, engineDir, options = {}) {
|
|
|
67
73
|
const machPath = join(engineDir, 'mach');
|
|
68
74
|
let stdout = '';
|
|
69
75
|
let stderr = '';
|
|
76
|
+
// Echo-path filters only: the captured strings above accumulate RAW so the
|
|
77
|
+
// classifier still sees the full teardown traceback. When the option is
|
|
78
|
+
// off, the filters are absent and echo is a straight write (unchanged).
|
|
79
|
+
// One shutdown context per run, shared by both stream filters: the
|
|
80
|
+
// SUITE_END marker usually arrives on stdout while the teardown traceback
|
|
81
|
+
// lands on stderr, so the shutdown-seen flag must span the pair.
|
|
82
|
+
const noiseContext = options.annotateKnownTeardownNoise === true ? createTeardownNoiseContext() : undefined;
|
|
83
|
+
const stdoutFilter = noiseContext && createKnownTeardownNoiseFilter(noiseContext);
|
|
84
|
+
const stderrFilter = noiseContext && createKnownTeardownNoiseFilter(noiseContext);
|
|
70
85
|
const exitCode = await execStream(python, [machPath, ...args], {
|
|
71
86
|
cwd: engineDir,
|
|
72
87
|
...(options.env ? { env: options.env } : {}),
|
|
88
|
+
// Every capture dispatch (test suites, protected builds, package,
|
|
89
|
+
// storybook) runs as a process-group leader and is group-reaped on
|
|
90
|
+
// exit/abort — see ExecOptions.processGroup (0.37.0 item 9a).
|
|
91
|
+
processGroup: true,
|
|
73
92
|
onStdout: (data) => {
|
|
74
93
|
stdout += data;
|
|
75
94
|
if (stdout.length > CAPTURE_TAIL_LIMIT) {
|
|
76
95
|
stdout = stdout.slice(-CAPTURE_TAIL_LIMIT);
|
|
77
96
|
}
|
|
78
|
-
process.stdout.write(data);
|
|
97
|
+
process.stdout.write(stdoutFilter ? stdoutFilter.transform(data) : data);
|
|
79
98
|
},
|
|
80
99
|
onStderr: (data) => {
|
|
81
100
|
stderr += data;
|
|
82
101
|
if (stderr.length > CAPTURE_TAIL_LIMIT) {
|
|
83
102
|
stderr = stderr.slice(-CAPTURE_TAIL_LIMIT);
|
|
84
103
|
}
|
|
85
|
-
process.stderr.write(data);
|
|
104
|
+
process.stderr.write(stderrFilter ? stderrFilter.transform(data) : data);
|
|
86
105
|
},
|
|
87
106
|
});
|
|
107
|
+
if (stdoutFilter) {
|
|
108
|
+
const residue = stdoutFilter.flush();
|
|
109
|
+
if (residue.length > 0)
|
|
110
|
+
process.stdout.write(residue);
|
|
111
|
+
}
|
|
112
|
+
if (stderrFilter) {
|
|
113
|
+
const residue = stderrFilter.flush();
|
|
114
|
+
if (residue.length > 0)
|
|
115
|
+
process.stderr.write(residue);
|
|
116
|
+
}
|
|
88
117
|
return { stdout, stderr, exitCode };
|
|
89
118
|
}
|
|
90
119
|
/**
|
|
@@ -99,6 +128,7 @@ export async function runMachInheritCapture(args, engineDir, options = {}) {
|
|
|
99
128
|
return execInheritCapture(python, [machPath, ...args], {
|
|
100
129
|
cwd: engineDir,
|
|
101
130
|
...(options.env ? { env: options.env } : {}),
|
|
131
|
+
processGroup: true,
|
|
102
132
|
});
|
|
103
133
|
}
|
|
104
134
|
/**
|
|
@@ -363,6 +393,7 @@ export async function testWithOutput(engineDir, testPaths = [], args = [], env)
|
|
|
363
393
|
const guard = await installMachResourceGuard(engineDir);
|
|
364
394
|
return runMachCapture(['test', ...testPaths, ...args], engineDir, {
|
|
365
395
|
env: { ...guard.env, ...env },
|
|
396
|
+
annotateKnownTeardownNoise: true,
|
|
366
397
|
});
|
|
367
398
|
}
|
|
368
399
|
/**
|
|
@@ -379,6 +410,7 @@ export async function xpcshellTestWithOutput(engineDir, testPaths = [], args = [
|
|
|
379
410
|
const guard = await installMachResourceGuard(engineDir);
|
|
380
411
|
return runMachCapture(['xpcshell-test', ...testPaths, ...args], engineDir, {
|
|
381
412
|
env: { ...guard.env, ...env },
|
|
413
|
+
annotateKnownTeardownNoise: true,
|
|
382
414
|
});
|
|
383
415
|
}
|
|
384
416
|
/**
|
|
@@ -390,5 +422,6 @@ export async function mochitestWithOutput(engineDir, testPaths = [], args = [],
|
|
|
390
422
|
const guard = await installMachResourceGuard(engineDir);
|
|
391
423
|
return runMachCapture(['mochitest', ...testPaths, ...args], engineDir, {
|
|
392
424
|
env: { ...guard.env, ...env },
|
|
425
|
+
annotateKnownTeardownNoise: true,
|
|
393
426
|
});
|
|
394
427
|
}
|
|
@@ -343,6 +343,14 @@ function findMatchingStagedDependency(entry, sitePath, specifier, laterOwners) {
|
|
|
343
343
|
laterOwners.some((owner) => owner.fullPath === dependency.creates &&
|
|
344
344
|
(dependency.owner === undefined || dependency.owner === owner.filename)));
|
|
345
345
|
}
|
|
346
|
+
/**
|
|
347
|
+
* Later-in-apply-order predicate shared by the forward-import scan and the
|
|
348
|
+
* registration validation: strictly higher ordinal, or same ordinal with a
|
|
349
|
+
* lexicographically later filename (the apply loop's tiebreak).
|
|
350
|
+
*/
|
|
351
|
+
function isLaterOwner(owner, entry) {
|
|
352
|
+
return (owner.order > entry.order || (owner.order === entry.order && owner.filename > entry.filename));
|
|
353
|
+
}
|
|
346
354
|
/** Builds the basename → later-creators index used by the forward-import scan. */
|
|
347
355
|
function buildNewFileIndex(ctx) {
|
|
348
356
|
const newFileIndex = new Map();
|
|
@@ -382,8 +390,7 @@ function eachForwardImportSite(ctx, newFileIndex, visit) {
|
|
|
382
390
|
const owners = newFileIndex.get(leaf);
|
|
383
391
|
if (!owners)
|
|
384
392
|
continue;
|
|
385
|
-
const laterOwners = owners.filter((owner) => owner
|
|
386
|
-
(owner.order === entry.order && owner.filename > entry.filename));
|
|
393
|
+
const laterOwners = owners.filter((owner) => isLaterOwner(owner, entry));
|
|
387
394
|
if (laterOwners.length === 0)
|
|
388
395
|
continue;
|
|
389
396
|
visit(entry, sitePath, specifier, cleaned, laterOwners);
|
|
@@ -477,9 +484,80 @@ export function lintPatchQueueForwardImports(ctx) {
|
|
|
477
484
|
severity: 'warning',
|
|
478
485
|
});
|
|
479
486
|
}
|
|
487
|
+
// Registration-kind declarations (0.37.0 item 5): a jar.mn packaging
|
|
488
|
+
// line, customElements registration, or actor registration referencing
|
|
489
|
+
// a later-created file. There is no import specifier to match — the
|
|
490
|
+
// declaration is "used" when the declared line (whitespace-trimmed)
|
|
491
|
+
// appears among the lines this patch adds to the declaring file AND
|
|
492
|
+
// its `creates` resolves to a file a later-ordered patch actually
|
|
493
|
+
// creates (with `owner`, when set, naming that patch) — mirroring
|
|
494
|
+
// findMatchingStagedDependency for the import kind.
|
|
495
|
+
for (const registration of entry.metadata?.stagedDependencies?.registrations ?? []) {
|
|
496
|
+
const failure = findStagedRegistrationFailure(entry, registration, newFileIndex);
|
|
497
|
+
if (failure === undefined)
|
|
498
|
+
continue;
|
|
499
|
+
issues.push({
|
|
500
|
+
file: registration.file,
|
|
501
|
+
check: 'staged-dependency-unused',
|
|
502
|
+
fingerprint: `staged-dependency-unused|${entry.filename}|${registration.file}|reg:${registration.line}|${registration.creates}|${registration.owner ?? ''}`,
|
|
503
|
+
message: `${entry.filename} declares a staged registration in ${registration.file} ` +
|
|
504
|
+
`("${registration.line}") for ${registration.creates}, but ${describeRegistrationFailure(failure)} ` +
|
|
505
|
+
'Remove the stale declaration with "fireforge patch staged-dependency --remove --kind registration" or update it to match the patch and queue.',
|
|
506
|
+
severity: 'warning',
|
|
507
|
+
});
|
|
508
|
+
}
|
|
480
509
|
}
|
|
481
510
|
return issues;
|
|
482
511
|
}
|
|
512
|
+
/**
|
|
513
|
+
* Validates one registration-kind declaration against the patch content
|
|
514
|
+
* and the queue, mirroring {@link findMatchingStagedDependency}: the
|
|
515
|
+
* declared line must be added by this patch, `creates` must exactly match
|
|
516
|
+
* a file a LATER-ordered patch creates (an earlier-only creator means the
|
|
517
|
+
* dependency is already satisfied and the declaration is stale), and
|
|
518
|
+
* `owner`, when set, must name one of those creating patches. Returns
|
|
519
|
+
* undefined when valid.
|
|
520
|
+
*/
|
|
521
|
+
function findStagedRegistrationFailure(entry, registration, newFileIndex) {
|
|
522
|
+
if (!isRegistrationLinePresent(entry, registration))
|
|
523
|
+
return { kind: 'line-absent' };
|
|
524
|
+
const creators = (newFileIndex.get(basename(registration.creates)) ?? []).filter((owner) => owner.fullPath === registration.creates && isLaterOwner(owner, entry));
|
|
525
|
+
if (creators.length === 0)
|
|
526
|
+
return { kind: 'creates-unresolved' };
|
|
527
|
+
if (registration.owner !== undefined &&
|
|
528
|
+
!creators.some((owner) => owner.filename === registration.owner)) {
|
|
529
|
+
return { kind: 'owner-mismatch', creators };
|
|
530
|
+
}
|
|
531
|
+
return undefined;
|
|
532
|
+
}
|
|
533
|
+
/** Message tail for each {@link StagedRegistrationFailure} shape. */
|
|
534
|
+
function describeRegistrationFailure(failure) {
|
|
535
|
+
switch (failure.kind) {
|
|
536
|
+
case 'line-absent':
|
|
537
|
+
return 'the patch does not add that line.';
|
|
538
|
+
case 'creates-unresolved':
|
|
539
|
+
return ('no later-ordered patch creates that file — the declaration is stale, ' +
|
|
540
|
+
'or the queue order no longer stages it.');
|
|
541
|
+
case 'owner-mismatch':
|
|
542
|
+
return `that file is created by ${failure.creators.map((o) => o.filename).join(', ')}, not the declared owner.`;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* True when the declared registration/packaging line appears (trimmed)
|
|
547
|
+
* among the lines the patch introduces in the declaring file — either as a
|
|
548
|
+
* newly-created file's content or as added lines to an existing file
|
|
549
|
+
* (where jar.mn / customElements.js / actor-registration edits land).
|
|
550
|
+
*/
|
|
551
|
+
function isRegistrationLinePresent(entry, registration) {
|
|
552
|
+
const declared = registration.line.trim();
|
|
553
|
+
if (declared.length === 0)
|
|
554
|
+
return false;
|
|
555
|
+
const introduced = [
|
|
556
|
+
entry.newFiles.get(registration.file),
|
|
557
|
+
entry.modifiedFileAdditions.get(registration.file),
|
|
558
|
+
];
|
|
559
|
+
return introduced.some((content) => content !== undefined && content.split('\n').some((line) => line.trim() === declared));
|
|
560
|
+
}
|
|
483
561
|
/**
|
|
484
562
|
* Cross-patch lint orchestrator. Runs every cross-patch rule against the
|
|
485
563
|
* provided context and returns combined issues.
|
|
@@ -102,8 +102,9 @@ export interface PatchRenameEntry {
|
|
|
102
102
|
newOrder: number;
|
|
103
103
|
}
|
|
104
104
|
/**
|
|
105
|
-
* Rewrites `stagedDependencies.forwardImports[].owner`
|
|
106
|
-
*
|
|
105
|
+
* Rewrites `stagedDependencies.forwardImports[].owner` and
|
|
106
|
+
* `stagedDependencies.registrations[].owner` references on one patch
|
|
107
|
+
* through a rename lookup. Owners embed exact patch filenames, so any
|
|
107
108
|
* renumber (compact, reorder, placement export, rename) that does not remap
|
|
108
109
|
* them leaves dangling references that surface as false forward-import
|
|
109
110
|
* errors on the next lint.
|
|
@@ -192,8 +192,9 @@ export async function removePatchFromManifest(patchesDir, filename) {
|
|
|
192
192
|
return true;
|
|
193
193
|
}
|
|
194
194
|
/**
|
|
195
|
-
* Rewrites `stagedDependencies.forwardImports[].owner`
|
|
196
|
-
*
|
|
195
|
+
* Rewrites `stagedDependencies.forwardImports[].owner` and
|
|
196
|
+
* `stagedDependencies.registrations[].owner` references on one patch
|
|
197
|
+
* through a rename lookup. Owners embed exact patch filenames, so any
|
|
197
198
|
* renumber (compact, reorder, placement export, rename) that does not remap
|
|
198
199
|
* them leaves dangling references that surface as false forward-import
|
|
199
200
|
* errors on the next lint.
|
|
@@ -207,25 +208,35 @@ export async function removePatchFromManifest(patchesDir, filename) {
|
|
|
207
208
|
* @returns The same row, or a copy with remapped owners
|
|
208
209
|
*/
|
|
209
210
|
export function rewriteStagedDependencyOwners(patch, renameLookup) {
|
|
210
|
-
const
|
|
211
|
-
if (!
|
|
211
|
+
const staged = patch.stagedDependencies;
|
|
212
|
+
if (!staged)
|
|
212
213
|
return patch;
|
|
213
|
-
const
|
|
214
|
-
if (!
|
|
215
|
-
return
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
214
|
+
const rewriteOwners = (entries) => {
|
|
215
|
+
if (!entries || entries.length === 0)
|
|
216
|
+
return { entries, changed: false };
|
|
217
|
+
const rewritten = entries.map((entry) => {
|
|
218
|
+
if (!entry.owner)
|
|
219
|
+
return entry;
|
|
220
|
+
const newOwner = renameLookup(entry.owner);
|
|
221
|
+
if (newOwner === undefined || newOwner === entry.owner)
|
|
222
|
+
return entry;
|
|
223
|
+
return { ...entry, owner: newOwner };
|
|
224
|
+
});
|
|
225
|
+
return {
|
|
226
|
+
entries: rewritten,
|
|
227
|
+
changed: rewritten.some((entry, index) => entry !== entries[index]),
|
|
228
|
+
};
|
|
229
|
+
};
|
|
230
|
+
const forwardImports = rewriteOwners(staged.forwardImports);
|
|
231
|
+
const registrations = rewriteOwners(staged.registrations);
|
|
232
|
+
if (!forwardImports.changed && !registrations.changed)
|
|
223
233
|
return patch;
|
|
224
234
|
return {
|
|
225
235
|
...patch,
|
|
226
236
|
stagedDependencies: {
|
|
227
|
-
...
|
|
228
|
-
forwardImports:
|
|
237
|
+
...staged,
|
|
238
|
+
...(forwardImports.entries !== undefined ? { forwardImports: forwardImports.entries } : {}),
|
|
239
|
+
...(registrations.entries !== undefined ? { registrations: registrations.entries } : {}),
|
|
229
240
|
},
|
|
230
241
|
};
|
|
231
242
|
}
|
|
@@ -24,13 +24,37 @@ function parseForwardImports(data, label) {
|
|
|
24
24
|
return dependency;
|
|
25
25
|
});
|
|
26
26
|
}
|
|
27
|
+
function parseRegistrations(data, label) {
|
|
28
|
+
if (!isArray(data)) {
|
|
29
|
+
throw new Error(`${label} must be an array`);
|
|
30
|
+
}
|
|
31
|
+
return data.map((entry, index) => {
|
|
32
|
+
const rec = parseObject(entry, `${label}[${index}]`);
|
|
33
|
+
const dependency = {
|
|
34
|
+
file: rec.string('file'),
|
|
35
|
+
line: rec.string('line'),
|
|
36
|
+
creates: rec.string('creates'),
|
|
37
|
+
};
|
|
38
|
+
const owner = rec.optionalString('owner');
|
|
39
|
+
if (owner !== undefined)
|
|
40
|
+
dependency.owner = owner;
|
|
41
|
+
const reason = rec.optionalString('reason');
|
|
42
|
+
if (reason !== undefined)
|
|
43
|
+
dependency.reason = reason;
|
|
44
|
+
return dependency;
|
|
45
|
+
});
|
|
46
|
+
}
|
|
27
47
|
function parseStagedDependencies(data, label) {
|
|
28
48
|
const rec = parseObject(data, label);
|
|
29
49
|
const rawForwardImports = rec.raw('forwardImports');
|
|
50
|
+
const rawRegistrations = rec.raw('registrations');
|
|
30
51
|
const staged = {};
|
|
31
52
|
if (rawForwardImports !== undefined) {
|
|
32
53
|
staged.forwardImports = parseForwardImports(rawForwardImports, `${label}.forwardImports`);
|
|
33
54
|
}
|
|
55
|
+
if (rawRegistrations !== undefined) {
|
|
56
|
+
staged.registrations = parseRegistrations(rawRegistrations, `${label}.registrations`);
|
|
57
|
+
}
|
|
34
58
|
return staged;
|
|
35
59
|
}
|
|
36
60
|
/**
|
|
@@ -34,6 +34,14 @@ export interface HarnessRunVerdict {
|
|
|
34
34
|
signature?: HarnessCrashSignature;
|
|
35
35
|
/** First concrete test-failure evidence line, when available. */
|
|
36
36
|
realFailureLine?: string;
|
|
37
|
+
/**
|
|
38
|
+
* First N `TEST-UNEXPECTED-*` lines verbatim, each with its trailing
|
|
39
|
+
* assertion/diff context lines (`Got …` / `Expected …` / Assert diff /
|
|
40
|
+
* stack head). Lets the failure summary echo the actual assertion so a
|
|
41
|
+
* one-off failure that does not reproduce is still diagnosable after the
|
|
42
|
+
* fact. Only set on `test-failures` verdicts when such lines exist.
|
|
43
|
+
*/
|
|
44
|
+
realFailureBlocks?: string[];
|
|
37
45
|
/** Harness noise seen in the same output as a real test failure. */
|
|
38
46
|
secondaryHarnessSignature?: HarnessCrashSignature;
|
|
39
47
|
/**
|
|
@@ -104,6 +112,14 @@ export declare function analyzeTestCompleteness(output: string, requestedPaths:
|
|
|
104
112
|
neverStarted: string[];
|
|
105
113
|
neverEnded: string[];
|
|
106
114
|
};
|
|
115
|
+
/**
|
|
116
|
+
* Collects the first `limit` TEST-UNEXPECTED-* lines with their trailing
|
|
117
|
+
* assertion/diff context, one string per block. The shutdown-reentry
|
|
118
|
+
* artifact is excluded (it is harness noise, not a test result). When more
|
|
119
|
+
* than `limit` blocks exist, a final `…(+N more …)` note is appended so the
|
|
120
|
+
* operator knows the echo was truncated. Pure; exported for unit tests.
|
|
121
|
+
*/
|
|
122
|
+
export declare function collectUnexpectedFailureBlocks(output: string, limit?: number): string[];
|
|
107
123
|
/**
|
|
108
124
|
* Detects the known harness-crash shapes in captured mach output.
|
|
109
125
|
* Returns undefined for anything that looks like a genuine test result.
|
|
@@ -294,6 +294,58 @@ function realUnexpectedFailureLines(output) {
|
|
|
294
294
|
}
|
|
295
295
|
return [...new Set(matches.map((line) => line.trim()))].filter((line) => !SHUTDOWN_REENTRY_PATTERN.test(line));
|
|
296
296
|
}
|
|
297
|
+
/**
|
|
298
|
+
* Non-global copies of the unexpected/assertion patterns for per-line use —
|
|
299
|
+
* the `g`-flagged module patterns carry `lastIndex` state across `.test()`
|
|
300
|
+
* calls and must never be reused line-by-line.
|
|
301
|
+
*/
|
|
302
|
+
const UNEXPECTED_LINE_SINGLE = /\bTEST-UNEXPECTED-[A-Z-]+\b/;
|
|
303
|
+
/**
|
|
304
|
+
* Context lines the chrome/xpcshell harnesses print directly under a
|
|
305
|
+
* TEST-UNEXPECTED line: `Got …` / `Expected …` / `Actual …`, Assert
|
|
306
|
+
* messages, diff markers, stack-trace heads, and indented continuation
|
|
307
|
+
* lines.
|
|
308
|
+
*/
|
|
309
|
+
const FAILURE_CONTEXT_LINE_PATTERN = /^(?:\s*(?:Got\b|Expected\b|Actual\b|Assert\w*\b|Stack trace:|[-+]\s)|\s{2,}\S)/;
|
|
310
|
+
/** Cap on context lines echoed under a single TEST-UNEXPECTED line. */
|
|
311
|
+
const FAILURE_BLOCK_CONTEXT_LIMIT = 6;
|
|
312
|
+
/**
|
|
313
|
+
* Collects the first `limit` TEST-UNEXPECTED-* lines with their trailing
|
|
314
|
+
* assertion/diff context, one string per block. The shutdown-reentry
|
|
315
|
+
* artifact is excluded (it is harness noise, not a test result). When more
|
|
316
|
+
* than `limit` blocks exist, a final `…(+N more …)` note is appended so the
|
|
317
|
+
* operator knows the echo was truncated. Pure; exported for unit tests.
|
|
318
|
+
*/
|
|
319
|
+
export function collectUnexpectedFailureBlocks(output, limit = 5) {
|
|
320
|
+
const lines = output.split(/\r?\n/);
|
|
321
|
+
const blocks = [];
|
|
322
|
+
let skipped = 0;
|
|
323
|
+
for (let i = 0; i < lines.length; i++) {
|
|
324
|
+
const line = lines[i] ?? '';
|
|
325
|
+
if (!UNEXPECTED_LINE_SINGLE.test(line) || SHUTDOWN_REENTRY_PATTERN.test(line))
|
|
326
|
+
continue;
|
|
327
|
+
if (blocks.length >= limit) {
|
|
328
|
+
skipped++;
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
const block = [line.trim()];
|
|
332
|
+
let contextCount = 0;
|
|
333
|
+
for (let j = i + 1; j < lines.length && contextCount < FAILURE_BLOCK_CONTEXT_LIMIT; j++) {
|
|
334
|
+
const next = lines[j] ?? '';
|
|
335
|
+
if (UNEXPECTED_LINE_SINGLE.test(next))
|
|
336
|
+
break;
|
|
337
|
+
if (!FAILURE_CONTEXT_LINE_PATTERN.test(next))
|
|
338
|
+
break;
|
|
339
|
+
block.push(next.trimEnd());
|
|
340
|
+
contextCount++;
|
|
341
|
+
}
|
|
342
|
+
blocks.push(block.join('\n'));
|
|
343
|
+
}
|
|
344
|
+
if (skipped > 0) {
|
|
345
|
+
blocks.push(`…(+${skipped} more TEST-UNEXPECTED line${skipped === 1 ? '' : 's'} not shown)`);
|
|
346
|
+
}
|
|
347
|
+
return blocks;
|
|
348
|
+
}
|
|
297
349
|
function detectSecondaryHarnessNoise(output) {
|
|
298
350
|
const evidence = stripNonSignalNoise(output);
|
|
299
351
|
if (TRACEBACK_PATTERN.test(evidence)) {
|
|
@@ -397,6 +449,7 @@ export function detectHarnessCrashSignature(output) {
|
|
|
397
449
|
export function classifyHarnessRun(exitCode, output, requestedPaths) {
|
|
398
450
|
const realFailures = realUnexpectedFailureLines(output);
|
|
399
451
|
const firstRealFailure = realFailures[0];
|
|
452
|
+
const failureBlocks = collectUnexpectedFailureBlocks(output);
|
|
400
453
|
const secondaryHarnessSignature = firstRealFailure !== undefined ? detectSecondaryHarnessNoise(output) : undefined;
|
|
401
454
|
const signature = detectHarnessCrashSignature(output);
|
|
402
455
|
if (signature) {
|
|
@@ -431,6 +484,7 @@ export function classifyHarnessRun(exitCode, output, requestedPaths) {
|
|
|
431
484
|
return {
|
|
432
485
|
kind: 'test-failures',
|
|
433
486
|
...(firstRealFailure !== undefined ? { realFailureLine: firstRealFailure } : {}),
|
|
487
|
+
...(failureBlocks.length > 0 ? { realFailureBlocks: failureBlocks } : {}),
|
|
434
488
|
...(secondaryHarnessSignature !== undefined ? { secondaryHarnessSignature } : {}),
|
|
435
489
|
greenSummaryRejected: {
|
|
436
490
|
...(crashLine !== undefined ? { crashLine } : {}),
|
|
@@ -444,6 +498,7 @@ export function classifyHarnessRun(exitCode, output, requestedPaths) {
|
|
|
444
498
|
return {
|
|
445
499
|
kind: 'test-failures',
|
|
446
500
|
...(firstRealFailure !== undefined ? { realFailureLine: firstRealFailure } : {}),
|
|
501
|
+
...(failureBlocks.length > 0 ? { realFailureBlocks: failureBlocks } : {}),
|
|
447
502
|
...(secondaryHarnessSignature !== undefined ? { secondaryHarnessSignature } : {}),
|
|
448
503
|
};
|
|
449
504
|
}
|