@testomatio/reporter 2.3.4 β†’ 2.3.5-beta-6-xml-import

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)
16
+ - πŸ—ƒοΈ Other test frameworks supported via [JUnit XML](./docs/junit.md) with [XML import configuration](./docs/xml-imports.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)_
@@ -1,5 +1,4 @@
1
1
  export default CSharpAdapter;
2
2
  declare class CSharpAdapter extends Adapter {
3
- getFilePath(t: any): string;
4
3
  }
5
4
  import Adapter from './adapter.js';
@@ -7,24 +7,53 @@ 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
- const title = t.title.replace(/\(.*?\)/, '').trim();
11
- const example = t.title.match(/\((.*?)\)/);
12
- if (example)
13
- t.example = { ...example[1].split(',') };
10
+ // Don't override example if it already exists from NUnit XML processing
11
+ // The xmlReader.js already extracts parameters correctly from <arguments>
12
+ if (!t.example) {
13
+ const title = t.title.replace(/\(.*?\)/, '').trim();
14
+ const exampleMatch = t.title.match(/\((.*?)\)/);
15
+ if (exampleMatch) {
16
+ // Keep as array for consistency with NUnit XML processing
17
+ t.example = exampleMatch[1].split(',').map(param => param.trim());
18
+ }
19
+ t.title = title.trim();
20
+ }
14
21
  const suite = t.suite_title.split('.');
15
22
  t.suite_title = suite.pop();
16
23
  t.file = namespaceToFileName(t.file);
17
- t.title = title.trim();
18
24
  return t;
19
25
  }
20
26
  getFilePath(t) {
21
- const fileName = namespaceToFileName(t.file);
27
+ if (!t.file)
28
+ return null;
29
+ // Normalize path separators for cross-platform compatibility
30
+ let filePath = t.file.replace(/\\/g, '/');
31
+ // If file already has .cs extension, use it directly
32
+ if (filePath.endsWith('.cs')) {
33
+ // Make relative path if it's absolute
34
+ if (path_1.default.isAbsolute(filePath)) {
35
+ // Try to find project-relative path
36
+ const cwd = process.cwd().replace(/\\/g, '/');
37
+ if (filePath.startsWith(cwd)) {
38
+ filePath = path_1.default.relative(cwd, filePath).replace(/\\/g, '/');
39
+ }
40
+ }
41
+ return filePath;
42
+ }
43
+ // Convert namespace path to file path
44
+ const fileName = namespaceToFileName(filePath);
22
45
  return fileName;
23
46
  }
24
47
  }
25
48
  module.exports = CSharpAdapter;
26
49
  function namespaceToFileName(fileName) {
50
+ if (!fileName)
51
+ return '';
52
+ // If already a .cs file path, clean it up
53
+ if (fileName.endsWith('.cs')) {
54
+ return fileName.replace(/\\/g, '/');
55
+ }
27
56
  const fileParts = fileName.split('.');
28
57
  fileParts[fileParts.length - 1] = fileParts[fileParts.length - 1]?.replace(/\$.*/, '');
29
- return `${fileParts.join(path_1.default.sep)}.cs`;
58
+ return `${fileParts.join('/')}.cs`;
30
59
  }
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 ?? true,
21
+ isEnabled: this.params.isBatchEnabled ?? (process.env.TESTOMATIO_DISABLE_BATCH_UPLOAD ? false : 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 ?? true,
26
+ isEnabled: params?.isBatchEnabled ?? (process.env.TESTOMATIO_DISABLE_BATCH_UPLOAD ? false : 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)
@@ -89,6 +89,28 @@ class TestomatioPipe {
89
89
  console.error(constants_js_1.APP_PREFIX, picocolors_1.default.red(`Error creating report on Testomat.io, report url '${this.url}' is invalid`));
90
90
  }
91
91
  }
92
+ /**
93
+ * Prepares data for sending to Testomat.io.
94
+ * @param {*} data - The data to be formatted.
95
+ * @returns
96
+ */
97
+ #formatData(data) {
98
+ data.api_key = this.apiKey;
99
+ data.create = this.createNewTests;
100
+ // add test ID + run ID
101
+ if (data.rid)
102
+ data.rid = `${this.runId}-${data.rid}`;
103
+ if (!process.env.TESTOMATIO_STACK_PASSED && data.status === constants_js_1.STATUS.PASSED) {
104
+ data.stack = null;
105
+ }
106
+ if (!process.env.TESTOMATIO_STEPS_PASSED && data.status === constants_js_1.STATUS.PASSED) {
107
+ data.steps = null;
108
+ }
109
+ if (process.env.TESTOMATIO_NO_STEPS) {
110
+ data.steps = null;
111
+ }
112
+ return data;
113
+ }
92
114
  /**
93
115
  * Asynchronously prepares and retrieves the Testomat.io test grepList based on the provided options.
94
116
  * @param {Object} opts - The options for preparing the test grepList.
@@ -171,7 +193,7 @@ class TestomatioPipe {
171
193
  method: 'PUT',
172
194
  url: `/api/reporter/${this.runId}`,
173
195
  data: runParams,
174
- responseType: 'json'
196
+ responseType: 'json',
175
197
  });
176
198
  if (resp.data.artifacts)
177
199
  (0, pipe_utils_js_1.setS3Credentials)(resp.data.artifacts);
@@ -184,7 +206,7 @@ class TestomatioPipe {
184
206
  url: '/api/reporter',
185
207
  data: runParams,
186
208
  maxContentLength: Infinity,
187
- responseType: 'json'
209
+ responseType: 'json',
188
210
  });
189
211
  this.runId = resp.data.uid;
190
212
  this.runUrl = `${this.url}/${resp.data.url.split('/').splice(3).join('/')}`;
@@ -234,28 +256,20 @@ class TestomatioPipe {
234
256
  return;
235
257
  if (this.#cancelTestReportingInCaseOfTooManyReqFailures())
236
258
  return;
237
- data.api_key = this.apiKey;
238
- data.create = this.createNewTests;
239
- if (!process.env.TESTOMATIO_STACK_PASSED && data.status === constants_js_1.STATUS.PASSED) {
240
- data.stack = null;
241
- }
242
- if (!process.env.TESTOMATIO_STEPS_PASSED && data.status === constants_js_1.STATUS.PASSED) {
243
- data.steps = null;
244
- }
245
- if (process.env.TESTOMATIO_NO_STEPS) {
246
- data.steps = null;
247
- }
259
+ this.#formatData(data);
248
260
  const json = json_cycle_1.default.stringify(data);
249
261
  debug('Adding test', json);
250
- return this.client.request({
262
+ return this.client
263
+ .request({
251
264
  method: 'POST',
252
265
  url: `/api/reporter/${this.runId}/testrun`,
253
266
  data: json,
254
267
  headers: {
255
268
  'Content-Type': 'application/json',
256
269
  },
257
- maxContentLength: Infinity
258
- }).catch(err => {
270
+ maxContentLength: Infinity,
271
+ })
272
+ .catch(err => {
259
273
  this.requestFailures++;
260
274
  this.notReportedTestsCount++;
261
275
  if (err.response) {
@@ -300,19 +314,21 @@ class TestomatioPipe {
300
314
  // get tests from batch and clear batch
301
315
  const testsToSend = this.batch.tests.splice(0);
302
316
  debug('πŸ“¨ Batch upload', testsToSend.length, 'tests');
303
- return this.client.request({
317
+ return this.client
318
+ .request({
304
319
  method: 'POST',
305
320
  url: `/api/reporter/${this.runId}/testrun`,
306
321
  data: {
307
322
  api_key: this.apiKey,
308
323
  tests: testsToSend,
309
- batch_index: this.batch.batchIndex
324
+ batch_index: this.batch.batchIndex,
310
325
  },
311
326
  headers: {
312
327
  'Content-Type': 'application/json',
313
328
  },
314
- maxContentLength: Infinity
315
- }).catch(err => {
329
+ maxContentLength: Infinity,
330
+ })
331
+ .catch(err => {
316
332
  this.requestFailures++;
317
333
  this.notReportedTestsCount += testsToSend.length;
318
334
  if (err.response) {
@@ -344,11 +360,7 @@ class TestomatioPipe {
344
360
  console.warn(constants_js_1.APP_PREFIX, picocolors_1.default.red('Run ID is not set, skipping test reporting'));
345
361
  return;
346
362
  }
347
- // add test ID + run ID
348
- if (data.rid)
349
- data.rid = `${this.runId}-${data.rid}`;
350
- data.api_key = this.apiKey;
351
- data.create = this.createNewTests;
363
+ this.#formatData(data);
352
364
  let uploading = null;
353
365
  if (!this.batch.isEnabled)
354
366
  uploading = this.#uploadSingleTest(data);
@@ -400,7 +412,7 @@ class TestomatioPipe {
400
412
  status_event,
401
413
  detach: params.detach,
402
414
  tests: params.tests,
403
- }
415
+ },
404
416
  });
405
417
  console.log(constants_js_1.APP_PREFIX, 'βœ… Testrun finished');
406
418
  if (this.runUrl) {
@@ -173,6 +173,8 @@ exports.fetchSourceCodeFromStackTrace = fetchSourceCodeFromStackTrace;
173
173
  exports.TEST_ID_REGEX = /@T([\w\d]{8})/;
174
174
  exports.SUITE_ID_REGEX = /@S([\w\d]{8})/;
175
175
  const fetchIdFromCode = (code, opts = {}) => {
176
+ if (!code)
177
+ return null;
176
178
  const comments = code
177
179
  .split('\n')
178
180
  .map(l => l.trim())
@@ -215,10 +217,29 @@ const fetchSourceCode = (contents, opts = {}) => {
215
217
  lineIndex = lines.findIndex(l => l.includes(`${title}(`));
216
218
  }
217
219
  else if (opts.lang === 'csharp') {
218
- if (lineIndex === -1)
219
- lineIndex = lines.findIndex(l => l.includes(`public void ${title}`));
220
- if (lineIndex === -1)
220
+ // Enhanced C# method detection for NUnit tests
221
+ lineIndex = lines.findIndex(l => l.includes(`public void ${title}(`));
222
+ if (lineIndex === -1) {
223
+ lineIndex = lines.findIndex(l => l.includes(`public async Task ${title}(`));
224
+ }
225
+ if (lineIndex === -1) {
221
226
  lineIndex = lines.findIndex(l => l.includes(`${title}(`));
227
+ }
228
+ // Look for TestCase or Test attributes above the method
229
+ if (lineIndex === -1) {
230
+ const testAttributeIndex = lines.findIndex((l, index) => {
231
+ if (l.includes('[TestCase') || l.includes('[Test')) {
232
+ // Check next few lines for the method
233
+ const nextLines = lines.slice(index, Math.min(lines.length, index + 5));
234
+ const hasMethod = nextLines.some(nextLine => nextLine.includes(`${title}(`));
235
+ return hasMethod;
236
+ }
237
+ return false;
238
+ });
239
+ if (testAttributeIndex !== -1) {
240
+ lineIndex = testAttributeIndex;
241
+ }
242
+ }
222
243
  }
223
244
  else {
224
245
  lineIndex = lines.findIndex(l => l.includes(title));
@@ -227,7 +248,7 @@ const fetchSourceCode = (contents, opts = {}) => {
227
248
  if (opts.prepend) {
228
249
  lineIndex -= opts.prepend;
229
250
  }
230
- if (lineIndex) {
251
+ if (lineIndex !== -1 && lineIndex !== undefined) {
231
252
  const result = [];
232
253
  for (let i = lineIndex; i < lineIndex + limit; i++) {
233
254
  if (lines[i] === undefined)
@@ -270,6 +291,14 @@ const fetchSourceCode = (contents, opts = {}) => {
270
291
  break;
271
292
  if (opts.lang === 'java' && lines[i].includes(' class '))
272
293
  break;
294
+ if (opts.lang === 'csharp' && lines[i].trim().match(/^\[Test/))
295
+ break;
296
+ if (opts.lang === 'csharp' && lines[i].includes(' public void '))
297
+ break;
298
+ if (opts.lang === 'csharp' && lines[i].includes(' public async Task '))
299
+ break;
300
+ if (opts.lang === 'csharp' && lines[i].includes(' class ') && lines[i].includes('public'))
301
+ break;
273
302
  }
274
303
  result.push(lines[i]);
275
304
  }
@@ -13,6 +13,8 @@ declare class XmlReader {
13
13
  runId: any;
14
14
  adapter: import("./junit-adapter/adapter.js").default;
15
15
  opts: {};
16
+ disableSourceCodeFetching: any;
17
+ suiteOrganization: any;
16
18
  store: {};
17
19
  pipesPromise: Promise<any[]>;
18
20
  parser: XMLParser;
@@ -77,6 +79,13 @@ declare class XmlReader {
77
79
  skipped_count: number;
78
80
  tests: any[];
79
81
  };
82
+ deduplicateTestsByFQN(tests: any): any[];
83
+ generateFQN(test: any): string;
84
+ generateNormalizedFQN(test: any): string;
85
+ extractAssemblyName(test: any): any;
86
+ extractNamespace(test: any): any;
87
+ extractClassName(test: any): any;
88
+ extractCsFileFromPath(test: any): any;
80
89
  calculateStats(): {};
81
90
  fetchSourceCode(): void;
82
91
  formatTests(): void;