@hughescr/stryker-bun-runner 1.2.1 → 1.2.2

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.d.ts CHANGED
@@ -83,7 +83,12 @@ export declare class BunTestRunner implements TestRunner {
83
83
  */
84
84
  private loadRegistryFile;
85
85
  /**
86
- * Build test results from inspector data
86
+ * Build test results from inspector data.
87
+ *
88
+ * @param inspectorIdToProjectFile - Optional mapping from inspector ID to project file path.
89
+ * When provided, the project file is used for TestResult.id, name, and fileName instead of
90
+ * testInfo.url. This is important for tests defined via helpers (e.g. RuleTester.run()) where
91
+ * Bun's inspector reports a url pointing to node_modules rather than the user's test file.
87
92
  */
88
93
  private buildTestsFromInspector;
89
94
  /**
package/dist/index.js CHANGED
@@ -5593,6 +5593,9 @@ function normalizeTestFilePath(url) {
5593
5593
  function normalizeTestName(testName) {
5594
5594
  return testName.replaceAll(/\p{Cc}/gu, "_").trim();
5595
5595
  }
5596
+ function buildProjectFileTestName(filePrefix, fullName) {
5597
+ return normalizeTestName(`${filePrefix} > ${fullName}`);
5598
+ }
5596
5599
  function buildUniqueTestName(fullName, url) {
5597
5600
  const normalizedPath = normalizeTestFilePath(url);
5598
5601
  if (normalizedPath) {
@@ -5604,16 +5607,16 @@ function buildUniqueTestName(fullName, url) {
5604
5607
  // src/coverage/coverage-mapper.ts
5605
5608
  function mapCoverageToInspectorIds(rawCoverage, executionOrder, testHierarchy, logger) {
5606
5609
  if (!rawCoverage?.perTest || Object.keys(rawCoverage.perTest).length === 0) {
5607
- return rawCoverage ?? undefined;
5610
+ return { coverage: rawCoverage ?? undefined, inspectorIdToProjectFile: new Map };
5608
5611
  }
5609
5612
  const firstKey = Object.keys(rawCoverage.perTest)[0];
5610
5613
  if (/@@test-\d+$/.test(firstKey)) {
5611
5614
  return mapFilePrefixedCounterKeys(rawCoverage, executionOrder, testHierarchy, logger);
5612
5615
  }
5613
5616
  if (/^test-\d+$/.test(firstKey)) {
5614
- return mapLegacyCounterKeys(rawCoverage, executionOrder, testHierarchy, logger);
5617
+ return { coverage: mapLegacyCounterKeys(rawCoverage, executionOrder, testHierarchy, logger), inspectorIdToProjectFile: new Map };
5615
5618
  }
5616
- return rawCoverage;
5619
+ return { coverage: rawCoverage, inspectorIdToProjectFile: new Map };
5617
5620
  }
5618
5621
  function countPerTestAppearances(perTestEntries) {
5619
5622
  const appearances = new Map;
@@ -5671,22 +5674,23 @@ function stabilizeCoverage(coverage) {
5671
5674
  const newPerTest = buildFilteredPerTest(perTestEntries, promoteToStatic);
5672
5675
  return { static: newStatic, perTest: newPerTest };
5673
5676
  }
5674
- function buildFileToInspectorIds(executionOrder, testHierarchy) {
5675
- const fileToInspectorIds = new Map;
5676
- for (const inspectorId of executionOrder) {
5677
- const testInfo = testHierarchy.get(inspectorId);
5678
- if (!testInfo) {
5679
- continue;
5680
- }
5681
- const relFile = normalizeTestFilePath(testInfo.url) ?? "";
5682
- const bucket = fileToInspectorIds.get(relFile);
5683
- if (bucket) {
5684
- bucket.push(inspectorId);
5685
- } else {
5686
- fileToInspectorIds.set(relFile, [inspectorId]);
5687
- }
5677
+ function pairKeysWithInspectorIds(globallyOrderedPerTestKeys, executionOrder, testHierarchy, logger) {
5678
+ const nonSkipped = executionOrder.filter((id) => {
5679
+ const status = testHierarchy.get(id)?.status;
5680
+ return status !== "skip" && status !== "todo";
5681
+ });
5682
+ if (globallyOrderedPerTestKeys.length < nonSkipped.length) {
5683
+ logger?.warn("Coverage/execution count mismatch: %s coverage entries vs %s non-skipped executed tests. " + "Performing partial mapping for %s tests.", globallyOrderedPerTestKeys.length, nonSkipped.length, Math.min(globallyOrderedPerTestKeys.length, nonSkipped.length));
5684
+ }
5685
+ const pairCount = Math.min(globallyOrderedPerTestKeys.length, nonSkipped.length);
5686
+ const pairs = [];
5687
+ for (let i = 0;i < pairCount; i++) {
5688
+ const key = globallyOrderedPerTestKeys[i];
5689
+ const sepIdx = key.indexOf("@@");
5690
+ const filePrefix = sepIdx === -1 ? key : key.slice(0, sepIdx);
5691
+ pairs.push({ filePrefix, inspectorId: nonSkipped[i] });
5688
5692
  }
5689
- return fileToInspectorIds;
5693
+ return pairs;
5690
5694
  }
5691
5695
  function resolveCounterKeys(counterIds, fileToInspectorIds, testHierarchy, logger) {
5692
5696
  return counterIds.map((key) => {
@@ -5695,51 +5699,115 @@ function resolveCounterKeys(counterIds, fileToInspectorIds, testHierarchy, logge
5695
5699
  const counterStr = key.slice(sepIdx + 2 + "test-".length);
5696
5700
  const n = Number.parseInt(counterStr, 10);
5697
5701
  const fileIds = fileToInspectorIds.get(filePrefix);
5698
- const inspectorId = fileIds?.[n - 1];
5702
+ const clampedIdx = fileIds ? Math.min(n - 1, fileIds.length - 1) : undefined;
5703
+ const inspectorId = clampedIdx === undefined ? undefined : fileIds?.[clampedIdx];
5699
5704
  const testInfo = inspectorId === undefined ? undefined : testHierarchy.get(inspectorId);
5700
5705
  if (testInfo) {
5701
- return { name: buildUniqueTestName(testInfo.fullName, testInfo.url), testInfo };
5706
+ const testName = buildProjectFileTestName(filePrefix, testInfo.fullName);
5707
+ return { name: testName, testInfo, inspectorId };
5702
5708
  }
5703
5709
  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 };
5710
+ return { name: `unknown-${key}`, testInfo: null, inspectorId: undefined };
5705
5711
  });
5706
5712
  }
5707
- function buildRemappedPerTest(counterIds, resolved, rawPerTest) {
5708
- const nameCounts = new Map;
5709
- for (const { name } of resolved) {
5710
- nameCounts.set(name, (nameCounts.get(name) ?? 0) + 1);
5713
+ function buildNameInspectorIds(resolved) {
5714
+ const nameInspectorIds = new Map;
5715
+ for (const { name, inspectorId, testInfo } of resolved) {
5716
+ if (!testInfo) {
5717
+ continue;
5718
+ }
5719
+ let ids = nameInspectorIds.get(name);
5720
+ if (!ids) {
5721
+ ids = new Set;
5722
+ nameInspectorIds.set(name, ids);
5723
+ }
5724
+ ids.add(inspectorId);
5711
5725
  }
5726
+ return nameInspectorIds;
5727
+ }
5728
+ function resolveEachTestName(baseName, inspectorId, nameInspectorIds, nameIndexes) {
5729
+ const distinctIds = nameInspectorIds.get(baseName);
5730
+ if ((distinctIds?.size ?? 1) <= 1) {
5731
+ return baseName;
5732
+ }
5733
+ const key_ = `${inspectorId}/${baseName}`;
5734
+ const existingIndex = nameIndexes.get(key_);
5735
+ if (existingIndex === undefined) {
5736
+ const nextIndex = nameIndexes.get(baseName) ?? 0;
5737
+ nameIndexes.set(key_, nextIndex);
5738
+ nameIndexes.set(baseName, nextIndex + 1);
5739
+ return `${baseName} [${nextIndex}]`;
5740
+ }
5741
+ return `${baseName} [${existingIndex}]`;
5742
+ }
5743
+ function buildRemappedPerTest(counterIds, resolved, rawPerTest) {
5744
+ const nameInspectorIds = buildNameInspectorIds(resolved);
5712
5745
  const remappedPerTest = {};
5713
5746
  const nameIndexes = new Map;
5714
5747
  for (const [i, key] of counterIds.entries()) {
5715
- const { name: baseName, testInfo } = resolved[i];
5748
+ const { name: baseName, testInfo, inspectorId } = resolved[i];
5716
5749
  if (!testInfo) {
5717
5750
  continue;
5718
5751
  }
5719
- const count = nameCounts.get(baseName) ?? 1;
5720
- let finalName = baseName;
5721
- if (count > 1) {
5722
- const index = nameIndexes.get(baseName) ?? 0;
5723
- finalName = `${baseName} [${index}]`;
5724
- nameIndexes.set(baseName, index + 1);
5752
+ const finalName = resolveEachTestName(baseName, inspectorId, nameInspectorIds, nameIndexes);
5753
+ const incoming = rawPerTest[key];
5754
+ const existing = remappedPerTest[finalName];
5755
+ if (existing) {
5756
+ for (const [mutantId, count_] of Object.entries(incoming)) {
5757
+ existing[mutantId] = (existing[mutantId] ?? 0) + count_;
5758
+ }
5759
+ } else {
5760
+ remappedPerTest[finalName] = { ...incoming };
5725
5761
  }
5726
- remappedPerTest[finalName] = rawPerTest[key];
5727
5762
  }
5728
5763
  return remappedPerTest;
5729
5764
  }
5765
+ function warnInteriorGapIfPresent(pairs, testHierarchy, logger) {
5766
+ const warnedFiles = new Set;
5767
+ for (const { filePrefix, inspectorId } of pairs) {
5768
+ const testUrl = testHierarchy.get(inspectorId)?.url;
5769
+ if (!testUrl) {
5770
+ continue;
5771
+ }
5772
+ const inspectorFile = normalizeTestFilePath(testUrl);
5773
+ if (!inspectorFile || testUrl.includes("node_modules")) {
5774
+ continue;
5775
+ }
5776
+ if (filePrefix !== inspectorFile && !warnedFiles.has(filePrefix)) {
5777
+ warnedFiles.add(filePrefix);
5778
+ logger?.warn('Interior coverage gap detected for "%s": coverage key paired with inspector test from "%s". ' + "Some tests may have been aborted mid-run (e.g. beforeAll failure). Coverage mapping may be inaccurate.", filePrefix, inspectorFile);
5779
+ }
5780
+ }
5781
+ }
5730
5782
  function mapFilePrefixedCounterKeys(rawCoverage, executionOrder, testHierarchy, logger) {
5731
- const fileToInspectorIds = buildFileToInspectorIds(executionOrder, testHierarchy);
5732
- const counterIds = Object.keys(rawCoverage.perTest).toSorted((a, b) => {
5783
+ const globallyOrderedKeys = Object.keys(rawCoverage.perTest);
5784
+ const pairs = pairKeysWithInspectorIds(globallyOrderedKeys, executionOrder, testHierarchy, logger);
5785
+ const fileToInspectorIds = new Map;
5786
+ const inspectorIdToProjectFile = new Map;
5787
+ for (const { filePrefix, inspectorId } of pairs) {
5788
+ const bucket = fileToInspectorIds.get(filePrefix);
5789
+ if (bucket) {
5790
+ bucket.push(inspectorId);
5791
+ } else {
5792
+ fileToInspectorIds.set(filePrefix, [inspectorId]);
5793
+ }
5794
+ inspectorIdToProjectFile.set(inspectorId, filePrefix);
5795
+ }
5796
+ if (globallyOrderedKeys.length === pairs.length) {
5797
+ warnInteriorGapIfPresent(pairs, testHierarchy, logger);
5798
+ }
5799
+ const counterIds = globallyOrderedKeys.toSorted((a, b) => {
5733
5800
  const nA = Number.parseInt(a.split("@@test-")[1] ?? "0", 10);
5734
5801
  const nB = Number.parseInt(b.split("@@test-")[1] ?? "0", 10);
5735
5802
  return nA - nB;
5736
5803
  });
5737
5804
  const resolved = resolveCounterKeys(counterIds, fileToInspectorIds, testHierarchy, logger);
5738
5805
  const remappedPerTest = buildRemappedPerTest(counterIds, resolved, rawCoverage.perTest);
5739
- return stabilizeCoverage({
5806
+ const coverage = stabilizeCoverage({
5740
5807
  static: rawCoverage.static,
5741
5808
  perTest: remappedPerTest
5742
5809
  });
5810
+ return { coverage, inspectorIdToProjectFile };
5743
5811
  }
5744
5812
  function mapLegacyCounterKeys(rawCoverage, executionOrder, testHierarchy, logger) {
5745
5813
  const counterIds = Object.keys(rawCoverage.perTest).toSorted((a, b) => Number.parseInt(a.split("-")[1] ?? "0", 10) - Number.parseInt(b.split("-")[1] ?? "0", 10));
@@ -7870,7 +7938,7 @@ class BunTestRunner {
7870
7938
  }
7871
7939
  }
7872
7940
  }
7873
- buildTestsFromInspector(testHierarchy, executionOrder, parsed, totalElapsedMs) {
7941
+ buildTestsFromInspector(testHierarchy, executionOrder, parsed, totalElapsedMs, inspectorIdToProjectFile) {
7874
7942
  if (executionOrder.length === 0) {
7875
7943
  return parsed.tests.map((t) => {
7876
7944
  const normalizedName = normalizeTestName(t.name);
@@ -7914,7 +7982,9 @@ class BunTestRunner {
7914
7982
  timeSpentMs: timePerTest
7915
7983
  };
7916
7984
  }
7917
- const uniqueName = buildUniqueTestName(testInfo.fullName, testInfo.url);
7985
+ const projectFile = inspectorIdToProjectFile?.get(inspectorId);
7986
+ const uniqueName = projectFile ? buildProjectFileTestName(projectFile, testInfo.fullName) : buildUniqueTestName(testInfo.fullName, testInfo.url);
7987
+ const fileName = projectFile ?? normalizeTestFilePath(testInfo.url);
7918
7988
  const status = testInfo.status;
7919
7989
  const elapsed = testInfo.elapsed === undefined ? timePerTest : Math.round(testInfo.elapsed / 1e6);
7920
7990
  const startPosition = testInfo.line === undefined ? undefined : { line: testInfo.line, column: 0 };
@@ -7923,7 +7993,7 @@ class BunTestRunner {
7923
7993
  return {
7924
7994
  id: uniqueName,
7925
7995
  name: uniqueName,
7926
- fileName: normalizeTestFilePath(testInfo.url),
7996
+ fileName,
7927
7997
  startPosition,
7928
7998
  status: TestStatus.Failed,
7929
7999
  failureMessage: parsedTest?.failureMessage ?? testInfo.error?.message ?? "Test failed",
@@ -7934,7 +8004,7 @@ class BunTestRunner {
7934
8004
  return {
7935
8005
  id: uniqueName,
7936
8006
  name: uniqueName,
7937
- fileName: normalizeTestFilePath(testInfo.url),
8007
+ fileName,
7938
8008
  startPosition,
7939
8009
  status: TestStatus.Skipped,
7940
8010
  timeSpentMs: elapsed
@@ -7943,7 +8013,7 @@ class BunTestRunner {
7943
8013
  return {
7944
8014
  id: uniqueName,
7945
8015
  name: uniqueName,
7946
- fileName: normalizeTestFilePath(testInfo.url),
8016
+ fileName,
7947
8017
  startPosition,
7948
8018
  status: TestStatus.Success,
7949
8019
  timeSpentMs: elapsed
@@ -8073,8 +8143,8 @@ exit=%s timedOut=%s
8073
8143
  if (earlyResult) {
8074
8144
  return earlyResult;
8075
8145
  }
8076
- const mutantCoverage = await this.collectAndRemapCoverage(testHierarchy, executionOrder);
8077
- const tests = this.buildTestsFromInspector(testHierarchy, executionOrder, parsed, totalElapsedMs);
8146
+ const { coverage: mutantCoverage, inspectorIdToProjectFile } = await this.collectAndRemapCoverage(testHierarchy, executionOrder);
8147
+ const tests = this.buildTestsFromInspector(testHierarchy, executionOrder, parsed, totalElapsedMs, inspectorIdToProjectFile);
8078
8148
  tests.sort((a, b) => a.name.localeCompare(b.name));
8079
8149
  await this.buildAndPersistTestRegistry(tests);
8080
8150
  return {
@@ -8229,16 +8299,17 @@ ${result.stderr}`
8229
8299
  return null;
8230
8300
  }
8231
8301
  async collectAndRemapCoverage(testHierarchy, executionOrder) {
8302
+ const testMap = new Map(testHierarchy.map((t) => [t.id, t]));
8232
8303
  if (!this.coverageFilePath) {
8233
- return;
8304
+ return { coverage: undefined, inspectorIdToProjectFile: new Map };
8234
8305
  }
8235
8306
  const rawCoverage = await collectCoverage(this.coverageFilePath, this.logger);
8236
8307
  await cleanupCoverageFile(this.coverageFilePath);
8237
8308
  if (!rawCoverage) {
8238
- return;
8309
+ return { coverage: undefined, inspectorIdToProjectFile: new Map };
8239
8310
  }
8240
- const testMap = new Map(testHierarchy.map((t) => [t.id, t]));
8241
- return mapCoverageToInspectorIds(rawCoverage, executionOrder, testMap, this.logger);
8311
+ const { coverage, inspectorIdToProjectFile } = mapCoverageToInspectorIds(rawCoverage, executionOrder, testMap, this.logger);
8312
+ return { coverage, inspectorIdToProjectFile };
8242
8313
  }
8243
8314
  buildLocalTestFilterIndex(testFilter) {
8244
8315
  const localRegistry = new Set(testFilter);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hughescr/stryker-bun-runner",
3
- "version": "1.2.1",
3
+ "version": "1.2.2",
4
4
  "description": "Stryker test runner plugin for Bun with perTest coverage support",
5
5
  "keywords": [
6
6
  "stryker",