@hughescr/stryker-bun-runner 1.2.0 → 1.2.1

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/dist/index.js CHANGED
@@ -4603,6 +4603,10 @@ var Scope;
4603
4603
  Scope2["Transient"] = "transient";
4604
4604
  Scope2["Singleton"] = "singleton";
4605
4605
  })(Scope || (Scope = {}));
4606
+ // src/bun-test-runner.ts
4607
+ import * as fsPromises2 from "node:fs/promises";
4608
+ import { tmpdir } from "node:os";
4609
+ import path4 from "node:path";
4606
4610
  // node_modules/@stryker-mutator/api/dist/src/test-runner/test-status.js
4607
4611
  var TestStatus;
4608
4612
  (function(TestStatus2) {
@@ -4625,267 +4629,9 @@ var DryRunStatus;
4625
4629
  DryRunStatus2["Error"] = "error";
4626
4630
  DryRunStatus2["Timeout"] = "timeout";
4627
4631
  })(DryRunStatus || (DryRunStatus = {}));
4628
- // src/process-runner.ts
4629
- import { spawn } from "node:child_process";
4630
- async function runBunTests(options) {
4631
- const args = ["test"];
4632
- if (options.inspectWaitPort) {
4633
- args.push(`--inspect=${options.inspectWaitPort}`);
4634
- }
4635
- if (options.bunfigPath) {
4636
- args.push(`--config=${options.bunfigPath}`);
4637
- }
4638
- if (options.preloadScript) {
4639
- args.push("--preload", options.preloadScript);
4640
- }
4641
- if (options.testNamePattern) {
4642
- args.push("--test-name-pattern", options.testNamePattern);
4643
- }
4644
- if (options.bail) {
4645
- args.push("--bail");
4646
- }
4647
- if (options.sequentialMode) {
4648
- args.push("--concurrency=1");
4649
- }
4650
- if (options.bunArgs && options.bunArgs.length > 0) {
4651
- args.push(...options.bunArgs);
4652
- }
4653
- if (options.testFiles && options.testFiles.length > 0) {
4654
- args.push(...options.testFiles);
4655
- }
4656
- const env = {
4657
- ...process.env,
4658
- ...options.env
4659
- };
4660
- if (options.activeMutant) {
4661
- env.__STRYKER_ACTIVE_MUTANT__ = options.activeMutant;
4662
- }
4663
- if (options.coverageFile) {
4664
- env.__STRYKER_COVERAGE_FILE__ = options.coverageFile;
4665
- }
4666
- if (options.syncPort) {
4667
- env.__STRYKER_SYNC_PORT__ = String(options.syncPort);
4668
- }
4669
- return new Promise((resolve) => {
4670
- const stdoutChunks = [];
4671
- const stderrChunks = [];
4672
- let timedOut = false;
4673
- let processKilled = false;
4674
- const childProcess = spawn(options.bunPath, args, {
4675
- env,
4676
- stdio: ["ignore", "pipe", "pipe"],
4677
- cwd: process.cwd()
4678
- });
4679
- const timeoutHandle = setTimeout(() => {
4680
- timedOut = true;
4681
- processKilled = true;
4682
- childProcess.kill("SIGKILL");
4683
- }, options.timeout);
4684
- if (childProcess.stdout) {
4685
- childProcess.stdout.on("data", (data) => {
4686
- stdoutChunks.push(data);
4687
- });
4688
- }
4689
- let inspectorUrlExtracted = false;
4690
- if (childProcess.stderr) {
4691
- childProcess.stderr.on("data", (data) => {
4692
- stderrChunks.push(data);
4693
- if (options.inspectWaitPort && !inspectorUrlExtracted && options.onInspectorReady) {
4694
- const text = Buffer.concat(stderrChunks).toString();
4695
- const match = /Listening:[\t\v\f\r \xa0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]*\n\s*(ws:\/\/\S+)/.exec(text);
4696
- if (match) {
4697
- inspectorUrlExtracted = true;
4698
- options.onInspectorReady(match[1]);
4699
- }
4700
- }
4701
- });
4702
- }
4703
- childProcess.on("close", (code) => {
4704
- clearTimeout(timeoutHandle);
4705
- resolve({
4706
- stdout: Buffer.concat(stdoutChunks).toString(),
4707
- stderr: Buffer.concat(stderrChunks).toString(),
4708
- exitCode: processKilled ? null : code,
4709
- timedOut
4710
- });
4711
- });
4712
- childProcess.on("error", (error) => {
4713
- clearTimeout(timeoutHandle);
4714
- const stderrOutput = Buffer.concat(stderrChunks).toString();
4715
- resolve({
4716
- stdout: Buffer.concat(stdoutChunks).toString(),
4717
- stderr: `${stderrOutput}
4718
- Process error: ${error.message}`,
4719
- exitCode: null,
4720
- timedOut
4721
- });
4722
- });
4723
- });
4724
- }
4725
-
4726
- // src/parsers/console-parser.ts
4727
- function parseFilePath(line) {
4728
- const fileMatch = /^([\w./-]+\.(?:test|spec)\.(?:ts|tsx|js|jsx|mts|mjs)):$/.exec(line);
4729
- return fileMatch ? fileMatch[1] : null;
4730
- }
4731
- function parseTestLine(line, currentFile) {
4732
- const passMatch = /^✓ +(\S.*?) \[([0-9.]+)ms\]$/.exec(line);
4733
- if (passMatch) {
4734
- const testName = passMatch[1].trim();
4735
- const fullName = currentFile ? `${currentFile} > ${testName}` : testName;
4736
- return {
4737
- test: {
4738
- name: fullName,
4739
- file: currentFile,
4740
- status: "passed",
4741
- duration: parseFloat(passMatch[2])
4742
- }
4743
- };
4744
- }
4745
- const failMatch = /^✗ +(\S.*?)(?: \[([0-9.]+)ms\])?$/.exec(line);
4746
- if (failMatch) {
4747
- const testName = failMatch[1].trim();
4748
- const fullName = currentFile ? `${currentFile} > ${testName}` : testName;
4749
- return {
4750
- test: {
4751
- name: fullName,
4752
- file: currentFile,
4753
- status: "failed",
4754
- duration: failMatch[2] ? parseFloat(failMatch[2]) : undefined
4755
- },
4756
- startedCollectingError: true
4757
- };
4758
- }
4759
- const bailFailMatch = /^\(fail\) +(\S.*?)(?: \[([0-9.]+)ms\])?$/.exec(line);
4760
- if (bailFailMatch) {
4761
- const testName = bailFailMatch[1].trim();
4762
- const fullName = currentFile ? `${currentFile} > ${testName}` : testName;
4763
- return {
4764
- test: {
4765
- name: fullName,
4766
- file: currentFile,
4767
- status: "failed",
4768
- duration: bailFailMatch[2] ? parseFloat(bailFailMatch[2]) : undefined
4769
- },
4770
- startedCollectingError: true
4771
- };
4772
- }
4773
- const skipMatch = /^⏭ +(\S.*)$/.exec(line);
4774
- if (skipMatch) {
4775
- const testName = skipMatch[1].trim();
4776
- const fullName = currentFile ? `${currentFile} > ${testName}` : testName;
4777
- return {
4778
- test: {
4779
- name: fullName,
4780
- file: currentFile,
4781
- status: "skipped"
4782
- }
4783
- };
4784
- }
4785
- return {};
4786
- }
4787
- function finalizeErrorMessage(currentTest, errorLines) {
4788
- if (currentTest && errorLines.length > 0) {
4789
- currentTest.failureMessage = errorLines.join(`
4790
- `).trim();
4791
- }
4792
- }
4793
- function shouldCollectErrorLine(line) {
4794
- if (!line.trim()) {
4795
- return false;
4796
- }
4797
- return !/^\s*\d+\s+(?:pass|fail|skip)/.exec(line);
4798
- }
4799
- function updateCounters(test, counters, parseResult) {
4800
- if (test.status === "passed") {
4801
- counters.passed++;
4802
- return false;
4803
- } else if (test.status === "failed") {
4804
- counters.failed++;
4805
- return parseResult.startedCollectingError ?? false;
4806
- } else {
4807
- counters.skipped++;
4808
- return false;
4809
- }
4810
- }
4811
- function parseSummaryLines(output) {
4812
- const counts = { passed: 0, failed: 0, skipped: 0 };
4813
- const passSummary = /\s(\d+)\s+pass\b/.exec(output);
4814
- const failSummary = /\s(\d+)\s+fail\b/.exec(output);
4815
- const skipSummary = /\s(\d+)\s+skip\b/.exec(output);
4816
- const bailSummary = /Bailed out after (\d+) failures?/.exec(output);
4817
- if (passSummary) {
4818
- counts.passed = parseInt(passSummary[1], 10);
4819
- }
4820
- if (failSummary) {
4821
- counts.failed = parseInt(failSummary[1], 10);
4822
- }
4823
- if (skipSummary) {
4824
- counts.skipped = parseInt(skipSummary[1], 10);
4825
- }
4826
- if (bailSummary) {
4827
- counts.failed = Math.max(counts.failed, parseInt(bailSummary[1], 10));
4828
- }
4829
- const ranTestsSummary = /Ran\s+(\d+)\s+tests?/.exec(output);
4830
- if (ranTestsSummary) {
4831
- const totalFromRan = parseInt(ranTestsSummary[1], 10);
4832
- const totalParsed = counts.passed + counts.failed + counts.skipped;
4833
- if (totalParsed !== totalFromRan && counts.passed === 0 && counts.failed === 0) {
4834
- counts.passed = totalFromRan;
4835
- }
4836
- }
4837
- return counts;
4838
- }
4839
- function parseBunTestOutput(stdout, stderr) {
4840
- const tests = [];
4841
- const counters = { passed: 0, failed: 0, skipped: 0 };
4842
- const output = stdout + `
4843
- ` + stderr;
4844
- const lines = output.split(`
4845
- `);
4846
- let currentTest = null;
4847
- let collectingError = false;
4848
- let errorLines = [];
4849
- let currentFile;
4850
- for (const line of lines) {
4851
- const filePath = parseFilePath(line);
4852
- if (filePath) {
4853
- currentFile = filePath;
4854
- continue;
4855
- }
4856
- const parseResult = parseTestLine(line, currentFile);
4857
- if (parseResult.test) {
4858
- if (currentTest && collectingError) {
4859
- finalizeErrorMessage(currentTest, errorLines);
4860
- errorLines = [];
4861
- }
4862
- currentTest = parseResult.test;
4863
- tests.push(currentTest);
4864
- collectingError = updateCounters(currentTest, counters, parseResult);
4865
- continue;
4866
- }
4867
- if (collectingError && currentTest && shouldCollectErrorLine(line)) {
4868
- errorLines.push(line);
4869
- }
4870
- }
4871
- if (currentTest && collectingError) {
4872
- finalizeErrorMessage(currentTest, errorLines);
4873
- }
4874
- const summaryCounts = parseSummaryLines(output);
4875
- counters.passed = Math.max(counters.passed, summaryCounts.passed);
4876
- counters.failed = Math.max(counters.failed, summaryCounts.failed);
4877
- counters.skipped = Math.max(counters.skipped, summaryCounts.skipped);
4878
- return {
4879
- tests,
4880
- totalTests: counters.passed + counters.failed + counters.skipped,
4881
- passed: counters.passed,
4882
- failed: counters.failed,
4883
- skipped: counters.skipped
4884
- };
4885
- }
4886
4632
  // src/coverage/preload-generator.ts
4887
4633
  import { mkdir, unlink, readFile, writeFile } from "node:fs/promises";
4888
- import { join, dirname as dirname2, resolve as resolve3 } from "node:path";
4634
+ import path from "node:path";
4889
4635
  import { fileURLToPath as fileURLToPath2 } from "node:url";
4890
4636
 
4891
4637
  // node_modules/tinyglobby/dist/index.mjs
@@ -5722,7 +5468,7 @@ async function resolveEagerModulesFromGlobs(mutateGlobs, cwd = process.cwd()) {
5722
5468
  if (p.startsWith("!")) {
5723
5469
  negativePatterns.push(p.slice(1));
5724
5470
  } else {
5725
- positivePatterns.push(p.replace(/:[0-9].*$/, ""));
5471
+ positivePatterns.push(p.replace(/:\d.*$/, ""));
5726
5472
  }
5727
5473
  }
5728
5474
  if (positivePatterns.length === 0) {
@@ -5737,21 +5483,21 @@ async function resolveEagerModulesFromGlobs(mutateGlobs, cwd = process.cwd()) {
5737
5483
  const sourceFileRe = /\.(?:tsx?|[cm]?js)$/;
5738
5484
  const dtsRe = /\.d\.[cm]?ts$/;
5739
5485
  const filtered = paths.filter((p) => sourceFileRe.test(p) && !dtsRe.test(p));
5740
- const resolved = filtered.map((p) => resolve3(p));
5741
- resolved.sort();
5486
+ const resolved = filtered.map((p) => path.resolve(p));
5487
+ resolved.sort((a, b) => a.localeCompare(b));
5742
5488
  return resolved;
5743
5489
  }
5744
5490
  async function generatePreloadScript(options) {
5745
- const preloadPath = join(options.tempDir, `stryker-coverage-preload-${process.pid}.ts`);
5491
+ const preloadPath = path.join(options.tempDir, `stryker-coverage-preload-${process.pid}.ts`);
5746
5492
  await mkdir(options.tempDir, { recursive: true });
5747
- const __dirname2 = dirname2(fileURLToPath2(import.meta.url));
5493
+ const __dirname2 = path.dirname(fileURLToPath2(import.meta.url));
5748
5494
  const isBundled = __dirname2.endsWith("dist") || __dirname2.includes("dist/");
5749
- const templatePath = isBundled ? join(__dirname2, "templates/coverage-preload.ts") : join(__dirname2, "../templates/coverage-preload.ts");
5750
- const template = await readFile(templatePath, "utf-8");
5751
- const preloadLogicPath = isBundled ? join(__dirname2, "coverage/preload-logic.js") : join(__dirname2, "preload-logic.ts");
5495
+ const templatePath = isBundled ? path.join(__dirname2, "templates/coverage-preload.ts") : path.join(__dirname2, "../templates/coverage-preload.ts");
5496
+ const template = await readFile(templatePath, "utf8");
5497
+ const preloadLogicPath = isBundled ? path.join(__dirname2, "coverage/preload-logic.js") : path.join(__dirname2, "preload-logic.ts");
5752
5498
  const eagerModules = options.eagerModules ?? [];
5753
5499
  const content = template.replace("__PRELOAD_LOGIC_PATH__", preloadLogicPath).replace("__EAGER_MODULES__", JSON.stringify(eagerModules));
5754
- await writeFile(preloadPath, content, "utf-8");
5500
+ await writeFile(preloadPath, content, "utf8");
5755
5501
  return preloadPath;
5756
5502
  }
5757
5503
  async function cleanupPreloadScript(preloadPath) {
@@ -5759,6 +5505,7 @@ async function cleanupPreloadScript(preloadPath) {
5759
5505
  await unlink(preloadPath);
5760
5506
  } catch {}
5761
5507
  }
5508
+
5762
5509
  // src/coverage/collector.ts
5763
5510
  import { readFile as readFile2, unlink as unlink2 } from "node:fs/promises";
5764
5511
  function arrayToCoverageData(mutantIds) {
@@ -5776,26 +5523,26 @@ function mergeCoverageData(dataList) {
5776
5523
  const staticSet = new Set;
5777
5524
  for (const data of dataList) {
5778
5525
  for (const [testId, mutantIds] of Object.entries(data.perTest)) {
5779
- if (!merged.perTest[testId]) {
5780
- merged.perTest[testId] = mutantIds;
5781
- } else {
5526
+ if (testId in merged.perTest) {
5782
5527
  const existingSet = new Set(merged.perTest[testId]);
5783
5528
  for (const mutantId of mutantIds) {
5784
5529
  existingSet.add(mutantId);
5785
5530
  }
5786
- merged.perTest[testId] = Array.from(existingSet);
5531
+ merged.perTest[testId] = [...existingSet];
5532
+ } else {
5533
+ merged.perTest[testId] = mutantIds;
5787
5534
  }
5788
5535
  }
5789
5536
  for (const mutantId of data.static) {
5790
5537
  staticSet.add(mutantId);
5791
5538
  }
5792
5539
  }
5793
- merged.static = Array.from(staticSet);
5540
+ merged.static = [...staticSet];
5794
5541
  return merged;
5795
5542
  }
5796
5543
  async function collectCoverage(coverageFile, logger) {
5797
5544
  try {
5798
- const content = await readFile2(coverageFile, "utf-8");
5545
+ const content = await readFile2(coverageFile, "utf8");
5799
5546
  const trimmed = content.trim();
5800
5547
  const lines = trimmed.split(`
5801
5548
  `).filter((line) => line.length > 0);
@@ -5831,6 +5578,7 @@ async function cleanupCoverageFile(coverageFile) {
5831
5578
  await unlink2(coverageFile);
5832
5579
  } catch {}
5833
5580
  }
5581
+
5834
5582
  // src/utils/test-name.ts
5835
5583
  function normalizeTestFilePath(url) {
5836
5584
  if (!url) {
@@ -5843,7 +5591,7 @@ function normalizeTestFilePath(url) {
5843
5591
  return url;
5844
5592
  }
5845
5593
  function normalizeTestName(testName) {
5846
- return testName.replace(/[\x00-\x1F\x7F]/g, "_").trim();
5594
+ return testName.replaceAll(/\p{Cc}/gu, "_").trim();
5847
5595
  }
5848
5596
  function buildUniqueTestName(fullName, url) {
5849
5597
  const normalizedPath = normalizeTestFilePath(url);
@@ -5856,46 +5604,36 @@ function buildUniqueTestName(fullName, url) {
5856
5604
  // src/coverage/coverage-mapper.ts
5857
5605
  function mapCoverageToInspectorIds(rawCoverage, executionOrder, testHierarchy, logger) {
5858
5606
  if (!rawCoverage?.perTest || Object.keys(rawCoverage.perTest).length === 0) {
5859
- return rawCoverage;
5607
+ return rawCoverage ?? undefined;
5860
5608
  }
5861
5609
  const firstKey = Object.keys(rawCoverage.perTest)[0];
5862
- if (/@@test-\d+$/.exec(firstKey)) {
5610
+ if (/@@test-\d+$/.test(firstKey)) {
5863
5611
  return mapFilePrefixedCounterKeys(rawCoverage, executionOrder, testHierarchy, logger);
5864
5612
  }
5865
- if (/^test-\d+$/.exec(firstKey)) {
5613
+ if (/^test-\d+$/.test(firstKey)) {
5866
5614
  return mapLegacyCounterKeys(rawCoverage, executionOrder, testHierarchy, logger);
5867
5615
  }
5868
5616
  return rawCoverage;
5869
5617
  }
5870
- function stabilizeCoverage(coverage) {
5871
- const perTestEntries = Object.entries(coverage.perTest ?? {});
5872
- if (perTestEntries.length === 0) {
5873
- return coverage;
5874
- }
5875
- const perTestAppearances = new Map;
5618
+ function countPerTestAppearances(perTestEntries) {
5619
+ const appearances = new Map;
5876
5620
  for (const [, counts] of perTestEntries) {
5877
5621
  for (const mutantId of Object.keys(counts)) {
5878
- perTestAppearances.set(mutantId, (perTestAppearances.get(mutantId) ?? 0) + 1);
5622
+ appearances.set(mutantId, (appearances.get(mutantId) ?? 0) + 1);
5879
5623
  }
5880
5624
  }
5881
- const promoteToStatic = new Set(Object.keys(coverage.static ?? {}));
5625
+ return appearances;
5626
+ }
5627
+ function buildPromoteToStaticSet(existingStaticKeys, perTestAppearances) {
5628
+ const promoteToStatic = new Set(existingStaticKeys);
5882
5629
  for (const [mutantId, count] of perTestAppearances) {
5883
5630
  if (count > 1) {
5884
5631
  promoteToStatic.add(mutantId);
5885
5632
  }
5886
5633
  }
5887
- const existingStatic = new Set(Object.keys(coverage.static ?? {}));
5888
- const hasNewPromotions = [...promoteToStatic].some((id) => !existingStatic.has(id));
5889
- const hasPerTestContamination = perTestEntries.some(([, counts]) => Object.keys(counts).some((id) => promoteToStatic.has(id)));
5890
- if (!hasNewPromotions && !hasPerTestContamination) {
5891
- return coverage;
5892
- }
5893
- const newStatic = { ...coverage.static ?? {} };
5894
- for (const mutantId of promoteToStatic) {
5895
- if (!(mutantId in newStatic)) {
5896
- newStatic[mutantId] = 1;
5897
- }
5898
- }
5634
+ return promoteToStatic;
5635
+ }
5636
+ function buildFilteredPerTest(perTestEntries, promoteToStatic) {
5899
5637
  const newPerTest = {};
5900
5638
  for (const [testId, counts] of perTestEntries) {
5901
5639
  const filteredCounts = {};
@@ -5908,9 +5646,32 @@ function stabilizeCoverage(coverage) {
5908
5646
  newPerTest[testId] = filteredCounts;
5909
5647
  }
5910
5648
  }
5649
+ return newPerTest;
5650
+ }
5651
+ function stabilizeCoverage(coverage) {
5652
+ const perTestEntries = Object.entries(coverage.perTest);
5653
+ if (perTestEntries.length === 0) {
5654
+ return coverage;
5655
+ }
5656
+ const existingStaticKeys = Object.keys(coverage.static);
5657
+ const perTestAppearances = countPerTestAppearances(perTestEntries);
5658
+ const promoteToStatic = buildPromoteToStaticSet(existingStaticKeys, perTestAppearances);
5659
+ const existingStatic = new Set(existingStaticKeys);
5660
+ const hasNewPromotions = [...promoteToStatic].some((id) => !existingStatic.has(id));
5661
+ const hasPerTestContamination = perTestEntries.some(([, counts]) => Object.keys(counts).some((id) => promoteToStatic.has(id)));
5662
+ if (!hasNewPromotions && !hasPerTestContamination) {
5663
+ return coverage;
5664
+ }
5665
+ const newStatic = { ...coverage.static };
5666
+ for (const mutantId of promoteToStatic) {
5667
+ if (!(mutantId in newStatic)) {
5668
+ newStatic[mutantId] = 1;
5669
+ }
5670
+ }
5671
+ const newPerTest = buildFilteredPerTest(perTestEntries, promoteToStatic);
5911
5672
  return { static: newStatic, perTest: newPerTest };
5912
5673
  }
5913
- function mapFilePrefixedCounterKeys(rawCoverage, executionOrder, testHierarchy, logger) {
5674
+ function buildFileToInspectorIds(executionOrder, testHierarchy) {
5914
5675
  const fileToInspectorIds = new Map;
5915
5676
  for (const inspectorId of executionOrder) {
5916
5677
  const testInfo = testHierarchy.get(inspectorId);
@@ -5925,43 +5686,36 @@ function mapFilePrefixedCounterKeys(rawCoverage, executionOrder, testHierarchy,
5925
5686
  fileToInspectorIds.set(relFile, [inspectorId]);
5926
5687
  }
5927
5688
  }
5928
- const counterIds = Object.keys(rawCoverage.perTest).sort((a, b) => {
5929
- const nA = parseInt(a.split("@@test-")[1] ?? "0", 10);
5930
- const nB = parseInt(b.split("@@test-")[1] ?? "0", 10);
5931
- return nA - nB;
5932
- });
5933
- const testNames = [];
5934
- const resolvedInfos = [];
5935
- for (const key of counterIds) {
5689
+ return fileToInspectorIds;
5690
+ }
5691
+ function resolveCounterKeys(counterIds, fileToInspectorIds, testHierarchy, logger) {
5692
+ return counterIds.map((key) => {
5936
5693
  const sepIdx = key.indexOf("@@");
5937
5694
  const filePrefix = key.slice(0, sepIdx);
5938
5695
  const counterStr = key.slice(sepIdx + 2 + "test-".length);
5939
- const n = parseInt(counterStr, 10);
5696
+ const n = Number.parseInt(counterStr, 10);
5940
5697
  const fileIds = fileToInspectorIds.get(filePrefix);
5941
5698
  const inspectorId = fileIds?.[n - 1];
5942
- const testInfo = inspectorId !== undefined ? testHierarchy.get(inspectorId) : undefined;
5699
+ const testInfo = inspectorId === undefined ? undefined : testHierarchy.get(inspectorId);
5943
5700
  if (testInfo) {
5944
- testNames.push(buildUniqueTestName(testInfo.fullName, testInfo.url));
5945
- resolvedInfos.push(testInfo);
5946
- } else {
5947
- logger?.warn('Coverage key %s: no inspector test found for file "%s" at position %s ' + "(file has %s tests in execution order). Skipping this test in coverage mapping.", key, filePrefix, n, fileToInspectorIds.get(filePrefix)?.length ?? 0);
5948
- testNames.push(`unknown-${key}`);
5949
- resolvedInfos.push(null);
5701
+ return { name: buildUniqueTestName(testInfo.fullName, testInfo.url), testInfo };
5950
5702
  }
5951
- }
5703
+ logger?.warn('Coverage key %s: no inspector test found for file "%s" at position %s ' + "(file has %s tests in execution order). Skipping this test in coverage mapping.", key, filePrefix, n, fileToInspectorIds.get(filePrefix)?.length ?? 0);
5704
+ return { name: `unknown-${key}`, testInfo: null };
5705
+ });
5706
+ }
5707
+ function buildRemappedPerTest(counterIds, resolved, rawPerTest) {
5952
5708
  const nameCounts = new Map;
5953
- for (const name of testNames) {
5709
+ for (const { name } of resolved) {
5954
5710
  nameCounts.set(name, (nameCounts.get(name) ?? 0) + 1);
5955
5711
  }
5956
5712
  const remappedPerTest = {};
5957
5713
  const nameIndexes = new Map;
5958
- for (let i = 0;i < counterIds.length; i++) {
5959
- const key = counterIds[i];
5960
- const testInfo = resolvedInfos[i];
5714
+ for (const [i, key] of counterIds.entries()) {
5715
+ const { name: baseName, testInfo } = resolved[i];
5961
5716
  if (!testInfo) {
5962
5717
  continue;
5963
5718
  }
5964
- const baseName = testNames[i];
5965
5719
  const count = nameCounts.get(baseName) ?? 1;
5966
5720
  let finalName = baseName;
5967
5721
  if (count > 1) {
@@ -5969,15 +5723,26 @@ function mapFilePrefixedCounterKeys(rawCoverage, executionOrder, testHierarchy,
5969
5723
  finalName = `${baseName} [${index}]`;
5970
5724
  nameIndexes.set(baseName, index + 1);
5971
5725
  }
5972
- remappedPerTest[finalName] = rawCoverage.perTest[key];
5726
+ remappedPerTest[finalName] = rawPerTest[key];
5973
5727
  }
5728
+ return remappedPerTest;
5729
+ }
5730
+ function mapFilePrefixedCounterKeys(rawCoverage, executionOrder, testHierarchy, logger) {
5731
+ const fileToInspectorIds = buildFileToInspectorIds(executionOrder, testHierarchy);
5732
+ const counterIds = Object.keys(rawCoverage.perTest).toSorted((a, b) => {
5733
+ const nA = Number.parseInt(a.split("@@test-")[1] ?? "0", 10);
5734
+ const nB = Number.parseInt(b.split("@@test-")[1] ?? "0", 10);
5735
+ return nA - nB;
5736
+ });
5737
+ const resolved = resolveCounterKeys(counterIds, fileToInspectorIds, testHierarchy, logger);
5738
+ const remappedPerTest = buildRemappedPerTest(counterIds, resolved, rawCoverage.perTest);
5974
5739
  return stabilizeCoverage({
5975
5740
  static: rawCoverage.static,
5976
5741
  perTest: remappedPerTest
5977
5742
  });
5978
5743
  }
5979
5744
  function mapLegacyCounterKeys(rawCoverage, executionOrder, testHierarchy, logger) {
5980
- const counterIds = Object.keys(rawCoverage.perTest).sort((a, b) => parseInt(a.split("-")[1], 10) - parseInt(b.split("-")[1], 10));
5745
+ const counterIds = Object.keys(rawCoverage.perTest).toSorted((a, b) => Number.parseInt(a.split("-")[1] ?? "0", 10) - Number.parseInt(b.split("-")[1] ?? "0", 10));
5981
5746
  if (counterIds.length !== executionOrder.length) {
5982
5747
  logger?.warn("Coverage/execution count mismatch: %s coverage entries vs %s executed tests. " + "Performing partial mapping for %s tests.", counterIds.length, executionOrder.length, Math.min(counterIds.length, executionOrder.length));
5983
5748
  }
@@ -6021,10 +5786,6 @@ function mapLegacyCounterKeys(rawCoverage, executionOrder, testHierarchy, logger
6021
5786
  perTest: remappedPerTest
6022
5787
  });
6023
5788
  }
6024
- // src/bun-test-runner.ts
6025
- import { tmpdir } from "node:os";
6026
- import { join as join3 } from "node:path";
6027
- import * as fsPromises2 from "node:fs/promises";
6028
5789
 
6029
5790
  // node_modules/ws/wrapper.mjs
6030
5791
  var import_stream = __toESM(require_stream(), 1);
@@ -6086,7 +5847,7 @@ class InspectorClient {
6086
5847
  if (this.ws) {
6087
5848
  throw new Error("Already connected");
6088
5849
  }
6089
- return new Promise((resolve4, reject) => {
5850
+ return new Promise((resolve3, reject) => {
6090
5851
  const timeoutTimer = setTimeout(() => {
6091
5852
  if (this.ws) {
6092
5853
  this.ws.close();
@@ -6098,7 +5859,7 @@ class InspectorClient {
6098
5859
  this.ws = ws;
6099
5860
  ws.addEventListener("open", () => {
6100
5861
  clearTimeout(timeoutTimer);
6101
- resolve4();
5862
+ resolve3();
6102
5863
  });
6103
5864
  ws.addEventListener("error", () => {
6104
5865
  clearTimeout(timeoutTimer);
@@ -6120,12 +5881,12 @@ class InspectorClient {
6120
5881
  }
6121
5882
  const id = ++this.messageId;
6122
5883
  const message = { id, method, params };
6123
- return new Promise((resolve4, reject) => {
5884
+ return new Promise((resolve3, reject) => {
6124
5885
  const timer = setTimeout(() => {
6125
5886
  this.pendingRequests.delete(id);
6126
5887
  reject(new InspectorTimeoutError(`Request timeout after ${this.state.requestTimeout}ms: ${method}`));
6127
5888
  }, this.state.requestTimeout);
6128
- this.pendingRequests.set(id, { resolve: resolve4, reject, timer });
5889
+ this.pendingRequests.set(id, { resolve: resolve3, reject, timer });
6129
5890
  try {
6130
5891
  this.ws.send(JSON.stringify(message));
6131
5892
  } catch (error) {
@@ -6141,7 +5902,7 @@ class InspectorClient {
6141
5902
  }
6142
5903
  this.isClosing = true;
6143
5904
  const error = new InspectorConnectionError("Connection closed");
6144
- for (const pending of Array.from(this.pendingRequests.values())) {
5905
+ for (const pending of this.pendingRequests.values()) {
6145
5906
  clearTimeout(pending.timer);
6146
5907
  pending.reject(error);
6147
5908
  }
@@ -6152,7 +5913,7 @@ class InspectorClient {
6152
5913
  this.ws = null;
6153
5914
  }
6154
5915
  getTests() {
6155
- return Array.from(this.testHierarchy.values());
5916
+ return [...this.testHierarchy.values()];
6156
5917
  }
6157
5918
  getExecutionOrder() {
6158
5919
  return [...this.executionOrder];
@@ -6240,7 +6001,7 @@ class InspectorClient {
6240
6001
  let currentId = parentId;
6241
6002
  while (currentId !== undefined) {
6242
6003
  if (visited.has(currentId)) {
6243
- this.handleError(new Error(`Circular reference detected in test hierarchy: ${Array.from(visited).join(" -> ")} -> ${currentId}`));
6004
+ this.handleError(new Error(`Circular reference detected in test hierarchy: ${[...visited].join(" -> ")} -> ${currentId}`));
6244
6005
  break;
6245
6006
  }
6246
6007
  visited.add(currentId);
@@ -6258,7 +6019,7 @@ class InspectorClient {
6258
6019
  return;
6259
6020
  }
6260
6021
  const error = new InspectorConnectionError("Connection closed unexpectedly");
6261
- for (const pending of Array.from(this.pendingRequests.values())) {
6022
+ for (const pending of this.pendingRequests.values()) {
6262
6023
  clearTimeout(pending.timer);
6263
6024
  pending.reject(error);
6264
6025
  }
@@ -6271,10 +6032,281 @@ class InspectorClient {
6271
6032
  }
6272
6033
  }
6273
6034
  }
6035
+ // src/parsers/console-parser.ts
6036
+ function parseFilePath(line) {
6037
+ const fileMatch = /^([\w./-]+\.(?:test|spec)\.(?:ts|tsx|js|jsx|mts|mjs)):$/.exec(line);
6038
+ return fileMatch ? fileMatch[1] : null;
6039
+ }
6040
+ function buildTestName(testName, currentFile) {
6041
+ return currentFile ? `${currentFile} > ${testName}` : testName;
6042
+ }
6043
+ function parseTestLine(line, currentFile) {
6044
+ const passMatch = /^✓ +(\S.*?) \[([0-9.]+)ms\]$/.exec(line);
6045
+ if (passMatch) {
6046
+ return {
6047
+ test: {
6048
+ name: buildTestName(passMatch[1].trim(), currentFile),
6049
+ file: currentFile,
6050
+ status: "passed",
6051
+ duration: Number.parseFloat(passMatch[2])
6052
+ }
6053
+ };
6054
+ }
6055
+ const failMatch = /^✗ +(\S.*?)(?: \[([0-9.]+)ms\])?$/.exec(line);
6056
+ if (failMatch) {
6057
+ return {
6058
+ test: {
6059
+ name: buildTestName(failMatch[1].trim(), currentFile),
6060
+ file: currentFile,
6061
+ status: "failed",
6062
+ duration: failMatch[2] ? Number.parseFloat(failMatch[2]) : undefined
6063
+ },
6064
+ startedCollectingError: true
6065
+ };
6066
+ }
6067
+ const bailFailMatch = /^\(fail\) +(\S.*?)(?: \[([0-9.]+)ms\])?$/.exec(line);
6068
+ if (bailFailMatch) {
6069
+ return {
6070
+ test: {
6071
+ name: buildTestName(bailFailMatch[1].trim(), currentFile),
6072
+ file: currentFile,
6073
+ status: "failed",
6074
+ duration: bailFailMatch[2] ? Number.parseFloat(bailFailMatch[2]) : undefined
6075
+ },
6076
+ startedCollectingError: true
6077
+ };
6078
+ }
6079
+ const skipMatch = /^⏭ +(\S.*)$/.exec(line);
6080
+ if (skipMatch) {
6081
+ return {
6082
+ test: {
6083
+ name: buildTestName(skipMatch[1].trim(), currentFile),
6084
+ file: currentFile,
6085
+ status: "skipped"
6086
+ }
6087
+ };
6088
+ }
6089
+ return {};
6090
+ }
6091
+ function finalizeErrorMessage(currentTest, errorLines) {
6092
+ if (currentTest && errorLines.length > 0) {
6093
+ currentTest.failureMessage = errorLines.join(`
6094
+ `).trim();
6095
+ }
6096
+ }
6097
+ function shouldCollectErrorLine(line) {
6098
+ if (!line.trim()) {
6099
+ return false;
6100
+ }
6101
+ return !/^\s*\d+\s+(?:pass|fail|skip)/.test(line);
6102
+ }
6103
+ function updateCounters(test, counters, parseResult) {
6104
+ if (test.status === "passed") {
6105
+ counters.passed++;
6106
+ return false;
6107
+ } else if (test.status === "failed") {
6108
+ counters.failed++;
6109
+ return parseResult.startedCollectingError ?? false;
6110
+ } else {
6111
+ counters.skipped++;
6112
+ return false;
6113
+ }
6114
+ }
6115
+ function parseSummaryLines(output) {
6116
+ const counts = { passed: 0, failed: 0, skipped: 0 };
6117
+ const passSummary = /\s(\d+)\s+pass\b/.exec(output);
6118
+ const failSummary = /\s(\d+)\s+fail\b/.exec(output);
6119
+ const skipSummary = /\s(\d+)\s+skip\b/.exec(output);
6120
+ const bailSummary = /Bailed out after (\d+) failures?/.exec(output);
6121
+ if (passSummary) {
6122
+ counts.passed = Number.parseInt(passSummary[1], 10);
6123
+ }
6124
+ if (failSummary) {
6125
+ counts.failed = Number.parseInt(failSummary[1], 10);
6126
+ }
6127
+ if (skipSummary) {
6128
+ counts.skipped = Number.parseInt(skipSummary[1], 10);
6129
+ }
6130
+ if (bailSummary) {
6131
+ counts.failed = Math.max(counts.failed, Number.parseInt(bailSummary[1], 10));
6132
+ }
6133
+ const ranTestsSummary = /Ran\s+(\d+)\s+tests?/.exec(output);
6134
+ if (ranTestsSummary) {
6135
+ const totalFromRan = Number.parseInt(ranTestsSummary[1], 10);
6136
+ const totalParsed = counts.passed + counts.failed + counts.skipped;
6137
+ if (totalParsed !== totalFromRan && counts.passed === 0 && counts.failed === 0) {
6138
+ counts.passed = totalFromRan;
6139
+ }
6140
+ }
6141
+ return counts;
6142
+ }
6143
+ function parseBunTestOutput(stdout, stderr) {
6144
+ const tests = [];
6145
+ const counters = { passed: 0, failed: 0, skipped: 0 };
6146
+ const output = `${stdout}
6147
+ ${stderr}`;
6148
+ const lines = output.split(`
6149
+ `);
6150
+ let currentTest = null;
6151
+ let collectingError = false;
6152
+ let errorLines = [];
6153
+ let currentFile;
6154
+ for (const line of lines) {
6155
+ const filePath = parseFilePath(line);
6156
+ if (filePath) {
6157
+ currentFile = filePath;
6158
+ continue;
6159
+ }
6160
+ const parseResult = parseTestLine(line, currentFile);
6161
+ if (parseResult.test) {
6162
+ if (currentTest && collectingError) {
6163
+ finalizeErrorMessage(currentTest, errorLines);
6164
+ errorLines = [];
6165
+ }
6166
+ currentTest = parseResult.test;
6167
+ tests.push(currentTest);
6168
+ collectingError = updateCounters(currentTest, counters, parseResult);
6169
+ continue;
6170
+ }
6171
+ if (collectingError && currentTest && shouldCollectErrorLine(line)) {
6172
+ errorLines.push(line);
6173
+ }
6174
+ }
6175
+ if (currentTest && collectingError) {
6176
+ finalizeErrorMessage(currentTest, errorLines);
6177
+ }
6178
+ const summaryCounts = parseSummaryLines(output);
6179
+ counters.passed = Math.max(counters.passed, summaryCounts.passed);
6180
+ counters.failed = Math.max(counters.failed, summaryCounts.failed);
6181
+ counters.skipped = Math.max(counters.skipped, summaryCounts.skipped);
6182
+ return {
6183
+ tests,
6184
+ totalTests: counters.passed + counters.failed + counters.skipped,
6185
+ passed: counters.passed,
6186
+ failed: counters.failed,
6187
+ skipped: counters.skipped
6188
+ };
6189
+ }
6190
+
6191
+ // src/process-runner.ts
6192
+ import { spawn } from "node:child_process";
6193
+ async function runBunTests(options) {
6194
+ const args = ["test"];
6195
+ if (options.inspectWaitPort) {
6196
+ args.push(`--inspect=${options.inspectWaitPort}`);
6197
+ }
6198
+ if (options.bunfigPath) {
6199
+ args.push(`--config=${options.bunfigPath}`);
6200
+ }
6201
+ if (options.preloadScript) {
6202
+ args.push("--preload", options.preloadScript);
6203
+ }
6204
+ if (options.testNamePattern) {
6205
+ args.push("--test-name-pattern", options.testNamePattern);
6206
+ }
6207
+ if (options.bail) {
6208
+ args.push("--bail");
6209
+ }
6210
+ if (options.sequentialMode) {
6211
+ args.push("--concurrency=1");
6212
+ }
6213
+ if (options.bunArgs && options.bunArgs.length > 0) {
6214
+ args.push(...options.bunArgs);
6215
+ }
6216
+ if (options.testFiles && options.testFiles.length > 0) {
6217
+ args.push(...options.testFiles);
6218
+ }
6219
+ const env = {
6220
+ ...process.env,
6221
+ ...options.env
6222
+ };
6223
+ if (options.activeMutant) {
6224
+ env.__STRYKER_ACTIVE_MUTANT__ = options.activeMutant;
6225
+ }
6226
+ if (options.coverageFile) {
6227
+ env.__STRYKER_COVERAGE_FILE__ = options.coverageFile;
6228
+ }
6229
+ if (options.syncPort) {
6230
+ env.__STRYKER_SYNC_PORT__ = String(options.syncPort);
6231
+ }
6232
+ return new Promise((resolve3) => {
6233
+ const stdoutChunks = [];
6234
+ const stderrChunks = [];
6235
+ let timedOut = false;
6236
+ let processKilled = false;
6237
+ if (options.signal?.aborted) {
6238
+ resolve3({ stdout: "", stderr: "", exitCode: null, timedOut: true });
6239
+ return;
6240
+ }
6241
+ const spawnOpts = {
6242
+ env,
6243
+ stdio: ["ignore", "pipe", "pipe"],
6244
+ cwd: process.cwd()
6245
+ };
6246
+ const childProcess = spawn(options.bunPath, args, spawnOpts);
6247
+ const timeoutHandle = setTimeout(() => {
6248
+ timedOut = true;
6249
+ processKilled = true;
6250
+ childProcess.kill("SIGKILL");
6251
+ }, options.timeout);
6252
+ if (options.signal) {
6253
+ const onAbort = () => {
6254
+ clearTimeout(timeoutHandle);
6255
+ processKilled = true;
6256
+ timedOut = true;
6257
+ childProcess.kill("SIGTERM");
6258
+ setTimeout(() => {
6259
+ childProcess.kill("SIGKILL");
6260
+ }, 500);
6261
+ };
6262
+ options.signal.addEventListener("abort", onAbort, { once: true });
6263
+ }
6264
+ if (childProcess.stdout) {
6265
+ childProcess.stdout.on("data", (data) => {
6266
+ stdoutChunks.push(data);
6267
+ });
6268
+ }
6269
+ let inspectorUrlExtracted = false;
6270
+ if (childProcess.stderr) {
6271
+ childProcess.stderr.on("data", (data) => {
6272
+ stderrChunks.push(data);
6273
+ if (options.inspectWaitPort && !inspectorUrlExtracted && options.onInspectorReady) {
6274
+ const text = Buffer.concat(stderrChunks).toString();
6275
+ const match = /Listening:[\t\v\f\r \u00A0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF]*\n\s*(ws:\/\/\S+)/.exec(text);
6276
+ if (match) {
6277
+ inspectorUrlExtracted = true;
6278
+ options.onInspectorReady(match[1]);
6279
+ }
6280
+ }
6281
+ });
6282
+ }
6283
+ childProcess.on("close", (code) => {
6284
+ clearTimeout(timeoutHandle);
6285
+ resolve3({
6286
+ stdout: Buffer.concat(stdoutChunks).toString(),
6287
+ stderr: Buffer.concat(stderrChunks).toString(),
6288
+ exitCode: processKilled ? null : code,
6289
+ timedOut
6290
+ });
6291
+ });
6292
+ childProcess.on("error", (error) => {
6293
+ clearTimeout(timeoutHandle);
6294
+ const stderrOutput = Buffer.concat(stderrChunks).toString();
6295
+ resolve3({
6296
+ stdout: Buffer.concat(stdoutChunks).toString(),
6297
+ stderr: `${stderrOutput}
6298
+ Process error: ${error.message}`,
6299
+ exitCode: null,
6300
+ timedOut
6301
+ });
6302
+ });
6303
+ });
6304
+ }
6305
+
6274
6306
  // src/utils/port.ts
6275
- import { createServer } from "net";
6307
+ import { createServer } from "node:net";
6276
6308
  async function getAvailablePort() {
6277
- return new Promise((resolve4, reject) => {
6309
+ return new Promise((resolve3, reject) => {
6278
6310
  const server = createServer();
6279
6311
  server.on("error", (err) => {
6280
6312
  reject(new Error(`Failed to get available port: ${err.message}`));
@@ -6292,7 +6324,7 @@ async function getAvailablePort() {
6292
6324
  reject(new Error(`Failed to close server: ${err.message}`));
6293
6325
  return;
6294
6326
  }
6295
- resolve4(port);
6327
+ resolve3(port);
6296
6328
  });
6297
6329
  });
6298
6330
  });
@@ -6303,6 +6335,7 @@ class SyncServer {
6303
6335
  httpServer = null;
6304
6336
  wss = null;
6305
6337
  clients = new Set;
6338
+ readyLatched = false;
6306
6339
  port;
6307
6340
  createHttpServer;
6308
6341
  WebSocketServerClass;
@@ -6314,15 +6347,15 @@ class SyncServer {
6314
6347
  this.webSocketOpenState = options.webSocketOpenState ?? import_websocket.default.OPEN;
6315
6348
  }
6316
6349
  async start() {
6317
- return new Promise((resolve4, reject) => {
6350
+ return new Promise((resolve3, reject) => {
6318
6351
  try {
6319
6352
  this.httpServer = this.createHttpServer((req, res) => {
6320
- if (req.url !== "/sync") {
6321
- res.writeHead(404);
6322
- res.end("Not found");
6323
- } else {
6353
+ if (req.url === "/sync") {
6324
6354
  res.writeHead(400);
6325
6355
  res.end("WebSocket upgrade failed");
6356
+ } else {
6357
+ res.writeHead(404);
6358
+ res.end("Not found");
6326
6359
  }
6327
6360
  });
6328
6361
  this.wss = new this.WebSocketServerClass({
@@ -6331,6 +6364,11 @@ class SyncServer {
6331
6364
  });
6332
6365
  this.wss.on("connection", (ws) => {
6333
6366
  this.clients.add(ws);
6367
+ if (this.readyLatched && ws.readyState === this.webSocketOpenState) {
6368
+ try {
6369
+ ws.send("ready");
6370
+ } catch {}
6371
+ }
6334
6372
  ws.on("close", () => {
6335
6373
  this.clients.delete(ws);
6336
6374
  });
@@ -6338,7 +6376,7 @@ class SyncServer {
6338
6376
  });
6339
6377
  this.httpServer.on("error", reject);
6340
6378
  this.httpServer.listen(this.port, () => {
6341
- resolve4();
6379
+ resolve3();
6342
6380
  });
6343
6381
  } catch (error) {
6344
6382
  reject(error instanceof Error ? error : new Error(String(error)));
@@ -6346,6 +6384,7 @@ class SyncServer {
6346
6384
  });
6347
6385
  }
6348
6386
  signalReady() {
6387
+ this.readyLatched = true;
6349
6388
  for (const client of this.clients) {
6350
6389
  try {
6351
6390
  if (client.readyState === this.webSocketOpenState) {
@@ -6355,6 +6394,7 @@ class SyncServer {
6355
6394
  }
6356
6395
  }
6357
6396
  async close() {
6397
+ this.readyLatched = false;
6358
6398
  for (const client of this.clients) {
6359
6399
  try {
6360
6400
  client.close();
@@ -6362,13 +6402,15 @@ class SyncServer {
6362
6402
  }
6363
6403
  this.clients.clear();
6364
6404
  if (this.wss) {
6365
- this.wss.close();
6405
+ await new Promise((resolve3) => {
6406
+ this.wss.close(() => resolve3());
6407
+ });
6366
6408
  this.wss = null;
6367
6409
  }
6368
6410
  if (this.httpServer) {
6369
- await new Promise((resolve4) => {
6411
+ await new Promise((resolve3) => {
6370
6412
  this.httpServer.close(() => {
6371
- resolve4();
6413
+ resolve3();
6372
6414
  });
6373
6415
  });
6374
6416
  this.httpServer = null;
@@ -6380,7 +6422,7 @@ class SyncServer {
6380
6422
  }
6381
6423
  // src/utils/bunfig-sanitizer.ts
6382
6424
  import { readFile as readFile3, unlink as unlink3, writeFile as writeFile2, mkdir as mkdir2 } from "node:fs/promises";
6383
- import path from "node:path";
6425
+ import path2 from "node:path";
6384
6426
 
6385
6427
  // node_modules/smol-toml/dist/error.js
6386
6428
  /*!
@@ -7490,7 +7532,7 @@ function absolutizePath(value, projectCwd) {
7490
7532
  if (typeof value !== "string") {
7491
7533
  return value;
7492
7534
  }
7493
- return path.isAbsolute(value) ? value : path.resolve(projectCwd, value);
7535
+ return path2.isAbsolute(value) ? value : path2.resolve(projectCwd, value);
7494
7536
  }
7495
7537
  function absolutizePathValue(value, projectCwd) {
7496
7538
  if (Array.isArray(value)) {
@@ -7501,7 +7543,7 @@ function absolutizePathValue(value, projectCwd) {
7501
7543
  async function generateSanitizedBunfig(projectCwd, tmpDir) {
7502
7544
  await mkdir2(tmpDir, { recursive: true });
7503
7545
  let rawConfig = {};
7504
- const bunfigPath = path.join(projectCwd, "bunfig.toml");
7546
+ const bunfigPath = path2.join(projectCwd, "bunfig.toml");
7505
7547
  try {
7506
7548
  const content = await readFile3(bunfigPath, "utf8");
7507
7549
  rawConfig = parse(content);
@@ -7525,7 +7567,7 @@ async function generateSanitizedBunfig(projectCwd, tmpDir) {
7525
7567
  sanitizedTest.onlyFailures = false;
7526
7568
  sanitized.test = sanitizedTest;
7527
7569
  const serialized = stringify(sanitized);
7528
- const outPath = path.join(tmpDir, `stryker-bun-runner-bunfig-${process.pid}-${Date.now()}.toml`);
7570
+ const outPath = path2.join(tmpDir, `stryker-bun-runner-bunfig-${process.pid}-${Date.now()}.toml`);
7529
7571
  await writeFile2(outPath, serialized, "utf8");
7530
7572
  return outPath;
7531
7573
  }
@@ -7545,20 +7587,14 @@ function buildTestNamePattern(testFilter) {
7545
7587
  }
7546
7588
  const fileExtRe = /\.(?:test|spec)\.(?:[jt]sx?|m[jt]s)$/;
7547
7589
  const dedupSuffixRe = / \[\d+\]$/;
7548
- const hierarchySepRe = / > /g;
7549
- const metaRe = /[.*+?^${}()|[\]\\\/]/g;
7590
+ const metaRe = /[.*+?^${}()|[\]\\/]/g;
7550
7591
  const alternatives = new Set;
7551
7592
  for (const id of testFilter) {
7552
7593
  const firstSepIdx = id.indexOf(" > ");
7553
- let name;
7554
- if (firstSepIdx !== -1 && fileExtRe.test(id.slice(0, firstSepIdx))) {
7555
- name = id.slice(firstSepIdx + 3);
7556
- } else {
7557
- name = id;
7558
- }
7594
+ let name = firstSepIdx !== -1 && fileExtRe.test(id.slice(0, firstSepIdx)) ? id.slice(firstSepIdx + 3) : id;
7559
7595
  name = name.replace(dedupSuffixRe, "");
7560
- name = name.replace(hierarchySepRe, " ");
7561
- name = name.replace(metaRe, "\\$&");
7596
+ name = name.replaceAll(" > ", " ");
7597
+ name = name.replaceAll(metaRe, String.raw`\$&`);
7562
7598
  if (name.length > 0) {
7563
7599
  alternatives.add(name);
7564
7600
  }
@@ -7566,27 +7602,85 @@ function buildTestNamePattern(testFilter) {
7566
7602
  if (alternatives.size === 0) {
7567
7603
  return;
7568
7604
  }
7569
- return `^(?:${Array.from(alternatives).join("|")})$`;
7605
+ return `^(?:${[...alternatives].join("|")})$`;
7570
7606
  }
7571
7607
  // src/utils/test-file-discovery.ts
7572
- import { join as join2, relative as relative2 } from "node:path";
7573
7608
  import * as fsPromises from "node:fs/promises";
7609
+ import path3 from "node:path";
7610
+ var testFileRe = /\.(?:test|spec)\.(?:[jt]sx?|m[jt]s)$/;
7611
+ var excludedDirs = new Set(["node_modules", ".stryker-tmp", "dist", "build", ".git"]);
7612
+ function hasExcludedAncestor(absolutePath) {
7613
+ return absolutePath.split("/").some((seg) => excludedDirs.has(seg));
7614
+ }
7615
+ async function tryRealpath(p) {
7616
+ try {
7617
+ return await fsPromises.realpath(p);
7618
+ } catch {
7619
+ return;
7620
+ }
7621
+ }
7622
+ async function tryStat(p) {
7623
+ try {
7624
+ return await fsPromises.stat(p);
7625
+ } catch {
7626
+ return;
7627
+ }
7628
+ }
7629
+ async function handleSymlinkDir(fullPath, entryName, ctx) {
7630
+ if (excludedDirs.has(entryName)) {
7631
+ return;
7632
+ }
7633
+ const resolvedTarget = await tryRealpath(fullPath);
7634
+ if (!resolvedTarget) {
7635
+ return;
7636
+ }
7637
+ if (hasExcludedAncestor(resolvedTarget)) {
7638
+ ctx.logger?.debug("discoverTestFiles: symlink %s resolves to excluded path %s; skipping", fullPath, resolvedTarget);
7639
+ } else {
7640
+ await ctx.walk(fullPath);
7641
+ }
7642
+ }
7643
+ async function handleSymlinkFile(fullPath, entryName, ctx) {
7644
+ if (!testFileRe.test(entryName)) {
7645
+ return;
7646
+ }
7647
+ const resolvedTarget = await tryRealpath(fullPath);
7648
+ if (resolvedTarget && !hasExcludedAncestor(resolvedTarget)) {
7649
+ ctx.results.push(path3.relative(ctx.cwd, fullPath));
7650
+ }
7651
+ }
7652
+ async function handleSymlink(fullPath, entryName, ctx) {
7653
+ const stat3 = await tryStat(fullPath);
7654
+ if (!stat3) {
7655
+ ctx.logger?.debug("discoverTestFiles: broken symlink at %s; skipping", fullPath);
7656
+ return;
7657
+ }
7658
+ if (stat3.isDirectory()) {
7659
+ await handleSymlinkDir(fullPath, entryName, ctx);
7660
+ } else if (stat3.isFile()) {
7661
+ await handleSymlinkFile(fullPath, entryName, ctx);
7662
+ }
7663
+ }
7664
+ async function processEntry(entry, dir, ctx) {
7665
+ const fullPath = path3.join(dir, entry.name);
7666
+ if (entry.isDirectory()) {
7667
+ if (!excludedDirs.has(entry.name)) {
7668
+ await ctx.walk(fullPath);
7669
+ }
7670
+ } else if (entry.isSymbolicLink()) {
7671
+ await handleSymlink(fullPath, entry.name, ctx);
7672
+ } else if (entry.isFile() && testFileRe.test(entry.name)) {
7673
+ ctx.results.push(path3.relative(ctx.cwd, fullPath));
7674
+ }
7675
+ }
7574
7676
  async function discoverTestFiles(cwd = process.cwd(), logger) {
7575
- const testFileRe = /\.(?:test|spec)\.(?:[jt]sx?|m[jt]s)$/;
7576
- const excludedDirs = new Set(["node_modules", ".stryker-tmp", "dist", "build", ".git"]);
7577
- const hasExcludedAncestor = (absolutePath) => {
7578
- return absolutePath.split("/").some((seg) => excludedDirs.has(seg));
7579
- };
7580
7677
  const results = [];
7581
7678
  const visitedRealPaths = new Set;
7582
- const walk = async (dir) => {
7583
- let realDir;
7584
- try {
7585
- realDir = await fsPromises.realpath(dir);
7586
- } catch {
7587
- if (logger) {
7588
- logger.debug("discoverTestFiles: could not resolve real path of %s; skipping", dir);
7589
- }
7679
+ const ctx = { cwd, results, visitedRealPaths, logger, walk };
7680
+ async function walk(dir) {
7681
+ const realDir = await tryRealpath(dir);
7682
+ if (!realDir) {
7683
+ logger?.debug("discoverTestFiles: could not resolve real path of %s; skipping", dir);
7590
7684
  return;
7591
7685
  }
7592
7686
  if (visitedRealPaths.has(realDir)) {
@@ -7599,69 +7693,27 @@ async function discoverTestFiles(cwd = process.cwd(), logger) {
7599
7693
  } catch {
7600
7694
  return;
7601
7695
  }
7602
- entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
7696
+ entries.sort((a, b) => a.name.localeCompare(b.name));
7603
7697
  for (const entry of entries) {
7604
- const fullPath = join2(dir, entry.name);
7605
- if (entry.isDirectory()) {
7606
- if (!excludedDirs.has(entry.name)) {
7607
- await walk(fullPath);
7608
- }
7609
- } else if (entry.isSymbolicLink()) {
7610
- let stat3;
7611
- try {
7612
- stat3 = await fsPromises.stat(fullPath);
7613
- } catch {
7614
- if (logger) {
7615
- logger.debug("discoverTestFiles: broken symlink at %s; skipping", fullPath);
7616
- }
7617
- continue;
7618
- }
7619
- if (stat3.isDirectory()) {
7620
- if (!excludedDirs.has(entry.name)) {
7621
- let resolvedTarget;
7622
- try {
7623
- resolvedTarget = await fsPromises.realpath(fullPath);
7624
- } catch {
7625
- continue;
7626
- }
7627
- if (hasExcludedAncestor(resolvedTarget)) {
7628
- if (logger) {
7629
- logger.debug("discoverTestFiles: symlink %s resolves to excluded path %s; skipping", fullPath, resolvedTarget);
7630
- }
7631
- } else {
7632
- await walk(fullPath);
7633
- }
7634
- }
7635
- } else if (stat3.isFile() && testFileRe.test(entry.name)) {
7636
- let resolvedTarget;
7637
- try {
7638
- resolvedTarget = await fsPromises.realpath(fullPath);
7639
- } catch {
7640
- continue;
7641
- }
7642
- if (!hasExcludedAncestor(resolvedTarget)) {
7643
- results.push(relative2(cwd, fullPath));
7644
- }
7645
- }
7646
- } else if (entry.isFile() && testFileRe.test(entry.name)) {
7647
- results.push(relative2(cwd, fullPath));
7648
- }
7698
+ await processEntry(entry, dir, ctx);
7649
7699
  }
7650
- };
7700
+ }
7651
7701
  await walk(cwd);
7652
7702
  if (results.length === 0) {
7653
- if (logger) {
7654
- logger.warn("discoverTestFiles: no test files found in %s; falling back to Bun discovery", cwd);
7655
- }
7703
+ logger?.warn("discoverTestFiles: no test files found in %s; falling back to Bun discovery", cwd);
7656
7704
  return;
7657
7705
  }
7658
- results.sort();
7659
- if (logger) {
7660
- logger.debug("discoverTestFiles: found %d test files in %s", results.length, cwd);
7661
- }
7706
+ results.sort((a, b) => a.localeCompare(b));
7707
+ logger?.debug("discoverTestFiles: found %d test files in %s", results.length, cwd);
7662
7708
  return results;
7663
7709
  }
7664
7710
  // src/bun-test-runner.ts
7711
+ function sleep(ms) {
7712
+ return new Promise((resolve3) => {
7713
+ setTimeout(resolve3, ms);
7714
+ });
7715
+ }
7716
+
7665
7717
  class BunTestRunner {
7666
7718
  logger;
7667
7719
  static inject = tokens(commonTokens.logger, commonTokens.options);
@@ -7670,6 +7722,7 @@ class BunTestRunner {
7670
7722
  inspectorTimeout;
7671
7723
  env;
7672
7724
  bunArgs;
7725
+ testFilesOverride;
7673
7726
  mutateGlobs;
7674
7727
  preloadScriptPath;
7675
7728
  coverageFilePath;
@@ -7679,7 +7732,9 @@ class BunTestRunner {
7679
7732
  cachedTestNames;
7680
7733
  baseNameIndex;
7681
7734
  cachedTestFiles;
7735
+ cachedTestFilesCwd;
7682
7736
  cachedEagerModules;
7737
+ cachedEagerModulesCwd;
7683
7738
  lastRegistryTmpPath;
7684
7739
  constructor(logger, options) {
7685
7740
  this.logger = logger;
@@ -7689,6 +7744,12 @@ class BunTestRunner {
7689
7744
  this.inspectorTimeout = bunOptions.inspectorTimeout ?? 5000;
7690
7745
  this.env = bunOptions.env;
7691
7746
  this.bunArgs = bunOptions.bunArgs;
7747
+ if (bunOptions.testFiles?.length === 0) {
7748
+ this.logger.warn("bun.testFiles was set to an empty array — treating as undefined and falling back to auto-discovery");
7749
+ this.testFilesOverride = undefined;
7750
+ } else {
7751
+ this.testFilesOverride = bunOptions.testFiles;
7752
+ }
7692
7753
  this.mutateGlobs = options.mutate ?? [];
7693
7754
  this.logger.debug("BunTestRunner initialized with options: %o", {
7694
7755
  bunPath: this.bunPath,
@@ -7697,26 +7758,68 @@ class BunTestRunner {
7697
7758
  env: this.env,
7698
7759
  bunArgs: this.bunArgs
7699
7760
  });
7761
+ if (this.testFilesOverride?.some((p) => path4.isAbsolute(p))) {
7762
+ const isSandbox = process.cwd().includes(".stryker-tmp/sandbox-");
7763
+ if (isSandbox) {
7764
+ this.logger.warn("bun.testFiles contains absolute path(s) and the current working directory appears to be a Stryker sandbox (%s). " + "Absolute paths point at the ORIGINAL (unmutated) source files — mutations will be silently bypassed. " + "Use relative paths so that Bun resolves them against the sandbox copy.", process.cwd());
7765
+ }
7766
+ }
7700
7767
  }
7701
7768
  get registryPath() {
7702
- return join3(process.cwd(), ".stryker-bun-runner-registry.json");
7769
+ return path4.join(process.cwd(), ".stryker-bun-runner-registry.json");
7703
7770
  }
7704
7771
  get registryTmpPath() {
7705
- return this.registryPath + ".tmp";
7772
+ return `${this.registryPath}.tmp`;
7706
7773
  }
7707
7774
  capabilities() {
7708
7775
  return {
7709
7776
  reloadEnvironment: true
7710
7777
  };
7711
7778
  }
7779
+ async getOrDiscoverTestFiles() {
7780
+ if (this.testFilesOverride !== undefined) {
7781
+ return this.testFilesOverride;
7782
+ }
7783
+ const cwd = process.cwd();
7784
+ if (this.cachedTestFiles !== undefined && this.cachedTestFilesCwd === cwd) {
7785
+ return this.cachedTestFiles;
7786
+ }
7787
+ this.cachedTestFiles = await discoverTestFiles(cwd, this.logger);
7788
+ this.cachedTestFilesCwd = cwd;
7789
+ return this.cachedTestFiles;
7790
+ }
7791
+ testFilesCacheHit(cwd) {
7792
+ if (this.testFilesOverride !== undefined) {
7793
+ return this.testFilesOverride;
7794
+ }
7795
+ return this.cachedTestFiles !== undefined && this.cachedTestFilesCwd === cwd ? this.cachedTestFiles : null;
7796
+ }
7712
7797
  async init() {
7713
7798
  this.logger.debug("BunTestRunner init starting...");
7714
- const tempDir = join3(tmpdir(), "stryker-bun-runner");
7799
+ if (this.preloadScriptPath) {
7800
+ try {
7801
+ await cleanupPreloadScript(this.preloadScriptPath);
7802
+ } catch (error) {
7803
+ this.logger.debug("Failed to clean up previous preload script on re-init: %s", error instanceof Error ? error.message : String(error));
7804
+ }
7805
+ this.preloadScriptPath = undefined;
7806
+ }
7807
+ if (this.coverageFilePath) {
7808
+ try {
7809
+ await cleanupCoverageFile(this.coverageFilePath);
7810
+ } catch (error) {
7811
+ this.logger.debug("Failed to clean up previous coverage file on re-init: %s", error instanceof Error ? error.message : String(error));
7812
+ }
7813
+ this.coverageFilePath = undefined;
7814
+ }
7815
+ const tempDir = path4.join(tmpdir(), "stryker-bun-runner");
7715
7816
  this.tempDir = tempDir;
7716
- this.coverageFilePath = join3(tempDir, `coverage-${Date.now()}.json`);
7817
+ this.coverageFilePath = path4.join(tempDir, `coverage-${Date.now()}.json`);
7717
7818
  this.logger.debug("Generating coverage preload script...");
7718
- if (!this.cachedEagerModules) {
7819
+ const eagerCwd = process.cwd();
7820
+ if (this.cachedEagerModules === undefined || this.cachedEagerModulesCwd !== eagerCwd) {
7719
7821
  this.cachedEagerModules = await resolveEagerModulesFromGlobs(this.mutateGlobs);
7822
+ this.cachedEagerModulesCwd = eagerCwd;
7720
7823
  this.logger.debug("Resolved %d eager modules from mutate globs", this.cachedEagerModules.length);
7721
7824
  }
7722
7825
  this.preloadScriptPath = await generatePreloadScript({
@@ -7726,7 +7829,7 @@ class BunTestRunner {
7726
7829
  });
7727
7830
  this.logger.debug("Preload script generated at: %s", this.preloadScriptPath);
7728
7831
  await this.ensureSanitizedBunfig();
7729
- this.cachedTestFiles = await discoverTestFiles(process.cwd(), this.logger);
7832
+ this.cachedTestFiles = await this.getOrDiscoverTestFiles();
7730
7833
  }
7731
7834
  async ensureSanitizedBunfig() {
7732
7835
  const cwd = process.cwd();
@@ -7736,7 +7839,7 @@ class BunTestRunner {
7736
7839
  if (this.sanitizedBunfigPath) {
7737
7840
  await cleanupSanitizedBunfig(this.sanitizedBunfigPath);
7738
7841
  }
7739
- const tempDir = this.tempDir ?? join3(tmpdir(), "stryker-bun-runner");
7842
+ const tempDir = this.tempDir ?? path4.join(tmpdir(), "stryker-bun-runner");
7740
7843
  this.sanitizedBunfigPath = await generateSanitizedBunfig(cwd, tempDir);
7741
7844
  this.sanitizedBunfigCwd = cwd;
7742
7845
  this.logger.debug("Sanitized bunfig (re)generated at: %s for cwd: %s", this.sanitizedBunfigPath, cwd);
@@ -7745,7 +7848,7 @@ class BunTestRunner {
7745
7848
  async loadRegistryFile() {
7746
7849
  const registryPath = this.registryPath;
7747
7850
  try {
7748
- const raw = await fsPromises2.readFile(registryPath, "utf-8");
7851
+ const raw = await fsPromises2.readFile(registryPath, "utf8");
7749
7852
  const parsed = JSON.parse(raw);
7750
7853
  if (parsed.version !== 1) {
7751
7854
  this.logger.warn("dryRun registry file has unexpected version %s; skipping", String(parsed.version));
@@ -7759,9 +7862,9 @@ class BunTestRunner {
7759
7862
  this.baseNameIndex = new Map(parsed.baseNameIndex);
7760
7863
  this.logger.debug("Loaded dryRun registry from %s (%d entries)", registryPath, this.cachedTestNames.size);
7761
7864
  } catch (err) {
7762
- const code = err?.code;
7865
+ const code = err.code;
7763
7866
  if (code === "ENOENT") {
7764
- this.logger.warn("dryRun registry file not found at %s; killedBy names for static-coverage mutants may be unresolved", registryPath);
7867
+ this.logger.debug("dryRun registry file not found at %s; this worker has no static-coverage registry (expected on non-dryRun workers)", registryPath);
7765
7868
  } else {
7766
7869
  this.logger.warn("Failed to load dryRun registry from %s: %s", registryPath, err instanceof Error ? err.message : String(err));
7767
7870
  }
@@ -7813,14 +7916,15 @@ class BunTestRunner {
7813
7916
  }
7814
7917
  const uniqueName = buildUniqueTestName(testInfo.fullName, testInfo.url);
7815
7918
  const status = testInfo.status;
7816
- const elapsed = testInfo.elapsed !== undefined ? Math.round(testInfo.elapsed / 1e6) : timePerTest;
7919
+ const elapsed = testInfo.elapsed === undefined ? timePerTest : Math.round(testInfo.elapsed / 1e6);
7920
+ const startPosition = testInfo.line === undefined ? undefined : { line: testInfo.line, column: 0 };
7817
7921
  if (status === "fail") {
7818
7922
  const parsedTest = parsed.tests.find((t) => t.name.includes(testInfo.name));
7819
7923
  return {
7820
7924
  id: uniqueName,
7821
7925
  name: uniqueName,
7822
7926
  fileName: normalizeTestFilePath(testInfo.url),
7823
- startPosition: testInfo.line !== undefined ? { line: testInfo.line, column: 0 } : undefined,
7927
+ startPosition,
7824
7928
  status: TestStatus.Failed,
7825
7929
  failureMessage: parsedTest?.failureMessage ?? testInfo.error?.message ?? "Test failed",
7826
7930
  timeSpentMs: elapsed
@@ -7831,7 +7935,7 @@ class BunTestRunner {
7831
7935
  id: uniqueName,
7832
7936
  name: uniqueName,
7833
7937
  fileName: normalizeTestFilePath(testInfo.url),
7834
- startPosition: testInfo.line !== undefined ? { line: testInfo.line, column: 0 } : undefined,
7938
+ startPosition,
7835
7939
  status: TestStatus.Skipped,
7836
7940
  timeSpentMs: elapsed
7837
7941
  };
@@ -7840,7 +7944,7 @@ class BunTestRunner {
7840
7944
  id: uniqueName,
7841
7945
  name: uniqueName,
7842
7946
  fileName: normalizeTestFilePath(testInfo.url),
7843
- startPosition: testInfo.line !== undefined ? { line: testInfo.line, column: 0 } : undefined,
7947
+ startPosition,
7844
7948
  status: TestStatus.Success,
7845
7949
  timeSpentMs: elapsed
7846
7950
  };
@@ -7866,8 +7970,7 @@ class BunTestRunner {
7866
7970
  const lineB = "startPosition" in b && b.startPosition ? b.startPosition.line : Infinity;
7867
7971
  return lineA - lineB;
7868
7972
  });
7869
- for (let i = 0;i < group.length; i++) {
7870
- const test = group[i];
7973
+ for (const [i, test] of group.entries()) {
7871
7974
  const uniqueName = `${test.name} [${i}]`;
7872
7975
  test.id = uniqueName;
7873
7976
  test.name = uniqueName;
@@ -7877,6 +7980,7 @@ class BunTestRunner {
7877
7980
  }
7878
7981
  async dryRun() {
7879
7982
  this.logger.debug("Running dry run with inspector-based coverage collection...");
7983
+ const abortController = new AbortController;
7880
7984
  const inspectPort = await getAvailablePort();
7881
7985
  const syncPort = await getAvailablePort();
7882
7986
  this.logger.debug("Using inspector port: %d, sync port: %d", inspectPort, syncPort);
@@ -7893,109 +7997,104 @@ class BunTestRunner {
7893
7997
  };
7894
7998
  }
7895
7999
  const startTime = Date.now();
7896
- let inspector = null;
7897
8000
  let inspectorUrl = null;
7898
- const cwd = process.cwd();
7899
- const bunfigPath = this.sanitizedBunfigPath && this.sanitizedBunfigCwd === cwd ? this.sanitizedBunfigPath : await this.ensureSanitizedBunfig();
7900
- if (!this.cachedTestFiles) {
7901
- this.cachedTestFiles = await discoverTestFiles(process.cwd(), this.logger);
7902
- }
7903
- const testProcess = runBunTests({
7904
- bunPath: this.bunPath,
7905
- timeout: this.timeout,
7906
- env: this.env,
7907
- bunArgs: this.bunArgs,
7908
- bunfigPath,
7909
- preloadScript: this.preloadScriptPath,
7910
- coverageFile: this.coverageFilePath,
7911
- inspectWaitPort: inspectPort,
7912
- sequentialMode: true,
7913
- syncPort,
7914
- testFiles: this.cachedTestFiles,
7915
- onInspectorReady: (url) => {
7916
- inspectorUrl = url;
7917
- }
7918
- });
7919
- const waitStart = Date.now();
7920
- while (!inspectorUrl && Date.now() - waitStart < this.inspectorTimeout) {
7921
- await new Promise((resolve4) => setTimeout(resolve4, 50));
7922
- }
7923
- if (!inspectorUrl) {
7924
- const diagnosticResult = await testProcess;
7925
- const stdoutPreview = (diagnosticResult.stdout ?? "").slice(0, 1000);
7926
- const stderrPreview = (diagnosticResult.stderr ?? "").slice(0, 1000);
7927
- this.logger.error(`Failed to get inspector URL within timeout (%dms).
8001
+ try {
8002
+ const cwd = process.cwd();
8003
+ const bunfigPath = this.sanitizedBunfigPath && this.sanitizedBunfigCwd === cwd ? this.sanitizedBunfigPath : await this.ensureSanitizedBunfig();
8004
+ const testFilesCached = this.testFilesCacheHit(cwd);
8005
+ const testFiles = testFilesCached === null ? await this.getOrDiscoverTestFiles() : testFilesCached;
8006
+ const testProcess = runBunTests({
8007
+ bunPath: this.bunPath,
8008
+ timeout: this.timeout,
8009
+ env: this.env,
8010
+ bunArgs: this.bunArgs,
8011
+ bunfigPath,
8012
+ preloadScript: this.preloadScriptPath,
8013
+ coverageFile: this.coverageFilePath,
8014
+ inspectWaitPort: inspectPort,
8015
+ sequentialMode: true,
8016
+ syncPort,
8017
+ testFiles,
8018
+ signal: abortController.signal,
8019
+ onInspectorReady: (url) => {
8020
+ inspectorUrl = url;
8021
+ }
8022
+ });
8023
+ const waitStart = Date.now();
8024
+ while (!inspectorUrl && Date.now() - waitStart < this.inspectorTimeout) {
8025
+ await sleep(50);
8026
+ }
8027
+ if (!inspectorUrl) {
8028
+ const diagnosticResult = await testProcess;
8029
+ const stdoutPreview = diagnosticResult.stdout.slice(0, 1000);
8030
+ const stderrPreview = diagnosticResult.stderr.slice(0, 1000);
8031
+ this.logger.error(`Failed to get inspector URL within timeout (%dms).
7928
8032
  exit=%s timedOut=%s
7929
8033
  ` + `--- STDOUT (first 1000 chars) ---
7930
8034
  %s
7931
8035
  ` + `--- STDERR (first 1000 chars) ---
7932
8036
  %s`, this.inspectorTimeout, String(diagnosticResult.exitCode), String(diagnosticResult.timedOut), stdoutPreview || "(empty)", stderrPreview || "(empty)");
7933
- await syncServer.close();
7934
- return {
7935
- status: DryRunStatus.Error,
7936
- errorMessage: "Timeout waiting for inspector URL"
7937
- };
7938
- }
7939
- this.logger.debug("Inspector URL: %s", inspectorUrl);
7940
- inspector = new InspectorClient({
7941
- url: inspectorUrl,
7942
- connectionTimeout: this.inspectorTimeout,
7943
- requestTimeout: this.inspectorTimeout,
7944
- handlers: {}
7945
- });
7946
- try {
7947
- await inspector.connect();
7948
- await inspector.send("TestReporter.enable", {});
7949
- this.logger.debug("Inspector connected and TestReporter enabled");
7950
- } catch (error) {
7951
- const errorMsg = error instanceof Error ? error.message : String(error);
7952
- this.logger.error("Failed to connect inspector: %s", errorMsg);
8037
+ return {
8038
+ status: DryRunStatus.Error,
8039
+ errorMessage: "Timeout waiting for inspector URL"
8040
+ };
8041
+ }
8042
+ this.logger.debug("Inspector URL: %s", inspectorUrl);
8043
+ const inspector = new InspectorClient({
8044
+ url: inspectorUrl,
8045
+ connectionTimeout: this.inspectorTimeout,
8046
+ requestTimeout: this.inspectorTimeout,
8047
+ handlers: {}
8048
+ });
8049
+ try {
8050
+ await inspector.connect();
8051
+ await inspector.send("TestReporter.enable", {});
8052
+ this.logger.debug("Inspector connected and TestReporter enabled");
8053
+ } catch (error) {
8054
+ const errorMsg = error instanceof Error ? error.message : String(error);
8055
+ this.logger.error("Failed to connect inspector: %s", errorMsg);
8056
+ abortController.abort();
8057
+ await inspector.close();
8058
+ return {
8059
+ status: DryRunStatus.Error,
8060
+ errorMessage: `Failed to connect to Bun inspector: ${errorMsg}`
8061
+ };
8062
+ }
8063
+ syncServer.signalReady();
8064
+ this.logger.debug("Signaled preload script to proceed");
8065
+ const result = await testProcess;
8066
+ const totalElapsedMs = Date.now() - startTime;
8067
+ const testHierarchy = inspector.getTests();
8068
+ const executionOrder = inspector.getExecutionOrder();
7953
8069
  await inspector.close();
7954
- await syncServer.close();
7955
- return {
7956
- status: DryRunStatus.Error,
7957
- errorMessage: `Failed to connect to Bun inspector: ${errorMsg}`
7958
- };
7959
- }
7960
- syncServer.signalReady();
7961
- this.logger.debug("Signaled preload script to proceed");
7962
- const result = await testProcess;
7963
- const totalElapsedMs = Date.now() - startTime;
7964
- const testHierarchy = inspector.getTests();
7965
- const executionOrder = inspector.getExecutionOrder();
7966
- await inspector.close();
7967
- await syncServer.close();
7968
- this.logger.debug("Inspector collected %d tests in hierarchy, %d in execution order", testHierarchy.length, executionOrder.length);
7969
- if (result.timedOut) {
7970
- this.logger.warn("Dry run timed out");
7971
- return { status: DryRunStatus.Timeout };
7972
- }
7973
- const parsed = parseBunTestOutput(result.stdout, result.stderr);
7974
- if (result.exitCode !== 0 && parsed.failed === 0) {
8070
+ this.logger.debug("Inspector collected %d tests in hierarchy, %d in execution order", testHierarchy.length, executionOrder.length);
8071
+ const parsed = parseBunTestOutput(result.stdout, result.stderr);
8072
+ const earlyResult = this.checkDryRunProcessResult(result, parsed);
8073
+ if (earlyResult) {
8074
+ return earlyResult;
8075
+ }
8076
+ const mutantCoverage = await this.collectAndRemapCoverage(testHierarchy, executionOrder);
8077
+ const tests = this.buildTestsFromInspector(testHierarchy, executionOrder, parsed, totalElapsedMs);
8078
+ tests.sort((a, b) => a.name.localeCompare(b.name));
8079
+ await this.buildAndPersistTestRegistry(tests);
7975
8080
  return {
7976
- status: DryRunStatus.Error,
7977
- errorMessage: `Bun test process failed with exit code ${result.exitCode}
7978
- ${result.stderr}`
8081
+ status: DryRunStatus.Complete,
8082
+ tests,
8083
+ mutantCoverage
7979
8084
  };
8085
+ } finally {
8086
+ abortController.abort();
8087
+ await syncServer.close();
7980
8088
  }
7981
- let mutantCoverage;
7982
- if (this.coverageFilePath) {
7983
- mutantCoverage = await collectCoverage(this.coverageFilePath, this.logger);
7984
- await cleanupCoverageFile(this.coverageFilePath);
7985
- }
7986
- if (mutantCoverage) {
7987
- const testMap = new Map(testHierarchy.map((t) => [t.id, t]));
7988
- mutantCoverage = mapCoverageToInspectorIds(mutantCoverage, executionOrder, testMap, this.logger);
7989
- }
7990
- const tests = this.buildTestsFromInspector(testHierarchy, executionOrder, parsed, totalElapsedMs);
7991
- tests.sort((a, b) => a.name.localeCompare(b.name));
8089
+ }
8090
+ async buildAndPersistTestRegistry(tests) {
7992
8091
  this.cachedTestNames = new Set(tests.map((t) => t.name));
7993
8092
  if (tests.length !== this.cachedTestNames.size) {
7994
8093
  const nameCount = new Map;
7995
8094
  for (const test of tests) {
7996
8095
  nameCount.set(test.name, (nameCount.get(test.name) ?? 0) + 1);
7997
8096
  }
7998
- const duplicates = Array.from(nameCount.entries()).filter(([_, count]) => count > 1).map(([name, count]) => `"${name}" (${count}x)`);
8097
+ const duplicates = [...nameCount.entries()].filter(([_, count]) => count > 1).map(([name, count]) => `"${name}" (${count}x)`);
7999
8098
  this.logger.warn("Found %d duplicate test names (total: %d, unique: %d): %s", tests.length - this.cachedTestNames.size, tests.length, this.cachedTestNames.size, duplicates.join(", "));
8000
8099
  }
8001
8100
  this.baseNameIndex = new Map;
@@ -8019,49 +8118,29 @@ ${result.stderr}`
8019
8118
  const registryData = JSON.stringify({
8020
8119
  version: 1,
8021
8120
  writtenAt: Date.now(),
8022
- cachedTestNames: Array.from(this.cachedTestNames),
8023
- baseNameIndex: Array.from(this.baseNameIndex.entries())
8121
+ cachedTestNames: [...this.cachedTestNames],
8122
+ baseNameIndex: [...this.baseNameIndex.entries()]
8024
8123
  });
8025
- await fsPromises2.writeFile(tmpPath, registryData, "utf-8");
8124
+ await fsPromises2.writeFile(tmpPath, registryData, "utf8");
8026
8125
  this.lastRegistryTmpPath = tmpPath;
8027
8126
  await fsPromises2.rename(tmpPath, registryPath);
8127
+ this.lastRegistryTmpPath = undefined;
8028
8128
  this.logger.debug("Wrote dryRun registry to %s (%d entries)", registryPath, this.cachedTestNames.size);
8029
- } catch (registryErr) {
8030
- this.logger.warn("Failed to write dryRun registry file: %s", registryErr instanceof Error ? registryErr.message : String(registryErr));
8129
+ } catch (error) {
8130
+ this.logger.warn("Failed to write dryRun registry file: %s", error instanceof Error ? error.message : String(error));
8031
8131
  }
8032
- return {
8033
- status: DryRunStatus.Complete,
8034
- tests,
8035
- mutantCoverage
8036
- };
8037
8132
  }
8038
8133
  async mutantRun(options) {
8039
8134
  this.logger.debug("Running mutant run for mutant %s", options.activeMutant.id);
8040
8135
  const testNamePattern = buildTestNamePattern(options.testFilter ?? []);
8041
8136
  const localTestFilter = options.testFilter ?? [];
8042
- const localRegistry = new Set(localTestFilter);
8043
- const localSuffixRe = / \[\d+\]$/;
8044
- const localBaseIndex = new Map;
8045
- for (const id of localRegistry) {
8046
- const base = localSuffixRe.test(id) ? id.replace(localSuffixRe, "") : id;
8047
- const bucket = localBaseIndex.get(base);
8048
- if (bucket) {
8049
- bucket.push(id);
8050
- } else {
8051
- localBaseIndex.set(base, [id]);
8052
- }
8053
- if (base !== id) {
8054
- localBaseIndex.set(id, [id]);
8055
- }
8056
- }
8137
+ const { localRegistry, localBaseIndex } = this.buildLocalTestFilterIndex(localTestFilter);
8057
8138
  if (!this.cachedTestNames) {
8058
8139
  await this.loadRegistryFile();
8059
8140
  }
8060
8141
  const mutantCwd = process.cwd();
8061
8142
  const bunfigPath = this.sanitizedBunfigPath && this.sanitizedBunfigCwd === mutantCwd ? this.sanitizedBunfigPath : await this.ensureSanitizedBunfig();
8062
- if (!this.cachedTestFiles) {
8063
- this.cachedTestFiles = await discoverTestFiles(process.cwd(), this.logger);
8064
- }
8143
+ this.cachedTestFiles = await this.getOrDiscoverTestFiles();
8065
8144
  const result = await runBunTests({
8066
8145
  bunPath: this.bunPath,
8067
8146
  timeout: this.timeout,
@@ -8089,72 +8168,127 @@ ${result.stderr}`
8089
8168
  exitCode: result.exitCode
8090
8169
  });
8091
8170
  if (result.exitCode !== 0) {
8092
- const rawFailedNames = parsed.tests.filter((test) => test.status === "failed").map((test) => normalizeTestName(test.name));
8093
- if (rawFailedNames.length === 0 && parsed.tests.length === 0) {
8094
- const stderr = result.stderr ?? "";
8095
- const isRuntimeError = stderr.includes("Unhandled error") || stderr.includes("Cannot find module") || stderr.includes("SyntaxError") || stderr.includes("TypeError") || stderr.includes("ReferenceError") || stderr.includes("is not defined") || stderr.includes("Unexpected token");
8096
- if (isRuntimeError) {
8097
- this.logger.debug("Mutant %s caused runtime error (tests could not run): %s", options.activeMutant.id, stderr.slice(0, 200));
8098
- return {
8099
- status: MutantRunStatus.Error,
8100
- errorMessage: stderr.slice(0, 500) || `Runtime error with exit code ${result.exitCode}`
8101
- };
8102
- }
8103
- }
8104
- const killedBySet = new Set;
8105
- for (const name of rawFailedNames) {
8106
- if (localRegistry.has(name)) {
8107
- killedBySet.add(name);
8108
- continue;
8109
- }
8110
- const localBucket = localBaseIndex.get(name);
8111
- if (localBucket) {
8112
- this.logger.debug('Expanded killedBy base name "%s" → %d local registry IDs for mutant %s', name, localBucket.length, options.activeMutant.id);
8113
- for (const id of localBucket) {
8114
- killedBySet.add(id);
8115
- }
8116
- continue;
8117
- }
8118
- if (this.cachedTestNames?.has(name)) {
8119
- killedBySet.add(name);
8120
- continue;
8121
- }
8122
- const instanceBucket = this.baseNameIndex?.get(name);
8123
- if (instanceBucket) {
8124
- this.logger.debug('Expanded killedBy base name "%s" → %d instance registry IDs for mutant %s', name, instanceBucket.length, options.activeMutant.id);
8125
- for (const id of instanceBucket) {
8126
- killedBySet.add(id);
8127
- }
8128
- continue;
8129
- }
8130
- this.logger.warn('killedBy name "%s" for mutant %s not found in test registry; ' + "including as-is (may break incremental cache)", name, options.activeMutant.id);
8131
- killedBySet.add(name);
8171
+ return this.buildMutantKilledResult(result, parsed, localRegistry, localBaseIndex, options.activeMutant.id);
8172
+ }
8173
+ return {
8174
+ status: MutantRunStatus.Survived,
8175
+ nrOfTests: parsed.totalTests
8176
+ };
8177
+ }
8178
+ buildMutantKilledResult(result, parsed, localRegistry, localBaseIndex, mutantId) {
8179
+ const rawFailedNames = parsed.tests.filter((test) => test.status === "failed").map((test) => normalizeTestName(test.name));
8180
+ if (rawFailedNames.length === 0 && parsed.tests.length === 0) {
8181
+ const runtimeResult = this.checkRuntimeError(result, mutantId);
8182
+ if (runtimeResult) {
8183
+ return runtimeResult;
8132
8184
  }
8133
- const killedBy = Array.from(killedBySet);
8134
- if (killedBy.length === 0) {
8135
- const stdoutPreview = (result.stdout ?? "").slice(0, 600);
8136
- const stderrPreview = (result.stderr ?? "").slice(0, 600);
8137
- this.logger.warn("No failed tests identified for mutant %s — Bun output could not be parsed; " + `using "unknown" fallback (breaks incremental cache)
8185
+ }
8186
+ const killedBy = this.resolveKilledBy(rawFailedNames, localRegistry, localBaseIndex, mutantId);
8187
+ if (killedBy.length === 0) {
8188
+ this.logger.warn("No failed tests identified for mutant %s — Bun output could not be parsed; " + `using "unknown" fallback (breaks incremental cache)
8138
8189
  ` + `exit=%s
8139
8190
  --- STDOUT (first 600 chars) ---
8140
8191
  %s
8141
8192
  ` + `--- STDERR (first 600 chars) ---
8142
- %s`, options.activeMutant.id, String(result.exitCode), stdoutPreview || "(empty)", stderrPreview || "(empty)");
8143
- killedBy.push("unknown");
8144
- }
8145
- return {
8146
- status: MutantRunStatus.Killed,
8147
- killedBy,
8148
- failureMessage: parsed.tests.filter((test) => test.status === "failed").map((test) => test.failureMessage).filter((msg) => !!msg).join(`
8193
+ %s`, mutantId, String(result.exitCode), result.stdout.slice(0, 600) || "(empty)", result.stderr.slice(0, 600) || "(empty)");
8194
+ killedBy.push("unknown");
8195
+ }
8196
+ return {
8197
+ status: MutantRunStatus.Killed,
8198
+ killedBy,
8199
+ failureMessage: parsed.tests.filter((test) => test.status === "failed").map((test) => test.failureMessage).filter((msg) => !!msg).join(`
8149
8200
 
8150
8201
  `) || `Tests failed with exit code ${result.exitCode}`,
8151
- nrOfTests: parsed.totalTests || 1
8202
+ nrOfTests: parsed.totalTests || 1
8203
+ };
8204
+ }
8205
+ checkRuntimeError(result, mutantId) {
8206
+ const stderr = result.stderr;
8207
+ const isRuntimeError = stderr.includes("Unhandled error") || stderr.includes("Cannot find module") || stderr.includes("SyntaxError") || stderr.includes("TypeError") || stderr.includes("ReferenceError") || stderr.includes("is not defined") || stderr.includes("Unexpected token");
8208
+ if (isRuntimeError) {
8209
+ this.logger.debug("Mutant %s caused runtime error (tests could not run): %s", mutantId, stderr.slice(0, 200));
8210
+ return {
8211
+ status: MutantRunStatus.Error,
8212
+ errorMessage: stderr.slice(0, 500) || `Runtime error with exit code ${result.exitCode}`
8152
8213
  };
8153
8214
  }
8154
- return {
8155
- status: MutantRunStatus.Survived,
8156
- nrOfTests: parsed.totalTests
8157
- };
8215
+ return null;
8216
+ }
8217
+ checkDryRunProcessResult(result, parsed) {
8218
+ if (result.timedOut) {
8219
+ this.logger.warn("Dry run timed out");
8220
+ return { status: DryRunStatus.Timeout };
8221
+ }
8222
+ if (result.exitCode !== 0 && parsed.failed === 0) {
8223
+ return {
8224
+ status: DryRunStatus.Error,
8225
+ errorMessage: `Bun test process failed with exit code ${result.exitCode}
8226
+ ${result.stderr}`
8227
+ };
8228
+ }
8229
+ return null;
8230
+ }
8231
+ async collectAndRemapCoverage(testHierarchy, executionOrder) {
8232
+ if (!this.coverageFilePath) {
8233
+ return;
8234
+ }
8235
+ const rawCoverage = await collectCoverage(this.coverageFilePath, this.logger);
8236
+ await cleanupCoverageFile(this.coverageFilePath);
8237
+ if (!rawCoverage) {
8238
+ return;
8239
+ }
8240
+ const testMap = new Map(testHierarchy.map((t) => [t.id, t]));
8241
+ return mapCoverageToInspectorIds(rawCoverage, executionOrder, testMap, this.logger);
8242
+ }
8243
+ buildLocalTestFilterIndex(testFilter) {
8244
+ const localRegistry = new Set(testFilter);
8245
+ const localSuffixRe = / \[\d+\]$/;
8246
+ const localBaseIndex = new Map;
8247
+ for (const id of localRegistry) {
8248
+ const base = localSuffixRe.test(id) ? id.replace(localSuffixRe, "") : id;
8249
+ const bucket = localBaseIndex.get(base);
8250
+ if (bucket) {
8251
+ bucket.push(id);
8252
+ } else {
8253
+ localBaseIndex.set(base, [id]);
8254
+ }
8255
+ if (base !== id) {
8256
+ localBaseIndex.set(id, [id]);
8257
+ }
8258
+ }
8259
+ return { localRegistry, localBaseIndex };
8260
+ }
8261
+ resolveKilledBy(rawFailedNames, localRegistry, localBaseIndex, mutantId) {
8262
+ const killedBySet = new Set;
8263
+ for (const name of rawFailedNames) {
8264
+ if (localRegistry.has(name)) {
8265
+ killedBySet.add(name);
8266
+ continue;
8267
+ }
8268
+ const localBucket = localBaseIndex.get(name);
8269
+ if (localBucket) {
8270
+ this.logger.debug('Expanded killedBy base name "%s" → %d local registry IDs for mutant %s', name, localBucket.length, mutantId);
8271
+ for (const id of localBucket) {
8272
+ killedBySet.add(id);
8273
+ }
8274
+ continue;
8275
+ }
8276
+ if (this.cachedTestNames?.has(name)) {
8277
+ killedBySet.add(name);
8278
+ continue;
8279
+ }
8280
+ const instanceBucket = this.baseNameIndex?.get(name);
8281
+ if (instanceBucket) {
8282
+ this.logger.debug('Expanded killedBy base name "%s" → %d instance registry IDs for mutant %s', name, instanceBucket.length, mutantId);
8283
+ for (const id of instanceBucket) {
8284
+ killedBySet.add(id);
8285
+ }
8286
+ continue;
8287
+ }
8288
+ this.logger.debug('killedBy name "%s" for mutant %s not found in test registry; including as-is', name, mutantId);
8289
+ killedBySet.add(name);
8290
+ }
8291
+ return [...killedBySet];
8158
8292
  }
8159
8293
  async dispose() {
8160
8294
  this.logger.debug("Disposing BunTestRunner");
@@ -8202,7 +8336,7 @@ var strykerValidationSchema = {
8202
8336
  timeout: {
8203
8337
  type: "number",
8204
8338
  minimum: 0,
8205
- description: "Timeout per test in milliseconds (default: 10000)",
8339
+ description: "Child-process timeout in milliseconds (default: 10000). Controls how long the entire bun test subprocess may run before being killed. Independent from the per-test timeout in bunfig.toml [test].timeout, which Bun uses to declare individual tests timed out.",
8206
8340
  default: 1e4
8207
8341
  },
8208
8342
  inspectorTimeout: {
@@ -8224,6 +8358,14 @@ var strykerValidationSchema = {
8224
8358
  items: {
8225
8359
  type: "string"
8226
8360
  }
8361
+ },
8362
+ testFiles: {
8363
+ type: "array",
8364
+ description: "Explicit list of test file paths (relative paths preferred in Stryker context — absolute paths bypass the sandbox copy and will NOT be mutated). When provided, skips auto-discovery and uses this list verbatim. Relative paths resolve against the bun subprocess's cwd. An empty array is invalid (use undefined/omit to enable auto-discovery).",
8365
+ minItems: 1,
8366
+ items: {
8367
+ type: "string"
8368
+ }
8227
8369
  }
8228
8370
  },
8229
8371
  additionalProperties: false