@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/src/xmlReader.js CHANGED
@@ -35,7 +35,6 @@ const {
35
35
  TESTOMATIO_ENV,
36
36
  TESTOMATIO_RUN,
37
37
  TESTOMATIO_MARK_DETACHED,
38
- TESTOMATIO_DISABLE_SOURCE_CODE,
39
38
  } = process.env;
40
39
 
41
40
  const options = {
@@ -67,10 +66,6 @@ class XmlReader {
67
66
  if (!this.adapter) throw new Error('XML adapter for this format not found');
68
67
 
69
68
  this.opts = opts || {};
70
- // Check if source code fetching should be disabled
71
- this.disableSourceCodeFetching = opts.disableSourceCodeFetching || TESTOMATIO_DISABLE_SOURCE_CODE;
72
- // Control suite organization strategy: 'classname' (default) or 'fullpath'
73
- this.suiteOrganization = opts.suiteOrganization || process.env.TESTOMATIO_SUITE_ORGANIZATION || 'classname';
74
69
  this.store = {};
75
70
  this.pipesPromise = pipesFactory(opts, this.store);
76
71
 
@@ -84,16 +79,6 @@ class XmlReader {
84
79
  const packageJsonPath = path.resolve(__dirname, '..', 'package.json');
85
80
  this.version = JSON.parse(fs.readFileSync(packageJsonPath).toString()).version;
86
81
  console.log(APP_PREFIX, `Testomatio Reporter v${this.version}`);
87
-
88
- if (this.disableSourceCodeFetching) {
89
- console.log(APP_PREFIX, '🚫 Source code fetching is disabled');
90
- }
91
-
92
- if (this.suiteOrganization === 'fullpath') {
93
- console.log(APP_PREFIX, '📁 Using fullpath suite organization (may create nested structure)');
94
- } else {
95
- console.log(APP_PREFIX, '📋 Using classname suite organization (avoids duplicates)');
96
- }
97
82
  }
98
83
 
99
84
  connectAdapter() {
@@ -175,32 +160,9 @@ class XmlReader {
175
160
  const { result, total, passed, failed, inconclusive, skipped } = jsonSuite;
176
161
 
177
162
  reduceOptions.preferClassname = this.stats.language === 'python';
178
- reduceOptions.suiteOrganization = this.suiteOrganization;
179
163
  const resultTests = processTestSuite(jsonSuite['test-suite']);
180
164
 
181
- debug('Raw tests extracted from NUnit XML:', resultTests.length);
182
- debug(
183
- 'Raw tests:',
184
- resultTests.map(t => ({ title: t.title, example: t.example, file: t.file })),
185
- );
186
-
187
- // Optional deduplication for complex NUnit scenarios - can be enabled via options
188
- let finalTests = resultTests;
189
- if (this.opts.enableNUnitDeduplication) {
190
- finalTests = this.deduplicateTestsByFQN(resultTests);
191
- debug('Tests after deduplication:', finalTests.length);
192
- debug(
193
- 'Deduplicated tests:',
194
- finalTests.map(t => ({
195
- title: t.title,
196
- examples: t.examples,
197
- example: t.example,
198
- file: t.file,
199
- })),
200
- );
201
- }
202
-
203
- this.tests = this.tests.concat(finalTests);
165
+ this.tests = this.tests.concat(resultTests);
204
166
 
205
167
  return {
206
168
  status: result?.toLowerCase(),
@@ -209,7 +171,7 @@ class XmlReader {
209
171
  passed_count: parseInt(passed, 10),
210
172
  failed_count: parseInt(failed, 10),
211
173
  skipped_count: parseInt(inconclusive + skipped, 10),
212
- tests: finalTests,
174
+ tests: resultTests,
213
175
  };
214
176
  }
215
177
 
@@ -357,194 +319,6 @@ class XmlReader {
357
319
  };
358
320
  }
359
321
 
360
- deduplicateTestsByFQN(tests) {
361
- const fqnMap = new Map();
362
-
363
- tests.forEach(test => {
364
- const fqn = this.generateNormalizedFQN(test);
365
-
366
- if (fqnMap.has(fqn)) {
367
- const existingTest = fqnMap.get(fqn);
368
-
369
- // For parameterized tests, merge as Examples
370
- if (test.example && Array.isArray(test.example) && test.example.length > 0) {
371
- // Initialize examples array if it doesn't exist
372
- if (!existingTest.examples) {
373
- existingTest.examples = [];
374
- // Add the existing test's example as the first item if it has parameters
375
- if (existingTest.example && Array.isArray(existingTest.example) && existingTest.example.length > 0) {
376
- existingTest.examples.push({
377
- parameters: existingTest.example,
378
- status: existingTest.status,
379
- run_time: existingTest.run_time,
380
- message: existingTest.message,
381
- stack: existingTest.stack,
382
- });
383
- // Clear the main test's example since it's now in examples array
384
- delete existingTest.example;
385
- }
386
- }
387
-
388
- // Add this test's execution as an example
389
- existingTest.examples.push({
390
- parameters: test.example,
391
- status: test.status,
392
- run_time: test.run_time,
393
- message: test.message,
394
- stack: test.stack,
395
- });
396
-
397
- // Update the main test status to reflect the worst status
398
- if (test.status === 'failed' || existingTest.status === 'failed') {
399
- existingTest.status = 'failed';
400
- } else if (test.status === 'skipped' && existingTest.status !== 'failed') {
401
- existingTest.status = 'skipped';
402
- }
403
-
404
- // Update total run time
405
- existingTest.run_time = (existingTest.run_time || 0) + (test.run_time || 0);
406
- } else {
407
- // Merge test properties for non-parameterized tests, prioritizing Test Explorer structure
408
- if (test.test_id && !existingTest.test_id) {
409
- existingTest.test_id = test.test_id;
410
- }
411
- // Keep the most complete test data
412
- if (test.stack && !existingTest.stack) {
413
- existingTest.stack = test.stack;
414
- }
415
- if (test.message && !existingTest.message) {
416
- existingTest.message = test.message;
417
- }
418
- }
419
-
420
- // Prefer Test Explorer structure (longer, more complete suite_title)
421
- if (test.suite_title && test.suite_title.length > existingTest.suite_title.length) {
422
- existingTest.suite_title = test.suite_title;
423
- }
424
-
425
- // Always use the source file path if available
426
- if (test.file && test.file.endsWith('.cs')) {
427
- existingTest.file = test.file;
428
- } else if (!existingTest.file || !existingTest.file.endsWith('.cs')) {
429
- existingTest.file = this.extractCsFileFromPath(test);
430
- }
431
- } else {
432
- // Fix file path to use proper .cs file names from source paths
433
- if (!test.file || !test.file.endsWith('.cs')) {
434
- test.file = this.extractCsFileFromPath(test);
435
- }
436
- fqnMap.set(fqn, test);
437
- }
438
- });
439
-
440
- return Array.from(fqnMap.values());
441
- }
442
-
443
- generateFQN(test) {
444
- // Generate Fully Qualified Name: Namespace + Class + Method (standard .NET FQN)
445
- // Don't include assembly as it can vary between different test structures
446
- const namespace = this.extractNamespace(test);
447
- const className = this.extractClassName(test);
448
- const methodName = test.title;
449
-
450
- // Use the most complete namespace.class structure available
451
- if (test.suite_title && test.suite_title.includes('.')) {
452
- return `${test.suite_title}.${methodName}`;
453
- }
454
-
455
- return `${namespace}.${className}.${methodName}`;
456
- }
457
-
458
- generateNormalizedFQN(test) {
459
- // Generate normalized FQN for deduplication by extracting the core namespace.class.method
460
- // For parameterized tests, we want the SAME FQN so they merge into one test with multiple Examples
461
-
462
- const fullClassName = test.suite_title || '';
463
- const methodName = test.title;
464
-
465
- // Extract the most specific namespace.class pattern
466
- if (fullClassName.includes('.')) {
467
- const parts = fullClassName.split('.');
468
-
469
- if (parts.length >= 2) {
470
- const className = parts[parts.length - 1];
471
-
472
- // Look for common .NET namespace patterns and normalize them:
473
- // TestProject.Tests.MyClass -> Tests.MyClass
474
- // Tests.MyClass -> Tests.MyClass
475
- // MyProject.SubNamespace.Tests.MyClass -> Tests.MyClass
476
-
477
- let normalizedNamespace = '';
478
- for (let i = parts.length - 2; i >= 0; i--) {
479
- const part = parts[i];
480
-
481
- // Build namespace from right to left, excluding project names
482
- if (part === 'Tests' || part.endsWith('Tests') || part.includes('Test')) {
483
- // Found a test namespace, use it as the normalized namespace
484
- normalizedNamespace = part;
485
- break;
486
- } else if (i === parts.length - 2) {
487
- // If no test namespace found, use the immediate parent as namespace
488
- normalizedNamespace = part;
489
- }
490
- }
491
-
492
- return `${normalizedNamespace}.${className}.${methodName}`;
493
- }
494
- }
495
-
496
- // Fallback for simple class names
497
- return `${fullClassName}.${methodName}`;
498
- }
499
-
500
- extractAssemblyName(test) {
501
- // Extract assembly name from file path or use default
502
- if (test.file) {
503
- const parts = test.file.split(/[/\\]/);
504
- return parts[0] || 'DefaultAssembly';
505
- }
506
- return 'DefaultAssembly';
507
- }
508
-
509
- extractNamespace(test) {
510
- // Extract namespace from suite_title or classname
511
- if (test.suite_title && test.suite_title.includes('.')) {
512
- const parts = test.suite_title.split('.');
513
- return parts.slice(0, -1).join('.');
514
- }
515
- return test.suite_title || 'DefaultNamespace';
516
- }
517
-
518
- extractClassName(test) {
519
- // Extract class name from suite_title
520
- if (test.suite_title && test.suite_title.includes('.')) {
521
- const parts = test.suite_title.split('.');
522
- return parts[parts.length - 1];
523
- }
524
- return test.suite_title || 'DefaultClass';
525
- }
526
-
527
- extractCsFileFromPath(test) {
528
- // Extract .cs file name from source file path, not namespace
529
- if (test.file) {
530
- // Look for actual .cs file path patterns
531
- const csFileMatch = test.file.match(/([^/\\]+\.cs)$/);
532
- if (csFileMatch) {
533
- return test.file;
534
- }
535
-
536
- // If no .cs extension, assume it's a namespace path and convert to likely file name
537
- const className = this.extractClassName(test);
538
- const pathParts = test.file.split(/[/\\]/);
539
- pathParts[pathParts.length - 1] = `${className}.cs`;
540
- return pathParts.join('/');
541
- }
542
-
543
- // Fallback to class name
544
- const className = this.extractClassName(test);
545
- return `${className}.cs`;
546
- }
547
-
548
322
  calculateStats() {
549
323
  this.stats = {
550
324
  ...this.stats,
@@ -567,12 +341,6 @@ class XmlReader {
567
341
  }
568
342
 
569
343
  fetchSourceCode() {
570
- // Skip source code fetching if disabled
571
- if (this.disableSourceCodeFetching) {
572
- debug('Source code fetching is disabled');
573
- return;
574
- }
575
-
576
344
  this.tests.forEach(t => {
577
345
  try {
578
346
  const file = this.adapter.getFilePath(t);
@@ -592,15 +360,9 @@ class XmlReader {
592
360
  debug('Failed to open file with the source code', file);
593
361
  return;
594
362
  }
595
-
596
363
  const contents = fs.readFileSync(file).toString();
597
-
598
- // Try original test name first (for parameterized tests), fallback to regular title
599
- const titleForLookup = t.originalTestName ? t.originalTestName.replace(/\(.*?\)/, '').trim() : t.title;
600
-
601
- t.code = fetchSourceCode(contents, { ...t, title: titleForLookup, lang: this.stats.language });
364
+ t.code = fetchSourceCode(contents, { ...t, lang: this.stats.language });
602
365
  if (t.code) debug('Fetched code for test %s', t.title);
603
-
604
366
  t.test_id = fetchIdFromCode(t.code, { lang: this.stats.language });
605
367
  if (t.test_id) debug('Fetched test id %s for test %s', t.test_id, t.title);
606
368
  } catch (err) {
@@ -723,13 +485,7 @@ function reduceTestCases(prev, item) {
723
485
  testCases
724
486
  .filter(t => !!t)
725
487
  .forEach(testCaseItem => {
726
- // Simple file extraction (version 2.1.1 approach) with fallback to enhanced extraction
727
- let file = testCaseItem.file || item.filepath || item.fullname || item.package || '';
728
-
729
- // If no file found with simple approach and we have enhanced extraction enabled, use it
730
- if (!file && item.filepath) {
731
- file = extractSourceFilePath(testCaseItem, item);
732
- }
488
+ const file = testCaseItem.file || item.filepath || item.fullname || item.package || '';
733
489
 
734
490
  let stack = '';
735
491
  let message = '';
@@ -749,31 +505,15 @@ function reduceTestCases(prev, item) {
749
505
  // SpecFlow config
750
506
  let { title, tags, testId } = fetchProperties(isParametrized ? item : testCaseItem);
751
507
  let example = null;
752
-
753
- // Smart suite title extraction to avoid duplicates
754
- const suiteTitle = getSuiteTitle(testCaseItem, item, isParametrized, reduceOptions.suiteOrganization);
508
+ const suiteTitle = preferClassname ? testCaseItem.classname : item.name || testCaseItem.classname;
755
509
 
756
510
  title ||= testCaseItem.name || testCaseItem.methodname || testCaseItem.classname;
757
511
  tags ||= [];
758
512
 
759
- // Store original test name for enhanced parameter extraction
760
- const originalTestName = testCaseItem.name || testCaseItem.methodname;
761
-
762
- // Enhanced NUnit-style arguments from <arguments> element
763
- if (testCaseItem.arguments && testCaseItem.arguments.arg) {
764
- const args = Array.isArray(testCaseItem.arguments.arg)
765
- ? testCaseItem.arguments.arg
766
- : [testCaseItem.arguments.arg];
767
- example = args; // Store as array instead of object
768
- // Remove parameters from title for NUnit tests
769
- title = (testCaseItem.methodname || title).replace(/\(.*?\)/, '').trim();
770
- } else {
771
- // Simple parameter extraction (version 2.1.1 approach)
772
- const exampleMatches = testCaseItem.name?.match(/\S\((.*?)\)/);
773
- if (exampleMatches) {
774
- example = { ...exampleMatches[1].split(',').map(v => v.trim().replace(/[^\w\s-]/g, '')) };
775
- title = title.replace(/\(.*?\)/, '').trim();
776
- }
513
+ const exampleMatches = testCaseItem.name?.match(/\S\((.*?)\)/);
514
+ if (exampleMatches) {
515
+ example = { ...exampleMatches[1].split(',').map(v => v.trim().replace(/[^\w\s-]/g, '')) };
516
+ title = title.replace(/\(.*?\)/, '').trim();
777
517
  }
778
518
 
779
519
  stack = `${
@@ -828,7 +568,6 @@ function reduceTestCases(prev, item) {
828
568
  run_time: parseFloat(testCaseItem.time || testCaseItem.duration) * 1000,
829
569
  status,
830
570
  title,
831
- originalTestName, // Store original name for enhanced features
832
571
  root_suite_id: TESTOMATIO_SUITE,
833
572
  suite_title: suiteTitle,
834
573
  files,
@@ -838,113 +577,6 @@ function reduceTestCases(prev, item) {
838
577
  return prev;
839
578
  }
840
579
 
841
- function extractSourceFilePath(testCaseItem, item) {
842
- // Priority order for file path extraction to match Test Explorer structure:
843
- // 1. filepath attribute (direct .cs file path from NUnit)
844
- // 2. fullname (contains full project path)
845
- // 3. file attribute from test case
846
- // 4. package (fallback)
847
-
848
- // NUnit provides filepath attribute with actual .cs file path - use this first
849
- if (item.filepath) {
850
- // Clean up Windows/Unix path separators and ensure proper format
851
- let filePath = item.filepath.replace(/\\/g, '/');
852
-
853
- // Make relative to current working directory if absolute
854
- if (path.isAbsolute(item.filepath)) {
855
- const cwd = process.cwd().replace(/\\/g, '/');
856
- if (filePath.startsWith(cwd)) {
857
- filePath = path.relative(cwd, item.filepath).replace(/\\/g, '/');
858
- } else {
859
- // Try to extract relative path from common patterns
860
- const commonPatterns = ['/Tests/', '/test/', '/src/', '/Test/'];
861
- for (const pattern of commonPatterns) {
862
- const index = filePath.lastIndexOf(pattern);
863
- if (index !== -1) {
864
- filePath = filePath.substring(index + 1);
865
- break;
866
- }
867
- }
868
- }
869
- }
870
- return filePath;
871
- }
872
-
873
- if (testCaseItem.file) {
874
- let filePath = testCaseItem.file.replace(/\\/g, '/');
875
-
876
- // Make relative to current working directory if absolute
877
- if (path.isAbsolute(testCaseItem.file)) {
878
- const cwd = process.cwd().replace(/\\/g, '/');
879
- if (filePath.startsWith(cwd)) {
880
- filePath = path.relative(cwd, testCaseItem.file).replace(/\\/g, '/');
881
- } else {
882
- // Try to extract relative path from common patterns
883
- const commonPatterns = ['/Tests/', '/test/', '/src/', '/Test/'];
884
- for (const pattern of commonPatterns) {
885
- const index = filePath.lastIndexOf(pattern);
886
- if (index !== -1) {
887
- filePath = filePath.substring(index + 1);
888
- break;
889
- }
890
- }
891
- }
892
- }
893
- return filePath;
894
- }
895
-
896
- if (item.fullname) {
897
- // Extract actual file path from fullname if it contains path separators
898
- const fullnameParts = item.fullname.split('.');
899
- if (fullnameParts.length > 2) {
900
- // For ParameterizedMethod, get the class name (not method name)
901
- // Example: "NUnit_sample_test.Tests.SampleTests.TestBooleanValue" -> "Tests/SampleTests.cs"
902
- let namespaceParts, className;
903
-
904
- if (item.type === 'ParameterizedMethod') {
905
- // For parameterized methods, the last part is the method name, second-to-last is class
906
- namespaceParts = fullnameParts.slice(1, -2); // Skip project name and method name
907
- className = fullnameParts[fullnameParts.length - 2]; // Get class name
908
- } else {
909
- // For regular classes/fixtures
910
- namespaceParts = fullnameParts.slice(1, -1); // Skip project name
911
- className = fullnameParts[fullnameParts.length - 1];
912
- }
913
-
914
- return `${namespaceParts.join('/')}/${className}.cs`;
915
- }
916
- }
917
-
918
- if (item.package) return item.package.replace(/\\/g, '/');
919
-
920
- // Fallback: construct from classname
921
- if (testCaseItem.classname) {
922
- const parts = testCaseItem.classname.split('.');
923
- const className = parts[parts.length - 1];
924
- const namespacePath = parts.slice(0, -1).join('/');
925
- return `${namespacePath}/${className}.cs`;
926
- }
927
-
928
- return '';
929
- }
930
-
931
- function extractTestExplorerSuiteTitle(testCaseItem, item) {
932
- // Extract suite title to match Test Explorer structure (Project/Namespace hierarchy)
933
- // Priority: fullname > classname > name
934
-
935
- if (item.fullname) {
936
- // Use fullname to maintain Test Explorer structure
937
- return item.fullname;
938
- }
939
-
940
- if (testCaseItem.classname) {
941
- return testCaseItem.classname;
942
- }
943
-
944
- // Fallback to item name but prefer classname structure
945
- return item.name || testCaseItem.classname || 'UnknownClass';
946
- }
947
-
948
580
  function processTestSuite(testsuite) {
949
581
  if (!testsuite) return [];
950
582
  if (testsuite.testsuite) return processTestSuite(testsuite.testsuite);
@@ -955,42 +587,11 @@ function processTestSuite(testsuite) {
955
587
  suites = [testsuite];
956
588
  }
957
589
 
958
- // Simple approach from version 2.1.1 with enhanced processing for complex scenarios
959
- const subSuites = suites.filter(s => s['test-suite'] && !s['test-case']);
590
+ const subSuites = suites.filter(s => s['test-suite'] && !testsuite['test-case']);
960
591
 
961
592
  return [...subSuites.map(s => processTestSuite(s['test-suite'])), ...suites.reduce(reduceTestCases, [])].flat();
962
593
  }
963
594
 
964
- function getSuiteTitle(testCaseItem, item, isParametrized, suiteOrganization = 'classname') {
965
- let suiteTitle;
966
-
967
- if (suiteOrganization === 'fullpath') {
968
- // Use full namespace path (old behavior that creates detailed structure)
969
- if (item.fullname) {
970
- return item.fullname;
971
- }
972
- suiteTitle = testCaseItem.classname || item.name;
973
- } else {
974
- // Use classname approach (default - avoids duplicates)
975
- if (isParametrized) {
976
- // For parameterized tests, use the class name to group them
977
- suiteTitle = item.name || testCaseItem.classname;
978
- } else {
979
- // For regular tests, prefer classname over fullname to avoid long paths
980
- suiteTitle = testCaseItem.classname || item.name;
981
- }
982
-
983
- // If still no suite title and we have fullname, extract just the class name
984
- if (!suiteTitle && item.fullname) {
985
- const fullnameParts = item.fullname.split('.');
986
- suiteTitle = fullnameParts[fullnameParts.length - 1]; // Just the class name
987
- }
988
- }
989
-
990
- // Fallback
991
- return suiteTitle || 'UnknownClass';
992
- }
993
-
994
595
  function fetchProperties(item) {
995
596
  const tags = [];
996
597
  let title = '';