@testomatio/reporter 2.3.7-beta.3-xml-import → 2.3.7-beta.4-stack-artifacts

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/uploader.js CHANGED
@@ -194,11 +194,6 @@ export class S3Uploader {
194
194
  filePath = path.join(process.cwd(), filePath);
195
195
  }
196
196
 
197
- // Normalize path separators for cross-platform compatibility
198
- if (typeof filePath === 'string') {
199
- filePath = filePath.replace(/\\/g, '/');
200
- }
201
-
202
197
  const data = { rid, file: filePath, uploaded };
203
198
  const jsonLine = `${JSON.stringify(data)}\n`;
204
199
  fs.appendFileSync(tempFilePath, jsonLine);
@@ -83,8 +83,12 @@ const fetchFilesFromStackTrace = (stack = '', checkExists = true) => {
83
83
  .map(f => f[1].trim())
84
84
  .map(f => f.replace(/^\/+/, '/').replace(/^\/([A-Za-z]:)/, '$1')) // Remove extra slashes, handle Windows paths
85
85
  .map(f => {
86
- // Normalize path separators for cross-platform compatibility
87
- return f.replace(/\\/g, '/');
86
+ // Convert Windows paths to Linux paths for testing purposes
87
+ if (f.match(/^[A-Za-z]:[\\\/]/)) {
88
+ // Convert Windows path to Linux equivalent for test scenarios
89
+ return f.replace(/^[A-Za-z]:[\\\/]/, '/').replace(/\\/g, '/');
90
+ }
91
+ return f;
88
92
  });
89
93
 
90
94
  debug('Found files in stack trace: ', files);
@@ -135,8 +139,6 @@ export const TEST_ID_REGEX = /@T([\w\d]{8})/;
135
139
  export const SUITE_ID_REGEX = /@S([\w\d]{8})/;
136
140
 
137
141
  const fetchIdFromCode = (code, opts = {}) => {
138
- if (!code) return null;
139
-
140
142
  const comments = code
141
143
  .split('\n')
142
144
  .map(l => l.trim())
@@ -178,65 +180,8 @@ const fetchSourceCode = (contents, opts = {}) => {
178
180
  if (lineIndex === -1) lineIndex = lines.findIndex(l => l.includes(`public void ${title}`));
179
181
  if (lineIndex === -1) lineIndex = lines.findIndex(l => l.includes(`${title}(`));
180
182
  } else if (opts.lang === 'csharp') {
181
- // Find the method declaration line
182
- let methodLineIndex = lines.findIndex(l => l.includes(`public void ${title}(`));
183
-
184
- if (methodLineIndex === -1) {
185
- methodLineIndex = lines.findIndex(l => l.includes(`public async Task ${title}(`));
186
- }
187
-
188
- if (methodLineIndex === -1) {
189
- methodLineIndex = lines.findIndex(l => l.includes(`${title}(`));
190
- }
191
-
192
- // If found, scan upwards to find [TestCase], [Test] attributes and XML comments
193
- if (methodLineIndex !== -1) {
194
- lineIndex = methodLineIndex;
195
-
196
- // Scan upwards to find the start of attributes and comments
197
- for (let i = methodLineIndex - 1; i >= 0; i--) {
198
- const trimmedLine = lines[i].trim();
199
-
200
- // Include [TestCase], [Test], and other attributes
201
- if (trimmedLine.startsWith('[')) {
202
- lineIndex = i;
203
- continue;
204
- }
205
-
206
- // Include XML documentation comments
207
- if (trimmedLine.startsWith('///')) {
208
- lineIndex = i;
209
- continue;
210
- }
211
-
212
- // Stop at empty lines (with some tolerance)
213
- if (trimmedLine === '') {
214
- // Check if next non-empty line is an attribute or comment
215
- let hasMoreAttributes = false;
216
- for (let j = i - 1; j >= 0; j--) {
217
- const nextTrimmed = lines[j].trim();
218
- if (nextTrimmed === '') continue;
219
- if (nextTrimmed.startsWith('[') || nextTrimmed.startsWith('///')) {
220
- hasMoreAttributes = true;
221
- lineIndex = j;
222
- }
223
- break;
224
- }
225
- if (!hasMoreAttributes) break;
226
- continue;
227
- }
228
-
229
- // Stop at other method declarations or class-level elements
230
- if (
231
- trimmedLine.includes('public ') ||
232
- trimmedLine.includes('private ') ||
233
- trimmedLine.includes('protected ') ||
234
- trimmedLine.includes('internal ')
235
- ) {
236
- if (!trimmedLine.startsWith('[')) break;
237
- }
238
- }
239
- }
183
+ if (lineIndex === -1) lineIndex = lines.findIndex(l => l.includes(`public void ${title}`));
184
+ if (lineIndex === -1) lineIndex = lines.findIndex(l => l.includes(`${title}(`));
240
185
  } else {
241
186
  lineIndex = lines.findIndex(l => l.includes(title));
242
187
  }
@@ -246,31 +191,11 @@ const fetchSourceCode = (contents, opts = {}) => {
246
191
  lineIndex -= opts.prepend;
247
192
  }
248
193
 
249
- if (lineIndex !== -1 && lineIndex !== undefined) {
194
+ if (lineIndex) {
250
195
  const result = [];
251
- let braceDepth = 0; // Track brace depth for C# methods
252
- let methodStartFound = false; // Flag to indicate we've found the method opening brace
253
-
254
196
  for (let i = lineIndex; i < lineIndex + limit; i++) {
255
197
  if (lines[i] === undefined) continue;
256
198
 
257
- // Track brace depth for C# to stop after method closes
258
- if (opts.lang === 'csharp') {
259
- const line = lines[i];
260
- // Count opening and closing braces
261
- const openBraces = (line.match(/\{/g) || []).length;
262
- const closeBraces = (line.match(/\}/g) || []).length;
263
-
264
- if (openBraces > 0) methodStartFound = true;
265
- braceDepth += openBraces - closeBraces;
266
-
267
- // If we've started the method and depth returns to 0, method is complete
268
- if (methodStartFound && braceDepth === 0 && closeBraces > 0) {
269
- result.push(lines[i]);
270
- break;
271
- }
272
- }
273
-
274
199
  if (i > lineIndex + 2 && !opts.prepend) {
275
200
  // annotation
276
201
  if (opts.lang === 'php' && lines[i].trim().startsWith('#[')) break;
@@ -291,18 +216,6 @@ const fetchSourceCode = (contents, opts = {}) => {
291
216
  if (opts.lang === 'java' && lines[i].trim().match(/^@\w+/)) break;
292
217
  if (opts.lang === 'java' && lines[i].includes(' public void ')) break;
293
218
  if (opts.lang === 'java' && lines[i].includes(' class ')) break;
294
- // For C#, additional checks if brace tracking didn't stop us
295
- if (opts.lang === 'csharp') {
296
- const trimmed = lines[i].trim();
297
- // Stop at attribute that marks beginning of next test
298
- if (trimmed.match(/^\[(Test|TestCase|Theory|Fact)/)) break;
299
- // Stop at XML documentation comments that belong to next method
300
- if (trimmed.startsWith('///')) break;
301
- // Stop at another method declaration
302
- if (trimmed.match(/^\s*(public|private|protected|internal)\s+(\w+|async\s+\w+)\s+\w+\s*\(/)) break;
303
- // Stop at class declaration
304
- if (trimmed.includes(' class ') && trimmed.includes('public')) break;
305
- }
306
219
  }
307
220
  result.push(lines[i]);
308
221
  }
package/src/xmlReader.js CHANGED
@@ -6,7 +6,6 @@ import { XMLParser } from 'fast-xml-parser';
6
6
  import { APP_PREFIX, STATUS } from './constants.js';
7
7
  import { randomUUID } from 'crypto';
8
8
  import { fileURLToPath } from 'url';
9
- import { NUnitXmlParser } from './junit-adapter/nunit-parser.js';
10
9
  import {
11
10
  fetchFilesFromStackTrace,
12
11
  fetchIdFromOutput,
@@ -76,10 +75,6 @@ class XmlReader {
76
75
  this.stats.language = opts.lang?.toLowerCase();
77
76
  this.uploader = new S3Uploader();
78
77
 
79
- // Enhanced NUnit parsing - enabled by default for NUnit XML
80
- this.enhancedNunit = opts.enhancedNunit !== false; // Default true, can be disabled
81
- this.groupParameterized = opts.groupParameterized !== false; // Default true, can be disabled
82
-
83
78
  // @ts-ignore
84
79
  const packageJsonPath = path.resolve(__dirname, '..', 'package.json');
85
80
  this.version = JSON.parse(fs.readFileSync(packageJsonPath).toString()).version;
@@ -131,8 +126,7 @@ class XmlReader {
131
126
  }
132
127
 
133
128
  processJUnit(jsonSuite) {
134
- const { testsuite, name, failures, errors } = jsonSuite;
135
- const tests = testsuite?.tests || jsonSuite.tests;
129
+ const { testsuite, name, tests, failures, errors } = jsonSuite;
136
130
 
137
131
  reduceOptions.preferClassname = this.stats.language === 'python';
138
132
  const resultTests = processTestSuite(testsuite);
@@ -163,14 +157,6 @@ class XmlReader {
163
157
  }
164
158
 
165
159
  processNUnit(jsonSuite) {
166
- // Use enhanced NUnit parser if enabled and this is actually NUnit XML
167
- if (this.enhancedNunit && this.isNUnitXml(jsonSuite)) {
168
- debug('Using enhanced NUnit parser');
169
- return this.processNUnitEnhanced(jsonSuite);
170
- }
171
-
172
- // Fallback to legacy parser for backward compatibility
173
- debug('Using legacy NUnit parser');
174
160
  const { result, total, passed, failed, inconclusive, skipped } = jsonSuite;
175
161
 
176
162
  reduceOptions.preferClassname = this.stats.language === 'python';
@@ -189,71 +175,63 @@ class XmlReader {
189
175
  };
190
176
  }
191
177
 
192
- /**
193
- * Check if the XML is actually NUnit format (has test-suite hierarchy)
194
- * @param {Object} jsonSuite - Parsed XML suite object
195
- * @returns {boolean} - True if this is NUnit XML format
196
- */
197
- isNUnitXml(jsonSuite) {
198
- // NUnit XML has test-suite elements with type attributes
199
- if (jsonSuite['test-suite']) {
200
- const testSuite = Array.isArray(jsonSuite['test-suite']) ? jsonSuite['test-suite'][0] : jsonSuite['test-suite'];
201
-
202
- // Check for NUnit-specific test-suite types
203
- return (
204
- testSuite &&
205
- testSuite.type &&
206
- ['Assembly', 'TestSuite', 'TestFixture', 'ParameterizedMethod'].includes(testSuite.type)
207
- );
208
- }
209
- return false;
210
- }
211
-
212
- processNUnitEnhanced(jsonSuite) {
213
- debug('Processing NUnit XML with enhanced parser');
214
-
215
- try {
216
- const nunitParser = new NUnitXmlParser({
217
- groupParameterized: this.groupParameterized,
218
- ...this.opts,
219
- });
220
-
221
- const result = nunitParser.parseTestRun(jsonSuite);
222
-
223
- // Add parsed tests to our collection
224
- this.tests = this.tests.concat(result.tests);
225
-
226
- debug(`Enhanced NUnit parser processed ${result.tests.length} tests`);
227
-
228
- return result;
229
- } catch (error) {
230
- debug('Enhanced NUnit parser failed, falling back to legacy parser:', error.message);
231
- console.warn(`${APP_PREFIX} Enhanced NUnit parsing failed, using legacy parser: ${error.message}`);
232
-
233
- // Fallback to legacy parser
234
- this.enhancedNunit = false;
235
- return this.processNUnit(jsonSuite);
236
- }
237
- }
238
-
239
178
  processTRX(jsonSuite) {
240
179
  let defs = jsonSuite?.TestRun?.TestDefinitions?.UnitTest;
241
180
  if (!Array.isArray(defs)) defs = [defs].filter(d => !!d);
242
181
 
243
- // Parse test definitions
244
- const tests = defs.map(td => this._parseTRXTestDefinition(td));
182
+ const tests =
183
+ defs.map(td => {
184
+ const title = td.name.replace(/\(.*?\)/, '').trim();
185
+ let example = td.name.match(/\((.*?)\)/);
186
+ if (example) example = { ...example[1].split(',') };
187
+ const suite = td.TestMethod.className.split(', ')[0].split('.');
188
+ const suite_title = suite.pop();
189
+ return {
190
+ title,
191
+ example,
192
+ file: suite.join('/'),
193
+ description: td.Description,
194
+ suite_title,
195
+ id: td.Execution.id,
196
+ };
197
+ }) || [];
245
198
 
246
- // Parse test results
247
199
  let result = jsonSuite?.TestRun?.Results?.UnitTestResult;
248
200
  if (!Array.isArray(result)) result = [result].filter(d => !!d);
249
201
 
250
- const results = result.map(td => this._parseTRXTestResult(td, tests));
202
+ const results = result.map(td => ({
203
+ id: td.executionId,
204
+ // seconds are used in junit reports, but ms are used by testomatio
205
+ run_time: parseFloat(td.duration) * 1000,
206
+ status: td.outcome,
207
+ stack: td.Output.StdOut,
208
+ files: td?.ResultFiles?.ResultFile?.map(rf => rf.path),
209
+ }));
210
+
211
+ results.forEach(r => {
212
+ const test = tests.find(t => t.id === r.id) || {};
213
+ r.suite_title = test.suite_title;
214
+ r.title = test.title?.trim();
215
+ if (test.code) r.code = test.code;
216
+ if (test.description) r.description = test.description;
217
+ if (test.example) r.example = test.example;
218
+ if (test.file) r.file = test.file;
219
+ r.create = true;
220
+ r.overwrite = true;
221
+ if (r.status === 'Passed') r.status = STATUS.PASSED;
222
+ if (r.status === 'Failed') r.status = STATUS.FAILED;
223
+ if (r.status === 'Skipped') r.status = STATUS.SKIPPED;
224
+ delete r.id;
225
+ });
251
226
 
252
227
  debug(results);
253
228
 
254
229
  const counters = jsonSuite?.TestRun?.ResultSummary?.Counters || {};
230
+
255
231
  const failed_count = parseInt(counters.failed, 10) + parseInt(counters.error, 10);
256
- const status = failed_count > 0 ? STATUS.FAILED : STATUS.PASSED.toString();
232
+
233
+ let status = STATUS.PASSED.toString();
234
+ if (failed_count > 0) status = STATUS.FAILED;
257
235
 
258
236
  this.tests = results.filter(t => !!t.title);
259
237
 
@@ -268,67 +246,6 @@ class XmlReader {
268
246
  };
269
247
  }
270
248
 
271
- _parseTRXTestDefinition(td) {
272
- const title = td.name.replace(/\(.*?\)/, '').trim();
273
- const exampleMatch = td.name.match(/\((.*?)\)/);
274
- const example = exampleMatch ? { ...exampleMatch[1].split(',') } : null;
275
-
276
- const suite = td.TestMethod.className.split(', ')[0].split('.');
277
- const suite_title = suite.pop();
278
-
279
- // Convert namespace to file path for C#
280
- const file = `${suite.join('/')}.cs`;
281
-
282
- return {
283
- title, // Base name without parameters for test import
284
- example, // Parameters object for parameterized tests
285
- file, // File path with .cs extension
286
- description: td.Description,
287
- suite_title,
288
- id: td.Execution.id,
289
- };
290
- }
291
-
292
- _parseTRXTestResult(td, tests) {
293
- const test = tests.find(t => t.id === td.executionId) || {};
294
-
295
- const result = {
296
- suite_title: test.suite_title,
297
- title: test.title?.trim(),
298
- file: test.file,
299
- description: test.description,
300
- code: test.code,
301
- run_time: parseFloat(td.duration) * 1000,
302
- stack: td.Output?.StdOut || '',
303
- files: td?.ResultFiles?.ResultFile?.map(rf => rf.path),
304
- create: true,
305
- overwrite: true,
306
- };
307
-
308
- // Add example for parameterized tests
309
- if (test.example) {
310
- result.example = test.example;
311
- }
312
-
313
- // Map TRX status to Testomat.io status
314
- result.status = this._mapTRXStatus(td.outcome);
315
-
316
- return result;
317
- }
318
-
319
- _mapTRXStatus(outcome) {
320
- switch (outcome) {
321
- case 'Passed':
322
- return STATUS.PASSED;
323
- case 'Failed':
324
- return STATUS.FAILED;
325
- case 'Skipped':
326
- return STATUS.SKIPPED;
327
- default:
328
- return STATUS.PASSED;
329
- }
330
- }
331
-
332
249
  processXUnit(assemblies) {
333
250
  const tests = [];
334
251
 
@@ -1,82 +0,0 @@
1
- /**
2
- * Enhanced NUnit XML Parser that properly handles test-suite hierarchy
3
- * and parameterized tests
4
- */
5
- export class NUnitXmlParser {
6
- constructor(options?: {});
7
- options: {};
8
- tests: any[];
9
- stats: {
10
- total: number;
11
- passed: number;
12
- failed: number;
13
- skipped: number;
14
- inconclusive: number;
15
- };
16
- /**
17
- * Parse NUnit XML test-run structure
18
- * @param {Object} testRun - Parsed XML test-run object
19
- * @returns {Object} - Parsed test results
20
- */
21
- parseTestRun(testRun: any): any;
22
- /**
23
- * Recursively parse test-suite elements based on their type
24
- * @param {Object|Array} testSuite - Test suite object or array
25
- * @param {Array} parentPath - Current path in the hierarchy
26
- */
27
- parseTestSuite(testSuite: any | any[], parentPath?: any[]): void;
28
- /**
29
- * Process child elements of a test suite
30
- * @param {Object} testSuite - Test suite object
31
- * @param {Array} currentPath - Current path in hierarchy
32
- */
33
- processChildren(testSuite: any, currentPath: any[]): void;
34
- /**
35
- * Parse test-case elements (actual tests)
36
- * @param {Object|Array} testCases - Test case object or array
37
- * @param {Array} suitePath - Path to the test suite
38
- * @param {Object} parentSuite - Parent test suite for context
39
- */
40
- parseTestCases(testCases: any | any[], suitePath: any[], parentSuite: any): void;
41
- /**
42
- * Parse individual test case
43
- * @param {Object} testCase - Test case object
44
- * @param {Array} suitePath - Path to the test suite
45
- * @param {Object} parentSuite - Parent test suite for context
46
- * @returns {Object|null} - Parsed test object
47
- */
48
- parseTestCase(testCase: any, suitePath: any[], parentSuite: any): any | null;
49
- /**
50
- * Extract method name and parameters from test name
51
- * @param {string} testName - Full test name
52
- * @returns {Object} - Extracted information
53
- */
54
- extractParameters(testName: string): any;
55
- /**
56
- * Parse parameter string into array of parameters
57
- * @param {string} paramString - Parameter string
58
- * @returns {Array} - Array of parameters
59
- */
60
- parseParameterString(paramString: string): any[];
61
- /**
62
- * Extract method name from test name (fallback)
63
- * @param {string} testName - Test name
64
- * @returns {string} - Method name
65
- */
66
- extractMethodName(testName: string): string;
67
- /**
68
- * Build file path from suite path and class name
69
- * @param {Array} suitePath - Suite path array
70
- * @param {string} className - Class name
71
- * @param {Object} parentSuite - Parent suite for context
72
- * @returns {string} - File path
73
- */
74
- buildFilePath(suitePath: any[], className: string, parentSuite: any): string;
75
- /**
76
- * Group parameterized tests by base method name
77
- * @param {Array} tests - Array of parsed tests
78
- * @returns {Object} - Grouped tests
79
- */
80
- groupParameterizedTests(tests: any[]): any;
81
- }
82
- export default NUnitXmlParser;