@testomatio/reporter 2.3.7-beta.2-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/lib/uploader.js CHANGED
@@ -170,10 +170,6 @@ class S3Uploader {
170
170
  if (typeof filePath === 'string' && !path_1.default.isAbsolute(filePath)) {
171
171
  filePath = path_1.default.join(process.cwd(), filePath);
172
172
  }
173
- // Normalize path separators for cross-platform compatibility
174
- if (typeof filePath === 'string') {
175
- filePath = filePath.replace(/\\/g, '/');
176
- }
177
173
  const data = { rid, file: filePath, uploaded };
178
174
  const jsonLine = `${JSON.stringify(data)}\n`;
179
175
  fs_1.default.appendFileSync(tempFilePath, jsonLine);
@@ -122,8 +122,12 @@ const fetchFilesFromStackTrace = (stack = '', checkExists = true) => {
122
122
  .map(f => f[1].trim())
123
123
  .map(f => f.replace(/^\/+/, '/').replace(/^\/([A-Za-z]:)/, '$1')) // Remove extra slashes, handle Windows paths
124
124
  .map(f => {
125
- // Normalize path separators for cross-platform compatibility
126
- return f.replace(/\\/g, '/');
125
+ // Convert Windows paths to Linux paths for testing purposes
126
+ if (f.match(/^[A-Za-z]:[\\\/]/)) {
127
+ // Convert Windows path to Linux equivalent for test scenarios
128
+ return f.replace(/^[A-Za-z]:[\\\/]/, '/').replace(/\\/g, '/');
129
+ }
130
+ return f;
127
131
  });
128
132
  debug('Found files in stack trace: ', files);
129
133
  return files.filter(f => {
@@ -170,8 +174,6 @@ exports.fetchSourceCodeFromStackTrace = fetchSourceCodeFromStackTrace;
170
174
  exports.TEST_ID_REGEX = /@T([\w\d]{8})/;
171
175
  exports.SUITE_ID_REGEX = /@S([\w\d]{8})/;
172
176
  const fetchIdFromCode = (code, opts = {}) => {
173
- if (!code)
174
- return null;
175
177
  const comments = code
176
178
  .split('\n')
177
179
  .map(l => l.trim())
@@ -214,58 +216,10 @@ const fetchSourceCode = (contents, opts = {}) => {
214
216
  lineIndex = lines.findIndex(l => l.includes(`${title}(`));
215
217
  }
216
218
  else if (opts.lang === 'csharp') {
217
- // Find the method declaration line
218
- let methodLineIndex = lines.findIndex(l => l.includes(`public void ${title}(`));
219
- if (methodLineIndex === -1) {
220
- methodLineIndex = lines.findIndex(l => l.includes(`public async Task ${title}(`));
221
- }
222
- if (methodLineIndex === -1) {
223
- methodLineIndex = lines.findIndex(l => l.includes(`${title}(`));
224
- }
225
- // If found, scan upwards to find [TestCase], [Test] attributes and XML comments
226
- if (methodLineIndex !== -1) {
227
- lineIndex = methodLineIndex;
228
- // Scan upwards to find the start of attributes and comments
229
- for (let i = methodLineIndex - 1; i >= 0; i--) {
230
- const trimmedLine = lines[i].trim();
231
- // Include [TestCase], [Test], and other attributes
232
- if (trimmedLine.startsWith('[')) {
233
- lineIndex = i;
234
- continue;
235
- }
236
- // Include XML documentation comments
237
- if (trimmedLine.startsWith('///')) {
238
- lineIndex = i;
239
- continue;
240
- }
241
- // Stop at empty lines (with some tolerance)
242
- if (trimmedLine === '') {
243
- // Check if next non-empty line is an attribute or comment
244
- let hasMoreAttributes = false;
245
- for (let j = i - 1; j >= 0; j--) {
246
- const nextTrimmed = lines[j].trim();
247
- if (nextTrimmed === '')
248
- continue;
249
- if (nextTrimmed.startsWith('[') || nextTrimmed.startsWith('///')) {
250
- hasMoreAttributes = true;
251
- lineIndex = j;
252
- }
253
- break;
254
- }
255
- if (!hasMoreAttributes)
256
- break;
257
- continue;
258
- }
259
- // Stop at other method declarations or class-level elements
260
- if (trimmedLine.includes('public ') ||
261
- trimmedLine.includes('private ') ||
262
- trimmedLine.includes('protected ') ||
263
- trimmedLine.includes('internal ')) {
264
- if (!trimmedLine.startsWith('['))
265
- break;
266
- }
267
- }
268
- }
219
+ if (lineIndex === -1)
220
+ lineIndex = lines.findIndex(l => l.includes(`public void ${title}`));
221
+ if (lineIndex === -1)
222
+ lineIndex = lines.findIndex(l => l.includes(`${title}(`));
269
223
  }
270
224
  else {
271
225
  lineIndex = lines.findIndex(l => l.includes(title));
@@ -274,28 +228,11 @@ const fetchSourceCode = (contents, opts = {}) => {
274
228
  if (opts.prepend) {
275
229
  lineIndex -= opts.prepend;
276
230
  }
277
- if (lineIndex !== -1 && lineIndex !== undefined) {
231
+ if (lineIndex) {
278
232
  const result = [];
279
- let braceDepth = 0; // Track brace depth for C# methods
280
- let methodStartFound = false; // Flag to indicate we've found the method opening brace
281
233
  for (let i = lineIndex; i < lineIndex + limit; i++) {
282
234
  if (lines[i] === undefined)
283
235
  continue;
284
- // Track brace depth for C# to stop after method closes
285
- if (opts.lang === 'csharp') {
286
- const line = lines[i];
287
- // Count opening and closing braces
288
- const openBraces = (line.match(/\{/g) || []).length;
289
- const closeBraces = (line.match(/\}/g) || []).length;
290
- if (openBraces > 0)
291
- methodStartFound = true;
292
- braceDepth += openBraces - closeBraces;
293
- // If we've started the method and depth returns to 0, method is complete
294
- if (methodStartFound && braceDepth === 0 && closeBraces > 0) {
295
- result.push(lines[i]);
296
- break;
297
- }
298
- }
299
236
  if (i > lineIndex + 2 && !opts.prepend) {
300
237
  // annotation
301
238
  if (opts.lang === 'php' && lines[i].trim().startsWith('#['))
@@ -334,22 +271,6 @@ const fetchSourceCode = (contents, opts = {}) => {
334
271
  break;
335
272
  if (opts.lang === 'java' && lines[i].includes(' class '))
336
273
  break;
337
- // For C#, additional checks if brace tracking didn't stop us
338
- if (opts.lang === 'csharp') {
339
- const trimmed = lines[i].trim();
340
- // Stop at attribute that marks beginning of next test
341
- if (trimmed.match(/^\[(Test|TestCase|Theory|Fact)/))
342
- break;
343
- // Stop at XML documentation comments that belong to next method
344
- if (trimmed.startsWith('///'))
345
- break;
346
- // Stop at another method declaration
347
- if (trimmed.match(/^\s*(public|private|protected|internal)\s+(\w+|async\s+\w+)\s+\w+\s*\(/))
348
- break;
349
- // Stop at class declaration
350
- if (trimmed.includes(' class ') && trimmed.includes('public'))
351
- break;
352
- }
353
274
  }
354
275
  result.push(lines[i]);
355
276
  }
@@ -19,11 +19,25 @@ declare class XmlReader {
19
19
  tests: any[];
20
20
  stats: {};
21
21
  uploader: S3Uploader;
22
- enhancedNunit: boolean;
23
- groupParameterized: boolean;
24
22
  version: any;
25
23
  connectAdapter(): import("./junit-adapter/adapter.js").default;
26
- parse(fileName: any): any;
24
+ parse(fileName: any): {
25
+ status: string;
26
+ create_tests: boolean;
27
+ tests_count: number;
28
+ passed_count: number;
29
+ skipped_count: number;
30
+ failed_count: number;
31
+ tests: any;
32
+ } | {
33
+ status: any;
34
+ create_tests: boolean;
35
+ tests_count: number;
36
+ passed_count: number;
37
+ failed_count: number;
38
+ skipped_count: number;
39
+ tests: any[];
40
+ };
27
41
  processJUnit(jsonSuite: any): {
28
42
  create_tests: boolean;
29
43
  duration: number;
@@ -35,14 +49,15 @@ declare class XmlReader {
35
49
  tests: any[];
36
50
  tests_count: number;
37
51
  };
38
- processNUnit(jsonSuite: any): any;
39
- /**
40
- * Check if the XML is actually NUnit format (has test-suite hierarchy)
41
- * @param {Object} jsonSuite - Parsed XML suite object
42
- * @returns {boolean} - True if this is NUnit XML format
43
- */
44
- isNUnitXml(jsonSuite: any): boolean;
45
- processNUnitEnhanced(jsonSuite: any): any;
52
+ processNUnit(jsonSuite: any): {
53
+ status: any;
54
+ create_tests: boolean;
55
+ tests_count: number;
56
+ passed_count: number;
57
+ failed_count: number;
58
+ skipped_count: number;
59
+ tests: any[];
60
+ };
46
61
  processTRX(jsonSuite: any): {
47
62
  status: string;
48
63
  create_tests: boolean;
@@ -52,27 +67,6 @@ declare class XmlReader {
52
67
  failed_count: number;
53
68
  tests: any;
54
69
  };
55
- _parseTRXTestDefinition(td: any): {
56
- title: any;
57
- example: any;
58
- file: string;
59
- description: any;
60
- suite_title: any;
61
- id: any;
62
- };
63
- _parseTRXTestResult(td: any, tests: any): {
64
- suite_title: any;
65
- title: any;
66
- file: any;
67
- description: any;
68
- code: any;
69
- run_time: number;
70
- stack: any;
71
- files: any;
72
- create: boolean;
73
- overwrite: boolean;
74
- };
75
- _mapTRXStatus(outcome: any): string;
76
70
  processXUnit(assemblies: any): {
77
71
  status: string;
78
72
  create_tests: boolean;
package/lib/xmlReader.js CHANGED
@@ -11,7 +11,6 @@ const fast_xml_parser_1 = require("fast-xml-parser");
11
11
  const constants_js_1 = require("./constants.js");
12
12
  const crypto_1 = require("crypto");
13
13
  const url_1 = require("url");
14
- const nunit_parser_js_1 = require("./junit-adapter/nunit-parser.js");
15
14
  const utils_js_1 = require("./utils/utils.js");
16
15
  const index_js_1 = require("./pipe/index.js");
17
16
  const index_js_2 = __importDefault(require("./junit-adapter/index.js"));
@@ -55,9 +54,6 @@ class XmlReader {
55
54
  this.stats = {};
56
55
  this.stats.language = opts.lang?.toLowerCase();
57
56
  this.uploader = new uploader_js_1.S3Uploader();
58
- // Enhanced NUnit parsing - enabled by default for NUnit XML
59
- this.enhancedNunit = opts.enhancedNunit !== false; // Default true, can be disabled
60
- this.groupParameterized = opts.groupParameterized !== false; // Default true, can be disabled
61
57
  // @ts-ignore
62
58
  const packageJsonPath = path_1.default.resolve(__dirname, '..', 'package.json');
63
59
  this.version = JSON.parse(fs_1.default.readFileSync(packageJsonPath).toString()).version;
@@ -106,8 +102,7 @@ class XmlReader {
106
102
  return this.processJUnit(jsonSuite);
107
103
  }
108
104
  processJUnit(jsonSuite) {
109
- const { testsuite, name, failures, errors } = jsonSuite;
110
- const tests = testsuite?.tests || jsonSuite.tests;
105
+ const { testsuite, name, tests, failures, errors } = jsonSuite;
111
106
  reduceOptions.preferClassname = this.stats.language === 'python';
112
107
  const resultTests = processTestSuite(testsuite);
113
108
  const hasFailures = resultTests.filter(t => t.status === 'failed').length > 0;
@@ -133,13 +128,6 @@ class XmlReader {
133
128
  };
134
129
  }
135
130
  processNUnit(jsonSuite) {
136
- // Use enhanced NUnit parser if enabled and this is actually NUnit XML
137
- if (this.enhancedNunit && this.isNUnitXml(jsonSuite)) {
138
- debug('Using enhanced NUnit parser');
139
- return this.processNUnitEnhanced(jsonSuite);
140
- }
141
- // Fallback to legacy parser for backward compatibility
142
- debug('Using legacy NUnit parser');
143
131
  const { result, total, passed, failed, inconclusive, skipped } = jsonSuite;
144
132
  reduceOptions.preferClassname = this.stats.language === 'python';
145
133
  const resultTests = processTestSuite(jsonSuite['test-suite']);
@@ -154,58 +142,65 @@ class XmlReader {
154
142
  tests: resultTests,
155
143
  };
156
144
  }
157
- /**
158
- * Check if the XML is actually NUnit format (has test-suite hierarchy)
159
- * @param {Object} jsonSuite - Parsed XML suite object
160
- * @returns {boolean} - True if this is NUnit XML format
161
- */
162
- isNUnitXml(jsonSuite) {
163
- // NUnit XML has test-suite elements with type attributes
164
- if (jsonSuite['test-suite']) {
165
- const testSuite = Array.isArray(jsonSuite['test-suite']) ? jsonSuite['test-suite'][0] : jsonSuite['test-suite'];
166
- // Check for NUnit-specific test-suite types
167
- return (testSuite &&
168
- testSuite.type &&
169
- ['Assembly', 'TestSuite', 'TestFixture', 'ParameterizedMethod'].includes(testSuite.type));
170
- }
171
- return false;
172
- }
173
- processNUnitEnhanced(jsonSuite) {
174
- debug('Processing NUnit XML with enhanced parser');
175
- try {
176
- const nunitParser = new nunit_parser_js_1.NUnitXmlParser({
177
- groupParameterized: this.groupParameterized,
178
- ...this.opts,
179
- });
180
- const result = nunitParser.parseTestRun(jsonSuite);
181
- // Add parsed tests to our collection
182
- this.tests = this.tests.concat(result.tests);
183
- debug(`Enhanced NUnit parser processed ${result.tests.length} tests`);
184
- return result;
185
- }
186
- catch (error) {
187
- debug('Enhanced NUnit parser failed, falling back to legacy parser:', error.message);
188
- console.warn(`${constants_js_1.APP_PREFIX} Enhanced NUnit parsing failed, using legacy parser: ${error.message}`);
189
- // Fallback to legacy parser
190
- this.enhancedNunit = false;
191
- return this.processNUnit(jsonSuite);
192
- }
193
- }
194
145
  processTRX(jsonSuite) {
195
146
  let defs = jsonSuite?.TestRun?.TestDefinitions?.UnitTest;
196
147
  if (!Array.isArray(defs))
197
148
  defs = [defs].filter(d => !!d);
198
- // Parse test definitions
199
- const tests = defs.map(td => this._parseTRXTestDefinition(td));
200
- // Parse test results
149
+ const tests = defs.map(td => {
150
+ const title = td.name.replace(/\(.*?\)/, '').trim();
151
+ let example = td.name.match(/\((.*?)\)/);
152
+ if (example)
153
+ example = { ...example[1].split(',') };
154
+ const suite = td.TestMethod.className.split(', ')[0].split('.');
155
+ const suite_title = suite.pop();
156
+ return {
157
+ title,
158
+ example,
159
+ file: suite.join('/'),
160
+ description: td.Description,
161
+ suite_title,
162
+ id: td.Execution.id,
163
+ };
164
+ }) || [];
201
165
  let result = jsonSuite?.TestRun?.Results?.UnitTestResult;
202
166
  if (!Array.isArray(result))
203
167
  result = [result].filter(d => !!d);
204
- const results = result.map(td => this._parseTRXTestResult(td, tests));
168
+ const results = result.map(td => ({
169
+ id: td.executionId,
170
+ // seconds are used in junit reports, but ms are used by testomatio
171
+ run_time: parseFloat(td.duration) * 1000,
172
+ status: td.outcome,
173
+ stack: td.Output.StdOut,
174
+ files: td?.ResultFiles?.ResultFile?.map(rf => rf.path),
175
+ }));
176
+ results.forEach(r => {
177
+ const test = tests.find(t => t.id === r.id) || {};
178
+ r.suite_title = test.suite_title;
179
+ r.title = test.title?.trim();
180
+ if (test.code)
181
+ r.code = test.code;
182
+ if (test.description)
183
+ r.description = test.description;
184
+ if (test.example)
185
+ r.example = test.example;
186
+ if (test.file)
187
+ r.file = test.file;
188
+ r.create = true;
189
+ r.overwrite = true;
190
+ if (r.status === 'Passed')
191
+ r.status = constants_js_1.STATUS.PASSED;
192
+ if (r.status === 'Failed')
193
+ r.status = constants_js_1.STATUS.FAILED;
194
+ if (r.status === 'Skipped')
195
+ r.status = constants_js_1.STATUS.SKIPPED;
196
+ delete r.id;
197
+ });
205
198
  debug(results);
206
199
  const counters = jsonSuite?.TestRun?.ResultSummary?.Counters || {};
207
200
  const failed_count = parseInt(counters.failed, 10) + parseInt(counters.error, 10);
208
- const status = failed_count > 0 ? constants_js_1.STATUS.FAILED : constants_js_1.STATUS.PASSED.toString();
201
+ let status = constants_js_1.STATUS.PASSED.toString();
202
+ if (failed_count > 0)
203
+ status = constants_js_1.STATUS.FAILED;
209
204
  this.tests = results.filter(t => !!t.title);
210
205
  return {
211
206
  status,
@@ -217,57 +212,6 @@ class XmlReader {
217
212
  tests: results,
218
213
  };
219
214
  }
220
- _parseTRXTestDefinition(td) {
221
- const title = td.name.replace(/\(.*?\)/, '').trim();
222
- const exampleMatch = td.name.match(/\((.*?)\)/);
223
- const example = exampleMatch ? { ...exampleMatch[1].split(',') } : null;
224
- const suite = td.TestMethod.className.split(', ')[0].split('.');
225
- const suite_title = suite.pop();
226
- // Convert namespace to file path for C#
227
- const file = `${suite.join('/')}.cs`;
228
- return {
229
- title, // Base name without parameters for test import
230
- example, // Parameters object for parameterized tests
231
- file, // File path with .cs extension
232
- description: td.Description,
233
- suite_title,
234
- id: td.Execution.id,
235
- };
236
- }
237
- _parseTRXTestResult(td, tests) {
238
- const test = tests.find(t => t.id === td.executionId) || {};
239
- const result = {
240
- suite_title: test.suite_title,
241
- title: test.title?.trim(),
242
- file: test.file,
243
- description: test.description,
244
- code: test.code,
245
- run_time: parseFloat(td.duration) * 1000,
246
- stack: td.Output?.StdOut || '',
247
- files: td?.ResultFiles?.ResultFile?.map(rf => rf.path),
248
- create: true,
249
- overwrite: true,
250
- };
251
- // Add example for parameterized tests
252
- if (test.example) {
253
- result.example = test.example;
254
- }
255
- // Map TRX status to Testomat.io status
256
- result.status = this._mapTRXStatus(td.outcome);
257
- return result;
258
- }
259
- _mapTRXStatus(outcome) {
260
- switch (outcome) {
261
- case 'Passed':
262
- return constants_js_1.STATUS.PASSED;
263
- case 'Failed':
264
- return constants_js_1.STATUS.FAILED;
265
- case 'Skipped':
266
- return constants_js_1.STATUS.SKIPPED;
267
- default:
268
- return constants_js_1.STATUS.PASSED;
269
- }
270
- }
271
215
  processXUnit(assemblies) {
272
216
  const tests = [];
273
217
  assemblies = Array.isArray(assemblies.assembly) ? assemblies.assembly : [assemblies.assembly];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testomatio/reporter",
3
- "version": "2.3.7-beta.2-xml-import",
3
+ "version": "2.3.7-beta.4-stack-artifacts",
4
4
  "description": "Testomatio Reporter Client",
5
5
  "engines": {
6
6
  "node": ">=18"
package/src/bin/cli.js CHANGED
@@ -158,7 +158,7 @@ program
158
158
  .option('--lang <lang>', 'Language used (python, ruby, java)')
159
159
  .option('--timelimit <time>', 'default time limit in seconds to kill a stuck process')
160
160
  .action(async (pattern, opts) => {
161
- if (!pattern.endsWith('.xml') && !pattern.includes('*')) {
161
+ if (!pattern.endsWith('.xml')) {
162
162
  pattern += '.xml';
163
163
  }
164
164
  let { javaTests, lang } = opts;
@@ -23,7 +23,7 @@ program
23
23
  .option('--timelimit <time>', 'default time limit in seconds to kill a stuck process')
24
24
  .option('--env-file <envfile>', 'Load environment variables from env file')
25
25
  .action(async (pattern, opts) => {
26
- if (!pattern.endsWith('.xml') && !pattern.includes('*')) {
26
+ if (!pattern.endsWith('.xml')) {
27
27
  pattern += '.xml';
28
28
  }
29
29
  let { javaTests, lang } = opts;
@@ -34,10 +34,7 @@ program
34
34
  }
35
35
  lang = lang?.toLowerCase();
36
36
  if (javaTests === true || (lang === 'java' && !javaTests)) javaTests = 'src/test/java';
37
- const runReader = new XmlReader({
38
- javaTests,
39
- lang,
40
- });
37
+ const runReader = new XmlReader({ javaTests, lang });
41
38
  const files = glob.sync(pattern, { cwd: opts.dir || process.cwd() });
42
39
  if (!files.length) {
43
40
  console.log(APP_PREFIX, `Report can't be created. No XML files found 😥`);
@@ -18,7 +18,7 @@ const newArgs = ['run'];
18
18
  let i = 0;
19
19
  while (i < args.length) {
20
20
  const arg = args[i];
21
-
21
+
22
22
  if (arg === '-c' || arg === '--command') {
23
23
  // Map -c/--command to positional argument for run command
24
24
  i++;
@@ -33,7 +33,7 @@ while (i < args.length) {
33
33
  // Map --launch to start command
34
34
  newArgs[0] = 'start';
35
35
  } else if (arg === '--finish') {
36
- // Map --finish to finish command
36
+ // Map --finish to finish command
37
37
  newArgs[0] = 'finish';
38
38
  } else {
39
39
  // Pass through other arguments
@@ -45,9 +45,9 @@ while (i < args.length) {
45
45
  // Execute the main CLI with mapped arguments
46
46
 
47
47
  const child = spawn(process.execPath, [cliPath, ...newArgs], {
48
- stdio: 'inherit',
48
+ stdio: 'inherit'
49
49
  });
50
50
 
51
- child.on('exit', code => {
51
+ child.on('exit', (code) => {
52
52
  process.exit(code);
53
- });
53
+ });