@testomatio/reporter 2.3.7-beta.3-xml-import β†’ 2.3.7-beta.5-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/README.md CHANGED
@@ -13,7 +13,7 @@ Testomat.io Reporter (this npm package) supports:
13
13
  - πŸ”Ž [Stack traces](./docs/stacktrace.md) and error messages
14
14
  - πŸ™ [GitHub](./docs/pipes/github.md), [GitLab](./docs/pipes/gitlab.md) & [Bitbucket](./docs/pipes/bitbucket.md) integration
15
15
  - πŸš… Realtime reports
16
- - πŸ—ƒοΈ Other test frameworks supported via [JUnit XML](./docs/junit.md) with [XML import configuration](./docs/xml-imports.md)
16
+ - πŸ—ƒοΈ Other test frameworks supported via [JUNit XML](./docs/junit.md)
17
17
  - πŸšΆβ€β™€οΈ Steps _(work in progress)_
18
18
  - πŸ“„ [Logger](./docs/logger.md) _(work in progress, supports Jest for now)_
19
19
  - ☁️ Custom properties and metadata _(work in progress)_
package/lib/bin/cli.js CHANGED
@@ -145,7 +145,7 @@ program
145
145
  .option('--lang <lang>', 'Language used (python, ruby, java)')
146
146
  .option('--timelimit <time>', 'default time limit in seconds to kill a stuck process')
147
147
  .action(async (pattern, opts) => {
148
- if (!pattern.endsWith('.xml') && !pattern.includes('*')) {
148
+ if (!pattern.endsWith('.xml')) {
149
149
  pattern += '.xml';
150
150
  }
151
151
  let { javaTests, lang } = opts;
@@ -25,7 +25,7 @@ program
25
25
  .option('--timelimit <time>', 'default time limit in seconds to kill a stuck process')
26
26
  .option('--env-file <envfile>', 'Load environment variables from env file')
27
27
  .action(async (pattern, opts) => {
28
- if (!pattern.endsWith('.xml') && !pattern.includes('*')) {
28
+ if (!pattern.endsWith('.xml')) {
29
29
  pattern += '.xml';
30
30
  }
31
31
  let { javaTests, lang } = opts;
@@ -37,10 +37,7 @@ program
37
37
  lang = lang?.toLowerCase();
38
38
  if (javaTests === true || (lang === 'java' && !javaTests))
39
39
  javaTests = 'src/test/java';
40
- const runReader = new xmlReader_js_1.default({
41
- javaTests,
42
- lang,
43
- });
40
+ const runReader = new xmlReader_js_1.default({ javaTests, lang });
44
41
  const files = glob_1.glob.sync(pattern, { cwd: opts.dir || process.cwd() });
45
42
  if (!files.length) {
46
43
  console.log(constants_js_1.APP_PREFIX, `Report can't be created. No XML files found πŸ˜₯`);
package/lib/client.js CHANGED
@@ -70,8 +70,8 @@ class Client {
70
70
  this.pipeStore = {};
71
71
  this.runId = '';
72
72
  this.queue = Promise.resolve();
73
- // Get package.json path - use a simple approach that works in both environments
74
- const pathToPackageJSON = path_1.default.join(process.cwd(), 'package.json');
73
+ // @ts-ignore this line will be removed in compiled code, because __dirname is defined in commonjs
74
+ const pathToPackageJSON = path_1.default.join(__dirname, '../package.json');
75
75
  try {
76
76
  this.version = JSON.parse(fs_1.default.readFileSync(pathToPackageJSON).toString()).version;
77
77
  console.log(constants_js_1.APP_PREFIX, `Testomatio Reporter v${this.version}`);
@@ -329,11 +329,7 @@ class Client {
329
329
  */
330
330
  formatLogs({ error, steps, logs }) {
331
331
  error = error?.trim();
332
- logs = logs
333
- ?.trim()
334
- .split('\n')
335
- .map(l => (0, utils_js_1.truncate)(l))
336
- .join('\n');
332
+ logs = logs?.trim().split('\n').map(l => (0, utils_js_1.truncate)(l)).join('\n');
337
333
  if (Array.isArray(steps)) {
338
334
  steps = steps
339
335
  .map(step => (0, utils_js_1.formatStep)(step))
@@ -1,4 +1,5 @@
1
1
  export default CSharpAdapter;
2
2
  declare class CSharpAdapter extends Adapter {
3
+ getFilePath(t: any): string;
3
4
  }
4
5
  import Adapter from './adapter.js';
@@ -7,57 +7,24 @@ const path_1 = __importDefault(require("path"));
7
7
  const adapter_js_1 = __importDefault(require("./adapter.js"));
8
8
  class CSharpAdapter extends adapter_js_1.default {
9
9
  formatTest(t) {
10
- // Extract example from title if not already present
11
- if (!t.example) {
12
- const exampleMatch = t.title.match(/\((.*?)\)/);
13
- if (exampleMatch) {
14
- // Extract parameters as object with numeric keys for API
15
- const params = exampleMatch[1].split(',').map(param => param.trim());
16
- t.example = {};
17
- params.forEach((param, index) => {
18
- t.example[index] = param;
19
- });
20
- }
21
- }
22
- // Remove parameters from title to avoid duplicates in Test Suite
23
- // The example field will be used for grouping on import
24
- t.title = t.title.replace(/\(.*?\)/, '').trim();
10
+ const title = t.title.replace(/\(.*?\)/, '').trim();
11
+ const example = t.title.match(/\((.*?)\)/);
12
+ if (example)
13
+ t.example = { ...example[1].split(',') };
25
14
  const suite = t.suite_title.split('.');
26
15
  t.suite_title = suite.pop();
27
16
  t.file = namespaceToFileName(t.file);
17
+ t.title = title.trim();
28
18
  return t;
29
19
  }
30
20
  getFilePath(t) {
31
- if (!t.file)
32
- return null;
33
- // Normalize path separators for cross-platform compatibility
34
- let filePath = t.file.replace(/\\/g, '/');
35
- // If file already has .cs extension, use it directly
36
- if (filePath.endsWith('.cs')) {
37
- // Make relative path if it's absolute
38
- if (path_1.default.isAbsolute(filePath)) {
39
- // Try to find project-relative path
40
- const cwd = process.cwd().replace(/\\/g, '/');
41
- if (filePath.startsWith(cwd)) {
42
- filePath = path_1.default.relative(cwd, filePath).replace(/\\/g, '/');
43
- }
44
- }
45
- return filePath;
46
- }
47
- // Convert namespace path to file path
48
- const fileName = namespaceToFileName(filePath);
21
+ const fileName = namespaceToFileName(t.file);
49
22
  return fileName;
50
23
  }
51
24
  }
52
25
  module.exports = CSharpAdapter;
53
26
  function namespaceToFileName(fileName) {
54
- if (!fileName)
55
- return '';
56
- // If already a .cs file path, clean it up
57
- if (fileName.endsWith('.cs')) {
58
- return fileName.replace(/\\/g, '/');
59
- }
60
27
  const fileParts = fileName.split('.');
61
28
  fileParts[fileParts.length - 1] = fileParts[fileParts.length - 1]?.replace(/\$.*/, '');
62
- return `${fileParts.join('/')}.cs`;
29
+ return `${fileParts.join(path_1.default.sep)}.cs`;
63
30
  }
package/lib/pipe/debug.js CHANGED
@@ -18,7 +18,7 @@ class DebugPipe {
18
18
  this.isEnabled = !!process.env.TESTOMATIO_DEBUG || !!process.env.DEBUG;
19
19
  if (this.isEnabled) {
20
20
  this.batch = {
21
- isEnabled: this.params.isBatchEnabled ?? (process.env.TESTOMATIO_DISABLE_BATCH_UPLOAD ? false : true),
21
+ isEnabled: this.params.isBatchEnabled ?? !process.env.TESTOMATIO_DISABLE_BATCH_UPLOAD ?? true,
22
22
  intervalFunction: null,
23
23
  intervalTime: 5000,
24
24
  tests: [],
@@ -23,7 +23,7 @@ if (process.env.TESTOMATIO_RUN)
23
23
  class TestomatioPipe {
24
24
  constructor(params, store) {
25
25
  this.batch = {
26
- isEnabled: params?.isBatchEnabled ?? (process.env.TESTOMATIO_DISABLE_BATCH_UPLOAD ? false : true),
26
+ isEnabled: params?.isBatchEnabled ?? !process.env.TESTOMATIO_DISABLE_BATCH_UPLOAD ?? true,
27
27
  intervalFunction: null, // will be created in createRun by setInterval function
28
28
  intervalTime: 5000, // how often tests are sent
29
29
  tests: [], // array of tests in batch
@@ -60,7 +60,7 @@ class TestomatioPipe {
60
60
  retry: constants_js_1.REPORTER_REQUEST_RETRIES.retriesPerRequest,
61
61
  retryDelay: constants_js_1.REPORTER_REQUEST_RETRIES.retryTimeout,
62
62
  httpMethodsToRetry: ['GET', 'PUT', 'HEAD', 'OPTIONS', 'DELETE', 'POST'],
63
- shouldRetry: error => {
63
+ shouldRetry: (error) => {
64
64
  if (!error.response)
65
65
  return false;
66
66
  switch (error.response?.status) {
@@ -73,8 +73,8 @@ class TestomatioPipe {
73
73
  break;
74
74
  }
75
75
  return error.response?.status >= 401; // Retry on 401+ and 5xx
76
- },
77
- },
76
+ }
77
+ }
78
78
  });
79
79
  this.isEnabled = true;
80
80
  // do not finish this run (for parallel testing)
@@ -193,7 +193,7 @@ class TestomatioPipe {
193
193
  method: 'PUT',
194
194
  url: `/api/reporter/${this.runId}`,
195
195
  data: runParams,
196
- responseType: 'json',
196
+ responseType: 'json'
197
197
  });
198
198
  if (resp.data.artifacts)
199
199
  (0, pipe_utils_js_1.setS3Credentials)(resp.data.artifacts);
@@ -206,7 +206,7 @@ class TestomatioPipe {
206
206
  url: '/api/reporter',
207
207
  data: runParams,
208
208
  maxContentLength: Infinity,
209
- responseType: 'json',
209
+ responseType: 'json'
210
210
  });
211
211
  this.runId = resp.data.uid;
212
212
  this.runUrl = `${this.url}/${resp.data.url.split('/').splice(3).join('/')}`;
@@ -259,17 +259,15 @@ class TestomatioPipe {
259
259
  this.#formatData(data);
260
260
  const json = json_cycle_1.default.stringify(data);
261
261
  debug('Adding test', json);
262
- return this.client
263
- .request({
262
+ return this.client.request({
264
263
  method: 'POST',
265
264
  url: `/api/reporter/${this.runId}/testrun`,
266
265
  data: json,
267
266
  headers: {
268
267
  'Content-Type': 'application/json',
269
268
  },
270
- maxContentLength: Infinity,
271
- })
272
- .catch(err => {
269
+ maxContentLength: Infinity
270
+ }).catch(err => {
273
271
  this.requestFailures++;
274
272
  this.notReportedTestsCount++;
275
273
  if (err.response) {
@@ -314,21 +312,19 @@ class TestomatioPipe {
314
312
  // get tests from batch and clear batch
315
313
  const testsToSend = this.batch.tests.splice(0);
316
314
  debug('πŸ“¨ Batch upload', testsToSend.length, 'tests');
317
- return this.client
318
- .request({
315
+ return this.client.request({
319
316
  method: 'POST',
320
317
  url: `/api/reporter/${this.runId}/testrun`,
321
318
  data: {
322
319
  api_key: this.apiKey,
323
320
  tests: testsToSend,
324
- batch_index: this.batch.batchIndex,
321
+ batch_index: this.batch.batchIndex
325
322
  },
326
323
  headers: {
327
324
  'Content-Type': 'application/json',
328
325
  },
329
- maxContentLength: Infinity,
330
- })
331
- .catch(err => {
326
+ maxContentLength: Infinity
327
+ }).catch(err => {
332
328
  this.requestFailures++;
333
329
  this.notReportedTestsCount += testsToSend.length;
334
330
  if (err.response) {
@@ -412,7 +408,7 @@ class TestomatioPipe {
412
408
  status_event,
413
409
  detach: params.detach,
414
410
  tests: params.tests,
415
- },
411
+ }
416
412
  });
417
413
  if (this.runUrl) {
418
414
  console.log(constants_js_1.APP_PREFIX, 'πŸ“Š Report Saved. Report URL:', picocolors_1.default.magenta(this.runUrl));
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;