@testomatio/reporter 2.3.5-beta-6-xml-import → 2.3.5
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 -1
- package/lib/junit-adapter/csharp.d.ts +1 -0
- package/lib/junit-adapter/csharp.js +7 -36
- package/lib/pipe/debug.js +1 -1
- package/lib/pipe/testomatio.js +14 -18
- package/lib/utils/utils.js +4 -33
- package/lib/xmlReader.d.ts +0 -9
- package/lib/xmlReader.js +11 -357
- package/package.json +1 -1
- package/src/junit-adapter/csharp.js +6 -40
- package/src/pipe/debug.js +3 -2
- package/src/pipe/testomatio.js +80 -74
- package/src/utils/utils.js +3 -33
- package/src/xmlReader.js +10 -409
package/lib/xmlReader.js
CHANGED
|
@@ -20,7 +20,7 @@ const uploader_js_1 = require("./uploader.js");
|
|
|
20
20
|
const debug = (0, debug_1.default)('@testomatio/reporter:xml');
|
|
21
21
|
const ridRunId = (0, crypto_1.randomUUID)();
|
|
22
22
|
const TESTOMATIO_URL = process.env.TESTOMATIO_URL || 'https://app.testomat.io';
|
|
23
|
-
const { TESTOMATIO_RUNGROUP_TITLE, TESTOMATIO_SUITE, TESTOMATIO_MAX_STACK_TRACE, TESTOMATIO_TITLE, TESTOMATIO_ENV, TESTOMATIO_RUN, TESTOMATIO_MARK_DETACHED,
|
|
23
|
+
const { TESTOMATIO_RUNGROUP_TITLE, TESTOMATIO_SUITE, TESTOMATIO_MAX_STACK_TRACE, TESTOMATIO_TITLE, TESTOMATIO_ENV, TESTOMATIO_RUN, TESTOMATIO_MARK_DETACHED, } = process.env;
|
|
24
24
|
const options = {
|
|
25
25
|
ignoreDeclaration: true,
|
|
26
26
|
ignoreAttributes: false,
|
|
@@ -47,10 +47,6 @@ class XmlReader {
|
|
|
47
47
|
if (!this.adapter)
|
|
48
48
|
throw new Error('XML adapter for this format not found');
|
|
49
49
|
this.opts = opts || {};
|
|
50
|
-
// Check if source code fetching should be disabled
|
|
51
|
-
this.disableSourceCodeFetching = opts.disableSourceCodeFetching || TESTOMATIO_DISABLE_SOURCE_CODE;
|
|
52
|
-
// Control suite organization strategy: 'classname' (default) or 'fullpath'
|
|
53
|
-
this.suiteOrganization = opts.suiteOrganization || process.env.TESTOMATIO_SUITE_ORGANIZATION || 'classname';
|
|
54
50
|
this.store = {};
|
|
55
51
|
this.pipesPromise = (0, index_js_1.pipesFactory)(opts, this.store);
|
|
56
52
|
this.parser = new fast_xml_parser_1.XMLParser(options);
|
|
@@ -62,15 +58,6 @@ class XmlReader {
|
|
|
62
58
|
const packageJsonPath = path_1.default.resolve(__dirname, '..', 'package.json');
|
|
63
59
|
this.version = JSON.parse(fs_1.default.readFileSync(packageJsonPath).toString()).version;
|
|
64
60
|
console.log(constants_js_1.APP_PREFIX, `Testomatio Reporter v${this.version}`);
|
|
65
|
-
if (this.disableSourceCodeFetching) {
|
|
66
|
-
console.log(constants_js_1.APP_PREFIX, '🚫 Source code fetching is disabled');
|
|
67
|
-
}
|
|
68
|
-
if (this.suiteOrganization === 'fullpath') {
|
|
69
|
-
console.log(constants_js_1.APP_PREFIX, '📁 Using fullpath suite organization (may create nested structure)');
|
|
70
|
-
}
|
|
71
|
-
else {
|
|
72
|
-
console.log(constants_js_1.APP_PREFIX, '📋 Using classname suite organization (avoids duplicates)');
|
|
73
|
-
}
|
|
74
61
|
}
|
|
75
62
|
connectAdapter() {
|
|
76
63
|
if (this.opts.javaTests) {
|
|
@@ -143,23 +130,8 @@ class XmlReader {
|
|
|
143
130
|
processNUnit(jsonSuite) {
|
|
144
131
|
const { result, total, passed, failed, inconclusive, skipped } = jsonSuite;
|
|
145
132
|
reduceOptions.preferClassname = this.stats.language === 'python';
|
|
146
|
-
reduceOptions.suiteOrganization = this.suiteOrganization;
|
|
147
133
|
const resultTests = processTestSuite(jsonSuite['test-suite']);
|
|
148
|
-
|
|
149
|
-
debug('Raw tests:', resultTests.map(t => ({ title: t.title, example: t.example, file: t.file })));
|
|
150
|
-
// Optional deduplication for complex NUnit scenarios - can be enabled via options
|
|
151
|
-
let finalTests = resultTests;
|
|
152
|
-
if (this.opts.enableNUnitDeduplication) {
|
|
153
|
-
finalTests = this.deduplicateTestsByFQN(resultTests);
|
|
154
|
-
debug('Tests after deduplication:', finalTests.length);
|
|
155
|
-
debug('Deduplicated tests:', finalTests.map(t => ({
|
|
156
|
-
title: t.title,
|
|
157
|
-
examples: t.examples,
|
|
158
|
-
example: t.example,
|
|
159
|
-
file: t.file,
|
|
160
|
-
})));
|
|
161
|
-
}
|
|
162
|
-
this.tests = this.tests.concat(finalTests);
|
|
134
|
+
this.tests = this.tests.concat(resultTests);
|
|
163
135
|
return {
|
|
164
136
|
status: result?.toLowerCase(),
|
|
165
137
|
create_tests: true,
|
|
@@ -167,7 +139,7 @@ class XmlReader {
|
|
|
167
139
|
passed_count: parseInt(passed, 10),
|
|
168
140
|
failed_count: parseInt(failed, 10),
|
|
169
141
|
skipped_count: parseInt(inconclusive + skipped, 10),
|
|
170
|
-
tests:
|
|
142
|
+
tests: resultTests,
|
|
171
143
|
};
|
|
172
144
|
}
|
|
173
145
|
processTRX(jsonSuite) {
|
|
@@ -303,171 +275,6 @@ class XmlReader {
|
|
|
303
275
|
tests,
|
|
304
276
|
};
|
|
305
277
|
}
|
|
306
|
-
deduplicateTestsByFQN(tests) {
|
|
307
|
-
const fqnMap = new Map();
|
|
308
|
-
tests.forEach(test => {
|
|
309
|
-
const fqn = this.generateNormalizedFQN(test);
|
|
310
|
-
if (fqnMap.has(fqn)) {
|
|
311
|
-
const existingTest = fqnMap.get(fqn);
|
|
312
|
-
// For parameterized tests, merge as Examples
|
|
313
|
-
if (test.example && Array.isArray(test.example) && test.example.length > 0) {
|
|
314
|
-
// Initialize examples array if it doesn't exist
|
|
315
|
-
if (!existingTest.examples) {
|
|
316
|
-
existingTest.examples = [];
|
|
317
|
-
// Add the existing test's example as the first item if it has parameters
|
|
318
|
-
if (existingTest.example && Array.isArray(existingTest.example) && existingTest.example.length > 0) {
|
|
319
|
-
existingTest.examples.push({
|
|
320
|
-
parameters: existingTest.example,
|
|
321
|
-
status: existingTest.status,
|
|
322
|
-
run_time: existingTest.run_time,
|
|
323
|
-
message: existingTest.message,
|
|
324
|
-
stack: existingTest.stack,
|
|
325
|
-
});
|
|
326
|
-
// Clear the main test's example since it's now in examples array
|
|
327
|
-
delete existingTest.example;
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
// Add this test's execution as an example
|
|
331
|
-
existingTest.examples.push({
|
|
332
|
-
parameters: test.example,
|
|
333
|
-
status: test.status,
|
|
334
|
-
run_time: test.run_time,
|
|
335
|
-
message: test.message,
|
|
336
|
-
stack: test.stack,
|
|
337
|
-
});
|
|
338
|
-
// Update the main test status to reflect the worst status
|
|
339
|
-
if (test.status === 'failed' || existingTest.status === 'failed') {
|
|
340
|
-
existingTest.status = 'failed';
|
|
341
|
-
}
|
|
342
|
-
else if (test.status === 'skipped' && existingTest.status !== 'failed') {
|
|
343
|
-
existingTest.status = 'skipped';
|
|
344
|
-
}
|
|
345
|
-
// Update total run time
|
|
346
|
-
existingTest.run_time = (existingTest.run_time || 0) + (test.run_time || 0);
|
|
347
|
-
}
|
|
348
|
-
else {
|
|
349
|
-
// Merge test properties for non-parameterized tests, prioritizing Test Explorer structure
|
|
350
|
-
if (test.test_id && !existingTest.test_id) {
|
|
351
|
-
existingTest.test_id = test.test_id;
|
|
352
|
-
}
|
|
353
|
-
// Keep the most complete test data
|
|
354
|
-
if (test.stack && !existingTest.stack) {
|
|
355
|
-
existingTest.stack = test.stack;
|
|
356
|
-
}
|
|
357
|
-
if (test.message && !existingTest.message) {
|
|
358
|
-
existingTest.message = test.message;
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
// Prefer Test Explorer structure (longer, more complete suite_title)
|
|
362
|
-
if (test.suite_title && test.suite_title.length > existingTest.suite_title.length) {
|
|
363
|
-
existingTest.suite_title = test.suite_title;
|
|
364
|
-
}
|
|
365
|
-
// Always use the source file path if available
|
|
366
|
-
if (test.file && test.file.endsWith('.cs')) {
|
|
367
|
-
existingTest.file = test.file;
|
|
368
|
-
}
|
|
369
|
-
else if (!existingTest.file || !existingTest.file.endsWith('.cs')) {
|
|
370
|
-
existingTest.file = this.extractCsFileFromPath(test);
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
else {
|
|
374
|
-
// Fix file path to use proper .cs file names from source paths
|
|
375
|
-
if (!test.file || !test.file.endsWith('.cs')) {
|
|
376
|
-
test.file = this.extractCsFileFromPath(test);
|
|
377
|
-
}
|
|
378
|
-
fqnMap.set(fqn, test);
|
|
379
|
-
}
|
|
380
|
-
});
|
|
381
|
-
return Array.from(fqnMap.values());
|
|
382
|
-
}
|
|
383
|
-
generateFQN(test) {
|
|
384
|
-
// Generate Fully Qualified Name: Namespace + Class + Method (standard .NET FQN)
|
|
385
|
-
// Don't include assembly as it can vary between different test structures
|
|
386
|
-
const namespace = this.extractNamespace(test);
|
|
387
|
-
const className = this.extractClassName(test);
|
|
388
|
-
const methodName = test.title;
|
|
389
|
-
// Use the most complete namespace.class structure available
|
|
390
|
-
if (test.suite_title && test.suite_title.includes('.')) {
|
|
391
|
-
return `${test.suite_title}.${methodName}`;
|
|
392
|
-
}
|
|
393
|
-
return `${namespace}.${className}.${methodName}`;
|
|
394
|
-
}
|
|
395
|
-
generateNormalizedFQN(test) {
|
|
396
|
-
// Generate normalized FQN for deduplication by extracting the core namespace.class.method
|
|
397
|
-
// For parameterized tests, we want the SAME FQN so they merge into one test with multiple Examples
|
|
398
|
-
const fullClassName = test.suite_title || '';
|
|
399
|
-
const methodName = test.title;
|
|
400
|
-
// Extract the most specific namespace.class pattern
|
|
401
|
-
if (fullClassName.includes('.')) {
|
|
402
|
-
const parts = fullClassName.split('.');
|
|
403
|
-
if (parts.length >= 2) {
|
|
404
|
-
const className = parts[parts.length - 1];
|
|
405
|
-
// Look for common .NET namespace patterns and normalize them:
|
|
406
|
-
// TestProject.Tests.MyClass -> Tests.MyClass
|
|
407
|
-
// Tests.MyClass -> Tests.MyClass
|
|
408
|
-
// MyProject.SubNamespace.Tests.MyClass -> Tests.MyClass
|
|
409
|
-
let normalizedNamespace = '';
|
|
410
|
-
for (let i = parts.length - 2; i >= 0; i--) {
|
|
411
|
-
const part = parts[i];
|
|
412
|
-
// Build namespace from right to left, excluding project names
|
|
413
|
-
if (part === 'Tests' || part.endsWith('Tests') || part.includes('Test')) {
|
|
414
|
-
// Found a test namespace, use it as the normalized namespace
|
|
415
|
-
normalizedNamespace = part;
|
|
416
|
-
break;
|
|
417
|
-
}
|
|
418
|
-
else if (i === parts.length - 2) {
|
|
419
|
-
// If no test namespace found, use the immediate parent as namespace
|
|
420
|
-
normalizedNamespace = part;
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
return `${normalizedNamespace}.${className}.${methodName}`;
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
// Fallback for simple class names
|
|
427
|
-
return `${fullClassName}.${methodName}`;
|
|
428
|
-
}
|
|
429
|
-
extractAssemblyName(test) {
|
|
430
|
-
// Extract assembly name from file path or use default
|
|
431
|
-
if (test.file) {
|
|
432
|
-
const parts = test.file.split(/[/\\]/);
|
|
433
|
-
return parts[0] || 'DefaultAssembly';
|
|
434
|
-
}
|
|
435
|
-
return 'DefaultAssembly';
|
|
436
|
-
}
|
|
437
|
-
extractNamespace(test) {
|
|
438
|
-
// Extract namespace from suite_title or classname
|
|
439
|
-
if (test.suite_title && test.suite_title.includes('.')) {
|
|
440
|
-
const parts = test.suite_title.split('.');
|
|
441
|
-
return parts.slice(0, -1).join('.');
|
|
442
|
-
}
|
|
443
|
-
return test.suite_title || 'DefaultNamespace';
|
|
444
|
-
}
|
|
445
|
-
extractClassName(test) {
|
|
446
|
-
// Extract class name from suite_title
|
|
447
|
-
if (test.suite_title && test.suite_title.includes('.')) {
|
|
448
|
-
const parts = test.suite_title.split('.');
|
|
449
|
-
return parts[parts.length - 1];
|
|
450
|
-
}
|
|
451
|
-
return test.suite_title || 'DefaultClass';
|
|
452
|
-
}
|
|
453
|
-
extractCsFileFromPath(test) {
|
|
454
|
-
// Extract .cs file name from source file path, not namespace
|
|
455
|
-
if (test.file) {
|
|
456
|
-
// Look for actual .cs file path patterns
|
|
457
|
-
const csFileMatch = test.file.match(/([^/\\]+\.cs)$/);
|
|
458
|
-
if (csFileMatch) {
|
|
459
|
-
return test.file;
|
|
460
|
-
}
|
|
461
|
-
// If no .cs extension, assume it's a namespace path and convert to likely file name
|
|
462
|
-
const className = this.extractClassName(test);
|
|
463
|
-
const pathParts = test.file.split(/[/\\]/);
|
|
464
|
-
pathParts[pathParts.length - 1] = `${className}.cs`;
|
|
465
|
-
return pathParts.join('/');
|
|
466
|
-
}
|
|
467
|
-
// Fallback to class name
|
|
468
|
-
const className = this.extractClassName(test);
|
|
469
|
-
return `${className}.cs`;
|
|
470
|
-
}
|
|
471
278
|
calculateStats() {
|
|
472
279
|
this.stats = {
|
|
473
280
|
...this.stats,
|
|
@@ -491,11 +298,6 @@ class XmlReader {
|
|
|
491
298
|
return this.stats;
|
|
492
299
|
}
|
|
493
300
|
fetchSourceCode() {
|
|
494
|
-
// Skip source code fetching if disabled
|
|
495
|
-
if (this.disableSourceCodeFetching) {
|
|
496
|
-
debug('Source code fetching is disabled');
|
|
497
|
-
return;
|
|
498
|
-
}
|
|
499
301
|
this.tests.forEach(t => {
|
|
500
302
|
try {
|
|
501
303
|
const file = this.adapter.getFilePath(t);
|
|
@@ -522,9 +324,7 @@ class XmlReader {
|
|
|
522
324
|
return;
|
|
523
325
|
}
|
|
524
326
|
const contents = fs_1.default.readFileSync(file).toString();
|
|
525
|
-
|
|
526
|
-
const titleForLookup = t.originalTestName ? t.originalTestName.replace(/\(.*?\)/, '').trim() : t.title;
|
|
527
|
-
t.code = (0, utils_js_1.fetchSourceCode)(contents, { ...t, title: titleForLookup, lang: this.stats.language });
|
|
327
|
+
t.code = (0, utils_js_1.fetchSourceCode)(contents, { ...t, lang: this.stats.language });
|
|
528
328
|
if (t.code)
|
|
529
329
|
debug('Fetched code for test %s', t.title);
|
|
530
330
|
t.test_id = (0, utils_js_1.fetchIdFromCode)(t.code, { lang: this.stats.language });
|
|
@@ -630,12 +430,7 @@ function reduceTestCases(prev, item) {
|
|
|
630
430
|
testCases
|
|
631
431
|
.filter(t => !!t)
|
|
632
432
|
.forEach(testCaseItem => {
|
|
633
|
-
|
|
634
|
-
let file = testCaseItem.file || item.filepath || item.fullname || item.package || '';
|
|
635
|
-
// If no file found with simple approach and we have enhanced extraction enabled, use it
|
|
636
|
-
if (!file && item.filepath) {
|
|
637
|
-
file = extractSourceFilePath(testCaseItem, item);
|
|
638
|
-
}
|
|
433
|
+
const file = testCaseItem.file || item.filepath || item.fullname || item.package || '';
|
|
639
434
|
let stack = '';
|
|
640
435
|
let message = '';
|
|
641
436
|
if (testCaseItem.error)
|
|
@@ -659,28 +454,13 @@ function reduceTestCases(prev, item) {
|
|
|
659
454
|
// SpecFlow config
|
|
660
455
|
let { title, tags, testId } = fetchProperties(isParametrized ? item : testCaseItem);
|
|
661
456
|
let example = null;
|
|
662
|
-
|
|
663
|
-
const suiteTitle = getSuiteTitle(testCaseItem, item, isParametrized, reduceOptions.suiteOrganization);
|
|
457
|
+
const suiteTitle = preferClassname ? testCaseItem.classname : item.name || testCaseItem.classname;
|
|
664
458
|
title ||= testCaseItem.name || testCaseItem.methodname || testCaseItem.classname;
|
|
665
459
|
tags ||= [];
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
const args = Array.isArray(testCaseItem.arguments.arg)
|
|
671
|
-
? testCaseItem.arguments.arg
|
|
672
|
-
: [testCaseItem.arguments.arg];
|
|
673
|
-
example = args; // Store as array instead of object
|
|
674
|
-
// Remove parameters from title for NUnit tests
|
|
675
|
-
title = (testCaseItem.methodname || title).replace(/\(.*?\)/, '').trim();
|
|
676
|
-
}
|
|
677
|
-
else {
|
|
678
|
-
// Simple parameter extraction (version 2.1.1 approach)
|
|
679
|
-
const exampleMatches = testCaseItem.name?.match(/\S\((.*?)\)/);
|
|
680
|
-
if (exampleMatches) {
|
|
681
|
-
example = { ...exampleMatches[1].split(',').map(v => v.trim().replace(/[^\w\s-]/g, '')) };
|
|
682
|
-
title = title.replace(/\(.*?\)/, '').trim();
|
|
683
|
-
}
|
|
460
|
+
const exampleMatches = testCaseItem.name?.match(/\S\((.*?)\)/);
|
|
461
|
+
if (exampleMatches) {
|
|
462
|
+
example = { ...exampleMatches[1].split(',').map(v => v.trim().replace(/[^\w\s-]/g, '')) };
|
|
463
|
+
title = title.replace(/\(.*?\)/, '').trim();
|
|
684
464
|
}
|
|
685
465
|
stack = `${testCaseItem['system-out'] || testCaseItem.output || testCaseItem.log || ''}\n\n${stack}\n\n${suiteOutput}\n\n${suiteErr}`.trim();
|
|
686
466
|
if (!testId)
|
|
@@ -728,7 +508,6 @@ function reduceTestCases(prev, item) {
|
|
|
728
508
|
run_time: parseFloat(testCaseItem.time || testCaseItem.duration) * 1000,
|
|
729
509
|
status,
|
|
730
510
|
title,
|
|
731
|
-
originalTestName, // Store original name for enhanced features
|
|
732
511
|
root_suite_id: TESTOMATIO_SUITE,
|
|
733
512
|
suite_title: suiteTitle,
|
|
734
513
|
files,
|
|
@@ -737,102 +516,6 @@ function reduceTestCases(prev, item) {
|
|
|
737
516
|
});
|
|
738
517
|
return prev;
|
|
739
518
|
}
|
|
740
|
-
function extractSourceFilePath(testCaseItem, item) {
|
|
741
|
-
// Priority order for file path extraction to match Test Explorer structure:
|
|
742
|
-
// 1. filepath attribute (direct .cs file path from NUnit)
|
|
743
|
-
// 2. fullname (contains full project path)
|
|
744
|
-
// 3. file attribute from test case
|
|
745
|
-
// 4. package (fallback)
|
|
746
|
-
// NUnit provides filepath attribute with actual .cs file path - use this first
|
|
747
|
-
if (item.filepath) {
|
|
748
|
-
// Clean up Windows/Unix path separators and ensure proper format
|
|
749
|
-
let filePath = item.filepath.replace(/\\/g, '/');
|
|
750
|
-
// Make relative to current working directory if absolute
|
|
751
|
-
if (path_1.default.isAbsolute(item.filepath)) {
|
|
752
|
-
const cwd = process.cwd().replace(/\\/g, '/');
|
|
753
|
-
if (filePath.startsWith(cwd)) {
|
|
754
|
-
filePath = path_1.default.relative(cwd, item.filepath).replace(/\\/g, '/');
|
|
755
|
-
}
|
|
756
|
-
else {
|
|
757
|
-
// Try to extract relative path from common patterns
|
|
758
|
-
const commonPatterns = ['/Tests/', '/test/', '/src/', '/Test/'];
|
|
759
|
-
for (const pattern of commonPatterns) {
|
|
760
|
-
const index = filePath.lastIndexOf(pattern);
|
|
761
|
-
if (index !== -1) {
|
|
762
|
-
filePath = filePath.substring(index + 1);
|
|
763
|
-
break;
|
|
764
|
-
}
|
|
765
|
-
}
|
|
766
|
-
}
|
|
767
|
-
}
|
|
768
|
-
return filePath;
|
|
769
|
-
}
|
|
770
|
-
if (testCaseItem.file) {
|
|
771
|
-
let filePath = testCaseItem.file.replace(/\\/g, '/');
|
|
772
|
-
// Make relative to current working directory if absolute
|
|
773
|
-
if (path_1.default.isAbsolute(testCaseItem.file)) {
|
|
774
|
-
const cwd = process.cwd().replace(/\\/g, '/');
|
|
775
|
-
if (filePath.startsWith(cwd)) {
|
|
776
|
-
filePath = path_1.default.relative(cwd, testCaseItem.file).replace(/\\/g, '/');
|
|
777
|
-
}
|
|
778
|
-
else {
|
|
779
|
-
// Try to extract relative path from common patterns
|
|
780
|
-
const commonPatterns = ['/Tests/', '/test/', '/src/', '/Test/'];
|
|
781
|
-
for (const pattern of commonPatterns) {
|
|
782
|
-
const index = filePath.lastIndexOf(pattern);
|
|
783
|
-
if (index !== -1) {
|
|
784
|
-
filePath = filePath.substring(index + 1);
|
|
785
|
-
break;
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
return filePath;
|
|
791
|
-
}
|
|
792
|
-
if (item.fullname) {
|
|
793
|
-
// Extract actual file path from fullname if it contains path separators
|
|
794
|
-
const fullnameParts = item.fullname.split('.');
|
|
795
|
-
if (fullnameParts.length > 2) {
|
|
796
|
-
// For ParameterizedMethod, get the class name (not method name)
|
|
797
|
-
// Example: "NUnit_sample_test.Tests.SampleTests.TestBooleanValue" -> "Tests/SampleTests.cs"
|
|
798
|
-
let namespaceParts, className;
|
|
799
|
-
if (item.type === 'ParameterizedMethod') {
|
|
800
|
-
// For parameterized methods, the last part is the method name, second-to-last is class
|
|
801
|
-
namespaceParts = fullnameParts.slice(1, -2); // Skip project name and method name
|
|
802
|
-
className = fullnameParts[fullnameParts.length - 2]; // Get class name
|
|
803
|
-
}
|
|
804
|
-
else {
|
|
805
|
-
// For regular classes/fixtures
|
|
806
|
-
namespaceParts = fullnameParts.slice(1, -1); // Skip project name
|
|
807
|
-
className = fullnameParts[fullnameParts.length - 1];
|
|
808
|
-
}
|
|
809
|
-
return `${namespaceParts.join('/')}/${className}.cs`;
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
if (item.package)
|
|
813
|
-
return item.package.replace(/\\/g, '/');
|
|
814
|
-
// Fallback: construct from classname
|
|
815
|
-
if (testCaseItem.classname) {
|
|
816
|
-
const parts = testCaseItem.classname.split('.');
|
|
817
|
-
const className = parts[parts.length - 1];
|
|
818
|
-
const namespacePath = parts.slice(0, -1).join('/');
|
|
819
|
-
return `${namespacePath}/${className}.cs`;
|
|
820
|
-
}
|
|
821
|
-
return '';
|
|
822
|
-
}
|
|
823
|
-
function extractTestExplorerSuiteTitle(testCaseItem, item) {
|
|
824
|
-
// Extract suite title to match Test Explorer structure (Project/Namespace hierarchy)
|
|
825
|
-
// Priority: fullname > classname > name
|
|
826
|
-
if (item.fullname) {
|
|
827
|
-
// Use fullname to maintain Test Explorer structure
|
|
828
|
-
return item.fullname;
|
|
829
|
-
}
|
|
830
|
-
if (testCaseItem.classname) {
|
|
831
|
-
return testCaseItem.classname;
|
|
832
|
-
}
|
|
833
|
-
// Fallback to item name but prefer classname structure
|
|
834
|
-
return item.name || testCaseItem.classname || 'UnknownClass';
|
|
835
|
-
}
|
|
836
519
|
function processTestSuite(testsuite) {
|
|
837
520
|
if (!testsuite)
|
|
838
521
|
return [];
|
|
@@ -844,38 +527,9 @@ function processTestSuite(testsuite) {
|
|
|
844
527
|
if (!Array.isArray(testsuite)) {
|
|
845
528
|
suites = [testsuite];
|
|
846
529
|
}
|
|
847
|
-
|
|
848
|
-
const subSuites = suites.filter(s => s['test-suite'] && !s['test-case']);
|
|
530
|
+
const subSuites = suites.filter(s => s['test-suite'] && !testsuite['test-case']);
|
|
849
531
|
return [...subSuites.map(s => processTestSuite(s['test-suite'])), ...suites.reduce(reduceTestCases, [])].flat();
|
|
850
532
|
}
|
|
851
|
-
function getSuiteTitle(testCaseItem, item, isParametrized, suiteOrganization = 'classname') {
|
|
852
|
-
let suiteTitle;
|
|
853
|
-
if (suiteOrganization === 'fullpath') {
|
|
854
|
-
// Use full namespace path (old behavior that creates detailed structure)
|
|
855
|
-
if (item.fullname) {
|
|
856
|
-
return item.fullname;
|
|
857
|
-
}
|
|
858
|
-
suiteTitle = testCaseItem.classname || item.name;
|
|
859
|
-
}
|
|
860
|
-
else {
|
|
861
|
-
// Use classname approach (default - avoids duplicates)
|
|
862
|
-
if (isParametrized) {
|
|
863
|
-
// For parameterized tests, use the class name to group them
|
|
864
|
-
suiteTitle = item.name || testCaseItem.classname;
|
|
865
|
-
}
|
|
866
|
-
else {
|
|
867
|
-
// For regular tests, prefer classname over fullname to avoid long paths
|
|
868
|
-
suiteTitle = testCaseItem.classname || item.name;
|
|
869
|
-
}
|
|
870
|
-
// If still no suite title and we have fullname, extract just the class name
|
|
871
|
-
if (!suiteTitle && item.fullname) {
|
|
872
|
-
const fullnameParts = item.fullname.split('.');
|
|
873
|
-
suiteTitle = fullnameParts[fullnameParts.length - 1]; // Just the class name
|
|
874
|
-
}
|
|
875
|
-
}
|
|
876
|
-
// Fallback
|
|
877
|
-
return suiteTitle || 'UnknownClass';
|
|
878
|
-
}
|
|
879
533
|
function fetchProperties(item) {
|
|
880
534
|
const tags = [];
|
|
881
535
|
let title = '';
|
package/package.json
CHANGED
|
@@ -3,45 +3,18 @@ import Adapter from './adapter.js';
|
|
|
3
3
|
|
|
4
4
|
class CSharpAdapter extends Adapter {
|
|
5
5
|
formatTest(t) {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
if (
|
|
9
|
-
const title = t.title.replace(/\(.*?\)/, '').trim();
|
|
10
|
-
const exampleMatch = t.title.match(/\((.*?)\)/);
|
|
11
|
-
if (exampleMatch) {
|
|
12
|
-
// Keep as array for consistency with NUnit XML processing
|
|
13
|
-
t.example = exampleMatch[1].split(',').map(param => param.trim());
|
|
14
|
-
}
|
|
15
|
-
t.title = title.trim();
|
|
16
|
-
}
|
|
17
|
-
|
|
6
|
+
const title = t.title.replace(/\(.*?\)/, '').trim();
|
|
7
|
+
const example = t.title.match(/\((.*?)\)/);
|
|
8
|
+
if (example) t.example = { ...example[1].split(',') };
|
|
18
9
|
const suite = t.suite_title.split('.');
|
|
19
10
|
t.suite_title = suite.pop();
|
|
20
11
|
t.file = namespaceToFileName(t.file);
|
|
12
|
+
t.title = title.trim();
|
|
21
13
|
return t;
|
|
22
14
|
}
|
|
23
15
|
|
|
24
16
|
getFilePath(t) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
// Normalize path separators for cross-platform compatibility
|
|
28
|
-
let filePath = t.file.replace(/\\/g, '/');
|
|
29
|
-
|
|
30
|
-
// If file already has .cs extension, use it directly
|
|
31
|
-
if (filePath.endsWith('.cs')) {
|
|
32
|
-
// Make relative path if it's absolute
|
|
33
|
-
if (path.isAbsolute(filePath)) {
|
|
34
|
-
// Try to find project-relative path
|
|
35
|
-
const cwd = process.cwd().replace(/\\/g, '/');
|
|
36
|
-
if (filePath.startsWith(cwd)) {
|
|
37
|
-
filePath = path.relative(cwd, filePath).replace(/\\/g, '/');
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
return filePath;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Convert namespace path to file path
|
|
44
|
-
const fileName = namespaceToFileName(filePath);
|
|
17
|
+
const fileName = namespaceToFileName(t.file);
|
|
45
18
|
return fileName;
|
|
46
19
|
}
|
|
47
20
|
}
|
|
@@ -49,14 +22,7 @@ class CSharpAdapter extends Adapter {
|
|
|
49
22
|
export default CSharpAdapter;
|
|
50
23
|
|
|
51
24
|
function namespaceToFileName(fileName) {
|
|
52
|
-
if (!fileName) return '';
|
|
53
|
-
|
|
54
|
-
// If already a .cs file path, clean it up
|
|
55
|
-
if (fileName.endsWith('.cs')) {
|
|
56
|
-
return fileName.replace(/\\/g, '/');
|
|
57
|
-
}
|
|
58
|
-
|
|
59
25
|
const fileParts = fileName.split('.');
|
|
60
26
|
fileParts[fileParts.length - 1] = fileParts[fileParts.length - 1]?.replace(/\$.*/, '');
|
|
61
|
-
return `${fileParts.join(
|
|
27
|
+
return `${fileParts.join(path.sep)}.cs`;
|
|
62
28
|
}
|
package/src/pipe/debug.js
CHANGED
|
@@ -15,7 +15,7 @@ export class DebugPipe {
|
|
|
15
15
|
this.isEnabled = !!process.env.TESTOMATIO_DEBUG || !!process.env.DEBUG;
|
|
16
16
|
if (this.isEnabled) {
|
|
17
17
|
this.batch = {
|
|
18
|
-
isEnabled: this.params.isBatchEnabled ??
|
|
18
|
+
isEnabled: this.params.isBatchEnabled ?? !process.env.TESTOMATIO_DISABLE_BATCH_UPLOAD ?? true,
|
|
19
19
|
intervalFunction: null,
|
|
20
20
|
intervalTime: 5000,
|
|
21
21
|
tests: [],
|
|
@@ -93,7 +93,8 @@ export class DebugPipe {
|
|
|
93
93
|
const logData = { action: 'addTest', testId: data };
|
|
94
94
|
if (this.store.runId) logData.runId = this.store.runId;
|
|
95
95
|
this.logToFile(logData);
|
|
96
|
-
}
|
|
96
|
+
}
|
|
97
|
+
else this.batch.tests.push(data);
|
|
97
98
|
|
|
98
99
|
if (!this.batch.intervalFunction) await this.batchUpload();
|
|
99
100
|
}
|