@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/README.md +1 -0
- package/dist/coverage/preload-logic.js +2 -2
- package/dist/index.d.ts +104 -1
- package/dist/index.js +761 -619
- package/dist/templates/coverage-preload.ts +14 -28
- package/package.json +4 -1
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
|
|
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(
|
|
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) =>
|
|
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 =
|
|
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, "
|
|
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, "
|
|
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 (
|
|
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] =
|
|
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 =
|
|
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, "
|
|
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.
|
|
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+$/.
|
|
5610
|
+
if (/@@test-\d+$/.test(firstKey)) {
|
|
5863
5611
|
return mapFilePrefixedCounterKeys(rawCoverage, executionOrder, testHierarchy, logger);
|
|
5864
5612
|
}
|
|
5865
|
-
if (/^test-\d+$/.
|
|
5613
|
+
if (/^test-\d+$/.test(firstKey)) {
|
|
5866
5614
|
return mapLegacyCounterKeys(rawCoverage, executionOrder, testHierarchy, logger);
|
|
5867
5615
|
}
|
|
5868
5616
|
return rawCoverage;
|
|
5869
5617
|
}
|
|
5870
|
-
function
|
|
5871
|
-
const
|
|
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
|
-
|
|
5622
|
+
appearances.set(mutantId, (appearances.get(mutantId) ?? 0) + 1);
|
|
5879
5623
|
}
|
|
5880
5624
|
}
|
|
5881
|
-
|
|
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
|
-
|
|
5888
|
-
|
|
5889
|
-
|
|
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
|
|
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
|
-
|
|
5929
|
-
|
|
5930
|
-
|
|
5931
|
-
|
|
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
|
|
5699
|
+
const testInfo = inspectorId === undefined ? undefined : testHierarchy.get(inspectorId);
|
|
5943
5700
|
if (testInfo) {
|
|
5944
|
-
|
|
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
|
|
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 (
|
|
5959
|
-
const
|
|
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] =
|
|
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).
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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:
|
|
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
|
|
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
|
|
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: ${
|
|
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
|
|
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((
|
|
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
|
-
|
|
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((
|
|
6350
|
+
return new Promise((resolve3, reject) => {
|
|
6318
6351
|
try {
|
|
6319
6352
|
this.httpServer = this.createHttpServer((req, res) => {
|
|
6320
|
-
if (req.url
|
|
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
|
-
|
|
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
|
-
|
|
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((
|
|
6411
|
+
await new Promise((resolve3) => {
|
|
6370
6412
|
this.httpServer.close(() => {
|
|
6371
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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.
|
|
7561
|
-
name = name.
|
|
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 `^(?:${
|
|
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
|
|
7583
|
-
|
|
7584
|
-
|
|
7585
|
-
|
|
7586
|
-
|
|
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
|
|
7696
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
7603
7697
|
for (const entry of entries) {
|
|
7604
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
7769
|
+
return path4.join(process.cwd(), ".stryker-bun-runner-registry.json");
|
|
7703
7770
|
}
|
|
7704
7771
|
get registryTmpPath() {
|
|
7705
|
-
return this.registryPath
|
|
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
|
-
|
|
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 =
|
|
7817
|
+
this.coverageFilePath = path4.join(tempDir, `coverage-${Date.now()}.json`);
|
|
7717
7818
|
this.logger.debug("Generating coverage preload script...");
|
|
7718
|
-
|
|
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
|
|
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 ??
|
|
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, "
|
|
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
|
|
7865
|
+
const code = err.code;
|
|
7763
7866
|
if (code === "ENOENT") {
|
|
7764
|
-
this.logger.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 (
|
|
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
|
-
|
|
7899
|
-
|
|
7900
|
-
|
|
7901
|
-
|
|
7902
|
-
|
|
7903
|
-
|
|
7904
|
-
|
|
7905
|
-
|
|
7906
|
-
|
|
7907
|
-
|
|
7908
|
-
|
|
7909
|
-
|
|
7910
|
-
|
|
7911
|
-
|
|
7912
|
-
|
|
7913
|
-
|
|
7914
|
-
|
|
7915
|
-
|
|
7916
|
-
|
|
7917
|
-
|
|
7918
|
-
|
|
7919
|
-
|
|
7920
|
-
|
|
7921
|
-
|
|
7922
|
-
|
|
7923
|
-
|
|
7924
|
-
|
|
7925
|
-
|
|
7926
|
-
|
|
7927
|
-
|
|
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
|
-
|
|
7934
|
-
|
|
7935
|
-
|
|
7936
|
-
|
|
7937
|
-
}
|
|
7938
|
-
|
|
7939
|
-
|
|
7940
|
-
|
|
7941
|
-
|
|
7942
|
-
|
|
7943
|
-
|
|
7944
|
-
|
|
7945
|
-
|
|
7946
|
-
|
|
7947
|
-
|
|
7948
|
-
|
|
7949
|
-
|
|
7950
|
-
|
|
7951
|
-
|
|
7952
|
-
|
|
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
|
-
|
|
7955
|
-
|
|
7956
|
-
|
|
7957
|
-
|
|
7958
|
-
|
|
7959
|
-
|
|
7960
|
-
|
|
7961
|
-
|
|
7962
|
-
|
|
7963
|
-
|
|
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.
|
|
7977
|
-
|
|
7978
|
-
|
|
8081
|
+
status: DryRunStatus.Complete,
|
|
8082
|
+
tests,
|
|
8083
|
+
mutantCoverage
|
|
7979
8084
|
};
|
|
8085
|
+
} finally {
|
|
8086
|
+
abortController.abort();
|
|
8087
|
+
await syncServer.close();
|
|
7980
8088
|
}
|
|
7981
|
-
|
|
7982
|
-
|
|
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 =
|
|
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:
|
|
8023
|
-
baseNameIndex:
|
|
8121
|
+
cachedTestNames: [...this.cachedTestNames],
|
|
8122
|
+
baseNameIndex: [...this.baseNameIndex.entries()]
|
|
8024
8123
|
});
|
|
8025
|
-
await fsPromises2.writeFile(tmpPath, registryData, "
|
|
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 (
|
|
8030
|
-
this.logger.warn("Failed to write dryRun registry file: %s",
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
8093
|
-
|
|
8094
|
-
|
|
8095
|
-
|
|
8096
|
-
|
|
8097
|
-
|
|
8098
|
-
|
|
8099
|
-
|
|
8100
|
-
|
|
8101
|
-
|
|
8102
|
-
|
|
8103
|
-
|
|
8104
|
-
|
|
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
|
-
|
|
8134
|
-
|
|
8135
|
-
|
|
8136
|
-
|
|
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`,
|
|
8143
|
-
|
|
8144
|
-
|
|
8145
|
-
|
|
8146
|
-
|
|
8147
|
-
|
|
8148
|
-
|
|
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
|
-
|
|
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
|
-
|
|
8156
|
-
|
|
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: "
|
|
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
|