@hominis/fireforge 0.50.0 → 0.51.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +47 -0
- package/dist/build-info.json +4 -4
- package/dist/src/commands/check-report.d.ts +17 -4
- package/dist/src/commands/check-report.js +19 -6
- package/dist/src/commands/check-run.d.ts +5 -0
- package/dist/src/commands/check-run.js +9 -1
- package/dist/src/commands/check-steps.d.ts +8 -0
- package/dist/src/commands/check-steps.js +31 -6
- package/dist/src/commands/check.js +1 -1
- package/dist/src/commands/declarations-report.d.ts +43 -0
- package/dist/src/commands/declarations-report.js +76 -0
- package/dist/src/commands/export-placement-gate.js +8 -0
- package/dist/src/commands/furnace/rename-registrations.js +7 -2
- package/dist/src/commands/furnace/validate.js +26 -1
- package/dist/src/commands/lint-per-patch.js +6 -2
- package/dist/src/commands/lint-per-run-checkjs.js +2 -2
- package/dist/src/commands/lint-report.js +7 -2
- package/dist/src/commands/manifest.js +2 -0
- package/dist/src/commands/park.d.ts +22 -0
- package/dist/src/commands/park.js +232 -0
- package/dist/src/commands/patch/delete.js +11 -6
- package/dist/src/commands/patch/index.d.ts +3 -3
- package/dist/src/commands/patch/index.js +8 -4
- package/dist/src/commands/patch/lint-ignore-from.d.ts +44 -0
- package/dist/src/commands/patch/lint-ignore-from.js +142 -0
- package/dist/src/commands/patch/lint-ignore.js +39 -29
- package/dist/src/commands/patch/move-files-create.js +11 -4
- package/dist/src/commands/patch/move-files-into.js +10 -4
- package/dist/src/commands/patch/move-files.js +3 -1
- package/dist/src/commands/patch/removal-created-files.d.ts +48 -0
- package/dist/src/commands/patch/removal-created-files.js +83 -0
- package/dist/src/commands/patch/rename.js +15 -3
- package/dist/src/commands/patch/reserve.d.ts +33 -0
- package/dist/src/commands/patch/reserve.js +152 -0
- package/dist/src/commands/patch/retire.js +11 -6
- package/dist/src/commands/patch/split-plan.d.ts +10 -1
- package/dist/src/commands/patch/split-plan.js +16 -1
- package/dist/src/commands/patch/split.js +14 -5
- package/dist/src/commands/patch/stage.d.ts +34 -0
- package/dist/src/commands/patch/stage.js +153 -0
- package/dist/src/commands/patch/staged-dependency-remove.d.ts +31 -0
- package/dist/src/commands/patch/staged-dependency-remove.js +80 -0
- package/dist/src/commands/patch/staged-dependency.js +40 -98
- package/dist/src/commands/re-export-adjacent.d.ts +5 -0
- package/dist/src/commands/re-export-adjacent.js +4 -3
- package/dist/src/commands/re-export-bulk-scan.d.ts +2 -1
- package/dist/src/commands/re-export-bulk-scan.js +22 -7
- package/dist/src/commands/re-export-hunks.d.ts +35 -0
- package/dist/src/commands/re-export-hunks.js +220 -0
- package/dist/src/commands/re-export-options.js +29 -2
- package/dist/src/commands/re-export-purity-scope.d.ts +18 -0
- package/dist/src/commands/re-export-purity-scope.js +131 -0
- package/dist/src/commands/re-export-register.js +15 -1
- package/dist/src/commands/re-export-removed.d.ts +1 -1
- package/dist/src/commands/re-export-removed.js +17 -2
- package/dist/src/commands/re-export-scan.d.ts +2 -0
- package/dist/src/commands/re-export-scan.js +4 -1
- package/dist/src/commands/re-export-single.js +102 -42
- package/dist/src/commands/re-export-stale-registrations.d.ts +18 -0
- package/dist/src/commands/re-export-stale-registrations.js +62 -0
- package/dist/src/commands/re-export.js +35 -34
- package/dist/src/commands/test-build-furnace.d.ts +3 -2
- package/dist/src/commands/test-build-furnace.js +8 -2
- package/dist/src/commands/test-build-phase.js +24 -7
- package/dist/src/commands/test-checkjs.js +3 -2
- package/dist/src/commands/test-dispatch.d.ts +39 -0
- package/dist/src/commands/test-dispatch.js +211 -0
- package/dist/src/commands/test-furnace-reach.d.ts +37 -0
- package/dist/src/commands/test-furnace-reach.js +132 -0
- package/dist/src/commands/test-orchestrate.d.ts +9 -0
- package/dist/src/commands/test-orchestrate.js +22 -4
- package/dist/src/commands/test-perf-scope.d.ts +21 -0
- package/dist/src/commands/test-perf-scope.js +55 -0
- package/dist/src/commands/test-prelock.js +100 -4
- package/dist/src/commands/test-register.js +18 -3
- package/dist/src/commands/test-results.d.ts +62 -0
- package/dist/src/commands/test-results.js +62 -0
- package/dist/src/commands/test-run.d.ts +13 -55
- package/dist/src/commands/test-run.js +73 -101
- package/dist/src/commands/test-shards.d.ts +110 -0
- package/dist/src/commands/test-shards.js +255 -0
- package/dist/src/commands/test-stale-gate.d.ts +5 -1
- package/dist/src/commands/test-stale-gate.js +4 -2
- package/dist/src/commands/test-verdict.d.ts +2 -1
- package/dist/src/commands/test.d.ts +1 -1
- package/dist/src/commands/test.js +65 -75
- package/dist/src/commands/token-lint.js +48 -8
- package/dist/src/commands/verify.js +9 -2
- package/dist/src/core/build-prepare.d.ts +6 -0
- package/dist/src/core/build-prepare.js +29 -3
- package/dist/src/core/config-paths.d.ts +1 -1
- package/dist/src/core/config-paths.js +2 -0
- package/dist/src/core/config-validate-check.js +83 -7
- package/dist/src/core/config-validate-test-toolchains.js +14 -0
- package/dist/src/core/config-validate-token-scales.d.ts +3 -0
- package/dist/src/core/config-validate-token-scales.js +47 -5
- package/dist/src/core/config-validate.js +6 -0
- package/dist/src/core/coverage-extend.js +2 -2
- package/dist/src/core/coverage-records.d.ts +11 -0
- package/dist/src/core/coverage-records.js +34 -0
- package/dist/src/core/engine-session-lock.d.ts +6 -0
- package/dist/src/core/engine-session-lock.js +10 -2
- package/dist/src/core/file-lock-shared.d.ts +57 -0
- package/dist/src/core/file-lock-shared.js +117 -0
- package/dist/src/core/file-lock.d.ts +9 -0
- package/dist/src/core/file-lock.js +58 -2
- package/dist/src/core/furnace-apply.js +10 -10
- package/dist/src/core/furnace-config-validate-exceptions.d.ts +10 -3
- package/dist/src/core/furnace-config-validate-exceptions.js +39 -8
- package/dist/src/core/furnace-config-validate.js +7 -1
- package/dist/src/core/furnace-registration-jar-inplace.d.ts +12 -0
- package/dist/src/core/furnace-registration-jar-inplace.js +80 -0
- package/dist/src/core/furnace-validate-accessibility.d.ts +4 -2
- package/dist/src/core/furnace-validate-accessibility.js +61 -27
- package/dist/src/core/furnace-validate-exceptions.d.ts +12 -4
- package/dist/src/core/furnace-validate-exceptions.js +41 -4
- package/dist/src/core/furnace-validate-icon-button.js +30 -1
- package/dist/src/core/history-log-file.js +8 -1
- package/dist/src/core/line-prefixer.d.ts +18 -0
- package/dist/src/core/line-prefixer.js +34 -0
- package/dist/src/core/mach.d.ts +7 -0
- package/dist/src/core/mach.js +24 -9
- package/dist/src/core/module-resolution-preflight.d.ts +19 -3
- package/dist/src/core/module-resolution-preflight.js +24 -6
- package/dist/src/core/negative-control-compiled.d.ts +27 -0
- package/dist/src/core/negative-control-compiled.js +73 -0
- package/dist/src/core/negative-control-diff.d.ts +14 -2
- package/dist/src/core/negative-control-diff.js +49 -6
- package/dist/src/core/negative-control.d.ts +12 -0
- package/dist/src/core/negative-control.js +48 -5
- package/dist/src/core/park-store.d.ts +63 -0
- package/dist/src/core/park-store.js +216 -0
- package/dist/src/core/patch-export-update.d.ts +4 -0
- package/dist/src/core/patch-export-update.js +12 -3
- package/dist/src/core/patch-export.d.ts +3 -2
- package/dist/src/core/patch-export.js +21 -14
- package/dist/src/core/patch-files.js +26 -8
- package/dist/src/core/patch-lint-checkjs-any-import.d.ts +6 -4
- package/dist/src/core/patch-lint-checkjs-any-import.js +9 -7
- package/dist/src/core/patch-lint-checkjs-diagnostics.d.ts +17 -0
- package/dist/src/core/patch-lint-checkjs-diagnostics.js +32 -0
- package/dist/src/core/patch-lint-checkjs.d.ts +10 -1
- package/dist/src/core/patch-lint-checkjs.js +13 -3
- package/dist/src/core/patch-lint-cross.d.ts +3 -1
- package/dist/src/core/patch-lint-cross.js +5 -0
- package/dist/src/core/patch-lint-css.js +14 -10
- package/dist/src/core/patch-lint-forward-registration-extended.d.ts +5 -0
- package/dist/src/core/patch-lint-forward-registration-extended.js +1 -1
- package/dist/src/core/patch-lint-ignore.d.ts +56 -12
- package/dist/src/core/patch-lint-ignore.js +173 -43
- package/dist/src/core/patch-lint-ownership.d.ts +10 -0
- package/dist/src/core/patch-lint-ownership.js +26 -0
- package/dist/src/core/patch-lint-size.js +9 -1
- package/dist/src/core/patch-lint-token-scale.d.ts +26 -8
- package/dist/src/core/patch-lint-token-scale.js +206 -29
- package/dist/src/core/patch-lint.js +2 -2
- package/dist/src/core/patch-manifest-io.js +8 -1
- package/dist/src/core/patch-manifest-key-order.d.ts +24 -0
- package/dist/src/core/patch-manifest-key-order.js +61 -0
- package/dist/src/core/patch-manifest-stage.d.ts +46 -0
- package/dist/src/core/patch-manifest-stage.js +96 -0
- package/dist/src/core/patch-manifest-validate.js +27 -1
- package/dist/src/core/patch-order-legal.d.ts +2 -1
- package/dist/src/core/patch-order-legal.js +7 -2
- package/dist/src/core/patch-policy.d.ts +6 -2
- package/dist/src/core/patch-policy.js +14 -7
- package/dist/src/core/patch-retired.d.ts +26 -4
- package/dist/src/core/patch-retired.js +40 -2
- package/dist/src/core/run-log.d.ts +6 -0
- package/dist/src/core/run-log.js +17 -1
- package/dist/src/core/test-harness-crash.d.ts +2 -1
- package/dist/src/core/test-harness-crash.js +5 -8
- package/dist/src/core/test-harness-signature.d.ts +16 -0
- package/dist/src/core/test-harness-signature.js +19 -1
- package/dist/src/core/test-raw-log.d.ts +66 -0
- package/dist/src/core/test-raw-log.js +122 -0
- package/dist/src/core/test-run-completeness.d.ts +36 -3
- package/dist/src/core/test-run-completeness.js +57 -17
- package/dist/src/core/test-xpcshell-retry.d.ts +2 -0
- package/dist/src/core/test-xpcshell-retry.js +2 -1
- package/dist/src/core/token-lint.d.ts +5 -2
- package/dist/src/core/token-lint.js +8 -3
- package/dist/src/core/token-sibling-sheets.d.ts +46 -0
- package/dist/src/core/token-sibling-sheets.js +119 -0
- package/dist/src/core/tree-guard.js +2 -0
- package/dist/src/core/typecheck-shim.d.ts +5 -1
- package/dist/src/core/typecheck-shim.js +115 -2
- package/dist/src/core/typecheck.js +3 -1
- package/dist/src/index.d.ts +9 -3
- package/dist/src/index.js +4 -1
- package/dist/src/types/commands/index.d.ts +2 -2
- package/dist/src/types/commands/options.d.ts +68 -7
- package/dist/src/types/commands/patches.d.ts +43 -0
- package/dist/src/types/config.d.ts +74 -2
- package/dist/src/types/furnace.d.ts +20 -0
- package/dist/src/types/index.d.ts +1 -1
- package/dist/src/utils/options.d.ts +3 -2
- package/dist/src/utils/options.js +9 -4
- package/docs/configuration.md +94 -9
- package/docs/furnace.md +43 -1
- package/docs/lifecycle-invariants.md +13 -11
- package/docs/machine-output.md +64 -18
- package/docs/patch-workflow.md +174 -13
- package/docs/testing.md +73 -4
- package/package.json +3 -3
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { parseObject } from '../utils/parse.js';
|
|
6
6
|
import { isArray, isObject, isValidFirefoxProduct, isValidFirefoxVersion, PATCH_CATEGORIES, } from '../utils/validation.js';
|
|
7
7
|
import { parseLintIgnoreEntries } from './patch-lint-ignore.js';
|
|
8
|
+
import { canonicalizePatchRow } from './patch-manifest-key-order.js';
|
|
8
9
|
import { checkDocumentVersion, describeNewerDocument } from './state-file.js';
|
|
9
10
|
function parseForwardImports(data, label) {
|
|
10
11
|
if (!isArray(data)) {
|
|
@@ -134,7 +135,7 @@ function validatePatchMetadata(data, index) {
|
|
|
134
135
|
if (rawStagedDependencies !== undefined) {
|
|
135
136
|
result.stagedDependencies = parseStagedDependencies(rawStagedDependencies, `patches[${index}].stagedDependencies`);
|
|
136
137
|
}
|
|
137
|
-
return result;
|
|
138
|
+
return canonicalizePatchRow(result);
|
|
138
139
|
}
|
|
139
140
|
/**
|
|
140
141
|
* Validates the `retired` placeholders `patch retire` leaves behind. Kept on
|
|
@@ -160,6 +161,29 @@ function parseRetiredSlots(data) {
|
|
|
160
161
|
return slot;
|
|
161
162
|
});
|
|
162
163
|
}
|
|
164
|
+
/**
|
|
165
|
+
* Validates the `reserved` placeholders `patch reserve` writes. Same
|
|
166
|
+
* survival rule as {@link parseRetiredSlots}: dropping one on a round trip
|
|
167
|
+
* would reopen the gap a peer reserved the order to close.
|
|
168
|
+
*/
|
|
169
|
+
function parseReservedSlots(data) {
|
|
170
|
+
if (!isArray(data)) {
|
|
171
|
+
throw new Error('patches.json field "reserved" must be an array');
|
|
172
|
+
}
|
|
173
|
+
return data.map((entry, index) => {
|
|
174
|
+
const rec = parseObject(entry, `reserved[${index}]`);
|
|
175
|
+
const slot = {
|
|
176
|
+
order: rec.nonNegativeInteger('order'),
|
|
177
|
+
category: rec.validatedString('category', (value) => /^[a-z][a-z0-9-]*$/.test(value), 'a lowercase category identifier (letters, numbers, hyphens)'),
|
|
178
|
+
owner: rec.validatedString('owner', (value) => value.trim() !== '', 'a non-empty label'),
|
|
179
|
+
reservedAt: rec.string('reservedAt'),
|
|
180
|
+
};
|
|
181
|
+
const reason = rec.optionalString('reason');
|
|
182
|
+
if (reason !== undefined)
|
|
183
|
+
slot.reason = reason;
|
|
184
|
+
return slot;
|
|
185
|
+
});
|
|
186
|
+
}
|
|
163
187
|
/** Manifest schema version this build writes and understands. */
|
|
164
188
|
const PATCHES_MANIFEST_VERSION = 1;
|
|
165
189
|
/**
|
|
@@ -191,6 +215,8 @@ export function validatePatchesManifest(data) {
|
|
|
191
215
|
};
|
|
192
216
|
if (data['retired'] !== undefined)
|
|
193
217
|
manifest.retired = parseRetiredSlots(data['retired']);
|
|
218
|
+
if (data['reserved'] !== undefined)
|
|
219
|
+
manifest.reserved = parseReservedSlots(data['reserved']);
|
|
194
220
|
return manifest;
|
|
195
221
|
}
|
|
196
222
|
/**
|
|
@@ -14,7 +14,8 @@ import type { FireForgeConfig } from '../types/config.js';
|
|
|
14
14
|
* Smallest order strictly greater than `after` that no patch occupies and
|
|
15
15
|
* that adds no order-related `patchPolicy` finding (collision, category
|
|
16
16
|
* range, reserved range, numeric gap) for a `category` patch. A retired
|
|
17
|
-
* slot counts as free, since a patch may fill it
|
|
17
|
+
* slot counts as free, since a patch may fill it; a reservation does not,
|
|
18
|
+
* since it is held for its owner's export. Returns null when no such
|
|
18
19
|
* order exists inside the category's configured ranges.
|
|
19
20
|
*
|
|
20
21
|
* @param config - Project config (only `patchPolicy` is read)
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { formatPatchOrder } from './patch-parse.js';
|
|
13
13
|
import { evaluatePatchPolicy } from './patch-policy.js';
|
|
14
|
+
import { reservedOrderSet } from './patch-retired.js';
|
|
14
15
|
/** Policy codes an order choice alone can trip. */
|
|
15
16
|
const ORDER_CODES = new Set([
|
|
16
17
|
'order-collision',
|
|
@@ -46,7 +47,8 @@ function orderIssueKeys(config, manifest) {
|
|
|
46
47
|
* Smallest order strictly greater than `after` that no patch occupies and
|
|
47
48
|
* that adds no order-related `patchPolicy` finding (collision, category
|
|
48
49
|
* range, reserved range, numeric gap) for a `category` patch. A retired
|
|
49
|
-
* slot counts as free, since a patch may fill it
|
|
50
|
+
* slot counts as free, since a patch may fill it; a reservation does not,
|
|
51
|
+
* since it is held for its owner's export. Returns null when no such
|
|
50
52
|
* order exists inside the category's configured ranges.
|
|
51
53
|
*
|
|
52
54
|
* @param config - Project config (only `patchPolicy` is read)
|
|
@@ -55,7 +57,10 @@ function orderIssueKeys(config, manifest) {
|
|
|
55
57
|
* @param after - The order the new patch must come after
|
|
56
58
|
*/
|
|
57
59
|
export function earliestLegalOrderAfter(config, manifest, category, after) {
|
|
58
|
-
const occupied = new Set(
|
|
60
|
+
const occupied = new Set([
|
|
61
|
+
...manifest.patches.map((patch) => patch.order),
|
|
62
|
+
...reservedOrderSet(manifest),
|
|
63
|
+
]);
|
|
59
64
|
const ranges = config.patchPolicy?.ranges.filter((range) => range.category === category) ?? [];
|
|
60
65
|
const limit = ranges.length > 0 ? Math.max(...ranges.map((range) => range.to)) : after + UNBOUNDED_SEARCH;
|
|
61
66
|
const baseline = config.patchPolicy ? orderIssueKeys(config, manifest) : new Set();
|
|
@@ -72,5 +72,9 @@ export declare function applyRenameMapToManifest(manifest: PatchesManifest, rena
|
|
|
72
72
|
}>): PatchesManifest;
|
|
73
73
|
/** Enforces patch policy according to the configured mutation mode. */
|
|
74
74
|
export declare function enforcePatchPolicy(input: PatchPolicyEnforcementInput): void;
|
|
75
|
-
/**
|
|
76
|
-
|
|
75
|
+
/**
|
|
76
|
+
* Allocates the next available order inside the configured ranges for a
|
|
77
|
+
* category. Orders in `reserved` (`patch reserve`) are skipped: they are
|
|
78
|
+
* held for their owner's export.
|
|
79
|
+
*/
|
|
80
|
+
export declare function allocatePolicyOrder(config: FireForgeConfig, patches: readonly PatchMetadata[], category: string, reserved?: ReadonlySet<number>): number | null;
|
|
@@ -12,7 +12,7 @@ import { warn } from '../utils/logger.js';
|
|
|
12
12
|
import { PATCH_CATEGORIES } from '../utils/validation.js';
|
|
13
13
|
import { rewriteStagedDependencyOwners } from './patch-manifest-io.js';
|
|
14
14
|
import { formatOrderRange, formatPatchOrder } from './patch-parse.js';
|
|
15
|
-
import { manifestWithRetiredSlots, retiredOrderSet } from './patch-retired.js';
|
|
15
|
+
import { manifestWithRetiredSlots, reservedOrderSet, retiredOrderSet } from './patch-retired.js';
|
|
16
16
|
/** Default patch filename contract used when a policy omits `filenamePattern`. */
|
|
17
17
|
const DEFAULT_PATCH_POLICY_FILENAME_PATTERN = '^(?<order>\\d{3})-(?<category>[a-z][a-z0-9-]*)-(?<slug>[a-z0-9-]+)\\.patch$';
|
|
18
18
|
function policy(config) {
|
|
@@ -244,8 +244,8 @@ function evaluateGaps(cfg, patches, severity, retiredOrders = new Set()) {
|
|
|
244
244
|
const last = occupied[occupied.length - 1];
|
|
245
245
|
const missing = [];
|
|
246
246
|
for (let order = first; order <= last; order++) {
|
|
247
|
-
// A retired slot (`patch retire`)
|
|
248
|
-
//
|
|
247
|
+
// A retired slot (`patch retire`) or a reservation (`patch reserve`)
|
|
248
|
+
// is an occupied order with no patch behind it, so it is not a gap.
|
|
249
249
|
if (!occupiedSet.has(order) &&
|
|
250
250
|
!retiredOrders.has(order) &&
|
|
251
251
|
reservedRangeForOrder(cfg, order) === null) {
|
|
@@ -295,7 +295,10 @@ export function evaluatePatchPolicy(config, manifest) {
|
|
|
295
295
|
const severity = mutationMode(config) === 'warn' ? 'warning' : 'error';
|
|
296
296
|
const issues = manifest.patches.flatMap((patch) => evaluatePatchMetadata(cfg, patch, severity));
|
|
297
297
|
issues.push(...evaluateOrderCollisions(manifest.patches, severity));
|
|
298
|
-
|
|
298
|
+
// Retired and reserved slots are both spent orders with no patch behind
|
|
299
|
+
// them: neither is a gap.
|
|
300
|
+
const placeholders = new Set([...retiredOrderSet(manifest), ...reservedOrderSet(manifest)]);
|
|
301
|
+
issues.push(...evaluateGaps(cfg, manifest.patches, severity, placeholders));
|
|
299
302
|
return issues;
|
|
300
303
|
}
|
|
301
304
|
/**
|
|
@@ -357,8 +360,12 @@ export function enforcePatchPolicy(input) {
|
|
|
357
360
|
: '';
|
|
358
361
|
throw new InvalidArgumentError(`${input.command} would violate patchPolicy:\n${details.join('\n')}${suffix}`, mode === 'force' ? '--force-unsafe' : 'patchPolicy');
|
|
359
362
|
}
|
|
360
|
-
/**
|
|
361
|
-
|
|
363
|
+
/**
|
|
364
|
+
* Allocates the next available order inside the configured ranges for a
|
|
365
|
+
* category. Orders in `reserved` (`patch reserve`) are skipped: they are
|
|
366
|
+
* held for their owner's export.
|
|
367
|
+
*/
|
|
368
|
+
export function allocatePolicyOrder(config, patches, category, reserved = new Set()) {
|
|
362
369
|
const cfg = policy(config);
|
|
363
370
|
if (!cfg)
|
|
364
371
|
return null;
|
|
@@ -367,7 +374,7 @@ export function allocatePolicyOrder(config, patches, category) {
|
|
|
367
374
|
.sort((a, b) => a.from - b.from || a.to - b.to);
|
|
368
375
|
if (ranges.length === 0)
|
|
369
376
|
return null;
|
|
370
|
-
const occupied = new Set(patches.map((patch) => patch.order));
|
|
377
|
+
const occupied = new Set([...patches.map((patch) => patch.order), ...reserved]);
|
|
371
378
|
const highestInRanges = patches.reduce((highest, patch) => {
|
|
372
379
|
if (!ranges.some((range) => patch.order >= range.from && patch.order <= range.to)) {
|
|
373
380
|
return highest;
|
|
@@ -10,8 +10,12 @@
|
|
|
10
10
|
* occupied. A patch that later takes the same order fills the slot, and
|
|
11
11
|
* {@link carryRetiredSlots} drops it on that write, so the manifest never
|
|
12
12
|
* holds a slot and a patch at one order.
|
|
13
|
+
*
|
|
14
|
+
* Reserved slots (`patch reserve`) follow the same rules: a placeholder for
|
|
15
|
+
* an order a session has claimed but not exported yet, counted as occupied
|
|
16
|
+
* by the gap check and filled (and dropped) by the export that lands on it.
|
|
13
17
|
*/
|
|
14
|
-
import type { PatchesManifest, PatchMetadata, PatchRetiredSlot } from '../types/commands/index.js';
|
|
18
|
+
import type { PatchesManifest, PatchMetadata, PatchReservedSlot, PatchRetiredSlot } from '../types/commands/index.js';
|
|
15
19
|
/**
|
|
16
20
|
* The retired slots of `current` that no patch in `patches` occupies, or
|
|
17
21
|
* undefined when none remain. Every manifest write that rebuilds the
|
|
@@ -22,14 +26,32 @@ import type { PatchesManifest, PatchMetadata, PatchRetiredSlot } from '../types/
|
|
|
22
26
|
* @param patches - The rows the written manifest will hold
|
|
23
27
|
*/
|
|
24
28
|
export declare function carryRetiredSlots(current: Pick<PatchesManifest, 'retired'> | null | undefined, patches: readonly Pick<PatchMetadata, 'order'>[]): PatchRetiredSlot[] | undefined;
|
|
29
|
+
/**
|
|
30
|
+
* Reserved twin of {@link carryRetiredSlots}: the reservations of `current`
|
|
31
|
+
* that no patch in `patches` has filled yet.
|
|
32
|
+
*
|
|
33
|
+
* @param current - Manifest the slots come from
|
|
34
|
+
* @param patches - The rows the written manifest will hold
|
|
35
|
+
*/
|
|
36
|
+
export declare function carryReservedSlots(current: Pick<PatchesManifest, 'reserved'> | null | undefined, patches: readonly Pick<PatchMetadata, 'order'>[]): PatchReservedSlot[] | undefined;
|
|
25
37
|
/**
|
|
26
38
|
* Builds a manifest from `patches` that keeps `current`'s unfilled retired
|
|
27
|
-
* slots.
|
|
28
|
-
* queue that never retired anything writes the same JSON as
|
|
39
|
+
* and reserved slots. Each key is omitted entirely when no slot remains, so
|
|
40
|
+
* a queue that never retired or reserved anything writes the same JSON as
|
|
41
|
+
* before.
|
|
29
42
|
*
|
|
30
43
|
* @param current - Manifest the slots come from
|
|
31
44
|
* @param patches - The rows the written manifest will hold
|
|
32
45
|
*/
|
|
33
|
-
export declare function manifestWithRetiredSlots(current: Pick<PatchesManifest, 'retired'> | null | undefined, patches: PatchMetadata[]): PatchesManifest;
|
|
46
|
+
export declare function manifestWithRetiredSlots(current: Pick<PatchesManifest, 'retired' | 'reserved'> | null | undefined, patches: PatchMetadata[]): PatchesManifest;
|
|
34
47
|
/** The set of order numbers held by retired slots. */
|
|
35
48
|
export declare function retiredOrderSet(manifest: Pick<PatchesManifest, 'retired'>): Set<number>;
|
|
49
|
+
/** The set of order numbers held by reserved slots. */
|
|
50
|
+
export declare function reservedOrderSet(manifest: Pick<PatchesManifest, 'reserved'>): Set<number>;
|
|
51
|
+
/**
|
|
52
|
+
* The reservation at `order`, if any.
|
|
53
|
+
*
|
|
54
|
+
* @param manifest - Manifest to search (null: none)
|
|
55
|
+
* @param order - Order number
|
|
56
|
+
*/
|
|
57
|
+
export declare function reservationAt(manifest: Pick<PatchesManifest, 'reserved'> | null | undefined, order: number): PatchReservedSlot | undefined;
|
|
@@ -11,6 +11,10 @@
|
|
|
11
11
|
* occupied. A patch that later takes the same order fills the slot, and
|
|
12
12
|
* {@link carryRetiredSlots} drops it on that write, so the manifest never
|
|
13
13
|
* holds a slot and a patch at one order.
|
|
14
|
+
*
|
|
15
|
+
* Reserved slots (`patch reserve`) follow the same rules: a placeholder for
|
|
16
|
+
* an order a session has claimed but not exported yet, counted as occupied
|
|
17
|
+
* by the gap check and filled (and dropped) by the export that lands on it.
|
|
14
18
|
*/
|
|
15
19
|
/**
|
|
16
20
|
* The retired slots of `current` that no patch in `patches` occupies, or
|
|
@@ -32,23 +36,57 @@ export function carryRetiredSlots(current, patches) {
|
|
|
32
36
|
.sort((a, b) => a.order - b.order);
|
|
33
37
|
return kept.length > 0 ? kept : undefined;
|
|
34
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Reserved twin of {@link carryRetiredSlots}: the reservations of `current`
|
|
41
|
+
* that no patch in `patches` has filled yet.
|
|
42
|
+
*
|
|
43
|
+
* @param current - Manifest the slots come from
|
|
44
|
+
* @param patches - The rows the written manifest will hold
|
|
45
|
+
*/
|
|
46
|
+
export function carryReservedSlots(current, patches) {
|
|
47
|
+
const slots = current?.reserved;
|
|
48
|
+
if (slots === undefined || slots.length === 0)
|
|
49
|
+
return undefined;
|
|
50
|
+
const occupied = new Set(patches.map((patch) => patch.order));
|
|
51
|
+
const kept = slots
|
|
52
|
+
.filter((slot) => !occupied.has(slot.order))
|
|
53
|
+
.map((slot) => ({ ...slot }))
|
|
54
|
+
.sort((a, b) => a.order - b.order);
|
|
55
|
+
return kept.length > 0 ? kept : undefined;
|
|
56
|
+
}
|
|
35
57
|
/**
|
|
36
58
|
* Builds a manifest from `patches` that keeps `current`'s unfilled retired
|
|
37
|
-
* slots.
|
|
38
|
-
* queue that never retired anything writes the same JSON as
|
|
59
|
+
* and reserved slots. Each key is omitted entirely when no slot remains, so
|
|
60
|
+
* a queue that never retired or reserved anything writes the same JSON as
|
|
61
|
+
* before.
|
|
39
62
|
*
|
|
40
63
|
* @param current - Manifest the slots come from
|
|
41
64
|
* @param patches - The rows the written manifest will hold
|
|
42
65
|
*/
|
|
43
66
|
export function manifestWithRetiredSlots(current, patches) {
|
|
44
67
|
const retired = carryRetiredSlots(current, patches);
|
|
68
|
+
const reserved = carryReservedSlots(current, patches);
|
|
45
69
|
return {
|
|
46
70
|
version: 1,
|
|
47
71
|
patches,
|
|
48
72
|
...(retired !== undefined ? { retired } : {}),
|
|
73
|
+
...(reserved !== undefined ? { reserved } : {}),
|
|
49
74
|
};
|
|
50
75
|
}
|
|
51
76
|
/** The set of order numbers held by retired slots. */
|
|
52
77
|
export function retiredOrderSet(manifest) {
|
|
53
78
|
return new Set((manifest.retired ?? []).map((slot) => slot.order));
|
|
54
79
|
}
|
|
80
|
+
/** The set of order numbers held by reserved slots. */
|
|
81
|
+
export function reservedOrderSet(manifest) {
|
|
82
|
+
return new Set((manifest.reserved ?? []).map((slot) => slot.order));
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* The reservation at `order`, if any.
|
|
86
|
+
*
|
|
87
|
+
* @param manifest - Manifest to search (null: none)
|
|
88
|
+
* @param order - Order number
|
|
89
|
+
*/
|
|
90
|
+
export function reservationAt(manifest, order) {
|
|
91
|
+
return manifest?.reserved?.find((slot) => slot.order === order);
|
|
92
|
+
}
|
|
@@ -65,6 +65,12 @@ export declare function openRunLog(projectRoot: string, command: string, now?: D
|
|
|
65
65
|
export declare function keepRunLogCopy(log: RunLog | undefined, flag: string | undefined, command: string): Promise<RunLog | undefined>;
|
|
66
66
|
/** Installs (or clears, with `undefined`) the run log for this process. */
|
|
67
67
|
export declare function setActiveRunLog(log: RunLog | undefined): void;
|
|
68
|
+
/**
|
|
69
|
+
* The 1-based line of the active run log the next write starts on.
|
|
70
|
+
* Redaction rewrites within a line and never adds or drops one, so the
|
|
71
|
+
* number holds for the file on disk.
|
|
72
|
+
*/
|
|
73
|
+
export declare function activeRunLogLine(): number;
|
|
68
74
|
/** Absolute path of the active run log, or undefined when none is open. */
|
|
69
75
|
export declare function getActiveRunLogPath(): string | undefined;
|
|
70
76
|
/** Appends to the active run log. A no-op when none is open. */
|
package/dist/src/core/run-log.js
CHANGED
|
@@ -271,9 +271,20 @@ export async function keepRunLogCopy(log, flag, command) {
|
|
|
271
271
|
* One command runs per process, so a single slot is the whole lifetime.
|
|
272
272
|
*/
|
|
273
273
|
let activeRunLog;
|
|
274
|
+
/** Newlines written to the active log so far, for line references into it. */
|
|
275
|
+
let activeRunLogNewlines = 0;
|
|
274
276
|
/** Installs (or clears, with `undefined`) the run log for this process. */
|
|
275
277
|
export function setActiveRunLog(log) {
|
|
276
278
|
activeRunLog = log;
|
|
279
|
+
activeRunLogNewlines = 0;
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* The 1-based line of the active run log the next write starts on.
|
|
283
|
+
* Redaction rewrites within a line and never adds or drops one, so the
|
|
284
|
+
* number holds for the file on disk.
|
|
285
|
+
*/
|
|
286
|
+
export function activeRunLogLine() {
|
|
287
|
+
return activeRunLogNewlines + 1;
|
|
277
288
|
}
|
|
278
289
|
/** Absolute path of the active run log, or undefined when none is open. */
|
|
279
290
|
export function getActiveRunLogPath() {
|
|
@@ -281,7 +292,12 @@ export function getActiveRunLogPath() {
|
|
|
281
292
|
}
|
|
282
293
|
/** Appends to the active run log. A no-op when none is open. */
|
|
283
294
|
export function writeToActiveRunLog(chunk) {
|
|
284
|
-
activeRunLog
|
|
295
|
+
if (activeRunLog === undefined)
|
|
296
|
+
return;
|
|
297
|
+
for (let index = chunk.indexOf('\n'); index !== -1; index = chunk.indexOf('\n', index + 1)) {
|
|
298
|
+
activeRunLogNewlines += 1;
|
|
299
|
+
}
|
|
300
|
+
activeRunLog.write(chunk);
|
|
285
301
|
}
|
|
286
302
|
/**
|
|
287
303
|
* Closes and clears the active run log. Resolves even when none is open.
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* reporting phantom test failures (or phantom passes).
|
|
23
23
|
*/
|
|
24
24
|
export type { HarnessCrashSignature } from './test-harness-signature.js';
|
|
25
|
-
import type
|
|
25
|
+
import { type HarnessCrashSignature, type IncompleteRunEvidence } from './test-harness-signature.js';
|
|
26
26
|
export { headedDisplayAsleepVerdictNote, headedNoOutputTimeoutHint } from './test-stall-triage.js';
|
|
27
27
|
/** How a completed harness run should be interpreted. */
|
|
28
28
|
export type HarnessRunClassification = 'tests-ran-ok' | 'test-failures' | 'harness-crash' | 'harness-arguments' | 'no-tests';
|
|
@@ -198,6 +198,7 @@ export declare function extractSummaryCounts(output: string): HarnessSummaryCoun
|
|
|
198
198
|
export declare function formatFireforgeVerdictLine(verdict: HarnessRunVerdict, shards?: {
|
|
199
199
|
passed: number;
|
|
200
200
|
total: number;
|
|
201
|
+
failed?: readonly string[];
|
|
201
202
|
}): string;
|
|
202
203
|
/**
|
|
203
204
|
* Builds the operator-facing failure message for a green-looking summary
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
import { hasKnownTeardownNoise } from './mach-known-noise-filter.js';
|
|
26
26
|
import { truncateEvidence, unmarkedFailureEvidenceNote, } from './test-harness-verdict-notes.js';
|
|
27
27
|
import { classifyProfileArgumentFailure } from './test-profile-files.js';
|
|
28
|
+
import { formatIncompleteSuffix, formatShardsSuffix, } from './test-harness-signature.js';
|
|
28
29
|
// The no-output-stall triage text lives in `test-stall-triage.ts` (it is
|
|
29
30
|
// operator advice, not classification) and is re-exported here so the
|
|
30
31
|
// command layer keeps importing its harness diagnostics from one module.
|
|
@@ -659,15 +660,11 @@ function lastCapturedCount(output, pattern) {
|
|
|
659
660
|
export function formatFireforgeVerdictLine(verdict, shards) {
|
|
660
661
|
const counts = (verdict.checks !== undefined ? ` checks=${verdict.checks}` : '') +
|
|
661
662
|
(verdict.unexpected !== undefined ? ` unexpected=${verdict.unexpected}` : '');
|
|
662
|
-
const
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
const note = verdict.note !== undefined ? ` (${verdict.note})` : '';
|
|
666
|
-
if (verdict.kind === 'tests-ran-ok') {
|
|
667
|
-
return `FIREFORGE-VERDICT: PASS${counts}${shardSuffix}${note}`;
|
|
668
|
-
}
|
|
663
|
+
const tail = `${formatShardsSuffix(shards)}${verdict.note !== undefined ? ` (${verdict.note})` : ''}`;
|
|
664
|
+
if (verdict.kind === 'tests-ran-ok')
|
|
665
|
+
return `FIREFORGE-VERDICT: PASS${counts}${tail}`;
|
|
669
666
|
const reason = verdict.kind === 'harness-crash' ? 'crash' : verdict.kind;
|
|
670
|
-
return `FIREFORGE-VERDICT: FAIL reason=${reason}${counts}${
|
|
667
|
+
return `FIREFORGE-VERDICT: FAIL reason=${reason}${counts}${formatIncompleteSuffix(verdict.incomplete)}${tail}`;
|
|
671
668
|
}
|
|
672
669
|
/**
|
|
673
670
|
* Builds the operator-facing failure message for a green-looking summary
|
|
@@ -20,6 +20,17 @@ export interface HarnessCrashSignature {
|
|
|
20
20
|
/** The concrete output line the recognition was based on. */
|
|
21
21
|
line: string;
|
|
22
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* The verdict line's `started=<n>/<m> first-unstarted=<path>
|
|
25
|
+
* [started-source=<src>]` keys, or nothing when every file started.
|
|
26
|
+
*/
|
|
27
|
+
export declare function formatIncompleteSuffix(evidence: IncompleteRunEvidence | undefined): string;
|
|
28
|
+
/** The verdict line's `shards=<p>/<t> [failed-shards=<labels>]` keys of a sharded run. */
|
|
29
|
+
export declare function formatShardsSuffix(shards: {
|
|
30
|
+
passed: number;
|
|
31
|
+
total: number;
|
|
32
|
+
failed?: readonly string[];
|
|
33
|
+
} | undefined): string;
|
|
23
34
|
/** Why an exit-0 run that would have read as a pass was failed instead. */
|
|
24
35
|
export interface IncompleteRunEvidence {
|
|
25
36
|
/** Requested test files that produced a start (or skip) marker. */
|
|
@@ -28,6 +39,11 @@ export interface IncompleteRunEvidence {
|
|
|
28
39
|
requested: number;
|
|
29
40
|
/** First requested file, in dispatch order, that never started. */
|
|
30
41
|
firstUnstarted?: string;
|
|
42
|
+
/**
|
|
43
|
+
* Which evidence the start count came from, stamped with a short count:
|
|
44
|
+
* the structured raw log, the streamed output, or the capped capture.
|
|
45
|
+
*/
|
|
46
|
+
startedSource?: 'raw' | 'stream' | 'capture';
|
|
31
47
|
/** The harness's "shut down in the middle of a test" line, when present. */
|
|
32
48
|
shutdownLine?: string;
|
|
33
49
|
/** The summary's non-zero `Unexpected results:` count, when it printed one. */
|
|
@@ -14,4 +14,22 @@
|
|
|
14
14
|
* `test-run-completeness.ts` produces it from helpers in
|
|
15
15
|
* `test-harness-crash.ts`, whose verdict type carries it.
|
|
16
16
|
*/
|
|
17
|
-
|
|
17
|
+
/**
|
|
18
|
+
* The verdict line's `started=<n>/<m> first-unstarted=<path>
|
|
19
|
+
* [started-source=<src>]` keys, or nothing when every file started.
|
|
20
|
+
*/
|
|
21
|
+
export function formatIncompleteSuffix(evidence) {
|
|
22
|
+
if (evidence?.firstUnstarted === undefined)
|
|
23
|
+
return '';
|
|
24
|
+
const source = evidence.startedSource === undefined ? '' : ` started-source=${evidence.startedSource}`;
|
|
25
|
+
return ` started=${evidence.started}/${evidence.requested} first-unstarted=${evidence.firstUnstarted}${source}`;
|
|
26
|
+
}
|
|
27
|
+
/** The verdict line's `shards=<p>/<t> [failed-shards=<labels>]` keys of a sharded run. */
|
|
28
|
+
export function formatShardsSuffix(shards) {
|
|
29
|
+
if (shards === undefined)
|
|
30
|
+
return '';
|
|
31
|
+
const failed = shards.failed !== undefined && shards.failed.length > 0
|
|
32
|
+
? ` failed-shards=${shards.failed.join(',')}`
|
|
33
|
+
: '';
|
|
34
|
+
return ` shards=${shards.passed}/${shards.total}${failed}`;
|
|
35
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The mozlog structured ("raw") log of one mach test dispatch.
|
|
3
|
+
*
|
|
4
|
+
* Everything else FireForge reads from a run is human text: the
|
|
5
|
+
* `TEST_START`/`TEST_END` lines and the `Unexpected results:` summary, in two
|
|
6
|
+
* dialects, out of a capture that keeps only its last 2 MB. A gate that
|
|
7
|
+
* wants to name the files a run failed had to regex that text, and agent-
|
|
8
|
+
* quiet mozbuild drops `TEST_START` from it altogether. The raw log is the
|
|
9
|
+
* record the summary itself is computed from: one JSON object per line,
|
|
10
|
+
* with `test_start`, `test_status` and `test_end` actions naming each test
|
|
11
|
+
* by its manifest id.
|
|
12
|
+
*
|
|
13
|
+
* It is requested per dispatch with mozlog's `--log-raw=<path>`, which
|
|
14
|
+
* `mach test`, `mach mochitest` and `mach xpcshell-test` all accept, only
|
|
15
|
+
* when a run asks for per-file results (`--results`, `--shard-size`).
|
|
16
|
+
*/
|
|
17
|
+
import type { StartMarkers } from './test-run-completeness.js';
|
|
18
|
+
/** One test file's outcome, from its `test_end` and failing `test_status` records. */
|
|
19
|
+
export interface RawTestResult {
|
|
20
|
+
/** The test id mozlog reports (a manifest-relative path). */
|
|
21
|
+
test: string;
|
|
22
|
+
/** `test_end` status (`OK`, `PASS`, `FAIL`, `ERROR`, `TIMEOUT`, `CRASH`, `SKIP`). */
|
|
23
|
+
status: string;
|
|
24
|
+
/** Expected status; equal to `status` when mozlog omitted it. */
|
|
25
|
+
expected: string;
|
|
26
|
+
/** True when the file ended unexpectedly or any subtest did. */
|
|
27
|
+
unexpected: boolean;
|
|
28
|
+
/** Subtests that reported an unexpected status. */
|
|
29
|
+
unexpectedSubtests: number;
|
|
30
|
+
/** Wall time between `test_start` and `test_end`, when both carried a time. */
|
|
31
|
+
durationMs?: number;
|
|
32
|
+
}
|
|
33
|
+
/** Everything one raw log says about the run. */
|
|
34
|
+
export interface RawTestLog {
|
|
35
|
+
/** Files in `test_end` order. A started file with no end is not listed. */
|
|
36
|
+
results: RawTestResult[];
|
|
37
|
+
/** Start markers for the completeness veto: every `test_start` id. */
|
|
38
|
+
markers: StartMarkers;
|
|
39
|
+
/** Started tests that never ended (a crash or a cut-off run). */
|
|
40
|
+
unfinished: string[];
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Parses a mozlog raw log. Unknown actions and malformed lines are skipped,
|
|
44
|
+
* so a log truncated by a crash still yields the files that finished.
|
|
45
|
+
*
|
|
46
|
+
* @param text - The raw log's contents
|
|
47
|
+
*/
|
|
48
|
+
export declare function parseRawTestLog(text: string): RawTestLog;
|
|
49
|
+
/**
|
|
50
|
+
* Reads and parses a raw log. Returns undefined when the file is missing or
|
|
51
|
+
* unreadable (the harness never started, or the engine's mach ignored the
|
|
52
|
+
* flag), so callers fall back to the streamed text.
|
|
53
|
+
*
|
|
54
|
+
* @param path - The `--log-raw` destination
|
|
55
|
+
*/
|
|
56
|
+
export declare function readRawTestLog(path: string): Promise<RawTestLog | undefined>;
|
|
57
|
+
/**
|
|
58
|
+
* The `--log-raw` destination the operator already forwarded with
|
|
59
|
+
* `--mach-arg`, when it names a file. FireForge then reads that file instead
|
|
60
|
+
* of adding its own flag, so the operator's log is not replaced.
|
|
61
|
+
*
|
|
62
|
+
* @param args - The dispatch's extra mach arguments
|
|
63
|
+
*/
|
|
64
|
+
export declare function forwardedRawLogPath(args: readonly string[]): string | undefined;
|
|
65
|
+
/** True when the dispatch arguments already carry any `--log-raw`. */
|
|
66
|
+
export declare function hasForwardedRawLog(args: readonly string[]): boolean;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* The mozlog structured ("raw") log of one mach test dispatch.
|
|
4
|
+
*
|
|
5
|
+
* Everything else FireForge reads from a run is human text: the
|
|
6
|
+
* `TEST_START`/`TEST_END` lines and the `Unexpected results:` summary, in two
|
|
7
|
+
* dialects, out of a capture that keeps only its last 2 MB. A gate that
|
|
8
|
+
* wants to name the files a run failed had to regex that text, and agent-
|
|
9
|
+
* quiet mozbuild drops `TEST_START` from it altogether. The raw log is the
|
|
10
|
+
* record the summary itself is computed from: one JSON object per line,
|
|
11
|
+
* with `test_start`, `test_status` and `test_end` actions naming each test
|
|
12
|
+
* by its manifest id.
|
|
13
|
+
*
|
|
14
|
+
* It is requested per dispatch with mozlog's `--log-raw=<path>`, which
|
|
15
|
+
* `mach test`, `mach mochitest` and `mach xpcshell-test` all accept, only
|
|
16
|
+
* when a run asks for per-file results (`--results`, `--shard-size`).
|
|
17
|
+
*/
|
|
18
|
+
import { readFile } from 'node:fs/promises';
|
|
19
|
+
function parseRecord(line) {
|
|
20
|
+
if (!line.startsWith('{'))
|
|
21
|
+
return undefined;
|
|
22
|
+
try {
|
|
23
|
+
const parsed = JSON.parse(line);
|
|
24
|
+
return typeof parsed === 'object' && parsed !== null ? parsed : undefined;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// A line cut off by a killed harness is not a record.
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Parses a mozlog raw log. Unknown actions and malformed lines are skipped,
|
|
33
|
+
* so a log truncated by a crash still yields the files that finished.
|
|
34
|
+
*
|
|
35
|
+
* @param text - The raw log's contents
|
|
36
|
+
*/
|
|
37
|
+
export function parseRawTestLog(text) {
|
|
38
|
+
const results = [];
|
|
39
|
+
const started = [];
|
|
40
|
+
const startTimes = new Map();
|
|
41
|
+
const subtestFailures = new Map();
|
|
42
|
+
for (const line of text.split(/\r?\n/)) {
|
|
43
|
+
const record = parseRecord(line.trim());
|
|
44
|
+
if (record === undefined || typeof record.test !== 'string')
|
|
45
|
+
continue;
|
|
46
|
+
const test = record.test;
|
|
47
|
+
if (record.action === 'test_start') {
|
|
48
|
+
started.push(test);
|
|
49
|
+
if (typeof record.time === 'number')
|
|
50
|
+
startTimes.set(test, record.time);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (record.action === 'test_status' && typeof record.expected === 'string') {
|
|
54
|
+
// mozlog sets `expected` on a status record only when it differs.
|
|
55
|
+
subtestFailures.set(test, (subtestFailures.get(test) ?? 0) + 1);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (record.action !== 'test_end' || typeof record.status !== 'string')
|
|
59
|
+
continue;
|
|
60
|
+
const expected = typeof record.expected === 'string' ? record.expected : record.status;
|
|
61
|
+
const unexpectedSubtests = subtestFailures.get(test) ?? 0;
|
|
62
|
+
const start = startTimes.get(test);
|
|
63
|
+
results.push({
|
|
64
|
+
test,
|
|
65
|
+
status: record.status,
|
|
66
|
+
expected,
|
|
67
|
+
unexpected: expected !== record.status || unexpectedSubtests > 0,
|
|
68
|
+
unexpectedSubtests,
|
|
69
|
+
...(start !== undefined && typeof record.time === 'number'
|
|
70
|
+
? { durationMs: record.time - start }
|
|
71
|
+
: {}),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
const ended = new Set(results.map((result) => result.test));
|
|
75
|
+
return {
|
|
76
|
+
results,
|
|
77
|
+
markers: {
|
|
78
|
+
started,
|
|
79
|
+
skipped: results.filter((r) => r.status === 'SKIP').map((r) => r.test),
|
|
80
|
+
},
|
|
81
|
+
unfinished: started.filter((test) => !ended.has(test)),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Reads and parses a raw log. Returns undefined when the file is missing or
|
|
86
|
+
* unreadable (the harness never started, or the engine's mach ignored the
|
|
87
|
+
* flag), so callers fall back to the streamed text.
|
|
88
|
+
*
|
|
89
|
+
* @param path - The `--log-raw` destination
|
|
90
|
+
*/
|
|
91
|
+
export async function readRawTestLog(path) {
|
|
92
|
+
try {
|
|
93
|
+
return parseRawTestLog(await readFile(path, 'utf8'));
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* The `--log-raw` destination the operator already forwarded with
|
|
101
|
+
* `--mach-arg`, when it names a file. FireForge then reads that file instead
|
|
102
|
+
* of adding its own flag, so the operator's log is not replaced.
|
|
103
|
+
*
|
|
104
|
+
* @param args - The dispatch's extra mach arguments
|
|
105
|
+
*/
|
|
106
|
+
export function forwardedRawLogPath(args) {
|
|
107
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
108
|
+
const arg = args[index] ?? '';
|
|
109
|
+
const value = arg.startsWith('--log-raw=')
|
|
110
|
+
? arg.slice('--log-raw='.length)
|
|
111
|
+
: arg === '--log-raw'
|
|
112
|
+
? args[index + 1]
|
|
113
|
+
: undefined;
|
|
114
|
+
if (value !== undefined)
|
|
115
|
+
return value === '-' ? undefined : value;
|
|
116
|
+
}
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
/** True when the dispatch arguments already carry any `--log-raw`. */
|
|
120
|
+
export function hasForwardedRawLog(args) {
|
|
121
|
+
return args.some((arg) => arg === '--log-raw' || arg.startsWith('--log-raw='));
|
|
122
|
+
}
|