@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.
Files changed (212) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/dist/build-info.json +4 -4
  3. package/dist/src/commands/check-report.d.ts +17 -4
  4. package/dist/src/commands/check-report.js +19 -6
  5. package/dist/src/commands/check-run.d.ts +5 -0
  6. package/dist/src/commands/check-run.js +9 -1
  7. package/dist/src/commands/check-steps.d.ts +8 -0
  8. package/dist/src/commands/check-steps.js +31 -6
  9. package/dist/src/commands/check.js +1 -1
  10. package/dist/src/commands/declarations-report.d.ts +43 -0
  11. package/dist/src/commands/declarations-report.js +76 -0
  12. package/dist/src/commands/export-placement-gate.js +8 -0
  13. package/dist/src/commands/furnace/rename-registrations.js +7 -2
  14. package/dist/src/commands/furnace/validate.js +26 -1
  15. package/dist/src/commands/lint-per-patch.js +6 -2
  16. package/dist/src/commands/lint-per-run-checkjs.js +2 -2
  17. package/dist/src/commands/lint-report.js +7 -2
  18. package/dist/src/commands/manifest.js +2 -0
  19. package/dist/src/commands/park.d.ts +22 -0
  20. package/dist/src/commands/park.js +232 -0
  21. package/dist/src/commands/patch/delete.js +11 -6
  22. package/dist/src/commands/patch/index.d.ts +3 -3
  23. package/dist/src/commands/patch/index.js +8 -4
  24. package/dist/src/commands/patch/lint-ignore-from.d.ts +44 -0
  25. package/dist/src/commands/patch/lint-ignore-from.js +142 -0
  26. package/dist/src/commands/patch/lint-ignore.js +39 -29
  27. package/dist/src/commands/patch/move-files-create.js +11 -4
  28. package/dist/src/commands/patch/move-files-into.js +10 -4
  29. package/dist/src/commands/patch/move-files.js +3 -1
  30. package/dist/src/commands/patch/removal-created-files.d.ts +48 -0
  31. package/dist/src/commands/patch/removal-created-files.js +83 -0
  32. package/dist/src/commands/patch/rename.js +15 -3
  33. package/dist/src/commands/patch/reserve.d.ts +33 -0
  34. package/dist/src/commands/patch/reserve.js +152 -0
  35. package/dist/src/commands/patch/retire.js +11 -6
  36. package/dist/src/commands/patch/split-plan.d.ts +10 -1
  37. package/dist/src/commands/patch/split-plan.js +16 -1
  38. package/dist/src/commands/patch/split.js +14 -5
  39. package/dist/src/commands/patch/stage.d.ts +34 -0
  40. package/dist/src/commands/patch/stage.js +153 -0
  41. package/dist/src/commands/patch/staged-dependency-remove.d.ts +31 -0
  42. package/dist/src/commands/patch/staged-dependency-remove.js +80 -0
  43. package/dist/src/commands/patch/staged-dependency.js +40 -98
  44. package/dist/src/commands/re-export-adjacent.d.ts +5 -0
  45. package/dist/src/commands/re-export-adjacent.js +4 -3
  46. package/dist/src/commands/re-export-bulk-scan.d.ts +2 -1
  47. package/dist/src/commands/re-export-bulk-scan.js +22 -7
  48. package/dist/src/commands/re-export-hunks.d.ts +35 -0
  49. package/dist/src/commands/re-export-hunks.js +220 -0
  50. package/dist/src/commands/re-export-options.js +29 -2
  51. package/dist/src/commands/re-export-purity-scope.d.ts +18 -0
  52. package/dist/src/commands/re-export-purity-scope.js +131 -0
  53. package/dist/src/commands/re-export-register.js +15 -1
  54. package/dist/src/commands/re-export-removed.d.ts +1 -1
  55. package/dist/src/commands/re-export-removed.js +17 -2
  56. package/dist/src/commands/re-export-scan.d.ts +2 -0
  57. package/dist/src/commands/re-export-scan.js +4 -1
  58. package/dist/src/commands/re-export-single.js +102 -42
  59. package/dist/src/commands/re-export-stale-registrations.d.ts +18 -0
  60. package/dist/src/commands/re-export-stale-registrations.js +62 -0
  61. package/dist/src/commands/re-export.js +35 -34
  62. package/dist/src/commands/status-output.js +5 -1
  63. package/dist/src/commands/test-build-furnace.d.ts +3 -2
  64. package/dist/src/commands/test-build-furnace.js +8 -2
  65. package/dist/src/commands/test-build-phase.d.ts +2 -0
  66. package/dist/src/commands/test-build-phase.js +34 -17
  67. package/dist/src/commands/test-checkjs.js +3 -2
  68. package/dist/src/commands/test-dispatch.d.ts +39 -0
  69. package/dist/src/commands/test-dispatch.js +220 -0
  70. package/dist/src/commands/test-furnace-reach.d.ts +37 -0
  71. package/dist/src/commands/test-furnace-reach.js +132 -0
  72. package/dist/src/commands/test-orchestrate.d.ts +14 -0
  73. package/dist/src/commands/test-orchestrate.js +25 -4
  74. package/dist/src/commands/test-perf-scope.d.ts +21 -0
  75. package/dist/src/commands/test-perf-scope.js +55 -0
  76. package/dist/src/commands/test-prelock.js +100 -4
  77. package/dist/src/commands/test-register.js +18 -3
  78. package/dist/src/commands/test-results.d.ts +62 -0
  79. package/dist/src/commands/test-results.js +62 -0
  80. package/dist/src/commands/test-run.d.ts +13 -55
  81. package/dist/src/commands/test-run.js +73 -101
  82. package/dist/src/commands/test-shards.d.ts +110 -0
  83. package/dist/src/commands/test-shards.js +255 -0
  84. package/dist/src/commands/test-stale-gate.d.ts +5 -1
  85. package/dist/src/commands/test-stale-gate.js +11 -2
  86. package/dist/src/commands/test-verdict.d.ts +2 -1
  87. package/dist/src/commands/test.d.ts +1 -1
  88. package/dist/src/commands/test.js +68 -75
  89. package/dist/src/commands/token-lint.js +48 -8
  90. package/dist/src/commands/verify.js +9 -2
  91. package/dist/src/core/build-baseline-types.d.ts +13 -0
  92. package/dist/src/core/build-baseline-validate.js +9 -0
  93. package/dist/src/core/build-baseline.js +7 -0
  94. package/dist/src/core/build-prepare.d.ts +6 -0
  95. package/dist/src/core/build-prepare.js +29 -3
  96. package/dist/src/core/config-paths.d.ts +1 -1
  97. package/dist/src/core/config-paths.js +2 -0
  98. package/dist/src/core/config-validate-check.js +83 -7
  99. package/dist/src/core/config-validate-test-toolchains.js +14 -0
  100. package/dist/src/core/config-validate-token-scales.d.ts +3 -0
  101. package/dist/src/core/config-validate-token-scales.js +47 -5
  102. package/dist/src/core/config-validate.js +6 -0
  103. package/dist/src/core/coverage-extend.js +2 -2
  104. package/dist/src/core/coverage-records.d.ts +11 -0
  105. package/dist/src/core/coverage-records.js +34 -0
  106. package/dist/src/core/engine-session-lock.d.ts +6 -0
  107. package/dist/src/core/engine-session-lock.js +10 -2
  108. package/dist/src/core/file-lock-shared.d.ts +57 -0
  109. package/dist/src/core/file-lock-shared.js +117 -0
  110. package/dist/src/core/file-lock.d.ts +9 -0
  111. package/dist/src/core/file-lock.js +58 -2
  112. package/dist/src/core/furnace-apply.js +10 -10
  113. package/dist/src/core/furnace-config-validate-exceptions.d.ts +11 -3
  114. package/dist/src/core/furnace-config-validate-exceptions.js +59 -8
  115. package/dist/src/core/furnace-config-validate.js +7 -1
  116. package/dist/src/core/furnace-registration-jar-inplace.d.ts +12 -0
  117. package/dist/src/core/furnace-registration-jar-inplace.js +80 -0
  118. package/dist/src/core/furnace-validate-accessibility.d.ts +5 -3
  119. package/dist/src/core/furnace-validate-accessibility.js +75 -30
  120. package/dist/src/core/furnace-validate-exceptions.d.ts +13 -4
  121. package/dist/src/core/furnace-validate-exceptions.js +59 -4
  122. package/dist/src/core/furnace-validate-icon-button.js +30 -1
  123. package/dist/src/core/history-log-file.js +8 -1
  124. package/dist/src/core/line-prefixer.d.ts +18 -0
  125. package/dist/src/core/line-prefixer.js +34 -0
  126. package/dist/src/core/mach.d.ts +7 -0
  127. package/dist/src/core/mach.js +24 -9
  128. package/dist/src/core/module-resolution-preflight.d.ts +19 -3
  129. package/dist/src/core/module-resolution-preflight.js +24 -6
  130. package/dist/src/core/negative-control-compiled.d.ts +36 -0
  131. package/dist/src/core/negative-control-compiled.js +87 -0
  132. package/dist/src/core/negative-control-diff.d.ts +14 -2
  133. package/dist/src/core/negative-control-diff.js +49 -6
  134. package/dist/src/core/negative-control.d.ts +12 -0
  135. package/dist/src/core/negative-control.js +48 -5
  136. package/dist/src/core/park-store.d.ts +63 -0
  137. package/dist/src/core/park-store.js +216 -0
  138. package/dist/src/core/patch-export-update.d.ts +4 -0
  139. package/dist/src/core/patch-export-update.js +12 -3
  140. package/dist/src/core/patch-export.d.ts +3 -2
  141. package/dist/src/core/patch-export.js +21 -14
  142. package/dist/src/core/patch-files.js +26 -8
  143. package/dist/src/core/patch-lint-checkjs-any-import.d.ts +6 -4
  144. package/dist/src/core/patch-lint-checkjs-any-import.js +9 -7
  145. package/dist/src/core/patch-lint-checkjs-diagnostics.d.ts +17 -0
  146. package/dist/src/core/patch-lint-checkjs-diagnostics.js +32 -0
  147. package/dist/src/core/patch-lint-checkjs.d.ts +10 -1
  148. package/dist/src/core/patch-lint-checkjs.js +13 -3
  149. package/dist/src/core/patch-lint-cross.d.ts +3 -1
  150. package/dist/src/core/patch-lint-cross.js +5 -0
  151. package/dist/src/core/patch-lint-css.js +14 -10
  152. package/dist/src/core/patch-lint-forward-registration-extended.d.ts +5 -0
  153. package/dist/src/core/patch-lint-forward-registration-extended.js +1 -1
  154. package/dist/src/core/patch-lint-ignore.d.ts +56 -12
  155. package/dist/src/core/patch-lint-ignore.js +173 -43
  156. package/dist/src/core/patch-lint-ownership.d.ts +10 -0
  157. package/dist/src/core/patch-lint-ownership.js +26 -0
  158. package/dist/src/core/patch-lint-size.js +9 -1
  159. package/dist/src/core/patch-lint-token-scale.d.ts +26 -8
  160. package/dist/src/core/patch-lint-token-scale.js +206 -29
  161. package/dist/src/core/patch-lint.js +2 -2
  162. package/dist/src/core/patch-manifest-io.js +8 -1
  163. package/dist/src/core/patch-manifest-key-order.d.ts +24 -0
  164. package/dist/src/core/patch-manifest-key-order.js +61 -0
  165. package/dist/src/core/patch-manifest-stage.d.ts +46 -0
  166. package/dist/src/core/patch-manifest-stage.js +96 -0
  167. package/dist/src/core/patch-manifest-validate.js +27 -1
  168. package/dist/src/core/patch-order-legal.d.ts +2 -1
  169. package/dist/src/core/patch-order-legal.js +7 -2
  170. package/dist/src/core/patch-policy.d.ts +6 -2
  171. package/dist/src/core/patch-policy.js +14 -7
  172. package/dist/src/core/patch-retired.d.ts +26 -4
  173. package/dist/src/core/patch-retired.js +40 -2
  174. package/dist/src/core/run-log.d.ts +6 -0
  175. package/dist/src/core/run-log.js +17 -1
  176. package/dist/src/core/test-harness-crash.d.ts +2 -1
  177. package/dist/src/core/test-harness-crash.js +5 -8
  178. package/dist/src/core/test-harness-signature.d.ts +16 -0
  179. package/dist/src/core/test-harness-signature.js +19 -1
  180. package/dist/src/core/test-raw-log.d.ts +66 -0
  181. package/dist/src/core/test-raw-log.js +122 -0
  182. package/dist/src/core/test-run-completeness.d.ts +47 -7
  183. package/dist/src/core/test-run-completeness.js +70 -23
  184. package/dist/src/core/test-stale-check.d.ts +10 -1
  185. package/dist/src/core/test-stale-check.js +15 -1
  186. package/dist/src/core/test-xpcshell-retry.d.ts +2 -0
  187. package/dist/src/core/test-xpcshell-retry.js +2 -1
  188. package/dist/src/core/token-lint.d.ts +5 -2
  189. package/dist/src/core/token-lint.js +8 -3
  190. package/dist/src/core/token-sibling-sheets.d.ts +46 -0
  191. package/dist/src/core/token-sibling-sheets.js +119 -0
  192. package/dist/src/core/tree-guard.js +2 -0
  193. package/dist/src/core/typecheck-shim.d.ts +6 -1
  194. package/dist/src/core/typecheck-shim.js +139 -2
  195. package/dist/src/core/typecheck.js +3 -1
  196. package/dist/src/index.d.ts +9 -3
  197. package/dist/src/index.js +4 -1
  198. package/dist/src/types/commands/index.d.ts +2 -2
  199. package/dist/src/types/commands/options.d.ts +68 -7
  200. package/dist/src/types/commands/patches.d.ts +43 -0
  201. package/dist/src/types/config.d.ts +74 -2
  202. package/dist/src/types/furnace.d.ts +29 -0
  203. package/dist/src/types/index.d.ts +1 -1
  204. package/dist/src/utils/options.d.ts +3 -2
  205. package/dist/src/utils/options.js +9 -4
  206. package/docs/configuration.md +110 -9
  207. package/docs/furnace.md +61 -1
  208. package/docs/lifecycle-invariants.md +13 -11
  209. package/docs/machine-output.md +68 -18
  210. package/docs/patch-workflow.md +174 -13
  211. package/docs/testing.md +89 -5
  212. package/package.json +3 -3
@@ -12,6 +12,7 @@
12
12
  import { type DisplaySleepState } from '../core/display-state.js';
13
13
  import { type MachCommandResult } from '../core/mach.js';
14
14
  import { type HarnessRunVerdict } from '../core/test-harness-crash.js';
15
+ import { type RawTestResult } from '../core/test-raw-log.js';
15
16
  /** Default bounded retry budget for recognized harness crashes. */
16
17
  export declare const DEFAULT_HARNESS_RETRIES = 2;
17
18
  /**
@@ -49,6 +50,15 @@ export interface TestRunContext {
49
50
  fullOutput?: boolean;
50
51
  /** `--pgid-file`: where each dispatch publishes its harness process group id. */
51
52
  pgidFile?: string | undefined;
53
+ /**
54
+ * Where each dispatch writes its mozlog raw log (`--results`,
55
+ * `--shard-size`). Unset: no `--log-raw` is added and nothing is parsed.
56
+ */
57
+ rawLogDir?: string | undefined;
58
+ /** `--repeat` run this dispatch belongs to (1-based), for `--results`. */
59
+ repetition?: number | undefined;
60
+ /** Prefix for every echoed and logged output line (`--jobs` instances). */
61
+ outputPrefix?: string | undefined;
52
62
  }
53
63
  /** Outcome of one (possibly retried) harness invocation. */
54
64
  export interface TestRunOutcome {
@@ -62,64 +72,12 @@ export interface TestRunOutcome {
62
72
  * when the shape did not apply and no probe ran.
63
73
  */
64
74
  displayState?: DisplaySleepState | undefined;
75
+ /** Per-file results from the raw log, when the run wrote one. */
76
+ fileResults?: RawTestResult[] | undefined;
65
77
  }
66
78
  /**
67
79
  * Runs one mach test invocation for `paths`, retrying recognized harness
68
80
  * crashes up to the configured budget. Every attempt goes through the
69
81
  * stale-xpcshell-symlink repair path the single-run flow already used.
70
82
  */
71
- export declare function runTestsWithRetries(ctx: TestRunContext, paths: string[]): Promise<TestRunOutcome>;
72
- /**
73
- * One sequential harness invocation of a sharded run: a requested path
74
- * argument and the mach paths dispatched for it. A file argument is a group
75
- * of one. A directory argument groups its enumerated test files so the whole
76
- * directory still runs in ONE browser instance, keeping cross-file state
77
- * behaviour within a directory run.
78
- */
79
- export interface ShardGroup {
80
- /** The path argument as the operator passed it (display label). */
81
- label: string;
82
- /** The mach dispatch paths for this argument. */
83
- paths: string[];
84
- }
85
- /** Per-shard summary entry. */
86
- export interface ShardOutcome {
87
- label: string;
88
- outcome: TestRunOutcome;
89
- }
90
- /** Aggregate result of a sharded run, verdict emission deferred to the caller. */
91
- export interface ShardedRunSummary {
92
- shards: ShardOutcome[];
93
- passed: number;
94
- total: number;
95
- /** Aggregate classifier verdict. See {@link deriveAggregateShardVerdict}. */
96
- aggregate: HarnessRunVerdict;
97
- }
98
- /**
99
- * Derives the aggregate classification for a sharded run: all-green is
100
- * `tests-ran-ok`. Otherwise the most structural failure wins. A crashed
101
- * shard makes the whole run `harness-crash`, then `no-tests`, then plain
102
- * `test-failures`, so the aggregate `reason=` keeps the classifier-driven
103
- * contract instead of collapsing every failure to `test-failures`.
104
- */
105
- export declare function deriveAggregateShardVerdict(shards: ShardOutcome[]): HarnessRunVerdict;
106
- /**
107
- * Runs each requested path argument as its own sequential harness
108
- * invocation (a directory argument's enumerated files stay together in
109
- * one invocation, see {@link ShardGroup}) and prints an aggregate
110
- * report. Per-shard failures are diagnosed via `diagnoseShardFailure`
111
- * (which receives the throwing diagnosis chain from the command layer)
112
- * but downgraded to warnings so every shard runs. The aggregate verdict
113
- * line and the aggregate error are not produced here: the caller runs
114
- * the engine-generation integrity check first and then calls
115
- * {@link finalizeShardedOutcome}, so a run invalidated by a concurrent
116
- * `engine/` mutation can never print `PASS` before the check fails.
117
- */
118
- export declare function runShardedTests(ctx: TestRunContext, groups: ShardGroup[], diagnoseShardFailure: (outcome: TestRunOutcome, label: string) => string | undefined): Promise<ShardedRunSummary>;
119
- /**
120
- * Emits the aggregate FIREFORGE-VERDICT line for a sharded run, then
121
- * throws the aggregate error when any shard did not pass, the same
122
- * emit-then-throw shape as `finalizeSingleRunOutcome`. Called only after
123
- * the engine-generation check succeeded.
124
- */
125
- export declare function finalizeShardedOutcome(summary: ShardedRunSummary): void;
83
+ export declare function runTestsWithRetries(ctx: TestRunContext, paths: string[], label?: string): Promise<TestRunOutcome>;
@@ -14,19 +14,21 @@ import { join } from 'node:path';
14
14
  import { probeDisplaySleepState } from '../core/display-state.js';
15
15
  import { runMachTestSuite } from '../core/mach.js';
16
16
  import { parentExited } from '../core/parent-exit-watchdog.js';
17
- import { getActiveRunLogPath } from '../core/run-log.js';
17
+ import { activeRunLogLine, getActiveRunLogPath } from '../core/run-log.js';
18
18
  import { changedPrefNoiseVerdictNote } from '../core/test-changed-prefs.js';
19
- import { buildHarnessCrashMessage, classifyHarnessRun, headedDisplayAsleepVerdictNote, headedNoOutputTimeoutHint, } from '../core/test-harness-crash.js';
19
+ import { classifyHarnessRun, headedDisplayAsleepVerdictNote, } from '../core/test-harness-crash.js';
20
20
  import { reportTestHost, sampleTestHost, stampPerfHost } from '../core/test-host-state.js';
21
- import { vetoIncompletePass } from '../core/test-run-completeness.js';
21
+ import { forwardedRawLogPath, hasForwardedRawLog, readRawTestLog, } from '../core/test-raw-log.js';
22
+ import { createStartMarkerTracker, vetoIncompletePass, } from '../core/test-run-completeness.js';
22
23
  import { retryAfterXpcshellSymlinkRepair } from '../core/test-xpcshell-retry.js';
23
24
  import { withXpcshellProfileDir } from '../core/xpcshell-profile-dir.js';
24
25
  import { TestFailureError } from '../errors/build.js';
25
- import { info, note, warn } from '../utils/logger.js';
26
+ import { warn } from '../utils/logger.js';
26
27
  import { getPlatform } from '../utils/platform.js';
27
28
  import { maybeInjectAppdirArg } from './test-appdir.js';
28
29
  import { createHarnessTeardown } from './test-harness-teardown.js';
29
- import { emitHarnessVerdict, setVerdictRunAttribute } from './test-verdict.js';
30
+ import { recordTestResults } from './test-results.js';
31
+ import { setVerdictRunAttribute } from './test-verdict.js';
30
32
  /** Default bounded retry budget for recognized harness crashes. */
31
33
  export const DEFAULT_HARNESS_RETRIES = 2;
32
34
  /** Resolves the mach test command a suite dispatches to. */
@@ -42,21 +44,21 @@ function machKindForSuite(suite) {
42
44
  * crashes up to the configured budget. Every attempt goes through the
43
45
  * stale-xpcshell-symlink repair path the single-run flow already used.
44
46
  */
45
- export async function runTestsWithRetries(ctx, paths) {
47
+ export async function runTestsWithRetries(ctx, paths, label = paths.join(' ')) {
46
48
  // Pure mochitest dispatches keep their env byte-identical, since the profile
47
49
  // variable is xpcshell-harness-specific. Generic `mach
48
50
  // test` runs may include xpcshell paths, so they get the isolation too.
49
51
  if (ctx.suite === 'mochitest') {
50
- return runTestsWithRetriesInner(ctx, paths, ctx.env);
52
+ return runTestsWithRetriesInner(ctx, paths, ctx.env, label);
51
53
  }
52
54
  // Fresh profile dir per harness invocation: shards isolate from each
53
55
  // other and from concurrent `fireforge test` processes. The crash /
54
56
  // symlink-repair retries inside one invocation reuse the same directory
55
57
  // on purpose (they are the same logical run and the harness
56
58
  // re-initialises the profile contents itself).
57
- return withXpcshellProfileDir(ctx.env, (env) => runTestsWithRetriesInner(ctx, paths, env));
59
+ return withXpcshellProfileDir(ctx.env, (env) => runTestsWithRetriesInner(ctx, paths, env, label));
58
60
  }
59
- async function runTestsWithRetriesInner(ctx, paths, env) {
61
+ async function runTestsWithRetriesInner(ctx, paths, env, label) {
60
62
  const extraArgs = [...ctx.baseExtraArgs];
61
63
  const appdirInjectionAttempted = await maybeInjectAppdirArg(ctx.engineDir, paths, ctx.objDir, extraArgs);
62
64
  const kind = machKindForSuite(ctx.suite);
@@ -64,9 +66,15 @@ async function runTestsWithRetriesInner(ctx, paths, env) {
64
66
  let attempts = 0;
65
67
  let result;
66
68
  let verdict;
69
+ let rawLog;
70
+ let logLine;
67
71
  for (;;) {
68
72
  attempts += 1;
69
- result = await runTrackedMachTestSuite(ctx, kind, paths, extraArgs, env);
73
+ let tracker = createStartMarkerTracker();
74
+ const raw = rawLogDispatchArgs(ctx, extraArgs);
75
+ logLine = activeRunLogLine();
76
+ const dispatched = await runTrackedMachTestSuite(ctx, kind, paths, raw.args, env, tracker);
77
+ result = dispatched;
70
78
  // The parent that was reading this run is gone and the watchdog has
71
79
  // already killed the harness. Classifying the corpse would call it a
72
80
  // harness crash and retry it under nobody's supervision.
@@ -74,21 +82,32 @@ async function runTestsWithRetriesInner(ctx, paths, env) {
74
82
  throw new TestFailureError('The parent process exited while the harness was running; the harness tree was ' +
75
83
  'terminated and the run ended without a result.', `mach ${kind}`, undefined, getActiveRunLogPath());
76
84
  }
85
+ const retryTracker = createStartMarkerTracker();
77
86
  result = await retryAfterXpcshellSymlinkRepair({
78
87
  engineDir: ctx.engineDir,
79
88
  objDir: ctx.objDir,
80
89
  result,
81
90
  classification: ctx.classification,
82
91
  normalizedPaths: paths,
83
- extraArgs,
92
+ extraArgs: raw.args,
84
93
  env,
85
94
  kind,
86
95
  fullOutput: ctx.fullOutput,
96
+ onOutputChunk: (stream, chunk) => {
97
+ retryTracker.onChunk(stream, chunk);
98
+ },
87
99
  });
100
+ if (result !== dispatched)
101
+ tracker = retryTracker;
102
+ rawLog = raw.path === undefined ? undefined : await readRawTestLog(raw.path);
88
103
  const combined = `${result.stdout}\n${result.stderr}`;
89
104
  // An exit-0 run is still not a pass when it stopped partway or
90
- // reported unexpected results (see test-run-completeness.ts).
91
- verdict = vetoIncompletePass(classifyHarnessRun(result.exitCode, combined, paths), combined, paths);
105
+ // reported unexpected results (see test-run-completeness.ts). The
106
+ // start count reads the structured raw log when the run wrote one, and
107
+ // the streamed markers otherwise, never the 2 MB capture tail.
108
+ verdict = vetoIncompletePass(classifyHarnessRun(result.exitCode, combined, paths), combined, paths, rawLog !== undefined && rawLog.markers.started.length > 0
109
+ ? { markers: rawLog.markers, source: 'raw' }
110
+ : { markers: tracker.markers(), source: 'stream' });
92
111
  if (verdict.kind !== 'harness-crash' || attempts >= maxAttempts)
93
112
  break;
94
113
  warn(`Harness crash detected (${verdict.signature?.reason ?? 'unknown shape'}): ` +
@@ -113,14 +132,45 @@ async function runTestsWithRetriesInner(ctx, paths, env) {
113
132
  if (note !== undefined)
114
133
  verdict = { ...verdict, note };
115
134
  }
116
- return { result, verdict, attempts, appdirInjectionAttempted, displayState };
135
+ if (rawLog !== undefined) {
136
+ recordTestResults(rawLog.results, rawLog.unfinished, {
137
+ run: ctx.repetition ?? 1,
138
+ shard: label,
139
+ logLine,
140
+ });
141
+ }
142
+ return {
143
+ result,
144
+ verdict,
145
+ attempts,
146
+ appdirInjectionAttempted,
147
+ displayState,
148
+ ...(rawLog === undefined ? {} : { fileResults: rawLog.results }),
149
+ };
150
+ }
151
+ /** Sequence for raw-log file names, unique within the process. */
152
+ let rawLogSequence = 0;
153
+ /**
154
+ * The dispatch arguments with a `--log-raw` destination added, when this run
155
+ * collects per-file results. An operator's own forwarded `--log-raw <file>`
156
+ * is read instead of adding a second one.
157
+ */
158
+ function rawLogDispatchArgs(ctx, extraArgs) {
159
+ if (ctx.rawLogDir === undefined)
160
+ return { args: [...extraArgs], path: undefined };
161
+ if (hasForwardedRawLog(extraArgs)) {
162
+ return { args: [...extraArgs], path: forwardedRawLogPath(extraArgs) };
163
+ }
164
+ rawLogSequence += 1;
165
+ const path = join(ctx.rawLogDir, `dispatch-${rawLogSequence}.raw.jsonl`);
166
+ return { args: [...extraArgs, `--log-raw=${path}`], path };
117
167
  }
118
168
  /**
119
169
  * One mach dispatch with its kill path attached: helper-pid tracking and
120
170
  * post-close reaping, the process-group announcement, and the parent-exit
121
171
  * watchdog for exactly as long as mach runs.
122
172
  */
123
- async function runTrackedMachTestSuite(ctx, kind, paths, extraArgs, env) {
173
+ async function runTrackedMachTestSuite(ctx, kind, paths, extraArgs, env, tracker) {
124
174
  const teardown = createHarnessTeardown({
125
175
  objDir: ctx.objDir === undefined ? undefined : join(ctx.engineDir, ctx.objDir),
126
176
  pgidFile: ctx.pgidFile,
@@ -142,7 +192,14 @@ async function runTrackedMachTestSuite(ctx, kind, paths, extraArgs, env) {
142
192
  args: extraArgs,
143
193
  env,
144
194
  fullOutput: ctx.fullOutput,
145
- teardown: teardown.hooks,
195
+ ...(ctx.outputPrefix === undefined ? {} : { outputPrefix: ctx.outputPrefix }),
196
+ teardown: {
197
+ ...teardown.hooks,
198
+ onOutputChunk: (stream, chunk) => {
199
+ tracker.onChunk(stream, chunk);
200
+ teardown.hooks.onOutputChunk?.(stream, chunk);
201
+ },
202
+ },
146
203
  });
147
204
  }
148
205
  finally {
@@ -173,88 +230,3 @@ async function probeDisplayStateForStall(verdict, headless) {
173
230
  return undefined;
174
231
  return probeDisplaySleepState(getPlatform());
175
232
  }
176
- const SHARD_STATUS_LABEL = {
177
- 'tests-ran-ok': 'PASS',
178
- 'test-failures': 'FAIL',
179
- 'harness-crash': 'CRASH',
180
- 'no-tests': 'NO-TESTS',
181
- 'harness-arguments': 'ARGUMENTS',
182
- };
183
- /**
184
- * Derives the aggregate classification for a sharded run: all-green is
185
- * `tests-ran-ok`. Otherwise the most structural failure wins. A crashed
186
- * shard makes the whole run `harness-crash`, then `no-tests`, then plain
187
- * `test-failures`, so the aggregate `reason=` keeps the classifier-driven
188
- * contract instead of collapsing every failure to `test-failures`.
189
- */
190
- export function deriveAggregateShardVerdict(shards) {
191
- const kinds = new Set(shards.map(({ outcome }) => outcome.verdict.kind));
192
- if (kinds.has('harness-arguments'))
193
- return { kind: 'harness-arguments' };
194
- if (kinds.has('harness-crash'))
195
- return { kind: 'harness-crash' };
196
- if (kinds.has('no-tests'))
197
- return { kind: 'no-tests' };
198
- if (kinds.has('test-failures'))
199
- return { kind: 'test-failures' };
200
- return { kind: 'tests-ran-ok' };
201
- }
202
- /**
203
- * Runs each requested path argument as its own sequential harness
204
- * invocation (a directory argument's enumerated files stay together in
205
- * one invocation, see {@link ShardGroup}) and prints an aggregate
206
- * report. Per-shard failures are diagnosed via `diagnoseShardFailure`
207
- * (which receives the throwing diagnosis chain from the command layer)
208
- * but downgraded to warnings so every shard runs. The aggregate verdict
209
- * line and the aggregate error are not produced here: the caller runs
210
- * the engine-generation integrity check first and then calls
211
- * {@link finalizeShardedOutcome}, so a run invalidated by a concurrent
212
- * `engine/` mutation can never print `PASS` before the check fails.
213
- */
214
- export async function runShardedTests(ctx, groups, diagnoseShardFailure) {
215
- const shards = [];
216
- for (const [index, group] of groups.entries()) {
217
- info(`— Shard ${index + 1}/${groups.length}: ${group.label}`);
218
- const outcome = await runTestsWithRetries(ctx, group.paths);
219
- shards.push({ label: group.label, outcome });
220
- if (outcome.verdict.kind === 'harness-crash' && outcome.verdict.signature) {
221
- const base = buildHarnessCrashMessage(outcome.verdict.signature, outcome.attempts);
222
- const hint = headedNoOutputTimeoutHint(outcome.verdict.signature, {
223
- headless: ctx.headless,
224
- platform: getPlatform(),
225
- ...(outcome.displayState !== undefined ? { displayState: outcome.displayState } : {}),
226
- });
227
- warn(hint ? `${base}\n\n${hint}` : base);
228
- }
229
- else if (outcome.verdict.kind !== 'tests-ran-ok') {
230
- const diagnosis = diagnoseShardFailure(outcome, group.label);
231
- if (diagnosis)
232
- warn(diagnosis);
233
- }
234
- }
235
- const lines = shards.map(({ label, outcome }) => `${SHARD_STATUS_LABEL[outcome.verdict.kind].padEnd(8)} ${label}` +
236
- (outcome.attempts > 1 ? ` (${outcome.attempts} attempts)` : ''));
237
- const failing = shards.filter(({ outcome }) => outcome.verdict.kind !== 'tests-ran-ok');
238
- note(`${lines.join('\n')}\n\n${shards.length - failing.length}/${shards.length} shard(s) passed`, 'Sharded Test Summary');
239
- return {
240
- shards,
241
- passed: shards.length - failing.length,
242
- total: shards.length,
243
- aggregate: deriveAggregateShardVerdict(shards),
244
- };
245
- }
246
- /**
247
- * Emits the aggregate FIREFORGE-VERDICT line for a sharded run, then
248
- * throws the aggregate error when any shard did not pass, the same
249
- * emit-then-throw shape as `finalizeSingleRunOutcome`. Called only after
250
- * the engine-generation check succeeded.
251
- */
252
- export function finalizeShardedOutcome(summary) {
253
- emitHarnessVerdict(summary.aggregate, { passed: summary.passed, total: summary.total });
254
- if (summary.passed < summary.total) {
255
- const failing = summary.shards.filter(({ outcome }) => outcome.verdict.kind !== 'tests-ran-ok');
256
- throw new TestFailureError(`${failing.length} of ${summary.total} sharded test run(s) did not pass: ` +
257
- `${failing.map(({ label }) => label).join(', ')}. ` +
258
- 'See the per-shard diagnosis above. Use --no-shard to reproduce the combined single-invocation behaviour.', 'mach test', undefined, getActiveRunLogPath());
259
- }
260
- }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Sharded `fireforge test` runs: one harness invocation per shard, run in
3
+ * order on one harness instance, or on a pool of `--jobs <n>` instances.
4
+ *
5
+ * Each finished shard prints a `FIREFORGE-SHARD:` line, and the aggregate
6
+ * verdict names the shards that did not pass as `failed-shards=`. Before,
7
+ * the failing shards were named only in the prose of the aggregate error
8
+ * and the human summary box, so a wrapper that reruns failed shards had to
9
+ * parse a sentence that a reworded message would silently break.
10
+ *
11
+ * Under `--shard-size <n>` a shard is a chunk of n files, and the run is
12
+ * judged per file from each dispatch's raw log: `shards=` then counts files
13
+ * and `failed-shards=` names the files that failed.
14
+ */
15
+ import { type HarnessRunVerdict } from '../core/test-harness-crash.js';
16
+ import { type TestRunContext, type TestRunOutcome } from './test-run.js';
17
+ /**
18
+ * One harness invocation of a sharded run: a requested path argument and
19
+ * the mach paths dispatched for it. A file argument is a group of one. A
20
+ * directory argument groups its enumerated test files so the whole
21
+ * directory still runs in ONE browser instance, keeping cross-file state
22
+ * behaviour within a directory run. A `--shard-size` chunk is a group of
23
+ * up to n files.
24
+ */
25
+ export interface ShardGroup {
26
+ /** The path argument as the operator passed it (display label). */
27
+ label: string;
28
+ /** The mach dispatch paths for this argument. */
29
+ paths: string[];
30
+ }
31
+ /** Per-shard summary entry. */
32
+ export interface ShardOutcome {
33
+ label: string;
34
+ outcome: TestRunOutcome;
35
+ /** The shard's dispatch paths (per-file judging). */
36
+ paths?: string[];
37
+ }
38
+ /** Aggregate result of a sharded run, verdict emission deferred to the caller. */
39
+ export interface ShardedRunSummary {
40
+ shards: ShardOutcome[];
41
+ passed: number;
42
+ total: number;
43
+ /** What did not pass: shard labels, or file paths when judged per file. */
44
+ failed: string[];
45
+ /** Aggregate classifier verdict. See {@link deriveAggregateShardVerdict}. */
46
+ aggregate: HarnessRunVerdict;
47
+ }
48
+ /** How {@link runShardedTests} schedules and judges its shards. */
49
+ export interface ShardRunOptions {
50
+ /** Harness instances to run shards on at once (`--jobs`). Default 1. */
51
+ jobs?: number;
52
+ /**
53
+ * The context for instance `index` (0-based). Instance 0 must be the
54
+ * base context; others carry their own ports and output prefix.
55
+ */
56
+ contextFor?: (index: number) => Promise<TestRunContext> | TestRunContext;
57
+ /** Judge per file from the raw log (`--shard-size`). */
58
+ perFile?: boolean;
59
+ }
60
+ /**
61
+ * Derives the aggregate classification for a sharded run: all-green is
62
+ * `tests-ran-ok`. Otherwise the most structural failure wins. A crashed
63
+ * shard makes the whole run `harness-crash`, then `no-tests`, then plain
64
+ * `test-failures`, so the aggregate `reason=` keeps the classifier-driven
65
+ * contract instead of collapsing every failure to `test-failures`.
66
+ */
67
+ export declare function deriveAggregateShardVerdict(shards: ShardOutcome[]): HarnessRunVerdict;
68
+ /**
69
+ * The `FIREFORGE-SHARD:` line for one finished shard: the verdict it would
70
+ * have printed alone, plus `shard=<i>/<n>` and `path=<label>`. Never a
71
+ * `FIREFORGE-VERDICT:` line, so the run keeps exactly one.
72
+ */
73
+ export declare function formatShardLine(shard: ShardOutcome, index: number, total: number, failedFiles?: readonly string[]): string;
74
+ /**
75
+ * The files of one shard that did not pass, judged from its raw log. A file
76
+ * with no `test_end` failed (it never started, or never finished). Returns
77
+ * undefined when the shard wrote no raw log, so the caller judges the
78
+ * shard as a whole.
79
+ */
80
+ export declare function failedFilesOf(shard: ShardOutcome): string[] | undefined;
81
+ /**
82
+ * Runs each shard as its own harness invocation and prints an aggregate
83
+ * report. Per-shard failures are diagnosed via `diagnoseShardFailure`
84
+ * (which receives the throwing diagnosis chain from the command layer)
85
+ * but downgraded to warnings so every shard runs. The aggregate verdict
86
+ * line and the aggregate error are not produced here: the caller runs
87
+ * the engine-generation integrity check first and then calls
88
+ * {@link finalizeShardedOutcome}, so a run invalidated by a concurrent
89
+ * `engine/` mutation can never print `PASS` before the check fails.
90
+ */
91
+ export declare function runShardedTests(ctx: TestRunContext, groups: ShardGroup[], diagnoseShardFailure: (outcome: TestRunOutcome, label: string) => string | undefined, options?: ShardRunOptions): Promise<ShardedRunSummary>;
92
+ /**
93
+ * Emits the aggregate FIREFORGE-VERDICT line for a sharded run, then
94
+ * throws the aggregate error when any shard did not pass, the same
95
+ * emit-then-throw shape as `finalizeSingleRunOutcome`. Called only after
96
+ * the engine-generation check succeeded.
97
+ */
98
+ export declare function finalizeShardedOutcome(summary: ShardedRunSummary): void;
99
+ /**
100
+ * Splits the dispatch paths of every group into consecutive chunks of at
101
+ * most `size` files (`--shard-size`). Order is kept, and a chunk never
102
+ * mixes groups' order: it is the flattened request, cut every `size`
103
+ * files.
104
+ */
105
+ export declare function chunkShardGroups(groups: readonly ShardGroup[], size: number): ShardGroup[];
106
+ /**
107
+ * Splits one combined request into `parts` contiguous chunks of near-equal
108
+ * size, in order (`--jobs` with `--no-shard`).
109
+ */
110
+ export declare function splitContiguous(paths: readonly string[], parts: number): ShardGroup[];