@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 +1 -0
- package/dist/index.js +105 -21
- package/package.json +1 -1
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
|
-
|
|
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
|
|
3670
|
-
|
|
3671
|
-
|
|
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:
|
|
3722
|
-
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:
|
|
3731
|
-
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:
|
|
3738
|
-
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
|
-
|
|
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
|
|
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:
|
|
3766
|
-
name:
|
|
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:
|
|
3777
|
-
name:
|
|
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:
|
|
3786
|
-
name:
|
|
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) =>
|
|
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"],
|