as-test 1.1.1 → 1.1.3

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.
@@ -2,7 +2,7 @@ import chalk from "chalk";
2
2
  import { spawn } from "child_process";
3
3
  import { glob } from "glob";
4
4
  import { Channel, MessageType } from "../wipc.js";
5
- import { applyMode, formatTime, getExec, loadConfig, tokenizeCommand, } from "../util.js";
5
+ import { applyMode, formatSpecDisplayPath, formatTime, getExec, loadConfig, tokenizeCommand, } from "../util.js";
6
6
  import * as path from "path";
7
7
  import { pathToFileURL } from "url";
8
8
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
@@ -390,7 +390,7 @@ function resolveSuiteSelectionMatches(suites, selectors, file) {
390
390
  if (normalized.includes("/")) {
391
391
  const resolved = resolveExplicitSuitePath(suites, normalized);
392
392
  if (!resolved) {
393
- throw new Error(`No suites matched "${selector}" in ${path.basename(file)}.`);
393
+ throw new Error(`No suites matched "${selector}" in ${formatSpecDisplayPath(file)}.`);
394
394
  }
395
395
  matches.push({
396
396
  kind: "path",
@@ -402,7 +402,7 @@ function resolveSuiteSelectionMatches(suites, selectors, file) {
402
402
  }
403
403
  const resolved = resolveBareSuiteSelector(suites, normalized);
404
404
  if (!resolved) {
405
- throw new Error(`No suites matched "${selector}" in ${path.basename(file)}.`);
405
+ throw new Error(`No suites matched "${selector}" in ${formatSpecDisplayPath(file)}.`);
406
406
  }
407
407
  matches.push({
408
408
  kind: "bare",
@@ -437,7 +437,9 @@ function resolveBareSuiteSelector(suites, selector) {
437
437
  return null;
438
438
  const matches = [];
439
439
  walkSuites(suites, (suite, depth) => {
440
- const leaf = String(suite.path ?? "").split("/").pop() ?? "";
440
+ const leaf = String(suite.path ?? "")
441
+ .split("/")
442
+ .pop() ?? "";
441
443
  if (leaf == slug) {
442
444
  matches.push({ path: String(suite.path), depth });
443
445
  }
@@ -457,7 +459,9 @@ function walkSuites(suites, visitor, depth = 0) {
457
459
  for (const suite of suites) {
458
460
  if (visitor(suite, depth))
459
461
  return true;
460
- const childSuites = Array.isArray(suite?.suites) ? suite.suites : [];
462
+ const childSuites = Array.isArray(suite?.suites)
463
+ ? suite.suites
464
+ : [];
461
465
  if (walkSuites(childSuites, visitor, depth + 1))
462
466
  return true;
463
467
  }
@@ -466,7 +470,9 @@ function walkSuites(suites, visitor, depth = 0) {
466
470
  function cloneSelectedSuites(suites, selected, file, modeName) {
467
471
  const out = [];
468
472
  for (const suite of suites) {
469
- const childSuites = Array.isArray(suite.suites) ? suite.suites : [];
473
+ const childSuites = Array.isArray(suite.suites)
474
+ ? suite.suites
475
+ : [];
470
476
  const selectedChildren = cloneSelectedSuites(childSuites, selected, file, modeName);
471
477
  const keep = selected.has(String(suite.path ?? "")) || selectedChildren.length > 0;
472
478
  if (!keep)
@@ -506,8 +512,8 @@ function collectReadableFailures(suites, file, pathParts) {
506
512
  out.push({
507
513
  title: `${nextPath.join(" > ")}#${i + 1}`,
508
514
  where: String(test.location ?? "").length
509
- ? `${path.basename(file)}:${String(test.location ?? "")}`
510
- : path.basename(file),
515
+ ? `${formatSpecDisplayPath(file)}:${String(test.location ?? "")}`
516
+ : formatSpecDisplayPath(file),
511
517
  message: String(test.message ?? ""),
512
518
  left: JSON.stringify(test.left ?? ""),
513
519
  right: JSON.stringify(test.right ?? ""),
@@ -648,7 +654,7 @@ export async function run(flags = {}, configPath = DEFAULT_CONFIG_PATH, selector
648
654
  catch (error) {
649
655
  const modeLabel = options.modeName ?? "default";
650
656
  const details = error instanceof Error ? error.message : String(error);
651
- throw new Error(`Failed to run ${path.basename(file)} in mode ${modeLabel} with ${details}`);
657
+ throw new Error(`Failed to run ${formatSpecDisplayPath(file)} in mode ${modeLabel} with ${details}`);
652
658
  }
653
659
  const normalized = normalizeReport(report);
654
660
  const selectedSuites = options.suiteSelectors?.length
@@ -665,6 +671,7 @@ export async function run(flags = {}, configPath = DEFAULT_CONFIG_PATH, selector
665
671
  suites: selectedSuites,
666
672
  coverage: normalized.coverage,
667
673
  runCommand: runCommandForLog,
674
+ buildCommand: options.buildCommandsByFile?.[file] ?? options.buildCommand ?? "",
668
675
  snapshotSummary: {
669
676
  matched: snapshotStore.matched,
670
677
  created: snapshotStore.created,
@@ -712,6 +719,8 @@ export async function run(flags = {}, configPath = DEFAULT_CONFIG_PATH, selector
712
719
  clean: cleanOutput,
713
720
  snapshotEnabled,
714
721
  showCoverage,
722
+ showCoverageAll: Boolean(flags.showCoverageAll),
723
+ verbose: Boolean(flags.verbose),
715
724
  buildTime,
716
725
  snapshotSummary,
717
726
  coverageSummary,
@@ -1197,6 +1206,10 @@ function normalizeCoverage(value) {
1197
1206
  column: Number(p.column ?? 0),
1198
1207
  type: String(p.type ?? ""),
1199
1208
  executed: Boolean(p.executed),
1209
+ parentHash: String(p.parentHash ?? ""),
1210
+ scopeKind: String(p.scopeKind ?? ""),
1211
+ scopeName: String(p.scopeName ?? ""),
1212
+ depth: Number(p.depth ?? 0),
1200
1213
  };
1201
1214
  })
1202
1215
  .filter((point) => point.file.length > 0);
@@ -1468,10 +1481,14 @@ function matchesCoverageTextPattern(value, pattern) {
1468
1481
  return globPatternToRegExp(normalized).test(value);
1469
1482
  }
1470
1483
  function compareCoveragePoints(a, b) {
1484
+ const depthA = a.depth ?? 0;
1485
+ const depthB = b.depth ?? 0;
1471
1486
  if (a.line !== b.line)
1472
1487
  return a.line - b.line;
1473
1488
  if (a.column !== b.column)
1474
1489
  return a.column - b.column;
1490
+ if (depthA !== depthB)
1491
+ return depthA - depthB;
1475
1492
  if (a.type !== b.type)
1476
1493
  return a.type.localeCompare(b.type);
1477
1494
  return a.hash.localeCompare(b.hash);
@@ -1492,7 +1509,7 @@ async function runProcess(invocation, specFile, crashDir, modeName, snapshots, s
1492
1509
  const runtimeEvents = {
1493
1510
  sawFileStart: false,
1494
1511
  sawFileEnd: false,
1495
- fileName: path.basename(specFile),
1512
+ fileName: formatSpecDisplayPath(specFile),
1496
1513
  fileVerdict: "none",
1497
1514
  fileTime: "",
1498
1515
  suiteStarts: 0,
@@ -1667,7 +1684,8 @@ async function runProcess(invocation, specFile, crashDir, modeName, snapshots, s
1667
1684
  const code = await new Promise((resolve) => {
1668
1685
  child.on("close", (exitCode) => resolve(exitCode ?? 1));
1669
1686
  });
1670
- if (stderrPendingLine.length && !shouldSuppressWasiWarningLine(stderrPendingLine)) {
1687
+ if (stderrPendingLine.length &&
1688
+ !shouldSuppressWasiWarningLine(stderrPendingLine)) {
1671
1689
  stderrBuffer += stderrPendingLine;
1672
1690
  }
1673
1691
  const processSpawnError = spawnError;
@@ -1686,8 +1704,7 @@ async function runProcess(invocation, specFile, crashDir, modeName, snapshots, s
1686
1704
  if (reportStream.sawChunkStart) {
1687
1705
  if (!reportStream.sawChunkEnd) {
1688
1706
  parseError =
1689
- parseError ??
1690
- "missing report:end marker for chunked report payload";
1707
+ parseError ?? "missing report:end marker for chunked report payload";
1691
1708
  }
1692
1709
  else {
1693
1710
  const chunkedPayload = reportStream.chunks.join("");
@@ -1806,7 +1823,7 @@ async function runWebSessionProcess(session, specFile, crashDir, modeName, snaps
1806
1823
  const runtimeEvents = {
1807
1824
  sawFileStart: false,
1808
1825
  sawFileEnd: false,
1809
- fileName: path.basename(specFile),
1826
+ fileName: formatSpecDisplayPath(specFile),
1810
1827
  fileVerdict: "none",
1811
1828
  fileTime: "",
1812
1829
  suiteStarts: 0,
@@ -1968,7 +1985,7 @@ async function runWebSessionProcess(session, specFile, crashDir, modeName, snaps
1968
1985
  });
1969
1986
  let code = 0;
1970
1987
  try {
1971
- await session.runJob(Object.fromEntries(Object.entries(env).filter((entry) => typeof entry[1] == "string")), path.basename(specFile), (frame) => {
1988
+ await session.runJob(Object.fromEntries(Object.entries(env).filter((entry) => typeof entry[1] == "string")), formatSpecDisplayPath(specFile), (frame) => {
1972
1989
  input.write(frame);
1973
1990
  });
1974
1991
  }
@@ -1976,8 +1993,9 @@ async function runWebSessionProcess(session, specFile, crashDir, modeName, snaps
1976
1993
  code = 1;
1977
1994
  await session.close(error instanceof Error ? error : new Error(String(error)));
1978
1995
  stderrBuffer +=
1979
- (error instanceof Error ? error.stack ?? error.message : String(error)) +
1980
- "\n";
1996
+ (error instanceof Error
1997
+ ? (error.stack ?? error.message)
1998
+ : String(error)) + "\n";
1981
1999
  }
1982
2000
  finally {
1983
2001
  input.end();
@@ -1986,8 +2004,7 @@ async function runWebSessionProcess(session, specFile, crashDir, modeName, snaps
1986
2004
  if (reportStream.sawChunkStart) {
1987
2005
  if (!reportStream.sawChunkEnd) {
1988
2006
  parseError =
1989
- parseError ??
1990
- "missing report:end marker for chunked report payload";
2007
+ parseError ?? "missing report:end marker for chunked report payload";
1991
2008
  }
1992
2009
  else {
1993
2010
  const chunkedPayload = reportStream.chunks.join("");
@@ -2091,7 +2108,7 @@ function synthesizeReportFromRuntimeEvents(specFile, runtimeEvents) {
2091
2108
  suites: [
2092
2109
  {
2093
2110
  file: specFile,
2094
- description: runtimeEvents.fileName || path.basename(specFile),
2111
+ description: runtimeEvents.fileName || formatSpecDisplayPath(specFile),
2095
2112
  depth: 0,
2096
2113
  kind: "file",
2097
2114
  verdict,
@@ -2136,7 +2153,7 @@ function appendRuntimeFailureReport(report, specFile, modeName, title, details,
2136
2153
  const suites = Array.isArray(report?.suites) ? report.suites : [];
2137
2154
  suites.push({
2138
2155
  file: specFile,
2139
- description: path.basename(specFile),
2156
+ description: formatSpecDisplayPath(specFile),
2140
2157
  depth: 0,
2141
2158
  kind: "runtime-error",
2142
2159
  verdict: "fail",
@@ -2221,9 +2238,11 @@ function readFileReport(stats, fileReport) {
2221
2238
  : [];
2222
2239
  const file = String(fileReportAny.file ?? "");
2223
2240
  const modeName = String(fileReportAny.modeName ?? "");
2241
+ const runCommand = String(fileReportAny.runCommand ?? "");
2242
+ const buildCommand = String(fileReportAny.buildCommand ?? "");
2224
2243
  let fileVerdict = "none";
2225
2244
  for (const suite of suites) {
2226
- fileVerdict = mergeVerdict(fileVerdict, readSuite(stats, suite, file, modeName));
2245
+ fileVerdict = mergeVerdict(fileVerdict, readSuite(stats, suite, file, modeName, runCommand, buildCommand));
2227
2246
  }
2228
2247
  if (fileVerdict == "fail") {
2229
2248
  stats.failedFiles++;
@@ -2235,7 +2254,7 @@ function readFileReport(stats, fileReport) {
2235
2254
  stats.skippedFiles++;
2236
2255
  }
2237
2256
  }
2238
- function readSuite(stats, suite, file, modeName) {
2257
+ function readSuite(stats, suite, file, modeName, runCommand, buildCommand) {
2239
2258
  const suiteAny = suite;
2240
2259
  const kind = String(suiteAny.kind ?? "");
2241
2260
  let verdict = normalizeVerdict(suiteAny.verdict);
@@ -2247,7 +2266,7 @@ function readSuite(stats, suite, file, modeName) {
2247
2266
  ? suiteAny.suites
2248
2267
  : [];
2249
2268
  for (const subSuite of subSuites) {
2250
- verdict = mergeVerdict(verdict, readSuite(stats, subSuite, file, modeName));
2269
+ verdict = mergeVerdict(verdict, readSuite(stats, subSuite, file, modeName, runCommand, buildCommand));
2251
2270
  }
2252
2271
  const tests = Array.isArray(suiteAny.tests)
2253
2272
  ? suiteAny.tests
@@ -2285,6 +2304,8 @@ function readSuite(stats, suite, file, modeName) {
2285
2304
  ...suiteAny,
2286
2305
  file,
2287
2306
  modeName,
2307
+ runCommand,
2308
+ buildCommand,
2288
2309
  });
2289
2310
  }
2290
2311
  else if (verdict == "ok") {
@@ -4,12 +4,14 @@ export async function executeRunCommand(rawArgs, flags, configPath, selectedMode
4
4
  const suiteSelectors = deps.resolveSuiteSelectors(rawArgs, "run");
5
5
  const listFlags = deps.resolveListFlags(rawArgs, "run");
6
6
  const featureToggles = deps.resolveFeatureToggles(rawArgs, "run");
7
+ const showCoverageMode = deps.resolveShowCoverageMode(rawArgs, "run");
7
8
  const runFlags = {
8
9
  snapshot: !flags.includes("--no-snapshot"),
9
10
  createSnapshots: flags.includes("--create-snapshots"),
10
11
  overwriteSnapshots: flags.includes("--overwrite-snapshots"),
11
12
  clean: flags.includes("--clean"),
12
- showCoverage: flags.includes("--show-coverage"),
13
+ showCoverage: showCoverageMode != undefined,
14
+ showCoverageAll: showCoverageMode == "all",
13
15
  verbose: flags.includes("--verbose"),
14
16
  ...deps.resolveParallelJobs(rawArgs, "run"),
15
17
  coverage: featureToggles.coverage,
@@ -8,12 +8,14 @@ export async function executeTestCommand(rawArgs, flags, configPath, selectedMod
8
8
  tryAs: featureToggles.tryAs,
9
9
  coverage: featureToggles.coverage,
10
10
  };
11
+ const showCoverageMode = deps.resolveShowCoverageMode(rawArgs, "test");
11
12
  const runFlags = {
12
13
  snapshot: !flags.includes("--no-snapshot"),
13
14
  createSnapshots: flags.includes("--create-snapshots"),
14
15
  overwriteSnapshots: flags.includes("--overwrite-snapshots"),
15
16
  clean: flags.includes("--clean"),
16
- showCoverage: flags.includes("--show-coverage"),
17
+ showCoverage: showCoverageMode != undefined,
18
+ showCoverageAll: showCoverageMode == "all",
17
19
  verbose: flags.includes("--verbose"),
18
20
  ...deps.resolveParallelJobs(rawArgs, "test"),
19
21
  coverage: featureToggles.coverage,
@@ -1,21 +1,21 @@
1
1
  export function buildWebRunnerSource() {
2
2
  return `// Feel free to edit this file!
3
- // Runner files use the name <mode>.<type>.js, where <type> is bindings, wasi, or web.
4
- // To create a runner for another mode, copy this file to <new-mode>.<type>.js.
3
+ // Runner files use the name <mode>.<type>.js, where <type> is bindings, wasi, or web.
4
+ // To create a runner for another mode, copy this file to <new-mode>.<type>.js.
5
5
 
6
- import { instantiate } from "as-test/lib";
6
+ import { instantiate } from "as-test/lib";
7
7
 
8
- let exports = null;
9
- const imports = {};
8
+ let exports = null;
9
+ const imports = {};
10
10
 
11
- instantiate(imports)
12
- .then((instance) => {
13
- exports = instance.exports;
14
- instance.exports.start?.();
15
- // Add extra startup logic here when needed.
16
- })
17
- .catch((error) => {
18
- throw new Error("Failed to run web module: " + String(error));
19
- });
11
+ instantiate(imports)
12
+ .then((instance) => {
13
+ exports = instance.exports;
14
+ instance.exports.start?.();
15
+ // Add extra startup logic here when needed.
16
+ })
17
+ .catch((error) => {
18
+ throw new Error("Failed to run web module: " + String(error));
19
+ });
20
20
  `;
21
21
  }
@@ -488,7 +488,12 @@ function resolveHeadlessFlags(commandValue) {
488
488
  const lower = commandValue.toLowerCase();
489
489
  if (lower.includes("firefox"))
490
490
  return ["-headless"];
491
- return ["--headless=new", "--disable-gpu", "--no-first-run", "--no-default-browser-check"];
491
+ return [
492
+ "--headless=new",
493
+ "--disable-gpu",
494
+ "--no-first-run",
495
+ "--no-default-browser-check",
496
+ ];
492
497
  }
493
498
  function hasExecutable(command) {
494
499
  if (!command.length)
@@ -13,7 +13,61 @@ export function describeCoveragePoint(file, line, column, fallbackType) {
13
13
  highlightEnd: 0,
14
14
  };
15
15
  }
16
- const declaration = detectCoverageDeclaration(context.visible);
16
+ const parameter = detectCoverageParameter(context.visible, context.focus, fallbackType);
17
+ if (parameter) {
18
+ return {
19
+ displayType: parameter.type,
20
+ subjectName: parameter.name,
21
+ visible: context.visible,
22
+ focus: context.focus,
23
+ highlightStart: parameter.start,
24
+ highlightEnd: parameter.end,
25
+ };
26
+ }
27
+ const ternary = detectCoverageTernary(context.visible, context.focus, fallbackType);
28
+ if (ternary) {
29
+ return {
30
+ displayType: ternary.type,
31
+ subjectName: null,
32
+ visible: context.visible,
33
+ focus: context.focus,
34
+ highlightStart: ternary.start,
35
+ highlightEnd: ternary.end,
36
+ };
37
+ }
38
+ const ifBranch = detectCoverageIfBranch(context.visible, fallbackType);
39
+ if (ifBranch) {
40
+ return {
41
+ displayType: ifBranch.type,
42
+ subjectName: null,
43
+ visible: context.visible,
44
+ focus: context.focus,
45
+ highlightStart: ifBranch.start,
46
+ highlightEnd: ifBranch.end,
47
+ };
48
+ }
49
+ const assignment = detectCoverageAssignment(context.visible, fallbackType);
50
+ if (assignment) {
51
+ return {
52
+ displayType: assignment.type,
53
+ subjectName: null,
54
+ visible: context.visible,
55
+ focus: context.focus,
56
+ highlightStart: assignment.start,
57
+ highlightEnd: assignment.end,
58
+ };
59
+ }
60
+ const declarationAllowed = fallbackType == "Expression" ||
61
+ fallbackType == "Block" ||
62
+ fallbackType == "Function" ||
63
+ fallbackType == "Method" ||
64
+ fallbackType == "Constructor" ||
65
+ fallbackType == "Variable" ||
66
+ fallbackType == "Property" ||
67
+ fallbackType == "Call";
68
+ const declaration = declarationAllowed
69
+ ? detectCoverageDeclaration(context.visible)
70
+ : null;
17
71
  if (declaration) {
18
72
  const [highlightStart, highlightEnd] = resolveCoverageHighlightSpan(context.visible, context.focus);
19
73
  return {
@@ -25,7 +79,10 @@ export function describeCoveragePoint(file, line, column, fallbackType) {
25
79
  highlightEnd,
26
80
  };
27
81
  }
28
- const call = detectCoverageCall(context.visible, context.focus);
82
+ const callAllowed = fallbackType == "Expression" || fallbackType == "Call";
83
+ const call = callAllowed
84
+ ? detectCoverageCall(context.visible, context.focus)
85
+ : null;
29
86
  if (call) {
30
87
  return {
31
88
  displayType: "Call",
@@ -130,6 +187,147 @@ function detectCoverageDeclaration(visible) {
130
187
  }
131
188
  return null;
132
189
  }
190
+ function detectCoverageParameter(visible, focus, fallbackType) {
191
+ const inlineParameter = detectCoverageInlineParameter(visible, focus, fallbackType);
192
+ if (inlineParameter) {
193
+ return inlineParameter;
194
+ }
195
+ const openParen = visible.indexOf("(");
196
+ const closeParen = visible.lastIndexOf(")");
197
+ if (openParen == -1 || closeParen == -1 || closeParen <= openParen) {
198
+ return null;
199
+ }
200
+ if (focus <= openParen || focus >= closeParen) {
201
+ return null;
202
+ }
203
+ const params = visible.slice(openParen + 1, closeParen);
204
+ const matches = [
205
+ ...params.matchAll(/([A-Za-z_]\w*)\s*:\s*[^,)=]+(?:=\s*[^,)]*)?/g),
206
+ ];
207
+ if (!matches.length)
208
+ return null;
209
+ for (const match of matches) {
210
+ const localStart = match.index ?? -1;
211
+ if (localStart == -1)
212
+ continue;
213
+ const localEnd = localStart + match[0].length;
214
+ const absoluteStart = openParen + 1 + localStart;
215
+ const absoluteEnd = openParen + 1 + localEnd;
216
+ if (focus < absoluteStart || focus > absoluteEnd)
217
+ continue;
218
+ const name = match[1] ?? null;
219
+ if (!name)
220
+ return null;
221
+ const nameOffset = match[0].indexOf(name);
222
+ const equalsOffset = match[0].indexOf("=");
223
+ if (fallbackType == "DefaultValue" && equalsOffset != -1) {
224
+ const valueStart = absoluteStart + equalsOffset + 1;
225
+ const valueVisibleStart = skipCoverageWhitespace(visible, valueStart);
226
+ const [start, end] = resolveCoverageHighlightSpan(visible, Math.max(valueVisibleStart, focus));
227
+ return {
228
+ type: "DefaultValue",
229
+ name,
230
+ start,
231
+ end,
232
+ };
233
+ }
234
+ return {
235
+ type: fallbackType == "Parameter" ? "Parameter" : "Property",
236
+ name,
237
+ start: absoluteStart + nameOffset,
238
+ end: absoluteStart + nameOffset + name.length,
239
+ };
240
+ }
241
+ return null;
242
+ }
243
+ function detectCoverageInlineParameter(visible, focus, fallbackType) {
244
+ const match = visible.match(/^([A-Za-z_]\w*)\s*:\s*[^=,]+(?:=\s*[^,]+)?[,]?$/);
245
+ if (!match)
246
+ return null;
247
+ const name = match[1] ?? null;
248
+ if (!name)
249
+ return null;
250
+ const nameStart = visible.indexOf(name);
251
+ const nameEnd = nameStart + name.length;
252
+ const equalsIndex = visible.indexOf("=");
253
+ if (fallbackType == "DefaultValue" && equalsIndex != -1) {
254
+ const valueStart = skipCoverageWhitespace(visible, equalsIndex + 1);
255
+ const [start, end] = resolveCoverageHighlightSpan(visible, Math.max(valueStart, focus));
256
+ return {
257
+ type: "DefaultValue",
258
+ name,
259
+ start,
260
+ end,
261
+ };
262
+ }
263
+ return {
264
+ type: fallbackType == "Parameter" ? "Parameter" : "Property",
265
+ name,
266
+ start: nameStart,
267
+ end: nameEnd,
268
+ };
269
+ }
270
+ function detectCoverageTernary(visible, focus, fallbackType) {
271
+ if (fallbackType != "Ternary" && fallbackType != "LogicalBranch") {
272
+ return null;
273
+ }
274
+ const q = visible.indexOf("?");
275
+ if (q == -1)
276
+ return null;
277
+ if (fallbackType == "LogicalBranch") {
278
+ const [start, end] = resolveCoverageHighlightSpan(visible, focus);
279
+ return { type: "LogicalBranch", start, end };
280
+ }
281
+ const colon = visible.indexOf(":", q + 1);
282
+ if (colon == -1) {
283
+ const [start, end] = resolveCoverageHighlightSpan(visible, focus);
284
+ return { type: "Ternary", start, end };
285
+ }
286
+ const branchStart = focus <= colon ? q + 1 : colon + 1;
287
+ const normalizedStart = skipCoverageWhitespace(visible, branchStart);
288
+ const [start, end] = resolveCoverageHighlightSpan(visible, Math.max(normalizedStart, focus));
289
+ return { type: "Ternary", start, end };
290
+ }
291
+ function detectCoverageIfBranch(visible, fallbackType) {
292
+ if (fallbackType != "IfBranch")
293
+ return null;
294
+ const match = visible.match(/^if\s*\(([^)]*)\)/);
295
+ if (!match)
296
+ return null;
297
+ const full = match[0];
298
+ const condition = match[1] ?? "";
299
+ const openParen = full.indexOf("(");
300
+ const conditionPadding = condition.length
301
+ ? condition.length - condition.trimStart().length
302
+ : 0;
303
+ const conditionStart = openParen == -1 ? -1 : openParen + 1 + conditionPadding;
304
+ if (conditionStart == -1 || !condition.length) {
305
+ return { type: "IfBranch", start: 0, end: full.length };
306
+ }
307
+ return {
308
+ type: "IfBranch",
309
+ start: conditionStart,
310
+ end: conditionStart + condition.length,
311
+ };
312
+ }
313
+ function detectCoverageAssignment(visible, fallbackType) {
314
+ if (fallbackType != "Assignment")
315
+ return null;
316
+ const match = visible.match(/([A-Za-z_]\w*(?:\.[A-Za-z_]\w*|\[[^\]]+\])?)\s*(=|\+=|-=|\*=|\*\*=|\/=|%=|<<=|>>=|>>>=|&=|\|=|\^=)/);
317
+ if (!match)
318
+ return null;
319
+ const full = match[0];
320
+ const lhs = match[1] ?? "";
321
+ const operator = match[2] ?? "=";
322
+ const fullStart = visible.indexOf(full);
323
+ const lhsStart = fullStart + full.indexOf(lhs);
324
+ const operatorStart = fullStart + full.lastIndexOf(operator);
325
+ return {
326
+ type: "Assignment",
327
+ start: lhsStart,
328
+ end: operatorStart + operator.length,
329
+ };
330
+ }
133
331
  function detectCoverageCall(visible, focus) {
134
332
  const matches = [...visible.matchAll(/\b([A-Za-z_]\w*)(?:<[^>()]+>)?\s*\(/g)];
135
333
  if (!matches.length)
@@ -171,3 +369,10 @@ function detectCoverageCall(visible, focus) {
171
369
  function isCoverageBoundary(ch) {
172
370
  return /[\s()[\]{}.,;:+\-*/%&|^!?=<>]/.test(ch);
173
371
  }
372
+ function skipCoverageWhitespace(visible, index) {
373
+ let current = Math.max(0, Math.min(visible.length - 1, index));
374
+ while (current < visible.length - 1 && /\s/.test(visible.charAt(current))) {
375
+ current++;
376
+ }
377
+ return current;
378
+ }
@@ -1,7 +1,9 @@
1
1
  import { mkdirSync, writeFileSync } from "fs";
2
2
  import * as path from "path";
3
3
  export function persistCrashRecord(rootDir, record) {
4
- const entry = record.entryKey?.length ? record.entryKey : crashEntryKey(record.file);
4
+ const entry = record.entryKey?.length
5
+ ? record.entryKey
6
+ : crashEntryKey(record.file);
5
7
  const dir = path.resolve(process.cwd(), rootDir);
6
8
  mkdirSync(dir, { recursive: true });
7
9
  const jsonPath = path.join(dir, `${entry}.json`);