@hughescr/stryker-bun-runner 1.2.0 → 1.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/coverage/preload-logic.js +2 -2
- package/dist/index.d.ts +110 -2
- package/dist/index.js +861 -648
- 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,10 @@ 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();
|
|
5595
|
+
}
|
|
5596
|
+
function buildProjectFileTestName(filePrefix, fullName) {
|
|
5597
|
+
return normalizeTestName(`${filePrefix} > ${fullName}`);
|
|
5847
5598
|
}
|
|
5848
5599
|
function buildUniqueTestName(fullName, url) {
|
|
5849
5600
|
const normalizedPath = normalizeTestFilePath(url);
|
|
@@ -5856,46 +5607,36 @@ function buildUniqueTestName(fullName, url) {
|
|
|
5856
5607
|
// src/coverage/coverage-mapper.ts
|
|
5857
5608
|
function mapCoverageToInspectorIds(rawCoverage, executionOrder, testHierarchy, logger) {
|
|
5858
5609
|
if (!rawCoverage?.perTest || Object.keys(rawCoverage.perTest).length === 0) {
|
|
5859
|
-
return rawCoverage;
|
|
5610
|
+
return { coverage: rawCoverage ?? undefined, inspectorIdToProjectFile: new Map };
|
|
5860
5611
|
}
|
|
5861
5612
|
const firstKey = Object.keys(rawCoverage.perTest)[0];
|
|
5862
|
-
if (/@@test-\d+$/.
|
|
5613
|
+
if (/@@test-\d+$/.test(firstKey)) {
|
|
5863
5614
|
return mapFilePrefixedCounterKeys(rawCoverage, executionOrder, testHierarchy, logger);
|
|
5864
5615
|
}
|
|
5865
|
-
if (/^test-\d+$/.
|
|
5866
|
-
return mapLegacyCounterKeys(rawCoverage, executionOrder, testHierarchy, logger);
|
|
5616
|
+
if (/^test-\d+$/.test(firstKey)) {
|
|
5617
|
+
return { coverage: mapLegacyCounterKeys(rawCoverage, executionOrder, testHierarchy, logger), inspectorIdToProjectFile: new Map };
|
|
5867
5618
|
}
|
|
5868
|
-
return rawCoverage;
|
|
5619
|
+
return { coverage: rawCoverage, inspectorIdToProjectFile: new Map };
|
|
5869
5620
|
}
|
|
5870
|
-
function
|
|
5871
|
-
const
|
|
5872
|
-
if (perTestEntries.length === 0) {
|
|
5873
|
-
return coverage;
|
|
5874
|
-
}
|
|
5875
|
-
const perTestAppearances = new Map;
|
|
5621
|
+
function countPerTestAppearances(perTestEntries) {
|
|
5622
|
+
const appearances = new Map;
|
|
5876
5623
|
for (const [, counts] of perTestEntries) {
|
|
5877
5624
|
for (const mutantId of Object.keys(counts)) {
|
|
5878
|
-
|
|
5625
|
+
appearances.set(mutantId, (appearances.get(mutantId) ?? 0) + 1);
|
|
5879
5626
|
}
|
|
5880
5627
|
}
|
|
5881
|
-
|
|
5628
|
+
return appearances;
|
|
5629
|
+
}
|
|
5630
|
+
function buildPromoteToStaticSet(existingStaticKeys, perTestAppearances) {
|
|
5631
|
+
const promoteToStatic = new Set(existingStaticKeys);
|
|
5882
5632
|
for (const [mutantId, count] of perTestAppearances) {
|
|
5883
5633
|
if (count > 1) {
|
|
5884
5634
|
promoteToStatic.add(mutantId);
|
|
5885
5635
|
}
|
|
5886
5636
|
}
|
|
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
|
-
}
|
|
5637
|
+
return promoteToStatic;
|
|
5638
|
+
}
|
|
5639
|
+
function buildFilteredPerTest(perTestEntries, promoteToStatic) {
|
|
5899
5640
|
const newPerTest = {};
|
|
5900
5641
|
for (const [testId, counts] of perTestEntries) {
|
|
5901
5642
|
const filteredCounts = {};
|
|
@@ -5908,76 +5649,168 @@ function stabilizeCoverage(coverage) {
|
|
|
5908
5649
|
newPerTest[testId] = filteredCounts;
|
|
5909
5650
|
}
|
|
5910
5651
|
}
|
|
5911
|
-
return
|
|
5652
|
+
return newPerTest;
|
|
5912
5653
|
}
|
|
5913
|
-
function
|
|
5914
|
-
const
|
|
5915
|
-
|
|
5916
|
-
|
|
5917
|
-
|
|
5918
|
-
|
|
5919
|
-
|
|
5920
|
-
|
|
5921
|
-
|
|
5922
|
-
|
|
5923
|
-
|
|
5924
|
-
|
|
5925
|
-
|
|
5654
|
+
function stabilizeCoverage(coverage) {
|
|
5655
|
+
const perTestEntries = Object.entries(coverage.perTest);
|
|
5656
|
+
if (perTestEntries.length === 0) {
|
|
5657
|
+
return coverage;
|
|
5658
|
+
}
|
|
5659
|
+
const existingStaticKeys = Object.keys(coverage.static);
|
|
5660
|
+
const perTestAppearances = countPerTestAppearances(perTestEntries);
|
|
5661
|
+
const promoteToStatic = buildPromoteToStaticSet(existingStaticKeys, perTestAppearances);
|
|
5662
|
+
const existingStatic = new Set(existingStaticKeys);
|
|
5663
|
+
const hasNewPromotions = [...promoteToStatic].some((id) => !existingStatic.has(id));
|
|
5664
|
+
const hasPerTestContamination = perTestEntries.some(([, counts]) => Object.keys(counts).some((id) => promoteToStatic.has(id)));
|
|
5665
|
+
if (!hasNewPromotions && !hasPerTestContamination) {
|
|
5666
|
+
return coverage;
|
|
5667
|
+
}
|
|
5668
|
+
const newStatic = { ...coverage.static };
|
|
5669
|
+
for (const mutantId of promoteToStatic) {
|
|
5670
|
+
if (!(mutantId in newStatic)) {
|
|
5671
|
+
newStatic[mutantId] = 1;
|
|
5926
5672
|
}
|
|
5927
5673
|
}
|
|
5928
|
-
const
|
|
5929
|
-
|
|
5930
|
-
|
|
5931
|
-
|
|
5674
|
+
const newPerTest = buildFilteredPerTest(perTestEntries, promoteToStatic);
|
|
5675
|
+
return { static: newStatic, perTest: newPerTest };
|
|
5676
|
+
}
|
|
5677
|
+
function pairKeysWithInspectorIds(globallyOrderedPerTestKeys, executionOrder, testHierarchy, logger) {
|
|
5678
|
+
const nonSkipped = executionOrder.filter((id) => {
|
|
5679
|
+
const status = testHierarchy.get(id)?.status;
|
|
5680
|
+
return status !== "skip" && status !== "todo";
|
|
5932
5681
|
});
|
|
5933
|
-
|
|
5934
|
-
|
|
5935
|
-
|
|
5682
|
+
if (globallyOrderedPerTestKeys.length < nonSkipped.length) {
|
|
5683
|
+
logger?.warn("Coverage/execution count mismatch: %s coverage entries vs %s non-skipped executed tests. " + "Performing partial mapping for %s tests.", globallyOrderedPerTestKeys.length, nonSkipped.length, Math.min(globallyOrderedPerTestKeys.length, nonSkipped.length));
|
|
5684
|
+
}
|
|
5685
|
+
const pairCount = Math.min(globallyOrderedPerTestKeys.length, nonSkipped.length);
|
|
5686
|
+
const pairs = [];
|
|
5687
|
+
for (let i = 0;i < pairCount; i++) {
|
|
5688
|
+
const key = globallyOrderedPerTestKeys[i];
|
|
5689
|
+
const sepIdx = key.indexOf("@@");
|
|
5690
|
+
const filePrefix = sepIdx === -1 ? key : key.slice(0, sepIdx);
|
|
5691
|
+
pairs.push({ filePrefix, inspectorId: nonSkipped[i] });
|
|
5692
|
+
}
|
|
5693
|
+
return pairs;
|
|
5694
|
+
}
|
|
5695
|
+
function resolveCounterKeys(counterIds, fileToInspectorIds, testHierarchy, logger) {
|
|
5696
|
+
return counterIds.map((key) => {
|
|
5936
5697
|
const sepIdx = key.indexOf("@@");
|
|
5937
5698
|
const filePrefix = key.slice(0, sepIdx);
|
|
5938
5699
|
const counterStr = key.slice(sepIdx + 2 + "test-".length);
|
|
5939
|
-
const n = parseInt(counterStr, 10);
|
|
5700
|
+
const n = Number.parseInt(counterStr, 10);
|
|
5940
5701
|
const fileIds = fileToInspectorIds.get(filePrefix);
|
|
5941
|
-
const
|
|
5942
|
-
const
|
|
5702
|
+
const clampedIdx = fileIds ? Math.min(n - 1, fileIds.length - 1) : undefined;
|
|
5703
|
+
const inspectorId = clampedIdx === undefined ? undefined : fileIds?.[clampedIdx];
|
|
5704
|
+
const testInfo = inspectorId === undefined ? undefined : testHierarchy.get(inspectorId);
|
|
5943
5705
|
if (testInfo) {
|
|
5944
|
-
|
|
5945
|
-
|
|
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);
|
|
5706
|
+
const testName = buildProjectFileTestName(filePrefix, testInfo.fullName);
|
|
5707
|
+
return { name: testName, testInfo, inspectorId };
|
|
5950
5708
|
}
|
|
5709
|
+
logger?.warn('Coverage key %s: no inspector test found for file "%s" at position %s ' + "(file has %s tests in execution order). Skipping this test in coverage mapping.", key, filePrefix, n, fileToInspectorIds.get(filePrefix)?.length ?? 0);
|
|
5710
|
+
return { name: `unknown-${key}`, testInfo: null, inspectorId: undefined };
|
|
5711
|
+
});
|
|
5712
|
+
}
|
|
5713
|
+
function buildNameInspectorIds(resolved) {
|
|
5714
|
+
const nameInspectorIds = new Map;
|
|
5715
|
+
for (const { name, inspectorId, testInfo } of resolved) {
|
|
5716
|
+
if (!testInfo) {
|
|
5717
|
+
continue;
|
|
5718
|
+
}
|
|
5719
|
+
let ids = nameInspectorIds.get(name);
|
|
5720
|
+
if (!ids) {
|
|
5721
|
+
ids = new Set;
|
|
5722
|
+
nameInspectorIds.set(name, ids);
|
|
5723
|
+
}
|
|
5724
|
+
ids.add(inspectorId);
|
|
5951
5725
|
}
|
|
5952
|
-
|
|
5953
|
-
|
|
5954
|
-
|
|
5955
|
-
|
|
5726
|
+
return nameInspectorIds;
|
|
5727
|
+
}
|
|
5728
|
+
function resolveEachTestName(baseName, inspectorId, nameInspectorIds, nameIndexes) {
|
|
5729
|
+
const distinctIds = nameInspectorIds.get(baseName);
|
|
5730
|
+
if ((distinctIds?.size ?? 1) <= 1) {
|
|
5731
|
+
return baseName;
|
|
5732
|
+
}
|
|
5733
|
+
const key_ = `${inspectorId}/${baseName}`;
|
|
5734
|
+
const existingIndex = nameIndexes.get(key_);
|
|
5735
|
+
if (existingIndex === undefined) {
|
|
5736
|
+
const nextIndex = nameIndexes.get(baseName) ?? 0;
|
|
5737
|
+
nameIndexes.set(key_, nextIndex);
|
|
5738
|
+
nameIndexes.set(baseName, nextIndex + 1);
|
|
5739
|
+
return `${baseName} [${nextIndex}]`;
|
|
5740
|
+
}
|
|
5741
|
+
return `${baseName} [${existingIndex}]`;
|
|
5742
|
+
}
|
|
5743
|
+
function buildRemappedPerTest(counterIds, resolved, rawPerTest) {
|
|
5744
|
+
const nameInspectorIds = buildNameInspectorIds(resolved);
|
|
5956
5745
|
const remappedPerTest = {};
|
|
5957
5746
|
const nameIndexes = new Map;
|
|
5958
|
-
for (
|
|
5959
|
-
const
|
|
5960
|
-
const testInfo = resolvedInfos[i];
|
|
5747
|
+
for (const [i, key] of counterIds.entries()) {
|
|
5748
|
+
const { name: baseName, testInfo, inspectorId } = resolved[i];
|
|
5961
5749
|
if (!testInfo) {
|
|
5962
5750
|
continue;
|
|
5963
5751
|
}
|
|
5964
|
-
const
|
|
5965
|
-
const
|
|
5966
|
-
|
|
5967
|
-
if (
|
|
5968
|
-
const
|
|
5969
|
-
|
|
5970
|
-
|
|
5752
|
+
const finalName = resolveEachTestName(baseName, inspectorId, nameInspectorIds, nameIndexes);
|
|
5753
|
+
const incoming = rawPerTest[key];
|
|
5754
|
+
const existing = remappedPerTest[finalName];
|
|
5755
|
+
if (existing) {
|
|
5756
|
+
for (const [mutantId, count_] of Object.entries(incoming)) {
|
|
5757
|
+
existing[mutantId] = (existing[mutantId] ?? 0) + count_;
|
|
5758
|
+
}
|
|
5759
|
+
} else {
|
|
5760
|
+
remappedPerTest[finalName] = { ...incoming };
|
|
5971
5761
|
}
|
|
5972
|
-
remappedPerTest[finalName] = rawCoverage.perTest[key];
|
|
5973
5762
|
}
|
|
5974
|
-
return
|
|
5763
|
+
return remappedPerTest;
|
|
5764
|
+
}
|
|
5765
|
+
function warnInteriorGapIfPresent(pairs, testHierarchy, logger) {
|
|
5766
|
+
const warnedFiles = new Set;
|
|
5767
|
+
for (const { filePrefix, inspectorId } of pairs) {
|
|
5768
|
+
const testUrl = testHierarchy.get(inspectorId)?.url;
|
|
5769
|
+
if (!testUrl) {
|
|
5770
|
+
continue;
|
|
5771
|
+
}
|
|
5772
|
+
const inspectorFile = normalizeTestFilePath(testUrl);
|
|
5773
|
+
if (!inspectorFile || testUrl.includes("node_modules")) {
|
|
5774
|
+
continue;
|
|
5775
|
+
}
|
|
5776
|
+
if (filePrefix !== inspectorFile && !warnedFiles.has(filePrefix)) {
|
|
5777
|
+
warnedFiles.add(filePrefix);
|
|
5778
|
+
logger?.warn('Interior coverage gap detected for "%s": coverage key paired with inspector test from "%s". ' + "Some tests may have been aborted mid-run (e.g. beforeAll failure). Coverage mapping may be inaccurate.", filePrefix, inspectorFile);
|
|
5779
|
+
}
|
|
5780
|
+
}
|
|
5781
|
+
}
|
|
5782
|
+
function mapFilePrefixedCounterKeys(rawCoverage, executionOrder, testHierarchy, logger) {
|
|
5783
|
+
const globallyOrderedKeys = Object.keys(rawCoverage.perTest);
|
|
5784
|
+
const pairs = pairKeysWithInspectorIds(globallyOrderedKeys, executionOrder, testHierarchy, logger);
|
|
5785
|
+
const fileToInspectorIds = new Map;
|
|
5786
|
+
const inspectorIdToProjectFile = new Map;
|
|
5787
|
+
for (const { filePrefix, inspectorId } of pairs) {
|
|
5788
|
+
const bucket = fileToInspectorIds.get(filePrefix);
|
|
5789
|
+
if (bucket) {
|
|
5790
|
+
bucket.push(inspectorId);
|
|
5791
|
+
} else {
|
|
5792
|
+
fileToInspectorIds.set(filePrefix, [inspectorId]);
|
|
5793
|
+
}
|
|
5794
|
+
inspectorIdToProjectFile.set(inspectorId, filePrefix);
|
|
5795
|
+
}
|
|
5796
|
+
if (globallyOrderedKeys.length === pairs.length) {
|
|
5797
|
+
warnInteriorGapIfPresent(pairs, testHierarchy, logger);
|
|
5798
|
+
}
|
|
5799
|
+
const counterIds = globallyOrderedKeys.toSorted((a, b) => {
|
|
5800
|
+
const nA = Number.parseInt(a.split("@@test-")[1] ?? "0", 10);
|
|
5801
|
+
const nB = Number.parseInt(b.split("@@test-")[1] ?? "0", 10);
|
|
5802
|
+
return nA - nB;
|
|
5803
|
+
});
|
|
5804
|
+
const resolved = resolveCounterKeys(counterIds, fileToInspectorIds, testHierarchy, logger);
|
|
5805
|
+
const remappedPerTest = buildRemappedPerTest(counterIds, resolved, rawCoverage.perTest);
|
|
5806
|
+
const coverage = stabilizeCoverage({
|
|
5975
5807
|
static: rawCoverage.static,
|
|
5976
5808
|
perTest: remappedPerTest
|
|
5977
5809
|
});
|
|
5810
|
+
return { coverage, inspectorIdToProjectFile };
|
|
5978
5811
|
}
|
|
5979
5812
|
function mapLegacyCounterKeys(rawCoverage, executionOrder, testHierarchy, logger) {
|
|
5980
|
-
const counterIds = Object.keys(rawCoverage.perTest).
|
|
5813
|
+
const counterIds = Object.keys(rawCoverage.perTest).toSorted((a, b) => Number.parseInt(a.split("-")[1] ?? "0", 10) - Number.parseInt(b.split("-")[1] ?? "0", 10));
|
|
5981
5814
|
if (counterIds.length !== executionOrder.length) {
|
|
5982
5815
|
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
5816
|
}
|
|
@@ -6021,10 +5854,6 @@ function mapLegacyCounterKeys(rawCoverage, executionOrder, testHierarchy, logger
|
|
|
6021
5854
|
perTest: remappedPerTest
|
|
6022
5855
|
});
|
|
6023
5856
|
}
|
|
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
5857
|
|
|
6029
5858
|
// node_modules/ws/wrapper.mjs
|
|
6030
5859
|
var import_stream = __toESM(require_stream(), 1);
|
|
@@ -6086,7 +5915,7 @@ class InspectorClient {
|
|
|
6086
5915
|
if (this.ws) {
|
|
6087
5916
|
throw new Error("Already connected");
|
|
6088
5917
|
}
|
|
6089
|
-
return new Promise((
|
|
5918
|
+
return new Promise((resolve3, reject) => {
|
|
6090
5919
|
const timeoutTimer = setTimeout(() => {
|
|
6091
5920
|
if (this.ws) {
|
|
6092
5921
|
this.ws.close();
|
|
@@ -6098,7 +5927,7 @@ class InspectorClient {
|
|
|
6098
5927
|
this.ws = ws;
|
|
6099
5928
|
ws.addEventListener("open", () => {
|
|
6100
5929
|
clearTimeout(timeoutTimer);
|
|
6101
|
-
|
|
5930
|
+
resolve3();
|
|
6102
5931
|
});
|
|
6103
5932
|
ws.addEventListener("error", () => {
|
|
6104
5933
|
clearTimeout(timeoutTimer);
|
|
@@ -6120,12 +5949,12 @@ class InspectorClient {
|
|
|
6120
5949
|
}
|
|
6121
5950
|
const id = ++this.messageId;
|
|
6122
5951
|
const message = { id, method, params };
|
|
6123
|
-
return new Promise((
|
|
5952
|
+
return new Promise((resolve3, reject) => {
|
|
6124
5953
|
const timer = setTimeout(() => {
|
|
6125
5954
|
this.pendingRequests.delete(id);
|
|
6126
5955
|
reject(new InspectorTimeoutError(`Request timeout after ${this.state.requestTimeout}ms: ${method}`));
|
|
6127
5956
|
}, this.state.requestTimeout);
|
|
6128
|
-
this.pendingRequests.set(id, { resolve:
|
|
5957
|
+
this.pendingRequests.set(id, { resolve: resolve3, reject, timer });
|
|
6129
5958
|
try {
|
|
6130
5959
|
this.ws.send(JSON.stringify(message));
|
|
6131
5960
|
} catch (error) {
|
|
@@ -6141,7 +5970,7 @@ class InspectorClient {
|
|
|
6141
5970
|
}
|
|
6142
5971
|
this.isClosing = true;
|
|
6143
5972
|
const error = new InspectorConnectionError("Connection closed");
|
|
6144
|
-
for (const pending of
|
|
5973
|
+
for (const pending of this.pendingRequests.values()) {
|
|
6145
5974
|
clearTimeout(pending.timer);
|
|
6146
5975
|
pending.reject(error);
|
|
6147
5976
|
}
|
|
@@ -6152,7 +5981,7 @@ class InspectorClient {
|
|
|
6152
5981
|
this.ws = null;
|
|
6153
5982
|
}
|
|
6154
5983
|
getTests() {
|
|
6155
|
-
return
|
|
5984
|
+
return [...this.testHierarchy.values()];
|
|
6156
5985
|
}
|
|
6157
5986
|
getExecutionOrder() {
|
|
6158
5987
|
return [...this.executionOrder];
|
|
@@ -6240,7 +6069,7 @@ class InspectorClient {
|
|
|
6240
6069
|
let currentId = parentId;
|
|
6241
6070
|
while (currentId !== undefined) {
|
|
6242
6071
|
if (visited.has(currentId)) {
|
|
6243
|
-
this.handleError(new Error(`Circular reference detected in test hierarchy: ${
|
|
6072
|
+
this.handleError(new Error(`Circular reference detected in test hierarchy: ${[...visited].join(" -> ")} -> ${currentId}`));
|
|
6244
6073
|
break;
|
|
6245
6074
|
}
|
|
6246
6075
|
visited.add(currentId);
|
|
@@ -6258,7 +6087,7 @@ class InspectorClient {
|
|
|
6258
6087
|
return;
|
|
6259
6088
|
}
|
|
6260
6089
|
const error = new InspectorConnectionError("Connection closed unexpectedly");
|
|
6261
|
-
for (const pending of
|
|
6090
|
+
for (const pending of this.pendingRequests.values()) {
|
|
6262
6091
|
clearTimeout(pending.timer);
|
|
6263
6092
|
pending.reject(error);
|
|
6264
6093
|
}
|
|
@@ -6271,10 +6100,281 @@ class InspectorClient {
|
|
|
6271
6100
|
}
|
|
6272
6101
|
}
|
|
6273
6102
|
}
|
|
6103
|
+
// src/parsers/console-parser.ts
|
|
6104
|
+
function parseFilePath(line) {
|
|
6105
|
+
const fileMatch = /^([\w./-]+\.(?:test|spec)\.(?:ts|tsx|js|jsx|mts|mjs)):$/.exec(line);
|
|
6106
|
+
return fileMatch ? fileMatch[1] : null;
|
|
6107
|
+
}
|
|
6108
|
+
function buildTestName(testName, currentFile) {
|
|
6109
|
+
return currentFile ? `${currentFile} > ${testName}` : testName;
|
|
6110
|
+
}
|
|
6111
|
+
function parseTestLine(line, currentFile) {
|
|
6112
|
+
const passMatch = /^✓ +(\S.*?) \[([0-9.]+)ms\]$/.exec(line);
|
|
6113
|
+
if (passMatch) {
|
|
6114
|
+
return {
|
|
6115
|
+
test: {
|
|
6116
|
+
name: buildTestName(passMatch[1].trim(), currentFile),
|
|
6117
|
+
file: currentFile,
|
|
6118
|
+
status: "passed",
|
|
6119
|
+
duration: Number.parseFloat(passMatch[2])
|
|
6120
|
+
}
|
|
6121
|
+
};
|
|
6122
|
+
}
|
|
6123
|
+
const failMatch = /^✗ +(\S.*?)(?: \[([0-9.]+)ms\])?$/.exec(line);
|
|
6124
|
+
if (failMatch) {
|
|
6125
|
+
return {
|
|
6126
|
+
test: {
|
|
6127
|
+
name: buildTestName(failMatch[1].trim(), currentFile),
|
|
6128
|
+
file: currentFile,
|
|
6129
|
+
status: "failed",
|
|
6130
|
+
duration: failMatch[2] ? Number.parseFloat(failMatch[2]) : undefined
|
|
6131
|
+
},
|
|
6132
|
+
startedCollectingError: true
|
|
6133
|
+
};
|
|
6134
|
+
}
|
|
6135
|
+
const bailFailMatch = /^\(fail\) +(\S.*?)(?: \[([0-9.]+)ms\])?$/.exec(line);
|
|
6136
|
+
if (bailFailMatch) {
|
|
6137
|
+
return {
|
|
6138
|
+
test: {
|
|
6139
|
+
name: buildTestName(bailFailMatch[1].trim(), currentFile),
|
|
6140
|
+
file: currentFile,
|
|
6141
|
+
status: "failed",
|
|
6142
|
+
duration: bailFailMatch[2] ? Number.parseFloat(bailFailMatch[2]) : undefined
|
|
6143
|
+
},
|
|
6144
|
+
startedCollectingError: true
|
|
6145
|
+
};
|
|
6146
|
+
}
|
|
6147
|
+
const skipMatch = /^⏭ +(\S.*)$/.exec(line);
|
|
6148
|
+
if (skipMatch) {
|
|
6149
|
+
return {
|
|
6150
|
+
test: {
|
|
6151
|
+
name: buildTestName(skipMatch[1].trim(), currentFile),
|
|
6152
|
+
file: currentFile,
|
|
6153
|
+
status: "skipped"
|
|
6154
|
+
}
|
|
6155
|
+
};
|
|
6156
|
+
}
|
|
6157
|
+
return {};
|
|
6158
|
+
}
|
|
6159
|
+
function finalizeErrorMessage(currentTest, errorLines) {
|
|
6160
|
+
if (currentTest && errorLines.length > 0) {
|
|
6161
|
+
currentTest.failureMessage = errorLines.join(`
|
|
6162
|
+
`).trim();
|
|
6163
|
+
}
|
|
6164
|
+
}
|
|
6165
|
+
function shouldCollectErrorLine(line) {
|
|
6166
|
+
if (!line.trim()) {
|
|
6167
|
+
return false;
|
|
6168
|
+
}
|
|
6169
|
+
return !/^\s*\d+\s+(?:pass|fail|skip)/.test(line);
|
|
6170
|
+
}
|
|
6171
|
+
function updateCounters(test, counters, parseResult) {
|
|
6172
|
+
if (test.status === "passed") {
|
|
6173
|
+
counters.passed++;
|
|
6174
|
+
return false;
|
|
6175
|
+
} else if (test.status === "failed") {
|
|
6176
|
+
counters.failed++;
|
|
6177
|
+
return parseResult.startedCollectingError ?? false;
|
|
6178
|
+
} else {
|
|
6179
|
+
counters.skipped++;
|
|
6180
|
+
return false;
|
|
6181
|
+
}
|
|
6182
|
+
}
|
|
6183
|
+
function parseSummaryLines(output) {
|
|
6184
|
+
const counts = { passed: 0, failed: 0, skipped: 0 };
|
|
6185
|
+
const passSummary = /\s(\d+)\s+pass\b/.exec(output);
|
|
6186
|
+
const failSummary = /\s(\d+)\s+fail\b/.exec(output);
|
|
6187
|
+
const skipSummary = /\s(\d+)\s+skip\b/.exec(output);
|
|
6188
|
+
const bailSummary = /Bailed out after (\d+) failures?/.exec(output);
|
|
6189
|
+
if (passSummary) {
|
|
6190
|
+
counts.passed = Number.parseInt(passSummary[1], 10);
|
|
6191
|
+
}
|
|
6192
|
+
if (failSummary) {
|
|
6193
|
+
counts.failed = Number.parseInt(failSummary[1], 10);
|
|
6194
|
+
}
|
|
6195
|
+
if (skipSummary) {
|
|
6196
|
+
counts.skipped = Number.parseInt(skipSummary[1], 10);
|
|
6197
|
+
}
|
|
6198
|
+
if (bailSummary) {
|
|
6199
|
+
counts.failed = Math.max(counts.failed, Number.parseInt(bailSummary[1], 10));
|
|
6200
|
+
}
|
|
6201
|
+
const ranTestsSummary = /Ran\s+(\d+)\s+tests?/.exec(output);
|
|
6202
|
+
if (ranTestsSummary) {
|
|
6203
|
+
const totalFromRan = Number.parseInt(ranTestsSummary[1], 10);
|
|
6204
|
+
const totalParsed = counts.passed + counts.failed + counts.skipped;
|
|
6205
|
+
if (totalParsed !== totalFromRan && counts.passed === 0 && counts.failed === 0) {
|
|
6206
|
+
counts.passed = totalFromRan;
|
|
6207
|
+
}
|
|
6208
|
+
}
|
|
6209
|
+
return counts;
|
|
6210
|
+
}
|
|
6211
|
+
function parseBunTestOutput(stdout, stderr) {
|
|
6212
|
+
const tests = [];
|
|
6213
|
+
const counters = { passed: 0, failed: 0, skipped: 0 };
|
|
6214
|
+
const output = `${stdout}
|
|
6215
|
+
${stderr}`;
|
|
6216
|
+
const lines = output.split(`
|
|
6217
|
+
`);
|
|
6218
|
+
let currentTest = null;
|
|
6219
|
+
let collectingError = false;
|
|
6220
|
+
let errorLines = [];
|
|
6221
|
+
let currentFile;
|
|
6222
|
+
for (const line of lines) {
|
|
6223
|
+
const filePath = parseFilePath(line);
|
|
6224
|
+
if (filePath) {
|
|
6225
|
+
currentFile = filePath;
|
|
6226
|
+
continue;
|
|
6227
|
+
}
|
|
6228
|
+
const parseResult = parseTestLine(line, currentFile);
|
|
6229
|
+
if (parseResult.test) {
|
|
6230
|
+
if (currentTest && collectingError) {
|
|
6231
|
+
finalizeErrorMessage(currentTest, errorLines);
|
|
6232
|
+
errorLines = [];
|
|
6233
|
+
}
|
|
6234
|
+
currentTest = parseResult.test;
|
|
6235
|
+
tests.push(currentTest);
|
|
6236
|
+
collectingError = updateCounters(currentTest, counters, parseResult);
|
|
6237
|
+
continue;
|
|
6238
|
+
}
|
|
6239
|
+
if (collectingError && currentTest && shouldCollectErrorLine(line)) {
|
|
6240
|
+
errorLines.push(line);
|
|
6241
|
+
}
|
|
6242
|
+
}
|
|
6243
|
+
if (currentTest && collectingError) {
|
|
6244
|
+
finalizeErrorMessage(currentTest, errorLines);
|
|
6245
|
+
}
|
|
6246
|
+
const summaryCounts = parseSummaryLines(output);
|
|
6247
|
+
counters.passed = Math.max(counters.passed, summaryCounts.passed);
|
|
6248
|
+
counters.failed = Math.max(counters.failed, summaryCounts.failed);
|
|
6249
|
+
counters.skipped = Math.max(counters.skipped, summaryCounts.skipped);
|
|
6250
|
+
return {
|
|
6251
|
+
tests,
|
|
6252
|
+
totalTests: counters.passed + counters.failed + counters.skipped,
|
|
6253
|
+
passed: counters.passed,
|
|
6254
|
+
failed: counters.failed,
|
|
6255
|
+
skipped: counters.skipped
|
|
6256
|
+
};
|
|
6257
|
+
}
|
|
6258
|
+
|
|
6259
|
+
// src/process-runner.ts
|
|
6260
|
+
import { spawn } from "node:child_process";
|
|
6261
|
+
async function runBunTests(options) {
|
|
6262
|
+
const args = ["test"];
|
|
6263
|
+
if (options.inspectWaitPort) {
|
|
6264
|
+
args.push(`--inspect=${options.inspectWaitPort}`);
|
|
6265
|
+
}
|
|
6266
|
+
if (options.bunfigPath) {
|
|
6267
|
+
args.push(`--config=${options.bunfigPath}`);
|
|
6268
|
+
}
|
|
6269
|
+
if (options.preloadScript) {
|
|
6270
|
+
args.push("--preload", options.preloadScript);
|
|
6271
|
+
}
|
|
6272
|
+
if (options.testNamePattern) {
|
|
6273
|
+
args.push("--test-name-pattern", options.testNamePattern);
|
|
6274
|
+
}
|
|
6275
|
+
if (options.bail) {
|
|
6276
|
+
args.push("--bail");
|
|
6277
|
+
}
|
|
6278
|
+
if (options.sequentialMode) {
|
|
6279
|
+
args.push("--concurrency=1");
|
|
6280
|
+
}
|
|
6281
|
+
if (options.bunArgs && options.bunArgs.length > 0) {
|
|
6282
|
+
args.push(...options.bunArgs);
|
|
6283
|
+
}
|
|
6284
|
+
if (options.testFiles && options.testFiles.length > 0) {
|
|
6285
|
+
args.push(...options.testFiles);
|
|
6286
|
+
}
|
|
6287
|
+
const env = {
|
|
6288
|
+
...process.env,
|
|
6289
|
+
...options.env
|
|
6290
|
+
};
|
|
6291
|
+
if (options.activeMutant) {
|
|
6292
|
+
env.__STRYKER_ACTIVE_MUTANT__ = options.activeMutant;
|
|
6293
|
+
}
|
|
6294
|
+
if (options.coverageFile) {
|
|
6295
|
+
env.__STRYKER_COVERAGE_FILE__ = options.coverageFile;
|
|
6296
|
+
}
|
|
6297
|
+
if (options.syncPort) {
|
|
6298
|
+
env.__STRYKER_SYNC_PORT__ = String(options.syncPort);
|
|
6299
|
+
}
|
|
6300
|
+
return new Promise((resolve3) => {
|
|
6301
|
+
const stdoutChunks = [];
|
|
6302
|
+
const stderrChunks = [];
|
|
6303
|
+
let timedOut = false;
|
|
6304
|
+
let processKilled = false;
|
|
6305
|
+
if (options.signal?.aborted) {
|
|
6306
|
+
resolve3({ stdout: "", stderr: "", exitCode: null, timedOut: true });
|
|
6307
|
+
return;
|
|
6308
|
+
}
|
|
6309
|
+
const spawnOpts = {
|
|
6310
|
+
env,
|
|
6311
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
6312
|
+
cwd: process.cwd()
|
|
6313
|
+
};
|
|
6314
|
+
const childProcess = spawn(options.bunPath, args, spawnOpts);
|
|
6315
|
+
const timeoutHandle = setTimeout(() => {
|
|
6316
|
+
timedOut = true;
|
|
6317
|
+
processKilled = true;
|
|
6318
|
+
childProcess.kill("SIGKILL");
|
|
6319
|
+
}, options.timeout);
|
|
6320
|
+
if (options.signal) {
|
|
6321
|
+
const onAbort = () => {
|
|
6322
|
+
clearTimeout(timeoutHandle);
|
|
6323
|
+
processKilled = true;
|
|
6324
|
+
timedOut = true;
|
|
6325
|
+
childProcess.kill("SIGTERM");
|
|
6326
|
+
setTimeout(() => {
|
|
6327
|
+
childProcess.kill("SIGKILL");
|
|
6328
|
+
}, 500);
|
|
6329
|
+
};
|
|
6330
|
+
options.signal.addEventListener("abort", onAbort, { once: true });
|
|
6331
|
+
}
|
|
6332
|
+
if (childProcess.stdout) {
|
|
6333
|
+
childProcess.stdout.on("data", (data) => {
|
|
6334
|
+
stdoutChunks.push(data);
|
|
6335
|
+
});
|
|
6336
|
+
}
|
|
6337
|
+
let inspectorUrlExtracted = false;
|
|
6338
|
+
if (childProcess.stderr) {
|
|
6339
|
+
childProcess.stderr.on("data", (data) => {
|
|
6340
|
+
stderrChunks.push(data);
|
|
6341
|
+
if (options.inspectWaitPort && !inspectorUrlExtracted && options.onInspectorReady) {
|
|
6342
|
+
const text = Buffer.concat(stderrChunks).toString();
|
|
6343
|
+
const match = /Listening:[\t\v\f\r \u00A0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF]*\n\s*(ws:\/\/\S+)/.exec(text);
|
|
6344
|
+
if (match) {
|
|
6345
|
+
inspectorUrlExtracted = true;
|
|
6346
|
+
options.onInspectorReady(match[1]);
|
|
6347
|
+
}
|
|
6348
|
+
}
|
|
6349
|
+
});
|
|
6350
|
+
}
|
|
6351
|
+
childProcess.on("close", (code) => {
|
|
6352
|
+
clearTimeout(timeoutHandle);
|
|
6353
|
+
resolve3({
|
|
6354
|
+
stdout: Buffer.concat(stdoutChunks).toString(),
|
|
6355
|
+
stderr: Buffer.concat(stderrChunks).toString(),
|
|
6356
|
+
exitCode: processKilled ? null : code,
|
|
6357
|
+
timedOut
|
|
6358
|
+
});
|
|
6359
|
+
});
|
|
6360
|
+
childProcess.on("error", (error) => {
|
|
6361
|
+
clearTimeout(timeoutHandle);
|
|
6362
|
+
const stderrOutput = Buffer.concat(stderrChunks).toString();
|
|
6363
|
+
resolve3({
|
|
6364
|
+
stdout: Buffer.concat(stdoutChunks).toString(),
|
|
6365
|
+
stderr: `${stderrOutput}
|
|
6366
|
+
Process error: ${error.message}`,
|
|
6367
|
+
exitCode: null,
|
|
6368
|
+
timedOut
|
|
6369
|
+
});
|
|
6370
|
+
});
|
|
6371
|
+
});
|
|
6372
|
+
}
|
|
6373
|
+
|
|
6274
6374
|
// src/utils/port.ts
|
|
6275
|
-
import { createServer } from "net";
|
|
6375
|
+
import { createServer } from "node:net";
|
|
6276
6376
|
async function getAvailablePort() {
|
|
6277
|
-
return new Promise((
|
|
6377
|
+
return new Promise((resolve3, reject) => {
|
|
6278
6378
|
const server = createServer();
|
|
6279
6379
|
server.on("error", (err) => {
|
|
6280
6380
|
reject(new Error(`Failed to get available port: ${err.message}`));
|
|
@@ -6292,7 +6392,7 @@ async function getAvailablePort() {
|
|
|
6292
6392
|
reject(new Error(`Failed to close server: ${err.message}`));
|
|
6293
6393
|
return;
|
|
6294
6394
|
}
|
|
6295
|
-
|
|
6395
|
+
resolve3(port);
|
|
6296
6396
|
});
|
|
6297
6397
|
});
|
|
6298
6398
|
});
|
|
@@ -6303,6 +6403,7 @@ class SyncServer {
|
|
|
6303
6403
|
httpServer = null;
|
|
6304
6404
|
wss = null;
|
|
6305
6405
|
clients = new Set;
|
|
6406
|
+
readyLatched = false;
|
|
6306
6407
|
port;
|
|
6307
6408
|
createHttpServer;
|
|
6308
6409
|
WebSocketServerClass;
|
|
@@ -6314,15 +6415,15 @@ class SyncServer {
|
|
|
6314
6415
|
this.webSocketOpenState = options.webSocketOpenState ?? import_websocket.default.OPEN;
|
|
6315
6416
|
}
|
|
6316
6417
|
async start() {
|
|
6317
|
-
return new Promise((
|
|
6418
|
+
return new Promise((resolve3, reject) => {
|
|
6318
6419
|
try {
|
|
6319
6420
|
this.httpServer = this.createHttpServer((req, res) => {
|
|
6320
|
-
if (req.url
|
|
6321
|
-
res.writeHead(404);
|
|
6322
|
-
res.end("Not found");
|
|
6323
|
-
} else {
|
|
6421
|
+
if (req.url === "/sync") {
|
|
6324
6422
|
res.writeHead(400);
|
|
6325
6423
|
res.end("WebSocket upgrade failed");
|
|
6424
|
+
} else {
|
|
6425
|
+
res.writeHead(404);
|
|
6426
|
+
res.end("Not found");
|
|
6326
6427
|
}
|
|
6327
6428
|
});
|
|
6328
6429
|
this.wss = new this.WebSocketServerClass({
|
|
@@ -6331,6 +6432,11 @@ class SyncServer {
|
|
|
6331
6432
|
});
|
|
6332
6433
|
this.wss.on("connection", (ws) => {
|
|
6333
6434
|
this.clients.add(ws);
|
|
6435
|
+
if (this.readyLatched && ws.readyState === this.webSocketOpenState) {
|
|
6436
|
+
try {
|
|
6437
|
+
ws.send("ready");
|
|
6438
|
+
} catch {}
|
|
6439
|
+
}
|
|
6334
6440
|
ws.on("close", () => {
|
|
6335
6441
|
this.clients.delete(ws);
|
|
6336
6442
|
});
|
|
@@ -6338,7 +6444,7 @@ class SyncServer {
|
|
|
6338
6444
|
});
|
|
6339
6445
|
this.httpServer.on("error", reject);
|
|
6340
6446
|
this.httpServer.listen(this.port, () => {
|
|
6341
|
-
|
|
6447
|
+
resolve3();
|
|
6342
6448
|
});
|
|
6343
6449
|
} catch (error) {
|
|
6344
6450
|
reject(error instanceof Error ? error : new Error(String(error)));
|
|
@@ -6346,6 +6452,7 @@ class SyncServer {
|
|
|
6346
6452
|
});
|
|
6347
6453
|
}
|
|
6348
6454
|
signalReady() {
|
|
6455
|
+
this.readyLatched = true;
|
|
6349
6456
|
for (const client of this.clients) {
|
|
6350
6457
|
try {
|
|
6351
6458
|
if (client.readyState === this.webSocketOpenState) {
|
|
@@ -6355,6 +6462,7 @@ class SyncServer {
|
|
|
6355
6462
|
}
|
|
6356
6463
|
}
|
|
6357
6464
|
async close() {
|
|
6465
|
+
this.readyLatched = false;
|
|
6358
6466
|
for (const client of this.clients) {
|
|
6359
6467
|
try {
|
|
6360
6468
|
client.close();
|
|
@@ -6362,13 +6470,15 @@ class SyncServer {
|
|
|
6362
6470
|
}
|
|
6363
6471
|
this.clients.clear();
|
|
6364
6472
|
if (this.wss) {
|
|
6365
|
-
|
|
6473
|
+
await new Promise((resolve3) => {
|
|
6474
|
+
this.wss.close(() => resolve3());
|
|
6475
|
+
});
|
|
6366
6476
|
this.wss = null;
|
|
6367
6477
|
}
|
|
6368
6478
|
if (this.httpServer) {
|
|
6369
|
-
await new Promise((
|
|
6479
|
+
await new Promise((resolve3) => {
|
|
6370
6480
|
this.httpServer.close(() => {
|
|
6371
|
-
|
|
6481
|
+
resolve3();
|
|
6372
6482
|
});
|
|
6373
6483
|
});
|
|
6374
6484
|
this.httpServer = null;
|
|
@@ -6380,7 +6490,7 @@ class SyncServer {
|
|
|
6380
6490
|
}
|
|
6381
6491
|
// src/utils/bunfig-sanitizer.ts
|
|
6382
6492
|
import { readFile as readFile3, unlink as unlink3, writeFile as writeFile2, mkdir as mkdir2 } from "node:fs/promises";
|
|
6383
|
-
import
|
|
6493
|
+
import path2 from "node:path";
|
|
6384
6494
|
|
|
6385
6495
|
// node_modules/smol-toml/dist/error.js
|
|
6386
6496
|
/*!
|
|
@@ -7490,7 +7600,7 @@ function absolutizePath(value, projectCwd) {
|
|
|
7490
7600
|
if (typeof value !== "string") {
|
|
7491
7601
|
return value;
|
|
7492
7602
|
}
|
|
7493
|
-
return
|
|
7603
|
+
return path2.isAbsolute(value) ? value : path2.resolve(projectCwd, value);
|
|
7494
7604
|
}
|
|
7495
7605
|
function absolutizePathValue(value, projectCwd) {
|
|
7496
7606
|
if (Array.isArray(value)) {
|
|
@@ -7501,7 +7611,7 @@ function absolutizePathValue(value, projectCwd) {
|
|
|
7501
7611
|
async function generateSanitizedBunfig(projectCwd, tmpDir) {
|
|
7502
7612
|
await mkdir2(tmpDir, { recursive: true });
|
|
7503
7613
|
let rawConfig = {};
|
|
7504
|
-
const bunfigPath =
|
|
7614
|
+
const bunfigPath = path2.join(projectCwd, "bunfig.toml");
|
|
7505
7615
|
try {
|
|
7506
7616
|
const content = await readFile3(bunfigPath, "utf8");
|
|
7507
7617
|
rawConfig = parse(content);
|
|
@@ -7525,7 +7635,7 @@ async function generateSanitizedBunfig(projectCwd, tmpDir) {
|
|
|
7525
7635
|
sanitizedTest.onlyFailures = false;
|
|
7526
7636
|
sanitized.test = sanitizedTest;
|
|
7527
7637
|
const serialized = stringify(sanitized);
|
|
7528
|
-
const outPath =
|
|
7638
|
+
const outPath = path2.join(tmpDir, `stryker-bun-runner-bunfig-${process.pid}-${Date.now()}.toml`);
|
|
7529
7639
|
await writeFile2(outPath, serialized, "utf8");
|
|
7530
7640
|
return outPath;
|
|
7531
7641
|
}
|
|
@@ -7545,20 +7655,14 @@ function buildTestNamePattern(testFilter) {
|
|
|
7545
7655
|
}
|
|
7546
7656
|
const fileExtRe = /\.(?:test|spec)\.(?:[jt]sx?|m[jt]s)$/;
|
|
7547
7657
|
const dedupSuffixRe = / \[\d+\]$/;
|
|
7548
|
-
const
|
|
7549
|
-
const metaRe = /[.*+?^${}()|[\]\\\/]/g;
|
|
7658
|
+
const metaRe = /[.*+?^${}()|[\]\\/]/g;
|
|
7550
7659
|
const alternatives = new Set;
|
|
7551
7660
|
for (const id of testFilter) {
|
|
7552
7661
|
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
|
-
}
|
|
7662
|
+
let name = firstSepIdx !== -1 && fileExtRe.test(id.slice(0, firstSepIdx)) ? id.slice(firstSepIdx + 3) : id;
|
|
7559
7663
|
name = name.replace(dedupSuffixRe, "");
|
|
7560
|
-
name = name.
|
|
7561
|
-
name = name.
|
|
7664
|
+
name = name.replaceAll(" > ", " ");
|
|
7665
|
+
name = name.replaceAll(metaRe, String.raw`\$&`);
|
|
7562
7666
|
if (name.length > 0) {
|
|
7563
7667
|
alternatives.add(name);
|
|
7564
7668
|
}
|
|
@@ -7566,27 +7670,85 @@ function buildTestNamePattern(testFilter) {
|
|
|
7566
7670
|
if (alternatives.size === 0) {
|
|
7567
7671
|
return;
|
|
7568
7672
|
}
|
|
7569
|
-
return `^(?:${
|
|
7673
|
+
return `^(?:${[...alternatives].join("|")})$`;
|
|
7570
7674
|
}
|
|
7571
7675
|
// src/utils/test-file-discovery.ts
|
|
7572
|
-
import { join as join2, relative as relative2 } from "node:path";
|
|
7573
7676
|
import * as fsPromises from "node:fs/promises";
|
|
7677
|
+
import path3 from "node:path";
|
|
7678
|
+
var testFileRe = /\.(?:test|spec)\.(?:[jt]sx?|m[jt]s)$/;
|
|
7679
|
+
var excludedDirs = new Set(["node_modules", ".stryker-tmp", "dist", "build", ".git"]);
|
|
7680
|
+
function hasExcludedAncestor(absolutePath) {
|
|
7681
|
+
return absolutePath.split("/").some((seg) => excludedDirs.has(seg));
|
|
7682
|
+
}
|
|
7683
|
+
async function tryRealpath(p) {
|
|
7684
|
+
try {
|
|
7685
|
+
return await fsPromises.realpath(p);
|
|
7686
|
+
} catch {
|
|
7687
|
+
return;
|
|
7688
|
+
}
|
|
7689
|
+
}
|
|
7690
|
+
async function tryStat(p) {
|
|
7691
|
+
try {
|
|
7692
|
+
return await fsPromises.stat(p);
|
|
7693
|
+
} catch {
|
|
7694
|
+
return;
|
|
7695
|
+
}
|
|
7696
|
+
}
|
|
7697
|
+
async function handleSymlinkDir(fullPath, entryName, ctx) {
|
|
7698
|
+
if (excludedDirs.has(entryName)) {
|
|
7699
|
+
return;
|
|
7700
|
+
}
|
|
7701
|
+
const resolvedTarget = await tryRealpath(fullPath);
|
|
7702
|
+
if (!resolvedTarget) {
|
|
7703
|
+
return;
|
|
7704
|
+
}
|
|
7705
|
+
if (hasExcludedAncestor(resolvedTarget)) {
|
|
7706
|
+
ctx.logger?.debug("discoverTestFiles: symlink %s resolves to excluded path %s; skipping", fullPath, resolvedTarget);
|
|
7707
|
+
} else {
|
|
7708
|
+
await ctx.walk(fullPath);
|
|
7709
|
+
}
|
|
7710
|
+
}
|
|
7711
|
+
async function handleSymlinkFile(fullPath, entryName, ctx) {
|
|
7712
|
+
if (!testFileRe.test(entryName)) {
|
|
7713
|
+
return;
|
|
7714
|
+
}
|
|
7715
|
+
const resolvedTarget = await tryRealpath(fullPath);
|
|
7716
|
+
if (resolvedTarget && !hasExcludedAncestor(resolvedTarget)) {
|
|
7717
|
+
ctx.results.push(path3.relative(ctx.cwd, fullPath));
|
|
7718
|
+
}
|
|
7719
|
+
}
|
|
7720
|
+
async function handleSymlink(fullPath, entryName, ctx) {
|
|
7721
|
+
const stat3 = await tryStat(fullPath);
|
|
7722
|
+
if (!stat3) {
|
|
7723
|
+
ctx.logger?.debug("discoverTestFiles: broken symlink at %s; skipping", fullPath);
|
|
7724
|
+
return;
|
|
7725
|
+
}
|
|
7726
|
+
if (stat3.isDirectory()) {
|
|
7727
|
+
await handleSymlinkDir(fullPath, entryName, ctx);
|
|
7728
|
+
} else if (stat3.isFile()) {
|
|
7729
|
+
await handleSymlinkFile(fullPath, entryName, ctx);
|
|
7730
|
+
}
|
|
7731
|
+
}
|
|
7732
|
+
async function processEntry(entry, dir, ctx) {
|
|
7733
|
+
const fullPath = path3.join(dir, entry.name);
|
|
7734
|
+
if (entry.isDirectory()) {
|
|
7735
|
+
if (!excludedDirs.has(entry.name)) {
|
|
7736
|
+
await ctx.walk(fullPath);
|
|
7737
|
+
}
|
|
7738
|
+
} else if (entry.isSymbolicLink()) {
|
|
7739
|
+
await handleSymlink(fullPath, entry.name, ctx);
|
|
7740
|
+
} else if (entry.isFile() && testFileRe.test(entry.name)) {
|
|
7741
|
+
ctx.results.push(path3.relative(ctx.cwd, fullPath));
|
|
7742
|
+
}
|
|
7743
|
+
}
|
|
7574
7744
|
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
7745
|
const results = [];
|
|
7581
7746
|
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
|
-
}
|
|
7747
|
+
const ctx = { cwd, results, visitedRealPaths, logger, walk };
|
|
7748
|
+
async function walk(dir) {
|
|
7749
|
+
const realDir = await tryRealpath(dir);
|
|
7750
|
+
if (!realDir) {
|
|
7751
|
+
logger?.debug("discoverTestFiles: could not resolve real path of %s; skipping", dir);
|
|
7590
7752
|
return;
|
|
7591
7753
|
}
|
|
7592
7754
|
if (visitedRealPaths.has(realDir)) {
|
|
@@ -7599,69 +7761,27 @@ async function discoverTestFiles(cwd = process.cwd(), logger) {
|
|
|
7599
7761
|
} catch {
|
|
7600
7762
|
return;
|
|
7601
7763
|
}
|
|
7602
|
-
entries.sort((a, b) => a.name
|
|
7764
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
7603
7765
|
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
|
-
}
|
|
7766
|
+
await processEntry(entry, dir, ctx);
|
|
7649
7767
|
}
|
|
7650
|
-
}
|
|
7768
|
+
}
|
|
7651
7769
|
await walk(cwd);
|
|
7652
7770
|
if (results.length === 0) {
|
|
7653
|
-
|
|
7654
|
-
logger.warn("discoverTestFiles: no test files found in %s; falling back to Bun discovery", cwd);
|
|
7655
|
-
}
|
|
7771
|
+
logger?.warn("discoverTestFiles: no test files found in %s; falling back to Bun discovery", cwd);
|
|
7656
7772
|
return;
|
|
7657
7773
|
}
|
|
7658
|
-
results.sort();
|
|
7659
|
-
|
|
7660
|
-
logger.debug("discoverTestFiles: found %d test files in %s", results.length, cwd);
|
|
7661
|
-
}
|
|
7774
|
+
results.sort((a, b) => a.localeCompare(b));
|
|
7775
|
+
logger?.debug("discoverTestFiles: found %d test files in %s", results.length, cwd);
|
|
7662
7776
|
return results;
|
|
7663
7777
|
}
|
|
7664
7778
|
// src/bun-test-runner.ts
|
|
7779
|
+
function sleep(ms) {
|
|
7780
|
+
return new Promise((resolve3) => {
|
|
7781
|
+
setTimeout(resolve3, ms);
|
|
7782
|
+
});
|
|
7783
|
+
}
|
|
7784
|
+
|
|
7665
7785
|
class BunTestRunner {
|
|
7666
7786
|
logger;
|
|
7667
7787
|
static inject = tokens(commonTokens.logger, commonTokens.options);
|
|
@@ -7670,6 +7790,7 @@ class BunTestRunner {
|
|
|
7670
7790
|
inspectorTimeout;
|
|
7671
7791
|
env;
|
|
7672
7792
|
bunArgs;
|
|
7793
|
+
testFilesOverride;
|
|
7673
7794
|
mutateGlobs;
|
|
7674
7795
|
preloadScriptPath;
|
|
7675
7796
|
coverageFilePath;
|
|
@@ -7679,7 +7800,9 @@ class BunTestRunner {
|
|
|
7679
7800
|
cachedTestNames;
|
|
7680
7801
|
baseNameIndex;
|
|
7681
7802
|
cachedTestFiles;
|
|
7803
|
+
cachedTestFilesCwd;
|
|
7682
7804
|
cachedEagerModules;
|
|
7805
|
+
cachedEagerModulesCwd;
|
|
7683
7806
|
lastRegistryTmpPath;
|
|
7684
7807
|
constructor(logger, options) {
|
|
7685
7808
|
this.logger = logger;
|
|
@@ -7689,6 +7812,12 @@ class BunTestRunner {
|
|
|
7689
7812
|
this.inspectorTimeout = bunOptions.inspectorTimeout ?? 5000;
|
|
7690
7813
|
this.env = bunOptions.env;
|
|
7691
7814
|
this.bunArgs = bunOptions.bunArgs;
|
|
7815
|
+
if (bunOptions.testFiles?.length === 0) {
|
|
7816
|
+
this.logger.warn("bun.testFiles was set to an empty array — treating as undefined and falling back to auto-discovery");
|
|
7817
|
+
this.testFilesOverride = undefined;
|
|
7818
|
+
} else {
|
|
7819
|
+
this.testFilesOverride = bunOptions.testFiles;
|
|
7820
|
+
}
|
|
7692
7821
|
this.mutateGlobs = options.mutate ?? [];
|
|
7693
7822
|
this.logger.debug("BunTestRunner initialized with options: %o", {
|
|
7694
7823
|
bunPath: this.bunPath,
|
|
@@ -7697,26 +7826,68 @@ class BunTestRunner {
|
|
|
7697
7826
|
env: this.env,
|
|
7698
7827
|
bunArgs: this.bunArgs
|
|
7699
7828
|
});
|
|
7829
|
+
if (this.testFilesOverride?.some((p) => path4.isAbsolute(p))) {
|
|
7830
|
+
const isSandbox = process.cwd().includes(".stryker-tmp/sandbox-");
|
|
7831
|
+
if (isSandbox) {
|
|
7832
|
+
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());
|
|
7833
|
+
}
|
|
7834
|
+
}
|
|
7700
7835
|
}
|
|
7701
7836
|
get registryPath() {
|
|
7702
|
-
return
|
|
7837
|
+
return path4.join(process.cwd(), ".stryker-bun-runner-registry.json");
|
|
7703
7838
|
}
|
|
7704
7839
|
get registryTmpPath() {
|
|
7705
|
-
return this.registryPath
|
|
7840
|
+
return `${this.registryPath}.tmp`;
|
|
7706
7841
|
}
|
|
7707
7842
|
capabilities() {
|
|
7708
7843
|
return {
|
|
7709
7844
|
reloadEnvironment: true
|
|
7710
7845
|
};
|
|
7711
7846
|
}
|
|
7847
|
+
async getOrDiscoverTestFiles() {
|
|
7848
|
+
if (this.testFilesOverride !== undefined) {
|
|
7849
|
+
return this.testFilesOverride;
|
|
7850
|
+
}
|
|
7851
|
+
const cwd = process.cwd();
|
|
7852
|
+
if (this.cachedTestFiles !== undefined && this.cachedTestFilesCwd === cwd) {
|
|
7853
|
+
return this.cachedTestFiles;
|
|
7854
|
+
}
|
|
7855
|
+
this.cachedTestFiles = await discoverTestFiles(cwd, this.logger);
|
|
7856
|
+
this.cachedTestFilesCwd = cwd;
|
|
7857
|
+
return this.cachedTestFiles;
|
|
7858
|
+
}
|
|
7859
|
+
testFilesCacheHit(cwd) {
|
|
7860
|
+
if (this.testFilesOverride !== undefined) {
|
|
7861
|
+
return this.testFilesOverride;
|
|
7862
|
+
}
|
|
7863
|
+
return this.cachedTestFiles !== undefined && this.cachedTestFilesCwd === cwd ? this.cachedTestFiles : null;
|
|
7864
|
+
}
|
|
7712
7865
|
async init() {
|
|
7713
7866
|
this.logger.debug("BunTestRunner init starting...");
|
|
7714
|
-
|
|
7867
|
+
if (this.preloadScriptPath) {
|
|
7868
|
+
try {
|
|
7869
|
+
await cleanupPreloadScript(this.preloadScriptPath);
|
|
7870
|
+
} catch (error) {
|
|
7871
|
+
this.logger.debug("Failed to clean up previous preload script on re-init: %s", error instanceof Error ? error.message : String(error));
|
|
7872
|
+
}
|
|
7873
|
+
this.preloadScriptPath = undefined;
|
|
7874
|
+
}
|
|
7875
|
+
if (this.coverageFilePath) {
|
|
7876
|
+
try {
|
|
7877
|
+
await cleanupCoverageFile(this.coverageFilePath);
|
|
7878
|
+
} catch (error) {
|
|
7879
|
+
this.logger.debug("Failed to clean up previous coverage file on re-init: %s", error instanceof Error ? error.message : String(error));
|
|
7880
|
+
}
|
|
7881
|
+
this.coverageFilePath = undefined;
|
|
7882
|
+
}
|
|
7883
|
+
const tempDir = path4.join(tmpdir(), "stryker-bun-runner");
|
|
7715
7884
|
this.tempDir = tempDir;
|
|
7716
|
-
this.coverageFilePath =
|
|
7885
|
+
this.coverageFilePath = path4.join(tempDir, `coverage-${Date.now()}.json`);
|
|
7717
7886
|
this.logger.debug("Generating coverage preload script...");
|
|
7718
|
-
|
|
7887
|
+
const eagerCwd = process.cwd();
|
|
7888
|
+
if (this.cachedEagerModules === undefined || this.cachedEagerModulesCwd !== eagerCwd) {
|
|
7719
7889
|
this.cachedEagerModules = await resolveEagerModulesFromGlobs(this.mutateGlobs);
|
|
7890
|
+
this.cachedEagerModulesCwd = eagerCwd;
|
|
7720
7891
|
this.logger.debug("Resolved %d eager modules from mutate globs", this.cachedEagerModules.length);
|
|
7721
7892
|
}
|
|
7722
7893
|
this.preloadScriptPath = await generatePreloadScript({
|
|
@@ -7726,7 +7897,7 @@ class BunTestRunner {
|
|
|
7726
7897
|
});
|
|
7727
7898
|
this.logger.debug("Preload script generated at: %s", this.preloadScriptPath);
|
|
7728
7899
|
await this.ensureSanitizedBunfig();
|
|
7729
|
-
this.cachedTestFiles = await
|
|
7900
|
+
this.cachedTestFiles = await this.getOrDiscoverTestFiles();
|
|
7730
7901
|
}
|
|
7731
7902
|
async ensureSanitizedBunfig() {
|
|
7732
7903
|
const cwd = process.cwd();
|
|
@@ -7736,7 +7907,7 @@ class BunTestRunner {
|
|
|
7736
7907
|
if (this.sanitizedBunfigPath) {
|
|
7737
7908
|
await cleanupSanitizedBunfig(this.sanitizedBunfigPath);
|
|
7738
7909
|
}
|
|
7739
|
-
const tempDir = this.tempDir ??
|
|
7910
|
+
const tempDir = this.tempDir ?? path4.join(tmpdir(), "stryker-bun-runner");
|
|
7740
7911
|
this.sanitizedBunfigPath = await generateSanitizedBunfig(cwd, tempDir);
|
|
7741
7912
|
this.sanitizedBunfigCwd = cwd;
|
|
7742
7913
|
this.logger.debug("Sanitized bunfig (re)generated at: %s for cwd: %s", this.sanitizedBunfigPath, cwd);
|
|
@@ -7745,7 +7916,7 @@ class BunTestRunner {
|
|
|
7745
7916
|
async loadRegistryFile() {
|
|
7746
7917
|
const registryPath = this.registryPath;
|
|
7747
7918
|
try {
|
|
7748
|
-
const raw = await fsPromises2.readFile(registryPath, "
|
|
7919
|
+
const raw = await fsPromises2.readFile(registryPath, "utf8");
|
|
7749
7920
|
const parsed = JSON.parse(raw);
|
|
7750
7921
|
if (parsed.version !== 1) {
|
|
7751
7922
|
this.logger.warn("dryRun registry file has unexpected version %s; skipping", String(parsed.version));
|
|
@@ -7759,15 +7930,15 @@ class BunTestRunner {
|
|
|
7759
7930
|
this.baseNameIndex = new Map(parsed.baseNameIndex);
|
|
7760
7931
|
this.logger.debug("Loaded dryRun registry from %s (%d entries)", registryPath, this.cachedTestNames.size);
|
|
7761
7932
|
} catch (err) {
|
|
7762
|
-
const code = err
|
|
7933
|
+
const code = err.code;
|
|
7763
7934
|
if (code === "ENOENT") {
|
|
7764
|
-
this.logger.
|
|
7935
|
+
this.logger.debug("dryRun registry file not found at %s; this worker has no static-coverage registry (expected on non-dryRun workers)", registryPath);
|
|
7765
7936
|
} else {
|
|
7766
7937
|
this.logger.warn("Failed to load dryRun registry from %s: %s", registryPath, err instanceof Error ? err.message : String(err));
|
|
7767
7938
|
}
|
|
7768
7939
|
}
|
|
7769
7940
|
}
|
|
7770
|
-
buildTestsFromInspector(testHierarchy, executionOrder, parsed, totalElapsedMs) {
|
|
7941
|
+
buildTestsFromInspector(testHierarchy, executionOrder, parsed, totalElapsedMs, inspectorIdToProjectFile) {
|
|
7771
7942
|
if (executionOrder.length === 0) {
|
|
7772
7943
|
return parsed.tests.map((t) => {
|
|
7773
7944
|
const normalizedName = normalizeTestName(t.name);
|
|
@@ -7811,16 +7982,19 @@ class BunTestRunner {
|
|
|
7811
7982
|
timeSpentMs: timePerTest
|
|
7812
7983
|
};
|
|
7813
7984
|
}
|
|
7814
|
-
const
|
|
7985
|
+
const projectFile = inspectorIdToProjectFile?.get(inspectorId);
|
|
7986
|
+
const uniqueName = projectFile ? buildProjectFileTestName(projectFile, testInfo.fullName) : buildUniqueTestName(testInfo.fullName, testInfo.url);
|
|
7987
|
+
const fileName = projectFile ?? normalizeTestFilePath(testInfo.url);
|
|
7815
7988
|
const status = testInfo.status;
|
|
7816
|
-
const elapsed = testInfo.elapsed
|
|
7989
|
+
const elapsed = testInfo.elapsed === undefined ? timePerTest : Math.round(testInfo.elapsed / 1e6);
|
|
7990
|
+
const startPosition = testInfo.line === undefined ? undefined : { line: testInfo.line, column: 0 };
|
|
7817
7991
|
if (status === "fail") {
|
|
7818
7992
|
const parsedTest = parsed.tests.find((t) => t.name.includes(testInfo.name));
|
|
7819
7993
|
return {
|
|
7820
7994
|
id: uniqueName,
|
|
7821
7995
|
name: uniqueName,
|
|
7822
|
-
fileName
|
|
7823
|
-
startPosition
|
|
7996
|
+
fileName,
|
|
7997
|
+
startPosition,
|
|
7824
7998
|
status: TestStatus.Failed,
|
|
7825
7999
|
failureMessage: parsedTest?.failureMessage ?? testInfo.error?.message ?? "Test failed",
|
|
7826
8000
|
timeSpentMs: elapsed
|
|
@@ -7830,8 +8004,8 @@ class BunTestRunner {
|
|
|
7830
8004
|
return {
|
|
7831
8005
|
id: uniqueName,
|
|
7832
8006
|
name: uniqueName,
|
|
7833
|
-
fileName
|
|
7834
|
-
startPosition
|
|
8007
|
+
fileName,
|
|
8008
|
+
startPosition,
|
|
7835
8009
|
status: TestStatus.Skipped,
|
|
7836
8010
|
timeSpentMs: elapsed
|
|
7837
8011
|
};
|
|
@@ -7839,8 +8013,8 @@ class BunTestRunner {
|
|
|
7839
8013
|
return {
|
|
7840
8014
|
id: uniqueName,
|
|
7841
8015
|
name: uniqueName,
|
|
7842
|
-
fileName
|
|
7843
|
-
startPosition
|
|
8016
|
+
fileName,
|
|
8017
|
+
startPosition,
|
|
7844
8018
|
status: TestStatus.Success,
|
|
7845
8019
|
timeSpentMs: elapsed
|
|
7846
8020
|
};
|
|
@@ -7866,8 +8040,7 @@ class BunTestRunner {
|
|
|
7866
8040
|
const lineB = "startPosition" in b && b.startPosition ? b.startPosition.line : Infinity;
|
|
7867
8041
|
return lineA - lineB;
|
|
7868
8042
|
});
|
|
7869
|
-
for (
|
|
7870
|
-
const test = group[i];
|
|
8043
|
+
for (const [i, test] of group.entries()) {
|
|
7871
8044
|
const uniqueName = `${test.name} [${i}]`;
|
|
7872
8045
|
test.id = uniqueName;
|
|
7873
8046
|
test.name = uniqueName;
|
|
@@ -7877,6 +8050,7 @@ class BunTestRunner {
|
|
|
7877
8050
|
}
|
|
7878
8051
|
async dryRun() {
|
|
7879
8052
|
this.logger.debug("Running dry run with inspector-based coverage collection...");
|
|
8053
|
+
const abortController = new AbortController;
|
|
7880
8054
|
const inspectPort = await getAvailablePort();
|
|
7881
8055
|
const syncPort = await getAvailablePort();
|
|
7882
8056
|
this.logger.debug("Using inspector port: %d, sync port: %d", inspectPort, syncPort);
|
|
@@ -7893,109 +8067,104 @@ class BunTestRunner {
|
|
|
7893
8067
|
};
|
|
7894
8068
|
}
|
|
7895
8069
|
const startTime = Date.now();
|
|
7896
|
-
let inspector = null;
|
|
7897
8070
|
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
|
-
|
|
8071
|
+
try {
|
|
8072
|
+
const cwd = process.cwd();
|
|
8073
|
+
const bunfigPath = this.sanitizedBunfigPath && this.sanitizedBunfigCwd === cwd ? this.sanitizedBunfigPath : await this.ensureSanitizedBunfig();
|
|
8074
|
+
const testFilesCached = this.testFilesCacheHit(cwd);
|
|
8075
|
+
const testFiles = testFilesCached === null ? await this.getOrDiscoverTestFiles() : testFilesCached;
|
|
8076
|
+
const testProcess = runBunTests({
|
|
8077
|
+
bunPath: this.bunPath,
|
|
8078
|
+
timeout: this.timeout,
|
|
8079
|
+
env: this.env,
|
|
8080
|
+
bunArgs: this.bunArgs,
|
|
8081
|
+
bunfigPath,
|
|
8082
|
+
preloadScript: this.preloadScriptPath,
|
|
8083
|
+
coverageFile: this.coverageFilePath,
|
|
8084
|
+
inspectWaitPort: inspectPort,
|
|
8085
|
+
sequentialMode: true,
|
|
8086
|
+
syncPort,
|
|
8087
|
+
testFiles,
|
|
8088
|
+
signal: abortController.signal,
|
|
8089
|
+
onInspectorReady: (url) => {
|
|
8090
|
+
inspectorUrl = url;
|
|
8091
|
+
}
|
|
8092
|
+
});
|
|
8093
|
+
const waitStart = Date.now();
|
|
8094
|
+
while (!inspectorUrl && Date.now() - waitStart < this.inspectorTimeout) {
|
|
8095
|
+
await sleep(50);
|
|
8096
|
+
}
|
|
8097
|
+
if (!inspectorUrl) {
|
|
8098
|
+
const diagnosticResult = await testProcess;
|
|
8099
|
+
const stdoutPreview = diagnosticResult.stdout.slice(0, 1000);
|
|
8100
|
+
const stderrPreview = diagnosticResult.stderr.slice(0, 1000);
|
|
8101
|
+
this.logger.error(`Failed to get inspector URL within timeout (%dms).
|
|
7928
8102
|
exit=%s timedOut=%s
|
|
7929
8103
|
` + `--- STDOUT (first 1000 chars) ---
|
|
7930
8104
|
%s
|
|
7931
8105
|
` + `--- STDERR (first 1000 chars) ---
|
|
7932
8106
|
%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
|
-
|
|
8107
|
+
return {
|
|
8108
|
+
status: DryRunStatus.Error,
|
|
8109
|
+
errorMessage: "Timeout waiting for inspector URL"
|
|
8110
|
+
};
|
|
8111
|
+
}
|
|
8112
|
+
this.logger.debug("Inspector URL: %s", inspectorUrl);
|
|
8113
|
+
const inspector = new InspectorClient({
|
|
8114
|
+
url: inspectorUrl,
|
|
8115
|
+
connectionTimeout: this.inspectorTimeout,
|
|
8116
|
+
requestTimeout: this.inspectorTimeout,
|
|
8117
|
+
handlers: {}
|
|
8118
|
+
});
|
|
8119
|
+
try {
|
|
8120
|
+
await inspector.connect();
|
|
8121
|
+
await inspector.send("TestReporter.enable", {});
|
|
8122
|
+
this.logger.debug("Inspector connected and TestReporter enabled");
|
|
8123
|
+
} catch (error) {
|
|
8124
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
8125
|
+
this.logger.error("Failed to connect inspector: %s", errorMsg);
|
|
8126
|
+
abortController.abort();
|
|
8127
|
+
await inspector.close();
|
|
8128
|
+
return {
|
|
8129
|
+
status: DryRunStatus.Error,
|
|
8130
|
+
errorMessage: `Failed to connect to Bun inspector: ${errorMsg}`
|
|
8131
|
+
};
|
|
8132
|
+
}
|
|
8133
|
+
syncServer.signalReady();
|
|
8134
|
+
this.logger.debug("Signaled preload script to proceed");
|
|
8135
|
+
const result = await testProcess;
|
|
8136
|
+
const totalElapsedMs = Date.now() - startTime;
|
|
8137
|
+
const testHierarchy = inspector.getTests();
|
|
8138
|
+
const executionOrder = inspector.getExecutionOrder();
|
|
7953
8139
|
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) {
|
|
8140
|
+
this.logger.debug("Inspector collected %d tests in hierarchy, %d in execution order", testHierarchy.length, executionOrder.length);
|
|
8141
|
+
const parsed = parseBunTestOutput(result.stdout, result.stderr);
|
|
8142
|
+
const earlyResult = this.checkDryRunProcessResult(result, parsed);
|
|
8143
|
+
if (earlyResult) {
|
|
8144
|
+
return earlyResult;
|
|
8145
|
+
}
|
|
8146
|
+
const { coverage: mutantCoverage, inspectorIdToProjectFile } = await this.collectAndRemapCoverage(testHierarchy, executionOrder);
|
|
8147
|
+
const tests = this.buildTestsFromInspector(testHierarchy, executionOrder, parsed, totalElapsedMs, inspectorIdToProjectFile);
|
|
8148
|
+
tests.sort((a, b) => a.name.localeCompare(b.name));
|
|
8149
|
+
await this.buildAndPersistTestRegistry(tests);
|
|
7975
8150
|
return {
|
|
7976
|
-
status: DryRunStatus.
|
|
7977
|
-
|
|
7978
|
-
|
|
8151
|
+
status: DryRunStatus.Complete,
|
|
8152
|
+
tests,
|
|
8153
|
+
mutantCoverage
|
|
7979
8154
|
};
|
|
8155
|
+
} finally {
|
|
8156
|
+
abortController.abort();
|
|
8157
|
+
await syncServer.close();
|
|
7980
8158
|
}
|
|
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));
|
|
8159
|
+
}
|
|
8160
|
+
async buildAndPersistTestRegistry(tests) {
|
|
7992
8161
|
this.cachedTestNames = new Set(tests.map((t) => t.name));
|
|
7993
8162
|
if (tests.length !== this.cachedTestNames.size) {
|
|
7994
8163
|
const nameCount = new Map;
|
|
7995
8164
|
for (const test of tests) {
|
|
7996
8165
|
nameCount.set(test.name, (nameCount.get(test.name) ?? 0) + 1);
|
|
7997
8166
|
}
|
|
7998
|
-
const duplicates =
|
|
8167
|
+
const duplicates = [...nameCount.entries()].filter(([_, count]) => count > 1).map(([name, count]) => `"${name}" (${count}x)`);
|
|
7999
8168
|
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
8169
|
}
|
|
8001
8170
|
this.baseNameIndex = new Map;
|
|
@@ -8019,49 +8188,29 @@ ${result.stderr}`
|
|
|
8019
8188
|
const registryData = JSON.stringify({
|
|
8020
8189
|
version: 1,
|
|
8021
8190
|
writtenAt: Date.now(),
|
|
8022
|
-
cachedTestNames:
|
|
8023
|
-
baseNameIndex:
|
|
8191
|
+
cachedTestNames: [...this.cachedTestNames],
|
|
8192
|
+
baseNameIndex: [...this.baseNameIndex.entries()]
|
|
8024
8193
|
});
|
|
8025
|
-
await fsPromises2.writeFile(tmpPath, registryData, "
|
|
8194
|
+
await fsPromises2.writeFile(tmpPath, registryData, "utf8");
|
|
8026
8195
|
this.lastRegistryTmpPath = tmpPath;
|
|
8027
8196
|
await fsPromises2.rename(tmpPath, registryPath);
|
|
8197
|
+
this.lastRegistryTmpPath = undefined;
|
|
8028
8198
|
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",
|
|
8199
|
+
} catch (error) {
|
|
8200
|
+
this.logger.warn("Failed to write dryRun registry file: %s", error instanceof Error ? error.message : String(error));
|
|
8031
8201
|
}
|
|
8032
|
-
return {
|
|
8033
|
-
status: DryRunStatus.Complete,
|
|
8034
|
-
tests,
|
|
8035
|
-
mutantCoverage
|
|
8036
|
-
};
|
|
8037
8202
|
}
|
|
8038
8203
|
async mutantRun(options) {
|
|
8039
8204
|
this.logger.debug("Running mutant run for mutant %s", options.activeMutant.id);
|
|
8040
8205
|
const testNamePattern = buildTestNamePattern(options.testFilter ?? []);
|
|
8041
8206
|
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
|
-
}
|
|
8207
|
+
const { localRegistry, localBaseIndex } = this.buildLocalTestFilterIndex(localTestFilter);
|
|
8057
8208
|
if (!this.cachedTestNames) {
|
|
8058
8209
|
await this.loadRegistryFile();
|
|
8059
8210
|
}
|
|
8060
8211
|
const mutantCwd = process.cwd();
|
|
8061
8212
|
const bunfigPath = this.sanitizedBunfigPath && this.sanitizedBunfigCwd === mutantCwd ? this.sanitizedBunfigPath : await this.ensureSanitizedBunfig();
|
|
8062
|
-
|
|
8063
|
-
this.cachedTestFiles = await discoverTestFiles(process.cwd(), this.logger);
|
|
8064
|
-
}
|
|
8213
|
+
this.cachedTestFiles = await this.getOrDiscoverTestFiles();
|
|
8065
8214
|
const result = await runBunTests({
|
|
8066
8215
|
bunPath: this.bunPath,
|
|
8067
8216
|
timeout: this.timeout,
|
|
@@ -8089,72 +8238,128 @@ ${result.stderr}`
|
|
|
8089
8238
|
exitCode: result.exitCode
|
|
8090
8239
|
});
|
|
8091
8240
|
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);
|
|
8241
|
+
return this.buildMutantKilledResult(result, parsed, localRegistry, localBaseIndex, options.activeMutant.id);
|
|
8242
|
+
}
|
|
8243
|
+
return {
|
|
8244
|
+
status: MutantRunStatus.Survived,
|
|
8245
|
+
nrOfTests: parsed.totalTests
|
|
8246
|
+
};
|
|
8247
|
+
}
|
|
8248
|
+
buildMutantKilledResult(result, parsed, localRegistry, localBaseIndex, mutantId) {
|
|
8249
|
+
const rawFailedNames = parsed.tests.filter((test) => test.status === "failed").map((test) => normalizeTestName(test.name));
|
|
8250
|
+
if (rawFailedNames.length === 0 && parsed.tests.length === 0) {
|
|
8251
|
+
const runtimeResult = this.checkRuntimeError(result, mutantId);
|
|
8252
|
+
if (runtimeResult) {
|
|
8253
|
+
return runtimeResult;
|
|
8132
8254
|
}
|
|
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)
|
|
8255
|
+
}
|
|
8256
|
+
const killedBy = this.resolveKilledBy(rawFailedNames, localRegistry, localBaseIndex, mutantId);
|
|
8257
|
+
if (killedBy.length === 0) {
|
|
8258
|
+
this.logger.warn("No failed tests identified for mutant %s — Bun output could not be parsed; " + `using "unknown" fallback (breaks incremental cache)
|
|
8138
8259
|
` + `exit=%s
|
|
8139
8260
|
--- STDOUT (first 600 chars) ---
|
|
8140
8261
|
%s
|
|
8141
8262
|
` + `--- STDERR (first 600 chars) ---
|
|
8142
|
-
%s`,
|
|
8143
|
-
|
|
8144
|
-
|
|
8145
|
-
|
|
8146
|
-
|
|
8147
|
-
|
|
8148
|
-
|
|
8263
|
+
%s`, mutantId, String(result.exitCode), result.stdout.slice(0, 600) || "(empty)", result.stderr.slice(0, 600) || "(empty)");
|
|
8264
|
+
killedBy.push("unknown");
|
|
8265
|
+
}
|
|
8266
|
+
return {
|
|
8267
|
+
status: MutantRunStatus.Killed,
|
|
8268
|
+
killedBy,
|
|
8269
|
+
failureMessage: parsed.tests.filter((test) => test.status === "failed").map((test) => test.failureMessage).filter((msg) => !!msg).join(`
|
|
8149
8270
|
|
|
8150
8271
|
`) || `Tests failed with exit code ${result.exitCode}`,
|
|
8151
|
-
|
|
8272
|
+
nrOfTests: parsed.totalTests || 1
|
|
8273
|
+
};
|
|
8274
|
+
}
|
|
8275
|
+
checkRuntimeError(result, mutantId) {
|
|
8276
|
+
const stderr = result.stderr;
|
|
8277
|
+
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");
|
|
8278
|
+
if (isRuntimeError) {
|
|
8279
|
+
this.logger.debug("Mutant %s caused runtime error (tests could not run): %s", mutantId, stderr.slice(0, 200));
|
|
8280
|
+
return {
|
|
8281
|
+
status: MutantRunStatus.Error,
|
|
8282
|
+
errorMessage: stderr.slice(0, 500) || `Runtime error with exit code ${result.exitCode}`
|
|
8152
8283
|
};
|
|
8153
8284
|
}
|
|
8154
|
-
return
|
|
8155
|
-
|
|
8156
|
-
|
|
8157
|
-
|
|
8285
|
+
return null;
|
|
8286
|
+
}
|
|
8287
|
+
checkDryRunProcessResult(result, parsed) {
|
|
8288
|
+
if (result.timedOut) {
|
|
8289
|
+
this.logger.warn("Dry run timed out");
|
|
8290
|
+
return { status: DryRunStatus.Timeout };
|
|
8291
|
+
}
|
|
8292
|
+
if (result.exitCode !== 0 && parsed.failed === 0) {
|
|
8293
|
+
return {
|
|
8294
|
+
status: DryRunStatus.Error,
|
|
8295
|
+
errorMessage: `Bun test process failed with exit code ${result.exitCode}
|
|
8296
|
+
${result.stderr}`
|
|
8297
|
+
};
|
|
8298
|
+
}
|
|
8299
|
+
return null;
|
|
8300
|
+
}
|
|
8301
|
+
async collectAndRemapCoverage(testHierarchy, executionOrder) {
|
|
8302
|
+
const testMap = new Map(testHierarchy.map((t) => [t.id, t]));
|
|
8303
|
+
if (!this.coverageFilePath) {
|
|
8304
|
+
return { coverage: undefined, inspectorIdToProjectFile: new Map };
|
|
8305
|
+
}
|
|
8306
|
+
const rawCoverage = await collectCoverage(this.coverageFilePath, this.logger);
|
|
8307
|
+
await cleanupCoverageFile(this.coverageFilePath);
|
|
8308
|
+
if (!rawCoverage) {
|
|
8309
|
+
return { coverage: undefined, inspectorIdToProjectFile: new Map };
|
|
8310
|
+
}
|
|
8311
|
+
const { coverage, inspectorIdToProjectFile } = mapCoverageToInspectorIds(rawCoverage, executionOrder, testMap, this.logger);
|
|
8312
|
+
return { coverage, inspectorIdToProjectFile };
|
|
8313
|
+
}
|
|
8314
|
+
buildLocalTestFilterIndex(testFilter) {
|
|
8315
|
+
const localRegistry = new Set(testFilter);
|
|
8316
|
+
const localSuffixRe = / \[\d+\]$/;
|
|
8317
|
+
const localBaseIndex = new Map;
|
|
8318
|
+
for (const id of localRegistry) {
|
|
8319
|
+
const base = localSuffixRe.test(id) ? id.replace(localSuffixRe, "") : id;
|
|
8320
|
+
const bucket = localBaseIndex.get(base);
|
|
8321
|
+
if (bucket) {
|
|
8322
|
+
bucket.push(id);
|
|
8323
|
+
} else {
|
|
8324
|
+
localBaseIndex.set(base, [id]);
|
|
8325
|
+
}
|
|
8326
|
+
if (base !== id) {
|
|
8327
|
+
localBaseIndex.set(id, [id]);
|
|
8328
|
+
}
|
|
8329
|
+
}
|
|
8330
|
+
return { localRegistry, localBaseIndex };
|
|
8331
|
+
}
|
|
8332
|
+
resolveKilledBy(rawFailedNames, localRegistry, localBaseIndex, mutantId) {
|
|
8333
|
+
const killedBySet = new Set;
|
|
8334
|
+
for (const name of rawFailedNames) {
|
|
8335
|
+
if (localRegistry.has(name)) {
|
|
8336
|
+
killedBySet.add(name);
|
|
8337
|
+
continue;
|
|
8338
|
+
}
|
|
8339
|
+
const localBucket = localBaseIndex.get(name);
|
|
8340
|
+
if (localBucket) {
|
|
8341
|
+
this.logger.debug('Expanded killedBy base name "%s" → %d local registry IDs for mutant %s', name, localBucket.length, mutantId);
|
|
8342
|
+
for (const id of localBucket) {
|
|
8343
|
+
killedBySet.add(id);
|
|
8344
|
+
}
|
|
8345
|
+
continue;
|
|
8346
|
+
}
|
|
8347
|
+
if (this.cachedTestNames?.has(name)) {
|
|
8348
|
+
killedBySet.add(name);
|
|
8349
|
+
continue;
|
|
8350
|
+
}
|
|
8351
|
+
const instanceBucket = this.baseNameIndex?.get(name);
|
|
8352
|
+
if (instanceBucket) {
|
|
8353
|
+
this.logger.debug('Expanded killedBy base name "%s" → %d instance registry IDs for mutant %s', name, instanceBucket.length, mutantId);
|
|
8354
|
+
for (const id of instanceBucket) {
|
|
8355
|
+
killedBySet.add(id);
|
|
8356
|
+
}
|
|
8357
|
+
continue;
|
|
8358
|
+
}
|
|
8359
|
+
this.logger.debug('killedBy name "%s" for mutant %s not found in test registry; including as-is', name, mutantId);
|
|
8360
|
+
killedBySet.add(name);
|
|
8361
|
+
}
|
|
8362
|
+
return [...killedBySet];
|
|
8158
8363
|
}
|
|
8159
8364
|
async dispose() {
|
|
8160
8365
|
this.logger.debug("Disposing BunTestRunner");
|
|
@@ -8202,7 +8407,7 @@ var strykerValidationSchema = {
|
|
|
8202
8407
|
timeout: {
|
|
8203
8408
|
type: "number",
|
|
8204
8409
|
minimum: 0,
|
|
8205
|
-
description: "
|
|
8410
|
+
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
8411
|
default: 1e4
|
|
8207
8412
|
},
|
|
8208
8413
|
inspectorTimeout: {
|
|
@@ -8224,6 +8429,14 @@ var strykerValidationSchema = {
|
|
|
8224
8429
|
items: {
|
|
8225
8430
|
type: "string"
|
|
8226
8431
|
}
|
|
8432
|
+
},
|
|
8433
|
+
testFiles: {
|
|
8434
|
+
type: "array",
|
|
8435
|
+
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).",
|
|
8436
|
+
minItems: 1,
|
|
8437
|
+
items: {
|
|
8438
|
+
type: "string"
|
|
8439
|
+
}
|
|
8227
8440
|
}
|
|
8228
8441
|
},
|
|
8229
8442
|
additionalProperties: false
|