@hughescr/stryker-bun-runner 1.1.2 → 1.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -21,6 +21,7 @@ export declare class BunTestRunner implements TestRunner {
21
21
  private readonly bunArgs?;
22
22
  private preloadScriptPath?;
23
23
  private coverageFilePath?;
24
+ private cachedTestNames?;
24
25
  constructor(logger: Logger, options: StrykerOptions);
25
26
  /**
26
27
  * Get test runner capabilities
package/dist/index.js CHANGED
@@ -3285,8 +3285,23 @@ function mapCoverageToInspectorIds(rawCoverage, executionOrder, testHierarchy) {
3285
3285
  if (counterIds.length !== executionOrder.length) {
3286
3286
  console.warn(`Coverage/execution count mismatch: ${counterIds.length} coverage entries vs ${executionOrder.length} executed tests. ` + `Performing partial mapping for ${Math.min(counterIds.length, executionOrder.length)} tests.`);
3287
3287
  }
3288
- const remappedPerTest = {};
3289
3288
  const maxIndex = Math.min(counterIds.length, executionOrder.length);
3289
+ const testNames = [];
3290
+ for (let i = 0;i < maxIndex; i++) {
3291
+ const inspectorId = executionOrder[i];
3292
+ const testInfo = testHierarchy.get(inspectorId);
3293
+ if (testInfo) {
3294
+ testNames.push(buildUniqueTestName(testInfo.fullName, testInfo.url));
3295
+ } else {
3296
+ testNames.push(`unknown-${inspectorId}`);
3297
+ }
3298
+ }
3299
+ const nameCounts = new Map;
3300
+ for (const name of testNames) {
3301
+ nameCounts.set(name, (nameCounts.get(name) ?? 0) + 1);
3302
+ }
3303
+ const remappedPerTest = {};
3304
+ const nameIndexes = new Map;
3290
3305
  for (let i = 0;i < maxIndex; i++) {
3291
3306
  const counterId = counterIds[i];
3292
3307
  const inspectorId = executionOrder[i];
@@ -3295,7 +3310,15 @@ function mapCoverageToInspectorIds(rawCoverage, executionOrder, testHierarchy) {
3295
3310
  console.warn(`Missing test info for inspector ID ${inspectorId} (counter ID: ${counterId}). Skipping this test in coverage mapping.`);
3296
3311
  continue;
3297
3312
  }
3298
- remappedPerTest[testInfo.fullName] = rawCoverage.perTest[counterId];
3313
+ const baseName = buildUniqueTestName(testInfo.fullName, testInfo.url);
3314
+ const count = nameCounts.get(baseName) ?? 1;
3315
+ let finalName = baseName;
3316
+ if (count > 1) {
3317
+ const index = nameIndexes.get(baseName) ?? 0;
3318
+ finalName = `${baseName} [${index}]`;
3319
+ nameIndexes.set(baseName, index + 1);
3320
+ }
3321
+ remappedPerTest[finalName] = rawCoverage.perTest[counterId];
3299
3322
  }
3300
3323
  return {
3301
3324
  static: rawCoverage.static,
@@ -3666,11 +3689,16 @@ function normalizeTestFilePath(url) {
3666
3689
  }
3667
3690
  return url;
3668
3691
  }
3669
- function stripFilePrefix(testName) {
3670
- const match = /^[^\s>]+\.(?:test|spec)\.[jt]sx? > (.+)$/.exec(testName);
3671
- return match ? match[1] : testName;
3692
+ function normalizeTestName(testName) {
3693
+ return testName.replace(/[^\x20-\x7E]/g, "_").trim();
3694
+ }
3695
+ function buildUniqueTestName(fullName, url) {
3696
+ const normalizedPath = normalizeTestFilePath(url);
3697
+ if (normalizedPath) {
3698
+ return normalizeTestName(`${normalizedPath} > ${fullName}`);
3699
+ }
3700
+ return normalizeTestName(fullName);
3672
3701
  }
3673
-
3674
3702
  class BunTestRunner {
3675
3703
  logger;
3676
3704
  static inject = tokens(commonTokens.logger, commonTokens.options);
@@ -3681,6 +3709,7 @@ class BunTestRunner {
3681
3709
  bunArgs;
3682
3710
  preloadScriptPath;
3683
3711
  coverageFilePath;
3712
+ cachedTestNames;
3684
3713
  constructor(logger, options) {
3685
3714
  this.logger = logger;
3686
3715
  const bunOptions = options.bun ?? {};
@@ -3716,10 +3745,11 @@ class BunTestRunner {
3716
3745
  buildTestsFromInspector(testHierarchy, executionOrder, parsed, totalElapsedMs) {
3717
3746
  if (executionOrder.length === 0) {
3718
3747
  return parsed.tests.map((t) => {
3748
+ const normalizedName = normalizeTestName(t.name);
3719
3749
  if (t.status === "failed") {
3720
3750
  return {
3721
- id: t.name,
3722
- name: t.name,
3751
+ id: normalizedName,
3752
+ name: normalizedName,
3723
3753
  status: TestStatus.Failed,
3724
3754
  failureMessage: t.failureMessage ?? "Test failed",
3725
3755
  timeSpentMs: Math.round(t.duration ?? 1)
@@ -3727,15 +3757,15 @@ class BunTestRunner {
3727
3757
  }
3728
3758
  if (t.status === "skipped") {
3729
3759
  return {
3730
- id: t.name,
3731
- name: t.name,
3760
+ id: normalizedName,
3761
+ name: normalizedName,
3732
3762
  status: TestStatus.Skipped,
3733
3763
  timeSpentMs: Math.round(t.duration ?? 1)
3734
3764
  };
3735
3765
  }
3736
3766
  return {
3737
- id: t.name,
3738
- name: t.name,
3767
+ id: normalizedName,
3768
+ name: normalizedName,
3739
3769
  status: TestStatus.Success,
3740
3770
  timeSpentMs: Math.round(t.duration ?? 1)
3741
3771
  };
@@ -3746,7 +3776,7 @@ class BunTestRunner {
3746
3776
  for (const test of testHierarchy) {
3747
3777
  testMap.set(test.id, test);
3748
3778
  }
3749
- return executionOrder.map((inspectorId) => {
3779
+ const tests = executionOrder.map((inspectorId) => {
3750
3780
  const testInfo = testMap.get(inspectorId);
3751
3781
  if (!testInfo) {
3752
3782
  return {
@@ -3756,14 +3786,14 @@ class BunTestRunner {
3756
3786
  timeSpentMs: timePerTest
3757
3787
  };
3758
3788
  }
3759
- const fullName = testInfo.fullName;
3789
+ const uniqueName = buildUniqueTestName(testInfo.fullName, testInfo.url);
3760
3790
  const status = testInfo.status;
3761
3791
  const elapsed = testInfo.elapsed !== undefined ? Math.round(testInfo.elapsed / 1e6) : timePerTest;
3762
3792
  if (status === "fail") {
3763
3793
  const parsedTest = parsed.tests.find((t) => t.name.includes(testInfo.name));
3764
3794
  return {
3765
- id: fullName,
3766
- name: fullName,
3795
+ id: uniqueName,
3796
+ name: uniqueName,
3767
3797
  fileName: normalizeTestFilePath(testInfo.url),
3768
3798
  startPosition: testInfo.line !== undefined ? { line: testInfo.line, column: 0 } : undefined,
3769
3799
  status: TestStatus.Failed,
@@ -3773,8 +3803,8 @@ class BunTestRunner {
3773
3803
  }
3774
3804
  if (status === "skip" || status === "todo") {
3775
3805
  return {
3776
- id: fullName,
3777
- name: fullName,
3806
+ id: uniqueName,
3807
+ name: uniqueName,
3778
3808
  fileName: normalizeTestFilePath(testInfo.url),
3779
3809
  startPosition: testInfo.line !== undefined ? { line: testInfo.line, column: 0 } : undefined,
3780
3810
  status: TestStatus.Skipped,
@@ -3782,14 +3812,31 @@ class BunTestRunner {
3782
3812
  };
3783
3813
  }
3784
3814
  return {
3785
- id: fullName,
3786
- name: fullName,
3815
+ id: uniqueName,
3816
+ name: uniqueName,
3787
3817
  fileName: normalizeTestFilePath(testInfo.url),
3788
3818
  startPosition: testInfo.line !== undefined ? { line: testInfo.line, column: 0 } : undefined,
3789
3819
  status: TestStatus.Success,
3790
3820
  timeSpentMs: elapsed
3791
3821
  };
3792
3822
  });
3823
+ const nameCounts = new Map;
3824
+ for (const test of tests) {
3825
+ nameCounts.set(test.name, (nameCounts.get(test.name) ?? 0) + 1);
3826
+ }
3827
+ const nameIndexes = new Map;
3828
+ for (const test of tests) {
3829
+ const originalName = test.name;
3830
+ const count = nameCounts.get(originalName) ?? 1;
3831
+ if (count > 1) {
3832
+ const index = nameIndexes.get(originalName) ?? 0;
3833
+ const uniqueName = `${originalName} [${index}]`;
3834
+ test.id = uniqueName;
3835
+ test.name = uniqueName;
3836
+ nameIndexes.set(originalName, index + 1);
3837
+ }
3838
+ }
3839
+ return tests;
3793
3840
  }
3794
3841
  async dryRun() {
3795
3842
  this.logger.debug("Running dry run with inspector-based coverage collection...");
@@ -3890,6 +3937,16 @@ ${result.stderr}`
3890
3937
  }
3891
3938
  const tests = this.buildTestsFromInspector(testHierarchy, executionOrder, parsed, totalElapsedMs);
3892
3939
  tests.sort((a, b) => a.name.localeCompare(b.name));
3940
+ this.cachedTestNames = new Set(tests.map((t) => t.name));
3941
+ if (tests.length !== this.cachedTestNames.size) {
3942
+ const nameCount = new Map;
3943
+ for (const test of tests) {
3944
+ nameCount.set(test.name, (nameCount.get(test.name) ?? 0) + 1);
3945
+ }
3946
+ const duplicates = Array.from(nameCount.entries()).filter(([_, count]) => count > 1).map(([name, count]) => `"${name}" (${count}x)`);
3947
+ 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(", "));
3948
+ }
3949
+ this.logger.debug("Cached %d test names from dry run for killedBy validation", this.cachedTestNames.size);
3893
3950
  return {
3894
3951
  status: DryRunStatus.Complete,
3895
3952
  tests,
@@ -3922,7 +3979,34 @@ ${result.stderr}`
3922
3979
  exitCode: result.exitCode
3923
3980
  });
3924
3981
  if (result.exitCode !== 0) {
3925
- const killedBy = parsed.tests.filter((test) => test.status === "failed").map((test) => stripFilePrefix(test.name));
3982
+ const killedBy = parsed.tests.filter((test) => test.status === "failed").map((test) => normalizeTestName(test.name));
3983
+ if (killedBy.length === 0 && parsed.tests.length === 0) {
3984
+ const stderr = result.stderr ?? "";
3985
+ 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");
3986
+ if (isRuntimeError) {
3987
+ this.logger.debug("Mutant %s caused runtime error (tests could not run): %s", options.activeMutant.id, stderr.slice(0, 200));
3988
+ return {
3989
+ status: MutantRunStatus.Error,
3990
+ errorMessage: stderr.slice(0, 500) || `Runtime error with exit code ${result.exitCode}`
3991
+ };
3992
+ }
3993
+ }
3994
+ if (killedBy.length === 0) {
3995
+ this.logger.debug('CACHE WARNING: No failed tests identified for mutant %s (will use "unknown" fallback). ' + "Exit code: %d, Parser found: %d total / %d passed / %d failed. " + "Parsed tests: %o", options.activeMutant.id, result.exitCode, parsed.totalTests, parsed.passed, parsed.failed, parsed.tests.map((t) => ({ name: t.name, status: t.status })));
3996
+ if (result.stdout || result.stderr) {
3997
+ const stdoutPreview = result.stdout?.slice(0, 500) || "(empty)";
3998
+ const stderrPreview = result.stderr?.slice(0, 500) || "(empty)";
3999
+ this.logger.debug("CACHE WARNING: Raw output for mutant %s - stdout: %s%s - stderr: %s%s", options.activeMutant.id, stdoutPreview, result.stdout && result.stdout.length > 500 ? "...(truncated)" : "", stderrPreview, result.stderr && result.stderr.length > 500 ? "...(truncated)" : "");
4000
+ }
4001
+ }
4002
+ if (killedBy.length > 0 && this.cachedTestNames) {
4003
+ const unknownKillers = killedBy.filter((k) => !this.cachedTestNames.has(k));
4004
+ if (unknownKillers.length > 0) {
4005
+ this.logger.debug("CACHE WARNING: killedBy for mutant %s contains %d test name(s) not in registry " + "(will break incremental cache): %s", options.activeMutant.id, unknownKillers.length, unknownKillers.join(", "));
4006
+ const sampleKnown = Array.from(this.cachedTestNames).slice(0, 5);
4007
+ this.logger.debug("CACHE WARNING: Sample of known test names from registry (first 5 of %d): %s", this.cachedTestNames.size, sampleKnown.join(", "));
4008
+ }
4009
+ }
3926
4010
  return {
3927
4011
  status: MutantRunStatus.Killed,
3928
4012
  killedBy: killedBy.length > 0 ? killedBy : ["unknown"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hughescr/stryker-bun-runner",
3
- "version": "1.1.2",
3
+ "version": "1.1.3",
4
4
  "description": "Stryker test runner plugin for Bun with perTest coverage support",
5
5
  "keywords": [
6
6
  "stryker",