@testomatio/reporter 2.1.3-beta.2-xml-import → 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/pipe/testomatio.js +2 -1
- package/lib/xmlReader.d.ts +0 -7
- package/lib/xmlReader.js +9 -231
- package/package.json +1 -1
- package/src/pipe/testomatio.js +2 -1
- package/src/xmlReader.js +9 -267
package/lib/pipe/testomatio.js
CHANGED
|
@@ -172,7 +172,8 @@ class TestomatioPipe {
|
|
|
172
172
|
const resp = await this.client.request({
|
|
173
173
|
method: 'PUT',
|
|
174
174
|
url: `/api/reporter/${this.runId}`,
|
|
175
|
-
data: runParams
|
|
175
|
+
data: runParams,
|
|
176
|
+
responseType: 'json'
|
|
176
177
|
});
|
|
177
178
|
if (resp.data.artifacts)
|
|
178
179
|
(0, pipe_utils_js_1.setS3Credentials)(resp.data.artifacts);
|
package/lib/xmlReader.d.ts
CHANGED
|
@@ -77,13 +77,6 @@ declare class XmlReader {
|
|
|
77
77
|
skipped_count: number;
|
|
78
78
|
tests: any[];
|
|
79
79
|
};
|
|
80
|
-
deduplicateTestsByFQN(tests: any): any[];
|
|
81
|
-
generateFQN(test: any): string;
|
|
82
|
-
generateNormalizedFQN(test: any): string;
|
|
83
|
-
extractAssemblyName(test: any): any;
|
|
84
|
-
extractNamespace(test: any): any;
|
|
85
|
-
extractClassName(test: any): any;
|
|
86
|
-
extractCsFileFromPath(test: any): any;
|
|
87
80
|
calculateStats(): {};
|
|
88
81
|
fetchSourceCode(): void;
|
|
89
82
|
formatTests(): void;
|
package/lib/xmlReader.js
CHANGED
|
@@ -131,9 +131,7 @@ class XmlReader {
|
|
|
131
131
|
const { result, total, passed, failed, inconclusive, skipped } = jsonSuite;
|
|
132
132
|
reduceOptions.preferClassname = this.stats.language === 'python';
|
|
133
133
|
const resultTests = processTestSuite(jsonSuite['test-suite']);
|
|
134
|
-
|
|
135
|
-
const deduplicatedTests = this.deduplicateTestsByFQN(resultTests);
|
|
136
|
-
this.tests = this.tests.concat(deduplicatedTests);
|
|
134
|
+
this.tests = this.tests.concat(resultTests);
|
|
137
135
|
return {
|
|
138
136
|
status: result?.toLowerCase(),
|
|
139
137
|
create_tests: true,
|
|
@@ -141,7 +139,7 @@ class XmlReader {
|
|
|
141
139
|
passed_count: parseInt(passed, 10),
|
|
142
140
|
failed_count: parseInt(failed, 10),
|
|
143
141
|
skipped_count: parseInt(inconclusive + skipped, 10),
|
|
144
|
-
tests:
|
|
142
|
+
tests: resultTests,
|
|
145
143
|
};
|
|
146
144
|
}
|
|
147
145
|
processTRX(jsonSuite) {
|
|
@@ -277,169 +275,6 @@ class XmlReader {
|
|
|
277
275
|
tests,
|
|
278
276
|
};
|
|
279
277
|
}
|
|
280
|
-
deduplicateTestsByFQN(tests) {
|
|
281
|
-
const fqnMap = new Map();
|
|
282
|
-
tests.forEach(test => {
|
|
283
|
-
const fqn = this.generateNormalizedFQN(test);
|
|
284
|
-
if (fqnMap.has(fqn)) {
|
|
285
|
-
const existingTest = fqnMap.get(fqn);
|
|
286
|
-
// For parameterized tests, merge as Examples
|
|
287
|
-
if (test.example) {
|
|
288
|
-
// Initialize examples array if it doesn't exist
|
|
289
|
-
if (!existingTest.examples) {
|
|
290
|
-
existingTest.examples = [];
|
|
291
|
-
// Add the existing test's example as the first item
|
|
292
|
-
if (existingTest.example) {
|
|
293
|
-
existingTest.examples.push({
|
|
294
|
-
parameters: existingTest.example,
|
|
295
|
-
status: existingTest.status,
|
|
296
|
-
run_time: existingTest.run_time,
|
|
297
|
-
message: existingTest.message,
|
|
298
|
-
stack: existingTest.stack
|
|
299
|
-
});
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
// Add this test's execution as an example
|
|
303
|
-
existingTest.examples.push({
|
|
304
|
-
parameters: test.example,
|
|
305
|
-
status: test.status,
|
|
306
|
-
run_time: test.run_time,
|
|
307
|
-
message: test.message,
|
|
308
|
-
stack: test.stack
|
|
309
|
-
});
|
|
310
|
-
// Update the main test status to reflect the worst status
|
|
311
|
-
if (test.status === 'failed' || existingTest.status === 'failed') {
|
|
312
|
-
existingTest.status = 'failed';
|
|
313
|
-
}
|
|
314
|
-
else if (test.status === 'skipped' && existingTest.status !== 'failed') {
|
|
315
|
-
existingTest.status = 'skipped';
|
|
316
|
-
}
|
|
317
|
-
// Update total run time
|
|
318
|
-
existingTest.run_time = (existingTest.run_time || 0) + (test.run_time || 0);
|
|
319
|
-
// Merge stack traces if they're different
|
|
320
|
-
if (test.stack && test.stack !== existingTest.stack) {
|
|
321
|
-
existingTest.stack = existingTest.stack + '\n\n---\n\n' + test.stack;
|
|
322
|
-
}
|
|
323
|
-
// Merge messages if they're different
|
|
324
|
-
if (test.message && test.message !== existingTest.message) {
|
|
325
|
-
existingTest.message = existingTest.message + '; ' + test.message;
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
else {
|
|
329
|
-
// Merge test properties for non-parameterized tests, prioritizing Test Explorer structure
|
|
330
|
-
if (test.test_id && !existingTest.test_id) {
|
|
331
|
-
existingTest.test_id = test.test_id;
|
|
332
|
-
}
|
|
333
|
-
// Keep the most complete test data
|
|
334
|
-
if (test.stack && !existingTest.stack) {
|
|
335
|
-
existingTest.stack = test.stack;
|
|
336
|
-
}
|
|
337
|
-
if (test.message && !existingTest.message) {
|
|
338
|
-
existingTest.message = test.message;
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
// Prefer Test Explorer structure (longer, more complete suite_title)
|
|
342
|
-
if (test.suite_title && test.suite_title.length > existingTest.suite_title.length) {
|
|
343
|
-
existingTest.suite_title = test.suite_title;
|
|
344
|
-
existingTest.file = this.extractCsFileFromPath(test);
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
else {
|
|
348
|
-
// Fix file path to use proper .cs file names from source paths
|
|
349
|
-
test.file = this.extractCsFileFromPath(test);
|
|
350
|
-
fqnMap.set(fqn, test);
|
|
351
|
-
}
|
|
352
|
-
});
|
|
353
|
-
return Array.from(fqnMap.values());
|
|
354
|
-
}
|
|
355
|
-
generateFQN(test) {
|
|
356
|
-
// Generate Fully Qualified Name: Namespace + Class + Method (standard .NET FQN)
|
|
357
|
-
// Don't include assembly as it can vary between different test structures
|
|
358
|
-
const namespace = this.extractNamespace(test);
|
|
359
|
-
const className = this.extractClassName(test);
|
|
360
|
-
const methodName = test.title;
|
|
361
|
-
// Use the most complete namespace.class structure available
|
|
362
|
-
if (test.suite_title && test.suite_title.includes('.')) {
|
|
363
|
-
return `${test.suite_title}.${methodName}`;
|
|
364
|
-
}
|
|
365
|
-
return `${namespace}.${className}.${methodName}`;
|
|
366
|
-
}
|
|
367
|
-
generateNormalizedFQN(test) {
|
|
368
|
-
// Generate normalized FQN for deduplication by extracting the core namespace.class.method
|
|
369
|
-
// For parameterized tests, we want the SAME FQN so they merge into one test with multiple Examples
|
|
370
|
-
const fullClassName = test.suite_title || '';
|
|
371
|
-
const methodName = test.title;
|
|
372
|
-
// Extract the most specific namespace.class pattern
|
|
373
|
-
if (fullClassName.includes('.')) {
|
|
374
|
-
const parts = fullClassName.split('.');
|
|
375
|
-
if (parts.length >= 2) {
|
|
376
|
-
const className = parts[parts.length - 1];
|
|
377
|
-
// Look for common .NET namespace patterns and normalize them:
|
|
378
|
-
// TestProject.Tests.MyClass -> Tests.MyClass
|
|
379
|
-
// Tests.MyClass -> Tests.MyClass
|
|
380
|
-
// MyProject.SubNamespace.Tests.MyClass -> Tests.MyClass
|
|
381
|
-
let normalizedNamespace = '';
|
|
382
|
-
for (let i = parts.length - 2; i >= 0; i--) {
|
|
383
|
-
const part = parts[i];
|
|
384
|
-
// Build namespace from right to left, excluding project names
|
|
385
|
-
if (part === 'Tests' || part.endsWith('Tests') || part.includes('Test')) {
|
|
386
|
-
// Found a test namespace, use it as the normalized namespace
|
|
387
|
-
normalizedNamespace = part;
|
|
388
|
-
break;
|
|
389
|
-
}
|
|
390
|
-
else if (i === parts.length - 2) {
|
|
391
|
-
// If no test namespace found, use the immediate parent as namespace
|
|
392
|
-
normalizedNamespace = part;
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
return `${normalizedNamespace}.${className}.${methodName}`;
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
// Fallback for simple class names
|
|
399
|
-
return `${fullClassName}.${methodName}`;
|
|
400
|
-
}
|
|
401
|
-
extractAssemblyName(test) {
|
|
402
|
-
// Extract assembly name from file path or use default
|
|
403
|
-
if (test.file) {
|
|
404
|
-
const parts = test.file.split(/[/\\]/);
|
|
405
|
-
return parts[0] || 'DefaultAssembly';
|
|
406
|
-
}
|
|
407
|
-
return 'DefaultAssembly';
|
|
408
|
-
}
|
|
409
|
-
extractNamespace(test) {
|
|
410
|
-
// Extract namespace from suite_title or classname
|
|
411
|
-
if (test.suite_title && test.suite_title.includes('.')) {
|
|
412
|
-
const parts = test.suite_title.split('.');
|
|
413
|
-
return parts.slice(0, -1).join('.');
|
|
414
|
-
}
|
|
415
|
-
return test.suite_title || 'DefaultNamespace';
|
|
416
|
-
}
|
|
417
|
-
extractClassName(test) {
|
|
418
|
-
// Extract class name from suite_title
|
|
419
|
-
if (test.suite_title && test.suite_title.includes('.')) {
|
|
420
|
-
const parts = test.suite_title.split('.');
|
|
421
|
-
return parts[parts.length - 1];
|
|
422
|
-
}
|
|
423
|
-
return test.suite_title || 'DefaultClass';
|
|
424
|
-
}
|
|
425
|
-
extractCsFileFromPath(test) {
|
|
426
|
-
// Extract .cs file name from source file path, not namespace
|
|
427
|
-
if (test.file) {
|
|
428
|
-
// Look for actual .cs file path patterns
|
|
429
|
-
const csFileMatch = test.file.match(/([^/\\]+\.cs)$/);
|
|
430
|
-
if (csFileMatch) {
|
|
431
|
-
return test.file;
|
|
432
|
-
}
|
|
433
|
-
// If no .cs extension, assume it's a namespace path and convert to likely file name
|
|
434
|
-
const className = this.extractClassName(test);
|
|
435
|
-
const pathParts = test.file.split(/[/\\]/);
|
|
436
|
-
pathParts[pathParts.length - 1] = `${className}.cs`;
|
|
437
|
-
return pathParts.join('/');
|
|
438
|
-
}
|
|
439
|
-
// Fallback to class name
|
|
440
|
-
const className = this.extractClassName(test);
|
|
441
|
-
return `${className}.cs`;
|
|
442
|
-
}
|
|
443
278
|
calculateStats() {
|
|
444
279
|
this.stats = {
|
|
445
280
|
...this.stats,
|
|
@@ -595,8 +430,7 @@ function reduceTestCases(prev, item) {
|
|
|
595
430
|
testCases
|
|
596
431
|
.filter(t => !!t)
|
|
597
432
|
.forEach(testCaseItem => {
|
|
598
|
-
|
|
599
|
-
const file = extractSourceFilePath(testCaseItem, item);
|
|
433
|
+
const file = testCaseItem.file || item.filepath || item.fullname || item.package || '';
|
|
600
434
|
let stack = '';
|
|
601
435
|
let message = '';
|
|
602
436
|
if (testCaseItem.error)
|
|
@@ -616,20 +450,16 @@ function reduceTestCases(prev, item) {
|
|
|
616
450
|
if (!message)
|
|
617
451
|
message = stack.trim().split('\n')[0];
|
|
618
452
|
const isParametrized = item.type === 'ParameterizedMethod';
|
|
453
|
+
const preferClassname = reduceOptions.preferClassname || isParametrized;
|
|
619
454
|
// SpecFlow config
|
|
620
455
|
let { title, tags, testId } = fetchProperties(isParametrized ? item : testCaseItem);
|
|
621
456
|
let example = null;
|
|
622
|
-
|
|
623
|
-
const suiteTitle = extractTestExplorerSuiteTitle(testCaseItem, item);
|
|
457
|
+
const suiteTitle = preferClassname ? testCaseItem.classname : item.name || testCaseItem.classname;
|
|
624
458
|
title ||= testCaseItem.name || testCaseItem.methodname || testCaseItem.classname;
|
|
625
459
|
tags ||= [];
|
|
626
|
-
|
|
627
|
-
const originalTestName = testCaseItem.name || testCaseItem.methodname;
|
|
628
|
-
const exampleMatches = originalTestName?.match(/\((.*?)\)$/);
|
|
460
|
+
const exampleMatches = testCaseItem.name?.match(/\S\((.*?)\)/);
|
|
629
461
|
if (exampleMatches) {
|
|
630
|
-
|
|
631
|
-
const parameterValues = exampleMatches[1].split(',').map(v => v.trim().replace(/['"]/g, ''));
|
|
632
|
-
example = { ...parameterValues };
|
|
462
|
+
example = { ...exampleMatches[1].split(',').map(v => v.trim().replace(/[^\w\s-]/g, '')) };
|
|
633
463
|
title = title.replace(/\(.*?\)/, '').trim();
|
|
634
464
|
}
|
|
635
465
|
stack = `${testCaseItem['system-out'] || testCaseItem.output || testCaseItem.log || ''}\n\n${stack}\n\n${suiteOutput}\n\n${suiteErr}`.trim();
|
|
@@ -678,7 +508,6 @@ function reduceTestCases(prev, item) {
|
|
|
678
508
|
run_time: parseFloat(testCaseItem.time || testCaseItem.duration) * 1000,
|
|
679
509
|
status,
|
|
680
510
|
title,
|
|
681
|
-
originalTestName, // Store original name for parameter-aware FQN generation
|
|
682
511
|
root_suite_id: TESTOMATIO_SUITE,
|
|
683
512
|
suite_title: suiteTitle,
|
|
684
513
|
files,
|
|
@@ -687,51 +516,6 @@ function reduceTestCases(prev, item) {
|
|
|
687
516
|
});
|
|
688
517
|
return prev;
|
|
689
518
|
}
|
|
690
|
-
function extractSourceFilePath(testCaseItem, item) {
|
|
691
|
-
// Priority order for file path extraction to match Test Explorer structure:
|
|
692
|
-
// 1. fullname (contains full project path)
|
|
693
|
-
// 2. filepath (direct file path)
|
|
694
|
-
// 3. file attribute from test case
|
|
695
|
-
// 4. package (fallback)
|
|
696
|
-
if (item.fullname) {
|
|
697
|
-
// Extract actual file path from fullname if it contains path separators
|
|
698
|
-
const fullnameParts = item.fullname.split('.');
|
|
699
|
-
if (fullnameParts.length > 2) {
|
|
700
|
-
// Reconstruct path from project.namespace.class structure
|
|
701
|
-
const projectName = fullnameParts[0];
|
|
702
|
-
const namespaceParts = fullnameParts.slice(1, -1);
|
|
703
|
-
const className = fullnameParts[fullnameParts.length - 1];
|
|
704
|
-
return `${projectName}/${namespaceParts.join('/')}/${className}.cs`;
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
if (item.filepath)
|
|
708
|
-
return item.filepath;
|
|
709
|
-
if (testCaseItem.file)
|
|
710
|
-
return testCaseItem.file;
|
|
711
|
-
if (item.package)
|
|
712
|
-
return item.package;
|
|
713
|
-
// Fallback: construct from classname
|
|
714
|
-
if (testCaseItem.classname) {
|
|
715
|
-
const parts = testCaseItem.classname.split('.');
|
|
716
|
-
const className = parts[parts.length - 1];
|
|
717
|
-
const namespacePath = parts.slice(0, -1).join('/');
|
|
718
|
-
return `${namespacePath}/${className}.cs`;
|
|
719
|
-
}
|
|
720
|
-
return '';
|
|
721
|
-
}
|
|
722
|
-
function extractTestExplorerSuiteTitle(testCaseItem, item) {
|
|
723
|
-
// Extract suite title to match Test Explorer structure (Project/Namespace hierarchy)
|
|
724
|
-
// Priority: fullname > classname > name
|
|
725
|
-
if (item.fullname) {
|
|
726
|
-
// Use fullname to maintain Test Explorer structure
|
|
727
|
-
return item.fullname;
|
|
728
|
-
}
|
|
729
|
-
if (testCaseItem.classname) {
|
|
730
|
-
return testCaseItem.classname;
|
|
731
|
-
}
|
|
732
|
-
// Fallback to item name but prefer classname structure
|
|
733
|
-
return item.name || testCaseItem.classname || 'UnknownClass';
|
|
734
|
-
}
|
|
735
519
|
function processTestSuite(testsuite) {
|
|
736
520
|
if (!testsuite)
|
|
737
521
|
return [];
|
|
@@ -743,14 +527,8 @@ function processTestSuite(testsuite) {
|
|
|
743
527
|
if (!Array.isArray(testsuite)) {
|
|
744
528
|
suites = [testsuite];
|
|
745
529
|
}
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
const leafSuites = suites.filter(s => s['test-case'] || s.testcase);
|
|
749
|
-
// Process child suites recursively
|
|
750
|
-
const childResults = subSuites.map(s => processTestSuite(s['test-suite'])).flat();
|
|
751
|
-
// Process leaf suites with actual test cases
|
|
752
|
-
const leafResults = leafSuites.reduce(reduceTestCases, []);
|
|
753
|
-
return [...childResults, ...leafResults];
|
|
530
|
+
const subSuites = suites.filter(s => s['test-suite'] && !testsuite['test-case']);
|
|
531
|
+
return [...subSuites.map(s => processTestSuite(s['test-suite'])), ...suites.reduce(reduceTestCases, [])].flat();
|
|
754
532
|
}
|
|
755
533
|
function fetchProperties(item) {
|
|
756
534
|
const tags = [];
|
package/package.json
CHANGED
package/src/pipe/testomatio.js
CHANGED
|
@@ -187,7 +187,8 @@ class TestomatioPipe {
|
|
|
187
187
|
const resp = await this.client.request({
|
|
188
188
|
method: 'PUT',
|
|
189
189
|
url: `/api/reporter/${this.runId}`,
|
|
190
|
-
data: runParams
|
|
190
|
+
data: runParams,
|
|
191
|
+
responseType: 'json'
|
|
191
192
|
});
|
|
192
193
|
if (resp.data.artifacts) setS3Credentials(resp.data.artifacts);
|
|
193
194
|
return;
|
package/src/xmlReader.js
CHANGED
|
@@ -161,11 +161,8 @@ class XmlReader {
|
|
|
161
161
|
|
|
162
162
|
reduceOptions.preferClassname = this.stats.language === 'python';
|
|
163
163
|
const resultTests = processTestSuite(jsonSuite['test-suite']);
|
|
164
|
-
|
|
165
|
-
// Deduplicate tests based on FQN (Assembly + Namespace + Class + Method)
|
|
166
|
-
const deduplicatedTests = this.deduplicateTestsByFQN(resultTests);
|
|
167
164
|
|
|
168
|
-
this.tests = this.tests.concat(
|
|
165
|
+
this.tests = this.tests.concat(resultTests);
|
|
169
166
|
|
|
170
167
|
return {
|
|
171
168
|
status: result?.toLowerCase(),
|
|
@@ -174,7 +171,7 @@ class XmlReader {
|
|
|
174
171
|
passed_count: parseInt(passed, 10),
|
|
175
172
|
failed_count: parseInt(failed, 10),
|
|
176
173
|
skipped_count: parseInt(inconclusive + skipped, 10),
|
|
177
|
-
tests:
|
|
174
|
+
tests: resultTests,
|
|
178
175
|
};
|
|
179
176
|
}
|
|
180
177
|
|
|
@@ -322,194 +319,6 @@ class XmlReader {
|
|
|
322
319
|
};
|
|
323
320
|
}
|
|
324
321
|
|
|
325
|
-
deduplicateTestsByFQN(tests) {
|
|
326
|
-
const fqnMap = new Map();
|
|
327
|
-
|
|
328
|
-
tests.forEach(test => {
|
|
329
|
-
const fqn = this.generateNormalizedFQN(test);
|
|
330
|
-
|
|
331
|
-
if (fqnMap.has(fqn)) {
|
|
332
|
-
const existingTest = fqnMap.get(fqn);
|
|
333
|
-
|
|
334
|
-
// For parameterized tests, merge as Examples
|
|
335
|
-
if (test.example) {
|
|
336
|
-
// Initialize examples array if it doesn't exist
|
|
337
|
-
if (!existingTest.examples) {
|
|
338
|
-
existingTest.examples = [];
|
|
339
|
-
// Add the existing test's example as the first item
|
|
340
|
-
if (existingTest.example) {
|
|
341
|
-
existingTest.examples.push({
|
|
342
|
-
parameters: existingTest.example,
|
|
343
|
-
status: existingTest.status,
|
|
344
|
-
run_time: existingTest.run_time,
|
|
345
|
-
message: existingTest.message,
|
|
346
|
-
stack: existingTest.stack
|
|
347
|
-
});
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
// Add this test's execution as an example
|
|
352
|
-
existingTest.examples.push({
|
|
353
|
-
parameters: test.example,
|
|
354
|
-
status: test.status,
|
|
355
|
-
run_time: test.run_time,
|
|
356
|
-
message: test.message,
|
|
357
|
-
stack: test.stack
|
|
358
|
-
});
|
|
359
|
-
|
|
360
|
-
// Update the main test status to reflect the worst status
|
|
361
|
-
if (test.status === 'failed' || existingTest.status === 'failed') {
|
|
362
|
-
existingTest.status = 'failed';
|
|
363
|
-
} else if (test.status === 'skipped' && existingTest.status !== 'failed') {
|
|
364
|
-
existingTest.status = 'skipped';
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
// Update total run time
|
|
368
|
-
existingTest.run_time = (existingTest.run_time || 0) + (test.run_time || 0);
|
|
369
|
-
|
|
370
|
-
// Merge stack traces if they're different
|
|
371
|
-
if (test.stack && test.stack !== existingTest.stack) {
|
|
372
|
-
existingTest.stack = existingTest.stack + '\n\n---\n\n' + test.stack;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
// Merge messages if they're different
|
|
376
|
-
if (test.message && test.message !== existingTest.message) {
|
|
377
|
-
existingTest.message = existingTest.message + '; ' + test.message;
|
|
378
|
-
}
|
|
379
|
-
} else {
|
|
380
|
-
// Merge test properties for non-parameterized tests, prioritizing Test Explorer structure
|
|
381
|
-
if (test.test_id && !existingTest.test_id) {
|
|
382
|
-
existingTest.test_id = test.test_id;
|
|
383
|
-
}
|
|
384
|
-
// Keep the most complete test data
|
|
385
|
-
if (test.stack && !existingTest.stack) {
|
|
386
|
-
existingTest.stack = test.stack;
|
|
387
|
-
}
|
|
388
|
-
if (test.message && !existingTest.message) {
|
|
389
|
-
existingTest.message = test.message;
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
// Prefer Test Explorer structure (longer, more complete suite_title)
|
|
394
|
-
if (test.suite_title && test.suite_title.length > existingTest.suite_title.length) {
|
|
395
|
-
existingTest.suite_title = test.suite_title;
|
|
396
|
-
existingTest.file = this.extractCsFileFromPath(test);
|
|
397
|
-
}
|
|
398
|
-
} else {
|
|
399
|
-
// Fix file path to use proper .cs file names from source paths
|
|
400
|
-
test.file = this.extractCsFileFromPath(test);
|
|
401
|
-
fqnMap.set(fqn, test);
|
|
402
|
-
}
|
|
403
|
-
});
|
|
404
|
-
|
|
405
|
-
return Array.from(fqnMap.values());
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
generateFQN(test) {
|
|
409
|
-
// Generate Fully Qualified Name: Namespace + Class + Method (standard .NET FQN)
|
|
410
|
-
// Don't include assembly as it can vary between different test structures
|
|
411
|
-
const namespace = this.extractNamespace(test);
|
|
412
|
-
const className = this.extractClassName(test);
|
|
413
|
-
const methodName = test.title;
|
|
414
|
-
|
|
415
|
-
// Use the most complete namespace.class structure available
|
|
416
|
-
if (test.suite_title && test.suite_title.includes('.')) {
|
|
417
|
-
return `${test.suite_title}.${methodName}`;
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
return `${namespace}.${className}.${methodName}`;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
generateNormalizedFQN(test) {
|
|
424
|
-
// Generate normalized FQN for deduplication by extracting the core namespace.class.method
|
|
425
|
-
// For parameterized tests, we want the SAME FQN so they merge into one test with multiple Examples
|
|
426
|
-
|
|
427
|
-
const fullClassName = test.suite_title || '';
|
|
428
|
-
const methodName = test.title;
|
|
429
|
-
|
|
430
|
-
// Extract the most specific namespace.class pattern
|
|
431
|
-
if (fullClassName.includes('.')) {
|
|
432
|
-
const parts = fullClassName.split('.');
|
|
433
|
-
|
|
434
|
-
if (parts.length >= 2) {
|
|
435
|
-
const className = parts[parts.length - 1];
|
|
436
|
-
|
|
437
|
-
// Look for common .NET namespace patterns and normalize them:
|
|
438
|
-
// TestProject.Tests.MyClass -> Tests.MyClass
|
|
439
|
-
// Tests.MyClass -> Tests.MyClass
|
|
440
|
-
// MyProject.SubNamespace.Tests.MyClass -> Tests.MyClass
|
|
441
|
-
|
|
442
|
-
let normalizedNamespace = '';
|
|
443
|
-
for (let i = parts.length - 2; i >= 0; i--) {
|
|
444
|
-
const part = parts[i];
|
|
445
|
-
|
|
446
|
-
// Build namespace from right to left, excluding project names
|
|
447
|
-
if (part === 'Tests' || part.endsWith('Tests') || part.includes('Test')) {
|
|
448
|
-
// Found a test namespace, use it as the normalized namespace
|
|
449
|
-
normalizedNamespace = part;
|
|
450
|
-
break;
|
|
451
|
-
} else if (i === parts.length - 2) {
|
|
452
|
-
// If no test namespace found, use the immediate parent as namespace
|
|
453
|
-
normalizedNamespace = part;
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
return `${normalizedNamespace}.${className}.${methodName}`;
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
// Fallback for simple class names
|
|
462
|
-
return `${fullClassName}.${methodName}`;
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
extractAssemblyName(test) {
|
|
466
|
-
// Extract assembly name from file path or use default
|
|
467
|
-
if (test.file) {
|
|
468
|
-
const parts = test.file.split(/[/\\]/);
|
|
469
|
-
return parts[0] || 'DefaultAssembly';
|
|
470
|
-
}
|
|
471
|
-
return 'DefaultAssembly';
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
extractNamespace(test) {
|
|
475
|
-
// Extract namespace from suite_title or classname
|
|
476
|
-
if (test.suite_title && test.suite_title.includes('.')) {
|
|
477
|
-
const parts = test.suite_title.split('.');
|
|
478
|
-
return parts.slice(0, -1).join('.');
|
|
479
|
-
}
|
|
480
|
-
return test.suite_title || 'DefaultNamespace';
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
extractClassName(test) {
|
|
484
|
-
// Extract class name from suite_title
|
|
485
|
-
if (test.suite_title && test.suite_title.includes('.')) {
|
|
486
|
-
const parts = test.suite_title.split('.');
|
|
487
|
-
return parts[parts.length - 1];
|
|
488
|
-
}
|
|
489
|
-
return test.suite_title || 'DefaultClass';
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
extractCsFileFromPath(test) {
|
|
493
|
-
// Extract .cs file name from source file path, not namespace
|
|
494
|
-
if (test.file) {
|
|
495
|
-
// Look for actual .cs file path patterns
|
|
496
|
-
const csFileMatch = test.file.match(/([^/\\]+\.cs)$/);
|
|
497
|
-
if (csFileMatch) {
|
|
498
|
-
return test.file;
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
// If no .cs extension, assume it's a namespace path and convert to likely file name
|
|
502
|
-
const className = this.extractClassName(test);
|
|
503
|
-
const pathParts = test.file.split(/[/\\]/);
|
|
504
|
-
pathParts[pathParts.length - 1] = `${className}.cs`;
|
|
505
|
-
return pathParts.join('/');
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
// Fallback to class name
|
|
509
|
-
const className = this.extractClassName(test);
|
|
510
|
-
return `${className}.cs`;
|
|
511
|
-
}
|
|
512
|
-
|
|
513
322
|
calculateStats() {
|
|
514
323
|
this.stats = {
|
|
515
324
|
...this.stats,
|
|
@@ -676,8 +485,7 @@ function reduceTestCases(prev, item) {
|
|
|
676
485
|
testCases
|
|
677
486
|
.filter(t => !!t)
|
|
678
487
|
.forEach(testCaseItem => {
|
|
679
|
-
|
|
680
|
-
const file = extractSourceFilePath(testCaseItem, item);
|
|
488
|
+
const file = testCaseItem.file || item.filepath || item.fullname || item.package || '';
|
|
681
489
|
|
|
682
490
|
let stack = '';
|
|
683
491
|
let message = '';
|
|
@@ -692,25 +500,19 @@ function reduceTestCases(prev, item) {
|
|
|
692
500
|
if (!message) message = stack.trim().split('\n')[0];
|
|
693
501
|
|
|
694
502
|
const isParametrized = item.type === 'ParameterizedMethod';
|
|
503
|
+
const preferClassname = reduceOptions.preferClassname || isParametrized;
|
|
695
504
|
|
|
696
505
|
// SpecFlow config
|
|
697
506
|
let { title, tags, testId } = fetchProperties(isParametrized ? item : testCaseItem);
|
|
698
507
|
let example = null;
|
|
699
|
-
|
|
700
|
-
// Use consistent Test Explorer structure for suite title
|
|
701
|
-
const suiteTitle = extractTestExplorerSuiteTitle(testCaseItem, item);
|
|
508
|
+
const suiteTitle = preferClassname ? testCaseItem.classname : item.name || testCaseItem.classname;
|
|
702
509
|
|
|
703
510
|
title ||= testCaseItem.name || testCaseItem.methodname || testCaseItem.classname;
|
|
704
511
|
tags ||= [];
|
|
705
512
|
|
|
706
|
-
|
|
707
|
-
const originalTestName = testCaseItem.name || testCaseItem.methodname;
|
|
708
|
-
|
|
709
|
-
const exampleMatches = originalTestName?.match(/\((.*?)\)$/);
|
|
513
|
+
const exampleMatches = testCaseItem.name?.match(/\S\((.*?)\)/);
|
|
710
514
|
if (exampleMatches) {
|
|
711
|
-
|
|
712
|
-
const parameterValues = exampleMatches[1].split(',').map(v => v.trim().replace(/['"]/g, ''));
|
|
713
|
-
example = { ...parameterValues };
|
|
515
|
+
example = { ...exampleMatches[1].split(',').map(v => v.trim().replace(/[^\w\s-]/g, '')) };
|
|
714
516
|
title = title.replace(/\(.*?\)/, '').trim();
|
|
715
517
|
}
|
|
716
518
|
|
|
@@ -766,7 +568,6 @@ function reduceTestCases(prev, item) {
|
|
|
766
568
|
run_time: parseFloat(testCaseItem.time || testCaseItem.duration) * 1000,
|
|
767
569
|
status,
|
|
768
570
|
title,
|
|
769
|
-
originalTestName, // Store original name for parameter-aware FQN generation
|
|
770
571
|
root_suite_id: TESTOMATIO_SUITE,
|
|
771
572
|
suite_title: suiteTitle,
|
|
772
573
|
files,
|
|
@@ -776,57 +577,6 @@ function reduceTestCases(prev, item) {
|
|
|
776
577
|
return prev;
|
|
777
578
|
}
|
|
778
579
|
|
|
779
|
-
function extractSourceFilePath(testCaseItem, item) {
|
|
780
|
-
// Priority order for file path extraction to match Test Explorer structure:
|
|
781
|
-
// 1. fullname (contains full project path)
|
|
782
|
-
// 2. filepath (direct file path)
|
|
783
|
-
// 3. file attribute from test case
|
|
784
|
-
// 4. package (fallback)
|
|
785
|
-
|
|
786
|
-
if (item.fullname) {
|
|
787
|
-
// Extract actual file path from fullname if it contains path separators
|
|
788
|
-
const fullnameParts = item.fullname.split('.');
|
|
789
|
-
if (fullnameParts.length > 2) {
|
|
790
|
-
// Reconstruct path from project.namespace.class structure
|
|
791
|
-
const projectName = fullnameParts[0];
|
|
792
|
-
const namespaceParts = fullnameParts.slice(1, -1);
|
|
793
|
-
const className = fullnameParts[fullnameParts.length - 1];
|
|
794
|
-
return `${projectName}/${namespaceParts.join('/')}/${className}.cs`;
|
|
795
|
-
}
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
if (item.filepath) return item.filepath;
|
|
799
|
-
if (testCaseItem.file) return testCaseItem.file;
|
|
800
|
-
if (item.package) return item.package;
|
|
801
|
-
|
|
802
|
-
// Fallback: construct from classname
|
|
803
|
-
if (testCaseItem.classname) {
|
|
804
|
-
const parts = testCaseItem.classname.split('.');
|
|
805
|
-
const className = parts[parts.length - 1];
|
|
806
|
-
const namespacePath = parts.slice(0, -1).join('/');
|
|
807
|
-
return `${namespacePath}/${className}.cs`;
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
return '';
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
function extractTestExplorerSuiteTitle(testCaseItem, item) {
|
|
814
|
-
// Extract suite title to match Test Explorer structure (Project/Namespace hierarchy)
|
|
815
|
-
// Priority: fullname > classname > name
|
|
816
|
-
|
|
817
|
-
if (item.fullname) {
|
|
818
|
-
// Use fullname to maintain Test Explorer structure
|
|
819
|
-
return item.fullname;
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
if (testCaseItem.classname) {
|
|
823
|
-
return testCaseItem.classname;
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
// Fallback to item name but prefer classname structure
|
|
827
|
-
return item.name || testCaseItem.classname || 'UnknownClass';
|
|
828
|
-
}
|
|
829
|
-
|
|
830
580
|
function processTestSuite(testsuite) {
|
|
831
581
|
if (!testsuite) return [];
|
|
832
582
|
if (testsuite.testsuite) return processTestSuite(testsuite.testsuite);
|
|
@@ -837,17 +587,9 @@ function processTestSuite(testsuite) {
|
|
|
837
587
|
suites = [testsuite];
|
|
838
588
|
}
|
|
839
589
|
|
|
840
|
-
|
|
841
|
-
const subSuites = suites.filter(s => s['test-suite'] && !s['test-case']);
|
|
842
|
-
const leafSuites = suites.filter(s => s['test-case'] || s.testcase);
|
|
843
|
-
|
|
844
|
-
// Process child suites recursively
|
|
845
|
-
const childResults = subSuites.map(s => processTestSuite(s['test-suite'])).flat();
|
|
846
|
-
|
|
847
|
-
// Process leaf suites with actual test cases
|
|
848
|
-
const leafResults = leafSuites.reduce(reduceTestCases, []);
|
|
590
|
+
const subSuites = suites.filter(s => s['test-suite'] && !testsuite['test-case']);
|
|
849
591
|
|
|
850
|
-
return [...
|
|
592
|
+
return [...subSuites.map(s => processTestSuite(s['test-suite'])), ...suites.reduce(reduceTestCases, [])].flat();
|
|
851
593
|
}
|
|
852
594
|
|
|
853
595
|
function fetchProperties(item) {
|