@testream/dotnet-reporter 1.1.1 → 1.2.0
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 +2 -0
- package/dist/index.js +898 -24
- package/dist/index.js.map +1 -1
- package/dist/source-snippets.d.ts +26 -0
- package/dist/source-snippets.d.ts.map +1 -0
- package/dist/trx-parser.d.ts.map +1 -1
- package/package.json +6 -4
package/dist/index.js
CHANGED
|
@@ -2327,7 +2327,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
2327
2327
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
2328
2328
|
};
|
|
2329
2329
|
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
2330
|
-
exports.ensureReportId = exports.mapAttachmentsToTestResults = exports.uploadArtifacts = exports.uploadTestRun = exports.detectCIContext = void 0;
|
|
2330
|
+
exports.createTestLocationCollector = exports.enrichReportWithJsTestNameSourceSnippets = exports.resolveSourceSnippetOptions = exports.enrichReportWithSourceSnippets = exports.ensureReportId = exports.mapAttachmentsToTestResults = exports.uploadArtifacts = exports.uploadTestRun = exports.detectCIContext = void 0;
|
|
2331
2331
|
// CTRF types
|
|
2332
2332
|
__exportStar(__nccwpck_require__(3827), exports);
|
|
2333
2333
|
var ci_detection_1 = __nccwpck_require__(2406);
|
|
@@ -2337,10 +2337,569 @@ Object.defineProperty(exports, "uploadTestRun", ({ enumerable: true, get: functi
|
|
|
2337
2337
|
Object.defineProperty(exports, "uploadArtifacts", ({ enumerable: true, get: function () { return upload_1.uploadArtifacts; } }));
|
|
2338
2338
|
Object.defineProperty(exports, "mapAttachmentsToTestResults", ({ enumerable: true, get: function () { return upload_1.mapAttachmentsToTestResults; } }));
|
|
2339
2339
|
Object.defineProperty(exports, "ensureReportId", ({ enumerable: true, get: function () { return upload_1.ensureReportId; } }));
|
|
2340
|
+
var source_snippets_1 = __nccwpck_require__(9304);
|
|
2341
|
+
Object.defineProperty(exports, "enrichReportWithSourceSnippets", ({ enumerable: true, get: function () { return source_snippets_1.enrichReportWithSourceSnippets; } }));
|
|
2342
|
+
Object.defineProperty(exports, "resolveSourceSnippetOptions", ({ enumerable: true, get: function () { return source_snippets_1.resolveSourceSnippetOptions; } }));
|
|
2343
|
+
var js_test_name_snippets_1 = __nccwpck_require__(6913);
|
|
2344
|
+
Object.defineProperty(exports, "enrichReportWithJsTestNameSourceSnippets", ({ enumerable: true, get: function () { return js_test_name_snippets_1.enrichReportWithJsTestNameSourceSnippets; } }));
|
|
2345
|
+
var test_locations_1 = __nccwpck_require__(2293);
|
|
2346
|
+
Object.defineProperty(exports, "createTestLocationCollector", ({ enumerable: true, get: function () { return test_locations_1.createTestLocationCollector; } }));
|
|
2340
2347
|
//# sourceMappingURL=index.js.map
|
|
2341
2348
|
|
|
2342
2349
|
/***/ }),
|
|
2343
2350
|
|
|
2351
|
+
/***/ 6913:
|
|
2352
|
+
/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
|
|
2353
|
+
|
|
2354
|
+
"use strict";
|
|
2355
|
+
|
|
2356
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
2357
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
2358
|
+
};
|
|
2359
|
+
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
2360
|
+
exports.enrichReportWithJsTestNameSourceSnippets = enrichReportWithJsTestNameSourceSnippets;
|
|
2361
|
+
const promises_1 = __nccwpck_require__(1455);
|
|
2362
|
+
const node_path_1 = __importDefault(__nccwpck_require__(6760));
|
|
2363
|
+
const source_snippets_1 = __nccwpck_require__(9304);
|
|
2364
|
+
const MAX_CANDIDATES = 3;
|
|
2365
|
+
// Finds the start of common JS/TS test declarations, then the title is parsed
|
|
2366
|
+
// manually so titles can contain non-delimiting quotes like `it("doesn't")`.
|
|
2367
|
+
const JS_TEST_DECLARATION_PATTERN = /\b(describe|it|test)(?:\.(only|skip))?\s*\(\s*(['"`])/g;
|
|
2368
|
+
async function enrichReportWithJsTestNameSourceSnippets(report, options) {
|
|
2369
|
+
const resolved = (0, source_snippets_1.resolveSourceSnippetOptions)(options);
|
|
2370
|
+
const fileCandidateCache = new Map();
|
|
2371
|
+
await Promise.all(report.results.tests.map(async (test) => {
|
|
2372
|
+
if (test.snippet || !test.filePath) {
|
|
2373
|
+
return;
|
|
2374
|
+
}
|
|
2375
|
+
const filePath = node_path_1.default.resolve(resolved.sourceRoot, test.filePath);
|
|
2376
|
+
if (!isInsideSourceRoot(filePath, resolved.sourceRoot)) {
|
|
2377
|
+
return;
|
|
2378
|
+
}
|
|
2379
|
+
const candidates = await getCachedJsTestCandidates(filePath, fileCandidateCache);
|
|
2380
|
+
if (!candidates) {
|
|
2381
|
+
return;
|
|
2382
|
+
}
|
|
2383
|
+
const matches = rankCandidatesForTest(test, candidates).slice(0, MAX_CANDIDATES);
|
|
2384
|
+
if (matches.length === 0) {
|
|
2385
|
+
return;
|
|
2386
|
+
}
|
|
2387
|
+
test.line ?? (test.line = matches[0].line);
|
|
2388
|
+
test.snippet = formatCandidateSnippet(matches, resolved);
|
|
2389
|
+
}));
|
|
2390
|
+
}
|
|
2391
|
+
function getCachedJsTestCandidates(filePath, fileCandidateCache) {
|
|
2392
|
+
let cached = fileCandidateCache.get(filePath);
|
|
2393
|
+
if (!cached) {
|
|
2394
|
+
cached = (0, promises_1.readFile)(filePath, 'utf-8')
|
|
2395
|
+
.then((content) => scanJsTestCandidates(content))
|
|
2396
|
+
.catch(() => undefined);
|
|
2397
|
+
fileCandidateCache.set(filePath, cached);
|
|
2398
|
+
}
|
|
2399
|
+
return cached;
|
|
2400
|
+
}
|
|
2401
|
+
// This scanner intentionally handles the common JS/TS test declaration shapes
|
|
2402
|
+
// without adding parser dependencies to every reporter package. Exact runner
|
|
2403
|
+
// line metadata still wins; this fallback exists to give AI review bounded
|
|
2404
|
+
// source context when the reporter only emitted file path and test name.
|
|
2405
|
+
function scanJsTestCandidates(content) {
|
|
2406
|
+
const lineOffsets = buildLineOffsets(content);
|
|
2407
|
+
const blocks = scanJsTestBlocks(content, lineOffsets);
|
|
2408
|
+
const describeBlocks = blocks.filter((block) => block.kind === 'describe');
|
|
2409
|
+
return blocks
|
|
2410
|
+
.filter((block) => block.kind === 'test')
|
|
2411
|
+
.map((block) => {
|
|
2412
|
+
const describeTitles = describeBlocks
|
|
2413
|
+
.filter((describeBlock) => describeBlock.startIndex < block.startIndex && describeBlock.endIndex >= block.endIndex)
|
|
2414
|
+
.sort((left, right) => left.startIndex - right.startIndex)
|
|
2415
|
+
.map((describeBlock) => describeBlock.title);
|
|
2416
|
+
return {
|
|
2417
|
+
title: block.title,
|
|
2418
|
+
describeTitles,
|
|
2419
|
+
fullName: [...describeTitles, block.title].join(' ').trim(),
|
|
2420
|
+
line: block.startLine,
|
|
2421
|
+
snippet: block.snippet,
|
|
2422
|
+
};
|
|
2423
|
+
});
|
|
2424
|
+
}
|
|
2425
|
+
function scanJsTestBlocks(content, lineOffsets) {
|
|
2426
|
+
const blocks = [];
|
|
2427
|
+
JS_TEST_DECLARATION_PATTERN.lastIndex = 0;
|
|
2428
|
+
let match;
|
|
2429
|
+
while ((match = JS_TEST_DECLARATION_PATTERN.exec(content)) !== null) {
|
|
2430
|
+
if (isCommentedLine(content, match.index)) {
|
|
2431
|
+
continue;
|
|
2432
|
+
}
|
|
2433
|
+
const title = readQuotedTitle(content, match.index + match[0].length, match[3]);
|
|
2434
|
+
if (!title) {
|
|
2435
|
+
continue;
|
|
2436
|
+
}
|
|
2437
|
+
const commaIndex = skipWhitespaceAndFindComma(content, title.endIndex + 1);
|
|
2438
|
+
if (commaIndex == null) {
|
|
2439
|
+
continue;
|
|
2440
|
+
}
|
|
2441
|
+
const kind = match[1] === 'describe' ? 'describe' : 'test';
|
|
2442
|
+
const startIndex = match.index;
|
|
2443
|
+
const openBraceIndex = findCallbackOpenBrace(content, commaIndex + 1);
|
|
2444
|
+
if (openBraceIndex == null) {
|
|
2445
|
+
continue;
|
|
2446
|
+
}
|
|
2447
|
+
const closeBraceIndex = findMatchingBrace(content, openBraceIndex);
|
|
2448
|
+
if (closeBraceIndex == null) {
|
|
2449
|
+
continue;
|
|
2450
|
+
}
|
|
2451
|
+
const endIndex = findSnippetEndIndex(content, closeBraceIndex);
|
|
2452
|
+
const snippet = content.slice(startIndex, endIndex).trim();
|
|
2453
|
+
if (!snippet) {
|
|
2454
|
+
continue;
|
|
2455
|
+
}
|
|
2456
|
+
blocks.push({
|
|
2457
|
+
kind,
|
|
2458
|
+
title: title.value.trim(),
|
|
2459
|
+
startIndex,
|
|
2460
|
+
endIndex,
|
|
2461
|
+
startLine: getLineNumber(lineOffsets, startIndex),
|
|
2462
|
+
snippet,
|
|
2463
|
+
});
|
|
2464
|
+
}
|
|
2465
|
+
return blocks;
|
|
2466
|
+
}
|
|
2467
|
+
function rankCandidatesForTest(test, candidates) {
|
|
2468
|
+
const normalizedName = normalizeWhitespace(test.name);
|
|
2469
|
+
const suiteChain = normalizeDescribeChain(test.suite);
|
|
2470
|
+
const exactFullNameMatches = candidates.filter((candidate) => normalizeWhitespace(candidate.fullName) === normalizedName);
|
|
2471
|
+
if (exactFullNameMatches.length > 0) {
|
|
2472
|
+
return exactFullNameMatches;
|
|
2473
|
+
}
|
|
2474
|
+
if (suiteChain) {
|
|
2475
|
+
const suiteMatches = candidates.filter((candidate) => normalizeWhitespace(candidate.title) === normalizedName &&
|
|
2476
|
+
normalizeWhitespace(candidate.describeTitles.join(' ')) === suiteChain);
|
|
2477
|
+
if (suiteMatches.length > 0) {
|
|
2478
|
+
return suiteMatches;
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2481
|
+
const leafTitleMatches = candidates.filter((candidate) => normalizeWhitespace(candidate.title) === normalizedName);
|
|
2482
|
+
if (leafTitleMatches.length > 0) {
|
|
2483
|
+
return leafTitleMatches;
|
|
2484
|
+
}
|
|
2485
|
+
return candidates.filter((candidate) => {
|
|
2486
|
+
const normalizedTitle = normalizeWhitespace(candidate.title);
|
|
2487
|
+
if (!normalizedName.endsWith(normalizedTitle)) {
|
|
2488
|
+
return false;
|
|
2489
|
+
}
|
|
2490
|
+
const prefix = normalizedName
|
|
2491
|
+
.slice(0, normalizedName.length - normalizedTitle.length)
|
|
2492
|
+
.trim();
|
|
2493
|
+
return prefix.length > 0 && normalizeWhitespace(candidate.describeTitles.join(' ')) === prefix;
|
|
2494
|
+
});
|
|
2495
|
+
}
|
|
2496
|
+
function formatCandidateSnippet(candidates, options) {
|
|
2497
|
+
const combined = candidates.length === 1
|
|
2498
|
+
? candidates[0].snippet
|
|
2499
|
+
: candidates
|
|
2500
|
+
.map((candidate, index) => `Candidate ${index + 1} (line ${candidate.line})\n${candidate.snippet}`)
|
|
2501
|
+
.join('\n\n');
|
|
2502
|
+
const boundedLines = combined.split(/\r?\n/).slice(0, options.maxLines).join('\n').trim();
|
|
2503
|
+
if (boundedLines.length <= options.maxChars) {
|
|
2504
|
+
return boundedLines;
|
|
2505
|
+
}
|
|
2506
|
+
return boundedLines.slice(0, options.maxChars);
|
|
2507
|
+
}
|
|
2508
|
+
function findCallbackOpenBrace(content, fromIndex) {
|
|
2509
|
+
for (let index = fromIndex; index < content.length; index++) {
|
|
2510
|
+
const char = content[index];
|
|
2511
|
+
if (char === "'" || char === '"' || char === '`') {
|
|
2512
|
+
index = skipQuotedString(content, index);
|
|
2513
|
+
continue;
|
|
2514
|
+
}
|
|
2515
|
+
if (char === '/' && content[index + 1] === '/') {
|
|
2516
|
+
index = skipLineComment(content, index + 2);
|
|
2517
|
+
continue;
|
|
2518
|
+
}
|
|
2519
|
+
if (char === '/' && content[index + 1] === '*') {
|
|
2520
|
+
index = skipBlockComment(content, index + 2);
|
|
2521
|
+
continue;
|
|
2522
|
+
}
|
|
2523
|
+
if (content.startsWith('=>', index)) {
|
|
2524
|
+
return findNextBlockBrace(content, index + 2);
|
|
2525
|
+
}
|
|
2526
|
+
if (startsWithWord(content, index, 'function')) {
|
|
2527
|
+
return findNextBlockBrace(content, index + 'function'.length);
|
|
2528
|
+
}
|
|
2529
|
+
if (char === ';') {
|
|
2530
|
+
return undefined;
|
|
2531
|
+
}
|
|
2532
|
+
}
|
|
2533
|
+
return undefined;
|
|
2534
|
+
}
|
|
2535
|
+
function findNextBlockBrace(content, fromIndex) {
|
|
2536
|
+
for (let index = fromIndex; index < content.length; index++) {
|
|
2537
|
+
const char = content[index];
|
|
2538
|
+
if (char === "'" || char === '"' || char === '`') {
|
|
2539
|
+
index = skipQuotedString(content, index);
|
|
2540
|
+
continue;
|
|
2541
|
+
}
|
|
2542
|
+
if (char === '{') {
|
|
2543
|
+
return index;
|
|
2544
|
+
}
|
|
2545
|
+
if (char === '\n' && looksLikeLineTerminator(content, index + 1)) {
|
|
2546
|
+
return undefined;
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
return undefined;
|
|
2550
|
+
}
|
|
2551
|
+
function findMatchingBrace(content, openBraceIndex) {
|
|
2552
|
+
let depth = 0;
|
|
2553
|
+
for (let index = openBraceIndex; index < content.length; index++) {
|
|
2554
|
+
const char = content[index];
|
|
2555
|
+
if (char === "'" || char === '"' || char === '`') {
|
|
2556
|
+
index = skipQuotedString(content, index);
|
|
2557
|
+
continue;
|
|
2558
|
+
}
|
|
2559
|
+
if (char === '/' && content[index + 1] === '/') {
|
|
2560
|
+
index = skipLineComment(content, index + 2);
|
|
2561
|
+
continue;
|
|
2562
|
+
}
|
|
2563
|
+
if (char === '/' && content[index + 1] === '*') {
|
|
2564
|
+
index = skipBlockComment(content, index + 2);
|
|
2565
|
+
continue;
|
|
2566
|
+
}
|
|
2567
|
+
if (char === '{') {
|
|
2568
|
+
depth++;
|
|
2569
|
+
continue;
|
|
2570
|
+
}
|
|
2571
|
+
if (char === '}') {
|
|
2572
|
+
depth--;
|
|
2573
|
+
if (depth === 0) {
|
|
2574
|
+
return index;
|
|
2575
|
+
}
|
|
2576
|
+
}
|
|
2577
|
+
}
|
|
2578
|
+
return undefined;
|
|
2579
|
+
}
|
|
2580
|
+
function readQuotedTitle(content, startIndex, quote) {
|
|
2581
|
+
let value = '';
|
|
2582
|
+
for (let index = startIndex; index < content.length; index++) {
|
|
2583
|
+
const char = content[index];
|
|
2584
|
+
if (char === '\\') {
|
|
2585
|
+
value += char;
|
|
2586
|
+
if (index + 1 < content.length) {
|
|
2587
|
+
index++;
|
|
2588
|
+
value += content[index];
|
|
2589
|
+
}
|
|
2590
|
+
continue;
|
|
2591
|
+
}
|
|
2592
|
+
if (char === quote) {
|
|
2593
|
+
return { value, endIndex: index };
|
|
2594
|
+
}
|
|
2595
|
+
if (char === '\n' && quote !== '`') {
|
|
2596
|
+
return undefined;
|
|
2597
|
+
}
|
|
2598
|
+
value += char;
|
|
2599
|
+
}
|
|
2600
|
+
return undefined;
|
|
2601
|
+
}
|
|
2602
|
+
function skipWhitespaceAndFindComma(content, fromIndex) {
|
|
2603
|
+
for (let index = fromIndex; index < content.length; index++) {
|
|
2604
|
+
const char = content[index];
|
|
2605
|
+
if (/\s/.test(char)) {
|
|
2606
|
+
continue;
|
|
2607
|
+
}
|
|
2608
|
+
return char === ',' ? index : undefined;
|
|
2609
|
+
}
|
|
2610
|
+
return undefined;
|
|
2611
|
+
}
|
|
2612
|
+
function findSnippetEndIndex(content, closeBraceIndex) {
|
|
2613
|
+
let index = closeBraceIndex + 1;
|
|
2614
|
+
while (index < content.length) {
|
|
2615
|
+
const char = content[index];
|
|
2616
|
+
if (char === '\r' || char === '\n') {
|
|
2617
|
+
return index;
|
|
2618
|
+
}
|
|
2619
|
+
index++;
|
|
2620
|
+
}
|
|
2621
|
+
return content.length;
|
|
2622
|
+
}
|
|
2623
|
+
function skipQuotedString(content, startIndex) {
|
|
2624
|
+
const quote = content[startIndex];
|
|
2625
|
+
for (let index = startIndex + 1; index < content.length; index++) {
|
|
2626
|
+
const char = content[index];
|
|
2627
|
+
if (char === '\\') {
|
|
2628
|
+
index++;
|
|
2629
|
+
continue;
|
|
2630
|
+
}
|
|
2631
|
+
if (char === quote) {
|
|
2632
|
+
return index;
|
|
2633
|
+
}
|
|
2634
|
+
}
|
|
2635
|
+
return content.length - 1;
|
|
2636
|
+
}
|
|
2637
|
+
function skipLineComment(content, startIndex) {
|
|
2638
|
+
for (let index = startIndex; index < content.length; index++) {
|
|
2639
|
+
if (content[index] === '\n') {
|
|
2640
|
+
return index;
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2643
|
+
return content.length - 1;
|
|
2644
|
+
}
|
|
2645
|
+
function skipBlockComment(content, startIndex) {
|
|
2646
|
+
for (let index = startIndex; index < content.length - 1; index++) {
|
|
2647
|
+
if (content[index] === '*' && content[index + 1] === '/') {
|
|
2648
|
+
return index + 1;
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2651
|
+
return content.length - 1;
|
|
2652
|
+
}
|
|
2653
|
+
function looksLikeLineTerminator(content, index) {
|
|
2654
|
+
for (let cursor = index; cursor < content.length; cursor++) {
|
|
2655
|
+
const char = content[cursor];
|
|
2656
|
+
if (!/\s/.test(char)) {
|
|
2657
|
+
return char !== '.' && char !== '(';
|
|
2658
|
+
}
|
|
2659
|
+
}
|
|
2660
|
+
return false;
|
|
2661
|
+
}
|
|
2662
|
+
function buildLineOffsets(content) {
|
|
2663
|
+
const offsets = [0];
|
|
2664
|
+
for (let index = 0; index < content.length; index++) {
|
|
2665
|
+
if (content[index] === '\n') {
|
|
2666
|
+
offsets.push(index + 1);
|
|
2667
|
+
}
|
|
2668
|
+
}
|
|
2669
|
+
return offsets;
|
|
2670
|
+
}
|
|
2671
|
+
function getLineNumber(lineOffsets, index) {
|
|
2672
|
+
let line = 0;
|
|
2673
|
+
for (let i = 0; i < lineOffsets.length; i++) {
|
|
2674
|
+
if (lineOffsets[i] > index) {
|
|
2675
|
+
break;
|
|
2676
|
+
}
|
|
2677
|
+
line = i;
|
|
2678
|
+
}
|
|
2679
|
+
return line + 1;
|
|
2680
|
+
}
|
|
2681
|
+
function isCommentedLine(content, index) {
|
|
2682
|
+
const lineStart = content.lastIndexOf('\n', index - 1) + 1;
|
|
2683
|
+
const prefix = content.slice(lineStart, index).trimStart();
|
|
2684
|
+
return prefix.startsWith('//') || prefix.startsWith('*');
|
|
2685
|
+
}
|
|
2686
|
+
function startsWithWord(content, index, word) {
|
|
2687
|
+
if (!content.startsWith(word, index)) {
|
|
2688
|
+
return false;
|
|
2689
|
+
}
|
|
2690
|
+
const before = index === 0 ? '' : content[index - 1];
|
|
2691
|
+
const after = content[index + word.length] ?? '';
|
|
2692
|
+
return !isIdentifierChar(before) && !isIdentifierChar(after);
|
|
2693
|
+
}
|
|
2694
|
+
function isIdentifierChar(char) {
|
|
2695
|
+
return /[A-Za-z0-9_$]/.test(char);
|
|
2696
|
+
}
|
|
2697
|
+
function normalizeWhitespace(value) {
|
|
2698
|
+
return (value ?? '').trim().replace(/\s+/g, ' ');
|
|
2699
|
+
}
|
|
2700
|
+
function normalizeDescribeChain(suite) {
|
|
2701
|
+
if (!suite) {
|
|
2702
|
+
return '';
|
|
2703
|
+
}
|
|
2704
|
+
const segments = suite
|
|
2705
|
+
.split('>')
|
|
2706
|
+
.map((segment) => segment.trim())
|
|
2707
|
+
.filter(Boolean);
|
|
2708
|
+
if (segments.length === 0) {
|
|
2709
|
+
return '';
|
|
2710
|
+
}
|
|
2711
|
+
const firstSegmentLooksLikeFile = segments[0].includes('.');
|
|
2712
|
+
const describeSegments = firstSegmentLooksLikeFile ? segments.slice(1) : segments;
|
|
2713
|
+
return normalizeWhitespace(describeSegments.join(' '));
|
|
2714
|
+
}
|
|
2715
|
+
function isInsideSourceRoot(filePath, sourceRoot) {
|
|
2716
|
+
const relative = node_path_1.default.relative(sourceRoot, filePath);
|
|
2717
|
+
return relative.length > 0 && !relative.startsWith('..') && !node_path_1.default.isAbsolute(relative);
|
|
2718
|
+
}
|
|
2719
|
+
//# sourceMappingURL=js-test-name-snippets.js.map
|
|
2720
|
+
|
|
2721
|
+
/***/ }),
|
|
2722
|
+
|
|
2723
|
+
/***/ 9304:
|
|
2724
|
+
/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
|
|
2725
|
+
|
|
2726
|
+
"use strict";
|
|
2727
|
+
|
|
2728
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
2729
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
2730
|
+
};
|
|
2731
|
+
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
2732
|
+
exports.resolveSourceSnippetOptions = resolveSourceSnippetOptions;
|
|
2733
|
+
exports.enrichReportWithSourceSnippets = enrichReportWithSourceSnippets;
|
|
2734
|
+
const promises_1 = __nccwpck_require__(1455);
|
|
2735
|
+
const node_path_1 = __importDefault(__nccwpck_require__(6760));
|
|
2736
|
+
const DEFAULT_MAX_CHARS = 2000;
|
|
2737
|
+
const DEFAULT_MAX_LINES = 40;
|
|
2738
|
+
function resolveSourceSnippetOptions(options) {
|
|
2739
|
+
return {
|
|
2740
|
+
sourceRoot: node_path_1.default.resolve(options?.sourceRoot ?? process.cwd()),
|
|
2741
|
+
maxChars: normalizePositiveInt(options?.maxChars, DEFAULT_MAX_CHARS),
|
|
2742
|
+
maxLines: normalizePositiveInt(options?.maxLines, DEFAULT_MAX_LINES),
|
|
2743
|
+
};
|
|
2744
|
+
}
|
|
2745
|
+
// This implementation only extracts JS/TS-style test()/it() blocks.
|
|
2746
|
+
// Other languages need reporter-specific snippet logic after that reporter
|
|
2747
|
+
// has supplied file/line metadata for the test.
|
|
2748
|
+
async function enrichReportWithSourceSnippets(report, options) {
|
|
2749
|
+
const resolved = resolveSourceSnippetOptions(options);
|
|
2750
|
+
const fileContentCache = new Map();
|
|
2751
|
+
await Promise.all(report.results.tests.map(async (test) => {
|
|
2752
|
+
if (test.snippet || !test.filePath || !test.line) {
|
|
2753
|
+
return;
|
|
2754
|
+
}
|
|
2755
|
+
const snippet = await tryExtractSnippet(test, resolved, fileContentCache);
|
|
2756
|
+
if (snippet) {
|
|
2757
|
+
test.snippet = snippet;
|
|
2758
|
+
}
|
|
2759
|
+
}));
|
|
2760
|
+
}
|
|
2761
|
+
async function tryExtractSnippet(test, options, fileContentCache) {
|
|
2762
|
+
const filePath = node_path_1.default.resolve(options.sourceRoot, test.filePath);
|
|
2763
|
+
if (!isInsideSourceRoot(filePath, options.sourceRoot)) {
|
|
2764
|
+
return undefined;
|
|
2765
|
+
}
|
|
2766
|
+
const content = await getCachedFileContent(filePath, fileContentCache);
|
|
2767
|
+
if (!content) {
|
|
2768
|
+
return undefined;
|
|
2769
|
+
}
|
|
2770
|
+
return extractEnclosingTestBlock(content, test.line, options);
|
|
2771
|
+
}
|
|
2772
|
+
function getCachedFileContent(filePath, fileContentCache) {
|
|
2773
|
+
let cached = fileContentCache.get(filePath);
|
|
2774
|
+
if (!cached) {
|
|
2775
|
+
cached = (0, promises_1.readFile)(filePath, 'utf-8').catch(() => undefined);
|
|
2776
|
+
fileContentCache.set(filePath, cached);
|
|
2777
|
+
}
|
|
2778
|
+
return cached;
|
|
2779
|
+
}
|
|
2780
|
+
function extractEnclosingTestBlock(content, lineNumber, options) {
|
|
2781
|
+
const lines = content.split(/\r?\n/);
|
|
2782
|
+
const index = Math.max(0, Math.min(lines.length - 1, lineNumber - 1));
|
|
2783
|
+
const start = findTestStart(lines, index);
|
|
2784
|
+
if (start == null) {
|
|
2785
|
+
return undefined;
|
|
2786
|
+
}
|
|
2787
|
+
const selected = [];
|
|
2788
|
+
let braceBalance = 0;
|
|
2789
|
+
let sawOpeningBrace = false;
|
|
2790
|
+
for (let i = start; i < lines.length && selected.length < options.maxLines; i++) {
|
|
2791
|
+
const line = lines[i];
|
|
2792
|
+
selected.push(line);
|
|
2793
|
+
for (const char of line) {
|
|
2794
|
+
if (char === '{') {
|
|
2795
|
+
braceBalance++;
|
|
2796
|
+
sawOpeningBrace = true;
|
|
2797
|
+
}
|
|
2798
|
+
else if (char === '}') {
|
|
2799
|
+
braceBalance--;
|
|
2800
|
+
}
|
|
2801
|
+
}
|
|
2802
|
+
if (sawOpeningBrace && braceBalance <= 0 && isJsTestBlockTerminator(line)) {
|
|
2803
|
+
break;
|
|
2804
|
+
}
|
|
2805
|
+
}
|
|
2806
|
+
const snippet = selected.join('\n').trim();
|
|
2807
|
+
if (!snippet) {
|
|
2808
|
+
return undefined;
|
|
2809
|
+
}
|
|
2810
|
+
return snippet.length <= options.maxChars
|
|
2811
|
+
? snippet
|
|
2812
|
+
: snippet.slice(0, options.maxChars);
|
|
2813
|
+
}
|
|
2814
|
+
function findTestStart(lines, fromIndex) {
|
|
2815
|
+
for (let i = fromIndex; i >= 0; i--) {
|
|
2816
|
+
// Walk upward from runner-provided line metadata until we hit a JS/TS
|
|
2817
|
+
// test/it call, including common modifiers used by Vitest/Playwright.
|
|
2818
|
+
if (/\b(?:test|it)(?:\.(?:only|skip|fixme|slow))?\s*\(/.test(lines[i])) {
|
|
2819
|
+
return i;
|
|
2820
|
+
}
|
|
2821
|
+
}
|
|
2822
|
+
return undefined;
|
|
2823
|
+
}
|
|
2824
|
+
function isJsTestBlockTerminator(line) {
|
|
2825
|
+
let i = line.length - 1;
|
|
2826
|
+
while (i >= 0 && /\s/.test(line[i])) {
|
|
2827
|
+
i--;
|
|
2828
|
+
}
|
|
2829
|
+
if (i >= 0 && line[i] === ';') {
|
|
2830
|
+
i--;
|
|
2831
|
+
}
|
|
2832
|
+
while (i >= 0 && /\s/.test(line[i])) {
|
|
2833
|
+
i--;
|
|
2834
|
+
}
|
|
2835
|
+
if (i < 1 || line[i] !== ')') {
|
|
2836
|
+
return false;
|
|
2837
|
+
}
|
|
2838
|
+
i--;
|
|
2839
|
+
while (i >= 0 && /\s/.test(line[i])) {
|
|
2840
|
+
i--;
|
|
2841
|
+
}
|
|
2842
|
+
return i >= 0 && line[i] === '}';
|
|
2843
|
+
}
|
|
2844
|
+
function isInsideSourceRoot(filePath, sourceRoot) {
|
|
2845
|
+
const relative = node_path_1.default.relative(sourceRoot, filePath);
|
|
2846
|
+
return relative.length > 0 && !relative.startsWith('..') && !node_path_1.default.isAbsolute(relative);
|
|
2847
|
+
}
|
|
2848
|
+
function normalizePositiveInt(value, fallback) {
|
|
2849
|
+
return typeof value === 'number' && Number.isInteger(value) && value > 0
|
|
2850
|
+
? value
|
|
2851
|
+
: fallback;
|
|
2852
|
+
}
|
|
2853
|
+
//# sourceMappingURL=source-snippets.js.map
|
|
2854
|
+
|
|
2855
|
+
/***/ }),
|
|
2856
|
+
|
|
2857
|
+
/***/ 2293:
|
|
2858
|
+
/***/ ((__unused_webpack_module, exports) => {
|
|
2859
|
+
|
|
2860
|
+
"use strict";
|
|
2861
|
+
|
|
2862
|
+
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
2863
|
+
exports.createTestLocationCollector = createTestLocationCollector;
|
|
2864
|
+
/**
|
|
2865
|
+
* Generic name-based location mapper for CTRF reports.
|
|
2866
|
+
*
|
|
2867
|
+
* Different reporters can collect test locations in their own format
|
|
2868
|
+
* (Playwright runtime metadata, parsed JUnit XML, etc.) and then feed
|
|
2869
|
+
* this collector to apply file/line metadata in a consistent FIFO way.
|
|
2870
|
+
*/
|
|
2871
|
+
function createTestLocationCollector() {
|
|
2872
|
+
const locationsByTestName = new Map();
|
|
2873
|
+
return {
|
|
2874
|
+
add(testName, location) {
|
|
2875
|
+
if (!testName || !isValidLocation(location)) {
|
|
2876
|
+
return;
|
|
2877
|
+
}
|
|
2878
|
+
const queue = locationsByTestName.get(testName) ?? [];
|
|
2879
|
+
queue.push(location);
|
|
2880
|
+
locationsByTestName.set(testName, queue);
|
|
2881
|
+
},
|
|
2882
|
+
applyToReport(report) {
|
|
2883
|
+
const queues = new Map([...locationsByTestName.entries()].map(([name, locations]) => [name, [...locations]]));
|
|
2884
|
+
for (const test of report.results.tests) {
|
|
2885
|
+
const queue = queues.get(test.name);
|
|
2886
|
+
const location = queue?.shift();
|
|
2887
|
+
if (!location) {
|
|
2888
|
+
continue;
|
|
2889
|
+
}
|
|
2890
|
+
test.filePath ?? (test.filePath = location.filePath);
|
|
2891
|
+
test.line ?? (test.line = location.line);
|
|
2892
|
+
}
|
|
2893
|
+
},
|
|
2894
|
+
};
|
|
2895
|
+
}
|
|
2896
|
+
function isValidLocation(location) {
|
|
2897
|
+
return Boolean(location.filePath) && Number.isInteger(location.line) && location.line > 0;
|
|
2898
|
+
}
|
|
2899
|
+
//# sourceMappingURL=test-locations.js.map
|
|
2900
|
+
|
|
2901
|
+
/***/ }),
|
|
2902
|
+
|
|
2344
2903
|
/***/ 8969:
|
|
2345
2904
|
/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
|
|
2346
2905
|
|
|
@@ -2352,7 +2911,8 @@ exports.uploadTestRun = uploadTestRun;
|
|
|
2352
2911
|
exports.mapAttachmentsToTestResults = mapAttachmentsToTestResults;
|
|
2353
2912
|
exports.uploadArtifacts = uploadArtifacts;
|
|
2354
2913
|
const ci_detection_1 = __nccwpck_require__(2406);
|
|
2355
|
-
const
|
|
2914
|
+
const source_snippets_1 = __nccwpck_require__(9304);
|
|
2915
|
+
const DEFAULT_API_URL = "https://api.testream.app";
|
|
2356
2916
|
function ensureReportId(report) {
|
|
2357
2917
|
if (typeof report.reportId === "string" &&
|
|
2358
2918
|
report.reportId.trim().length > 0) {
|
|
@@ -2448,6 +3008,7 @@ async function uploadTestRun(options) {
|
|
|
2448
3008
|
const apiUrl = resolveApiUrl(options.apiUrl);
|
|
2449
3009
|
const reportId = ensureReportId(report);
|
|
2450
3010
|
const uploadContext = resolveUploadContext(options);
|
|
3011
|
+
await (0, source_snippets_1.enrichReportWithSourceSnippets)(report);
|
|
2451
3012
|
// Build type-safe IngestRequest payload
|
|
2452
3013
|
const ingestPayload = {
|
|
2453
3014
|
report,
|
|
@@ -2588,8 +3149,8 @@ async function uploadArtifacts(options) {
|
|
|
2588
3149
|
async function uploadSingleArtifact(options) {
|
|
2589
3150
|
const { testResultId, attachment, reportId, apiKey, apiUrl } = options;
|
|
2590
3151
|
// Dynamically import fs for Node.js environments
|
|
2591
|
-
const fs = await Promise.resolve(/* import() */).then(__nccwpck_require__.t.bind(__nccwpck_require__,
|
|
2592
|
-
const path = await Promise.resolve(/* import() */).then(__nccwpck_require__.t.bind(__nccwpck_require__,
|
|
3152
|
+
const fs = await Promise.resolve(/* import() */).then(__nccwpck_require__.t.bind(__nccwpck_require__, 1455, 23));
|
|
3153
|
+
const path = await Promise.resolve(/* import() */).then(__nccwpck_require__.t.bind(__nccwpck_require__, 6760, 23));
|
|
2593
3154
|
const filePath = path.resolve(attachment.path);
|
|
2594
3155
|
// Check if file exists
|
|
2595
3156
|
try {
|
|
@@ -2672,7 +3233,7 @@ async function uploadWithPresignedUrl(options) {
|
|
|
2672
3233
|
}
|
|
2673
3234
|
const uploadInitResult = (await initResponse.json());
|
|
2674
3235
|
const uploadHeaders = {
|
|
2675
|
-
...(uploadInitResult.requiredHeaders
|
|
3236
|
+
...(uploadInitResult.requiredHeaders),
|
|
2676
3237
|
};
|
|
2677
3238
|
if (!hasHeaderIgnoreCase(uploadHeaders, "Content-Type")) {
|
|
2678
3239
|
uploadHeaders["Content-Type"] = contentType;
|
|
@@ -3100,6 +3661,307 @@ async function main() {
|
|
|
3100
3661
|
main();
|
|
3101
3662
|
|
|
3102
3663
|
|
|
3664
|
+
/***/ }),
|
|
3665
|
+
|
|
3666
|
+
/***/ 148:
|
|
3667
|
+
/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
|
|
3668
|
+
|
|
3669
|
+
"use strict";
|
|
3670
|
+
|
|
3671
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3672
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
3673
|
+
};
|
|
3674
|
+
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
3675
|
+
exports.createDotnetSourceSnippetResolver = createDotnetSourceSnippetResolver;
|
|
3676
|
+
exports.extractDotnetSourceSnippet = extractDotnetSourceSnippet;
|
|
3677
|
+
exports.resolveDotnetSourceSnippet = resolveDotnetSourceSnippet;
|
|
3678
|
+
const promises_1 = __nccwpck_require__(1455);
|
|
3679
|
+
const node_path_1 = __importDefault(__nccwpck_require__(6760));
|
|
3680
|
+
const DEFAULT_MAX_CHARS = 2000;
|
|
3681
|
+
const DEFAULT_MAX_LINES = 40;
|
|
3682
|
+
function createDotnetSourceSnippetResolver() {
|
|
3683
|
+
const cache = {
|
|
3684
|
+
candidateSourceFiles: new Map(),
|
|
3685
|
+
fileContentByPath: new Map(),
|
|
3686
|
+
};
|
|
3687
|
+
return {
|
|
3688
|
+
resolve: (options) => resolveDotnetSourceSnippet(options, cache),
|
|
3689
|
+
};
|
|
3690
|
+
}
|
|
3691
|
+
async function extractDotnetSourceSnippet(filePath, methodName, lineNumber) {
|
|
3692
|
+
const result = await resolveDotnetSourceSnippet({ filePath, methodName, lineNumber });
|
|
3693
|
+
return result?.snippet;
|
|
3694
|
+
}
|
|
3695
|
+
async function resolveDotnetSourceSnippet(options, cache) {
|
|
3696
|
+
if (!options.filePath || !options.methodName) {
|
|
3697
|
+
return undefined;
|
|
3698
|
+
}
|
|
3699
|
+
const direct = await tryExtractFromSourceFile(options.filePath, options.methodName, options.lineNumber, cache);
|
|
3700
|
+
if (direct) {
|
|
3701
|
+
return direct;
|
|
3702
|
+
}
|
|
3703
|
+
const candidates = await getCandidateSourceFiles(options.filePath, options.className, cache);
|
|
3704
|
+
for (const candidate of candidates) {
|
|
3705
|
+
const resolved = await tryExtractFromSourceFile(candidate, options.methodName, options.lineNumber, cache);
|
|
3706
|
+
if (resolved) {
|
|
3707
|
+
return resolved;
|
|
3708
|
+
}
|
|
3709
|
+
}
|
|
3710
|
+
return undefined;
|
|
3711
|
+
}
|
|
3712
|
+
async function tryExtractFromSourceFile(filePath, methodName, lineNumber, cache) {
|
|
3713
|
+
if (node_path_1.default.extname(filePath).toLowerCase() !== '.cs') {
|
|
3714
|
+
return undefined;
|
|
3715
|
+
}
|
|
3716
|
+
const resolvedPath = node_path_1.default.resolve(filePath);
|
|
3717
|
+
const content = await getFileContent(resolvedPath, cache);
|
|
3718
|
+
if (!content) {
|
|
3719
|
+
return undefined;
|
|
3720
|
+
}
|
|
3721
|
+
for (const candidateMethodName of getMethodNameCandidates(methodName)) {
|
|
3722
|
+
const extracted = extractCSharpMethodBlock(content, candidateMethodName, lineNumber);
|
|
3723
|
+
if (extracted) {
|
|
3724
|
+
return { filePath: resolvedPath, ...extracted };
|
|
3725
|
+
}
|
|
3726
|
+
}
|
|
3727
|
+
return undefined;
|
|
3728
|
+
}
|
|
3729
|
+
function getFileContent(filePath, cache) {
|
|
3730
|
+
if (!cache) {
|
|
3731
|
+
return (0, promises_1.readFile)(filePath, 'utf-8').catch(() => undefined);
|
|
3732
|
+
}
|
|
3733
|
+
let cached = cache.fileContentByPath.get(filePath);
|
|
3734
|
+
if (!cached) {
|
|
3735
|
+
cached = (0, promises_1.readFile)(filePath, 'utf-8').catch(() => undefined);
|
|
3736
|
+
cache.fileContentByPath.set(filePath, cached);
|
|
3737
|
+
}
|
|
3738
|
+
return cached;
|
|
3739
|
+
}
|
|
3740
|
+
function extractCSharpMethodBlock(content, methodName, lineNumber) {
|
|
3741
|
+
const lines = content.split(/\r?\n/);
|
|
3742
|
+
const definitionIndex = findMethodDefinitionNearLine(lines, methodName, lineNumber) ??
|
|
3743
|
+
findMethodDefinition(lines, methodName);
|
|
3744
|
+
if (definitionIndex == null) {
|
|
3745
|
+
return undefined;
|
|
3746
|
+
}
|
|
3747
|
+
const start = includeCSharpAttributes(lines, definitionIndex);
|
|
3748
|
+
const selected = [];
|
|
3749
|
+
let braceBalance = 0;
|
|
3750
|
+
let sawOpeningBrace = false;
|
|
3751
|
+
let isExpressionBodied = false;
|
|
3752
|
+
for (let i = start; i < lines.length && selected.length < DEFAULT_MAX_LINES; i++) {
|
|
3753
|
+
const line = lines[i];
|
|
3754
|
+
selected.push(line);
|
|
3755
|
+
if (!sawOpeningBrace && line.includes('=>')) {
|
|
3756
|
+
isExpressionBodied = true;
|
|
3757
|
+
}
|
|
3758
|
+
for (const char of line) {
|
|
3759
|
+
if (char === '{') {
|
|
3760
|
+
braceBalance++;
|
|
3761
|
+
sawOpeningBrace = true;
|
|
3762
|
+
}
|
|
3763
|
+
else if (char === '}') {
|
|
3764
|
+
braceBalance--;
|
|
3765
|
+
}
|
|
3766
|
+
}
|
|
3767
|
+
if (isExpressionBodied && line.includes(';')) {
|
|
3768
|
+
break;
|
|
3769
|
+
}
|
|
3770
|
+
if (sawOpeningBrace && braceBalance <= 0) {
|
|
3771
|
+
break;
|
|
3772
|
+
}
|
|
3773
|
+
}
|
|
3774
|
+
const snippet = selected.join('\n').trim();
|
|
3775
|
+
if (!snippet) {
|
|
3776
|
+
return undefined;
|
|
3777
|
+
}
|
|
3778
|
+
return {
|
|
3779
|
+
line: start + 1,
|
|
3780
|
+
snippet: snippet.length <= DEFAULT_MAX_CHARS ? snippet : snippet.slice(0, DEFAULT_MAX_CHARS),
|
|
3781
|
+
};
|
|
3782
|
+
}
|
|
3783
|
+
function findMethodDefinitionNearLine(lines, methodName, lineNumber) {
|
|
3784
|
+
if (!lineNumber) {
|
|
3785
|
+
return undefined;
|
|
3786
|
+
}
|
|
3787
|
+
const startIndex = Math.max(0, Math.min(lines.length - 1, lineNumber - 1));
|
|
3788
|
+
for (let i = startIndex; i >= 0; i--) {
|
|
3789
|
+
if (containsMethodSignatureName(lines[i], methodName)) {
|
|
3790
|
+
return i;
|
|
3791
|
+
}
|
|
3792
|
+
}
|
|
3793
|
+
return undefined;
|
|
3794
|
+
}
|
|
3795
|
+
function findMethodDefinition(lines, methodName) {
|
|
3796
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3797
|
+
if (containsMethodSignatureName(lines[i], methodName)) {
|
|
3798
|
+
return i;
|
|
3799
|
+
}
|
|
3800
|
+
}
|
|
3801
|
+
return undefined;
|
|
3802
|
+
}
|
|
3803
|
+
function containsMethodSignatureName(line, methodName) {
|
|
3804
|
+
const nameIndex = line.indexOf(methodName);
|
|
3805
|
+
if (nameIndex < 0) {
|
|
3806
|
+
return false;
|
|
3807
|
+
}
|
|
3808
|
+
const before = nameIndex === 0 ? '' : line[nameIndex - 1];
|
|
3809
|
+
if (before && isIdentifierChar(before)) {
|
|
3810
|
+
return false;
|
|
3811
|
+
}
|
|
3812
|
+
let i = nameIndex + methodName.length;
|
|
3813
|
+
while (i < line.length && /\s/.test(line[i])) {
|
|
3814
|
+
i++;
|
|
3815
|
+
}
|
|
3816
|
+
return line[i] === '(';
|
|
3817
|
+
}
|
|
3818
|
+
function includeCSharpAttributes(lines, definitionIndex) {
|
|
3819
|
+
let start = definitionIndex;
|
|
3820
|
+
while (start > 0) {
|
|
3821
|
+
const previous = lines[start - 1].trim();
|
|
3822
|
+
if (!previous.startsWith('[') || !previous.endsWith(']')) {
|
|
3823
|
+
break;
|
|
3824
|
+
}
|
|
3825
|
+
start--;
|
|
3826
|
+
}
|
|
3827
|
+
return start;
|
|
3828
|
+
}
|
|
3829
|
+
function getMethodNameCandidates(methodName) {
|
|
3830
|
+
const trimmed = methodName.trim();
|
|
3831
|
+
const candidates = [trimmed];
|
|
3832
|
+
const invocationIndex = trimmed.indexOf('(');
|
|
3833
|
+
// NUnit can put TestCase argument values in TestMethod/@name, e.g.
|
|
3834
|
+
// Password_MinimumLength_Validation("pass",False). Source matching needs
|
|
3835
|
+
// the underlying C# method identifier.
|
|
3836
|
+
if (invocationIndex > 0) {
|
|
3837
|
+
const sourceMethodName = trimmed.slice(0, invocationIndex).trim();
|
|
3838
|
+
if (sourceMethodName && sourceMethodName !== trimmed) {
|
|
3839
|
+
candidates.push(sourceMethodName);
|
|
3840
|
+
}
|
|
3841
|
+
}
|
|
3842
|
+
return candidates;
|
|
3843
|
+
}
|
|
3844
|
+
async function findCandidateSourceFiles(storagePath, className) {
|
|
3845
|
+
const sourceRoot = getProjectSourceRootFromStorage(storagePath);
|
|
3846
|
+
if (!sourceRoot) {
|
|
3847
|
+
return [];
|
|
3848
|
+
}
|
|
3849
|
+
const resolvedSourceRoot = await resolveExistingDirectoryCaseInsensitive(sourceRoot);
|
|
3850
|
+
if (!resolvedSourceRoot) {
|
|
3851
|
+
return [];
|
|
3852
|
+
}
|
|
3853
|
+
const files = await listCSharpFiles(resolvedSourceRoot);
|
|
3854
|
+
const classFileName = getClassFileName(className);
|
|
3855
|
+
if (!classFileName) {
|
|
3856
|
+
return files;
|
|
3857
|
+
}
|
|
3858
|
+
// TRX commonly stores passed tests against the compiled DLL. Prefer the class
|
|
3859
|
+
// filename when available, then fall back to every source file for cases where
|
|
3860
|
+
// a project keeps multiple test classes in one file.
|
|
3861
|
+
return [
|
|
3862
|
+
...files.filter((file) => node_path_1.default.basename(file).toLowerCase() === classFileName),
|
|
3863
|
+
...files.filter((file) => node_path_1.default.basename(file).toLowerCase() !== classFileName),
|
|
3864
|
+
];
|
|
3865
|
+
}
|
|
3866
|
+
function getCandidateSourceFiles(storagePath, className, cache) {
|
|
3867
|
+
if (!cache) {
|
|
3868
|
+
return findCandidateSourceFiles(storagePath, className);
|
|
3869
|
+
}
|
|
3870
|
+
const key = `${node_path_1.default.resolve(storagePath)}\0${className ?? ''}`;
|
|
3871
|
+
let cached = cache.candidateSourceFiles.get(key);
|
|
3872
|
+
if (!cached) {
|
|
3873
|
+
cached = findCandidateSourceFiles(storagePath, className);
|
|
3874
|
+
cache.candidateSourceFiles.set(key, cached);
|
|
3875
|
+
}
|
|
3876
|
+
return cached;
|
|
3877
|
+
}
|
|
3878
|
+
function getProjectSourceRootFromStorage(storagePath) {
|
|
3879
|
+
const resolved = node_path_1.default.resolve(storagePath);
|
|
3880
|
+
const parts = resolved.split(node_path_1.default.sep);
|
|
3881
|
+
// /Project/bin/Debug/net8.0/Project.dll is the normal TRX storage path for
|
|
3882
|
+
// passed tests, so the source root is the directory before bin/.
|
|
3883
|
+
const binIndex = parts.findIndex((part) => part.toLowerCase() === 'bin');
|
|
3884
|
+
if (binIndex <= 0) {
|
|
3885
|
+
return undefined;
|
|
3886
|
+
}
|
|
3887
|
+
return parts.slice(0, binIndex).join(node_path_1.default.sep) || node_path_1.default.sep;
|
|
3888
|
+
}
|
|
3889
|
+
async function resolveExistingDirectoryCaseInsensitive(dirPath) {
|
|
3890
|
+
try {
|
|
3891
|
+
await (0, promises_1.readdir)(dirPath, { withFileTypes: true });
|
|
3892
|
+
return dirPath;
|
|
3893
|
+
}
|
|
3894
|
+
catch {
|
|
3895
|
+
// TRX storage paths can lowercase project output paths on Linux
|
|
3896
|
+
// (`mstesttests/bin/...`) while the checked-out source directory is
|
|
3897
|
+
// case-sensitive (`MSTestTests`). Walk each segment case-insensitively so
|
|
3898
|
+
// passed tests can still resolve their project source root.
|
|
3899
|
+
}
|
|
3900
|
+
const resolved = node_path_1.default.resolve(dirPath);
|
|
3901
|
+
const parsed = node_path_1.default.parse(resolved);
|
|
3902
|
+
const relativeParts = node_path_1.default.relative(parsed.root, resolved).split(node_path_1.default.sep).filter(Boolean);
|
|
3903
|
+
let current = parsed.root;
|
|
3904
|
+
for (const part of relativeParts) {
|
|
3905
|
+
let entries;
|
|
3906
|
+
try {
|
|
3907
|
+
entries = await (0, promises_1.readdir)(current, { withFileTypes: true });
|
|
3908
|
+
}
|
|
3909
|
+
catch {
|
|
3910
|
+
return undefined;
|
|
3911
|
+
}
|
|
3912
|
+
const match = entries.find((entry) => entry.name === part) ??
|
|
3913
|
+
entries.find((entry) => entry.name.toLowerCase() === part.toLowerCase());
|
|
3914
|
+
if (!match) {
|
|
3915
|
+
return undefined;
|
|
3916
|
+
}
|
|
3917
|
+
current = node_path_1.default.join(current, match.name);
|
|
3918
|
+
}
|
|
3919
|
+
try {
|
|
3920
|
+
await (0, promises_1.readdir)(current, { withFileTypes: true });
|
|
3921
|
+
return current;
|
|
3922
|
+
}
|
|
3923
|
+
catch {
|
|
3924
|
+
return undefined;
|
|
3925
|
+
}
|
|
3926
|
+
}
|
|
3927
|
+
async function listCSharpFiles(root) {
|
|
3928
|
+
const files = [];
|
|
3929
|
+
async function visit(dir) {
|
|
3930
|
+
let entries;
|
|
3931
|
+
try {
|
|
3932
|
+
entries = await (0, promises_1.readdir)(dir, { withFileTypes: true });
|
|
3933
|
+
}
|
|
3934
|
+
catch {
|
|
3935
|
+
return;
|
|
3936
|
+
}
|
|
3937
|
+
for (const entry of entries) {
|
|
3938
|
+
const entryPath = node_path_1.default.join(dir, entry.name);
|
|
3939
|
+
if (entry.isDirectory()) {
|
|
3940
|
+
const lowerName = entry.name.toLowerCase();
|
|
3941
|
+
if (lowerName === 'bin' || lowerName === 'obj') {
|
|
3942
|
+
continue;
|
|
3943
|
+
}
|
|
3944
|
+
await visit(entryPath);
|
|
3945
|
+
continue;
|
|
3946
|
+
}
|
|
3947
|
+
if (entry.isFile() && node_path_1.default.extname(entry.name).toLowerCase() === '.cs') {
|
|
3948
|
+
files.push(entryPath);
|
|
3949
|
+
}
|
|
3950
|
+
}
|
|
3951
|
+
}
|
|
3952
|
+
await visit(root);
|
|
3953
|
+
return files;
|
|
3954
|
+
}
|
|
3955
|
+
function getClassFileName(className) {
|
|
3956
|
+
const shortName = className?.split(/[.+]/).filter(Boolean).pop();
|
|
3957
|
+
return shortName ? `${shortName.toLowerCase()}.cs` : undefined;
|
|
3958
|
+
}
|
|
3959
|
+
function isIdentifierChar(char) {
|
|
3960
|
+
// Prevent matching a method name inside another common C# identifier.
|
|
3961
|
+
return /[A-Za-z0-9_]/.test(char);
|
|
3962
|
+
}
|
|
3963
|
+
|
|
3964
|
+
|
|
3103
3965
|
/***/ }),
|
|
3104
3966
|
|
|
3105
3967
|
/***/ 8914:
|
|
@@ -3143,13 +4005,14 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
3143
4005
|
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
3144
4006
|
exports.parseTrxFile = parseTrxFile;
|
|
3145
4007
|
exports.parseTrxFiles = parseTrxFiles;
|
|
3146
|
-
const fs = __importStar(__nccwpck_require__(
|
|
4008
|
+
const fs = __importStar(__nccwpck_require__(1455));
|
|
3147
4009
|
const fast_xml_parser_1 = __nccwpck_require__(1537);
|
|
3148
|
-
const
|
|
4010
|
+
const node_crypto_1 = __nccwpck_require__(7598);
|
|
4011
|
+
const source_snippets_1 = __nccwpck_require__(148);
|
|
3149
4012
|
const TOOL_NAME = 'dotnet';
|
|
3150
4013
|
// Use crypto.randomUUID() for secure UUID generation (available in Node.js 15.7.0+)
|
|
3151
4014
|
function generateUUID() {
|
|
3152
|
-
return (0,
|
|
4015
|
+
return (0, node_crypto_1.randomUUID)();
|
|
3153
4016
|
}
|
|
3154
4017
|
/**
|
|
3155
4018
|
* Parse TRX duration string (HH:MM:SS.mmmmmmm) to milliseconds
|
|
@@ -3162,15 +4025,15 @@ function parseDuration(duration) {
|
|
|
3162
4025
|
if (parts.length !== 3)
|
|
3163
4026
|
return 0;
|
|
3164
4027
|
const hours = parseInt(parts[0], 10);
|
|
3165
|
-
if (isNaN(hours))
|
|
4028
|
+
if (Number.isNaN(hours))
|
|
3166
4029
|
return 0;
|
|
3167
4030
|
const minutes = parseInt(parts[1], 10);
|
|
3168
|
-
if (isNaN(minutes))
|
|
4031
|
+
if (Number.isNaN(minutes))
|
|
3169
4032
|
return 0;
|
|
3170
4033
|
// Handle seconds and fractional part (may contain decimal point)
|
|
3171
4034
|
const secondsParts = parts[2].split('.');
|
|
3172
|
-
const seconds = parseInt(secondsParts[0], 10);
|
|
3173
|
-
if (isNaN(seconds))
|
|
4035
|
+
const seconds = Number.parseInt(secondsParts[0], 10);
|
|
4036
|
+
if (Number.isNaN(seconds))
|
|
3174
4037
|
return 0;
|
|
3175
4038
|
const fraction = secondsParts[1] ? parseFloat(`0.${secondsParts[1]}`) * 1000 : 0;
|
|
3176
4039
|
return hours * 3600000 + minutes * 60000 + seconds * 1000 + Math.round(fraction);
|
|
@@ -3182,7 +4045,7 @@ function parseTimestamp(dateStr) {
|
|
|
3182
4045
|
if (!dateStr)
|
|
3183
4046
|
return Date.now();
|
|
3184
4047
|
const date = new Date(dateStr);
|
|
3185
|
-
return isNaN(date.getTime()) ? Date.now() : date.getTime();
|
|
4048
|
+
return Number.isNaN(date.getTime()) ? Date.now() : date.getTime();
|
|
3186
4049
|
}
|
|
3187
4050
|
/**
|
|
3188
4051
|
* Map TRX outcome to CTRF status
|
|
@@ -3210,9 +4073,9 @@ function extractLineNumber(stackTrace) {
|
|
|
3210
4073
|
if (!stackTrace)
|
|
3211
4074
|
return undefined;
|
|
3212
4075
|
// Match patterns like ":line 79" or ".cs:79"
|
|
3213
|
-
const match =
|
|
4076
|
+
const match = new RegExp(/:line (\d+)|\.cs:(\d+)/i).exec(stackTrace);
|
|
3214
4077
|
if (match) {
|
|
3215
|
-
return parseInt(match[1] || match[2], 10);
|
|
4078
|
+
return Number.parseInt(match[1] || match[2], 10);
|
|
3216
4079
|
}
|
|
3217
4080
|
return undefined;
|
|
3218
4081
|
}
|
|
@@ -3223,7 +4086,7 @@ function extractFilePath(stackTrace) {
|
|
|
3223
4086
|
if (!stackTrace)
|
|
3224
4087
|
return undefined;
|
|
3225
4088
|
// Match file paths ending in .cs with bounded length to prevent ReDoS
|
|
3226
|
-
const match =
|
|
4089
|
+
const match = new RegExp(/([A-Za-z]:\\[^\s:]{1,256}\.cs|\/[^\s:]{1,256}\.cs)/).exec(stackTrace);
|
|
3227
4090
|
return match ? match[1] : undefined;
|
|
3228
4091
|
}
|
|
3229
4092
|
/**
|
|
@@ -3257,6 +4120,7 @@ async function parseTrxFile(trxPath) {
|
|
|
3257
4120
|
// Parse test results
|
|
3258
4121
|
const results = testRun.Results?.UnitTestResult || [];
|
|
3259
4122
|
const tests = [];
|
|
4123
|
+
const sourceSnippetResolver = (0, source_snippets_1.createDotnetSourceSnippetResolver)();
|
|
3260
4124
|
let passed = 0;
|
|
3261
4125
|
let failed = 0;
|
|
3262
4126
|
let skipped = 0;
|
|
@@ -3294,6 +4158,15 @@ async function parseTrxFile(trxPath) {
|
|
|
3294
4158
|
const stackTrace = result.Output?.ErrorInfo?.StackTrace;
|
|
3295
4159
|
const stdout = result.Output?.StdOut;
|
|
3296
4160
|
const stderr = result.Output?.StdErr;
|
|
4161
|
+
const stackTraceFilePath = extractFilePath(stackTrace);
|
|
4162
|
+
const storagePath = testDef?.['@_storage'];
|
|
4163
|
+
const line = extractLineNumber(stackTrace);
|
|
4164
|
+
const sourceSnippet = await sourceSnippetResolver.resolve({
|
|
4165
|
+
filePath: stackTraceFilePath || storagePath,
|
|
4166
|
+
className,
|
|
4167
|
+
methodName: testDef?.TestMethod?.['@_name'],
|
|
4168
|
+
lineNumber: line,
|
|
4169
|
+
});
|
|
3297
4170
|
const test = {
|
|
3298
4171
|
name: result['@_testName'],
|
|
3299
4172
|
status,
|
|
@@ -3310,8 +4183,9 @@ async function parseTrxFile(trxPath) {
|
|
|
3310
4183
|
stderr: stderr ? [stderr] : [],
|
|
3311
4184
|
message: errorMessage,
|
|
3312
4185
|
trace: stackTrace,
|
|
3313
|
-
line:
|
|
3314
|
-
filePath:
|
|
4186
|
+
line: line ?? sourceSnippet?.line,
|
|
4187
|
+
filePath: sourceSnippet?.filePath ?? stackTraceFilePath ?? storagePath,
|
|
4188
|
+
snippet: sourceSnippet?.snippet,
|
|
3315
4189
|
};
|
|
3316
4190
|
tests.push(test);
|
|
3317
4191
|
}
|
|
@@ -3458,27 +4332,27 @@ module.exports = require("child_process");
|
|
|
3458
4332
|
|
|
3459
4333
|
/***/ }),
|
|
3460
4334
|
|
|
3461
|
-
/***/
|
|
4335
|
+
/***/ 9896:
|
|
3462
4336
|
/***/ ((module) => {
|
|
3463
4337
|
|
|
3464
4338
|
"use strict";
|
|
3465
|
-
module.exports = require("
|
|
4339
|
+
module.exports = require("fs");
|
|
3466
4340
|
|
|
3467
4341
|
/***/ }),
|
|
3468
4342
|
|
|
3469
|
-
/***/
|
|
4343
|
+
/***/ 1943:
|
|
3470
4344
|
/***/ ((module) => {
|
|
3471
4345
|
|
|
3472
4346
|
"use strict";
|
|
3473
|
-
module.exports = require("fs");
|
|
4347
|
+
module.exports = require("fs/promises");
|
|
3474
4348
|
|
|
3475
4349
|
/***/ }),
|
|
3476
4350
|
|
|
3477
|
-
/***/
|
|
4351
|
+
/***/ 7598:
|
|
3478
4352
|
/***/ ((module) => {
|
|
3479
4353
|
|
|
3480
4354
|
"use strict";
|
|
3481
|
-
module.exports = require("
|
|
4355
|
+
module.exports = require("node:crypto");
|
|
3482
4356
|
|
|
3483
4357
|
/***/ }),
|
|
3484
4358
|
|