@hominis/fireforge 0.50.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 +67 -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/status-output.js +5 -1
- 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.d.ts +2 -0
- package/dist/src/commands/test-build-phase.js +34 -17
- 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 +220 -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 +14 -0
- package/dist/src/commands/test-orchestrate.js +25 -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 +11 -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 +68 -75
- package/dist/src/commands/token-lint.js +48 -8
- package/dist/src/commands/verify.js +9 -2
- 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/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 +11 -3
- package/dist/src/core/furnace-config-validate-exceptions.js +59 -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 +5 -3
- package/dist/src/core/furnace-validate-accessibility.js +75 -30
- package/dist/src/core/furnace-validate-exceptions.d.ts +13 -4
- package/dist/src/core/furnace-validate-exceptions.js +59 -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 +36 -0
- package/dist/src/core/negative-control-compiled.js +87 -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 +47 -7
- package/dist/src/core/test-run-completeness.js +70 -23
- 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/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 +6 -1
- package/dist/src/core/typecheck-shim.js +139 -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 +29 -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 +110 -9
- package/docs/furnace.md +61 -1
- package/docs/lifecycle-invariants.md +13 -11
- package/docs/machine-output.md +68 -18
- package/docs/patch-workflow.md +174 -13
- package/docs/testing.md +89 -5
- package/package.json +3 -3
|
@@ -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
|
+
}
|
|
@@ -17,22 +17,60 @@
|
|
|
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';
|
|
31
|
+
/** Start and skip marker names seen in a run's output. */
|
|
32
|
+
export interface StartMarkers {
|
|
33
|
+
started: string[];
|
|
34
|
+
skipped: string[];
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Collects start and skip markers from a dispatch's output as it streams.
|
|
38
|
+
*
|
|
39
|
+
* The capture `runMachCapture` hands back keeps only the last 2 MB of each
|
|
40
|
+
* stream, so a suite that prints more than that (a large browser directory,
|
|
41
|
+
* or any run under `--full-output`) loses its first files' start lines from
|
|
42
|
+
* the capture, and counting from that tail named files that ran and passed
|
|
43
|
+
* as never started. The tracker sees every chunk, splits it into whole lines
|
|
44
|
+
* per stream, and keeps only the marker names, so it grows with the file
|
|
45
|
+
* count, not the output size.
|
|
46
|
+
*/
|
|
47
|
+
export interface StartMarkerTracker {
|
|
48
|
+
/** Feeds one output chunk; wire it to `MachOptions.onOutputChunk`. */
|
|
49
|
+
onChunk(stream: 'stdout' | 'stderr', chunk: string): void;
|
|
50
|
+
/** The markers seen so far, including any unterminated last line. */
|
|
51
|
+
markers(): StartMarkers;
|
|
52
|
+
}
|
|
53
|
+
/** Markers collected outside the capped capture, and where they came from. */
|
|
54
|
+
export interface TrackedStartMarkers {
|
|
55
|
+
markers: StartMarkers;
|
|
56
|
+
/** `stream`: every output chunk; `raw`: the mozlog structured log. */
|
|
57
|
+
source: 'stream' | 'raw';
|
|
58
|
+
}
|
|
59
|
+
/** Creates a {@link StartMarkerTracker}. */
|
|
60
|
+
export declare function createStartMarkerTracker(): StartMarkerTracker;
|
|
27
61
|
/**
|
|
28
62
|
* Counts the requested test files that produced a start or skip marker.
|
|
29
63
|
* Returns undefined when nothing can be counted: no test-file-shaped
|
|
30
64
|
* request, or no start line that names any requested file.
|
|
31
65
|
*
|
|
32
|
-
* @param
|
|
66
|
+
* @param evidence - Captured mach stdout and stderr, or the markers
|
|
67
|
+
* collected from the complete stream
|
|
33
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
|
|
34
72
|
*/
|
|
35
|
-
export declare function countStartedFiles(
|
|
73
|
+
export declare function countStartedFiles(evidence: string | StartMarkers, requestedPaths: readonly string[], authoritative?: boolean): {
|
|
36
74
|
started: number;
|
|
37
75
|
requested: number;
|
|
38
76
|
firstUnstarted?: string;
|
|
@@ -45,8 +83,10 @@ export declare function countStartedFiles(output: string, requestedPaths: readon
|
|
|
45
83
|
* @param verdict - The classifier's verdict
|
|
46
84
|
* @param output - Captured mach stdout and stderr
|
|
47
85
|
* @param requestedPaths - Dispatched paths, in dispatch order
|
|
86
|
+
* @param tracked - Start markers collected from the complete stream (or
|
|
87
|
+
* the structured raw log); without them the capped capture is scanned
|
|
48
88
|
*/
|
|
49
|
-
export declare function vetoIncompletePass(verdict: HarnessRunVerdict, output: string, requestedPaths: readonly string[]): HarnessRunVerdict;
|
|
89
|
+
export declare function vetoIncompletePass(verdict: HarnessRunVerdict, output: string, requestedPaths: readonly string[], tracked?: TrackedStartMarkers): HarnessRunVerdict;
|
|
50
90
|
/**
|
|
51
91
|
* Operator-facing explanation for a pass {@link vetoIncompletePass}
|
|
52
92
|
* refused. Names the first file that never started, because that is
|