@testomatio/reporter 2.0.0-beta.1-gaxios → 2.0.0-beta.1-xml

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.
@@ -1,6 +1,12 @@
1
1
  import createDebugMessages from 'debug';
2
2
  import pc from 'picocolors';
3
- import { Gaxios } from 'gaxios';
3
+
4
+ // Retry interceptor function
5
+ import axiosRetry from 'axios-retry';
6
+
7
+ // Default axios instance
8
+ import axios from 'axios';
9
+
4
10
  import JsonCycle from 'json-cycle';
5
11
  import { APP_PREFIX, STATUS, AXIOS_TIMEOUT, REPORTER_REQUEST_RETRIES } from '../constants.js';
6
12
  import { isValidUrl, foundedTestLog } from '../utils/utils.js';
@@ -51,30 +57,43 @@ class TestomatioPipe {
51
57
  this.groupTitle = params.groupTitle || process.env.TESTOMATIO_RUNGROUP_TITLE;
52
58
  this.env = process.env.TESTOMATIO_ENV;
53
59
  this.label = process.env.TESTOMATIO_LABEL;
54
-
55
- // Create a new instance of gaxios with a custom config
56
- this.client = new Gaxios({
60
+ // Create a new instance of axios with a custom config
61
+ this.axios = axios.create({
57
62
  baseURL: `${this.url.trim()}`,
58
63
  timeout: AXIOS_TIMEOUT,
59
- proxy: proxy ? proxy.toString() : undefined,
60
- retry: true,
61
- retryConfig: {
62
- retry: REPORTER_REQUEST_RETRIES.retriesPerRequest,
63
- retryDelay: REPORTER_REQUEST_RETRIES.retryTimeout,
64
- shouldRetry: (error) => {
65
- if (!error.response) return false;
66
- switch (error.response?.status) {
67
- case 400: // Bad request (probably wrong API key)
68
- case 404: // Test not matched
69
- case 429: // Rate limit exceeded
70
- case 500: // Internal server error
71
- return false;
72
- default:
73
- break;
64
+ proxy: proxy
65
+ ? {
66
+ host: proxy.hostname,
67
+ port: parseInt(proxy.port, 10),
68
+ protocol: proxy.protocol,
74
69
  }
75
- return error.response?.status >= 401; // Retry on 401+ and 5xx
70
+ : false,
71
+ });
72
+
73
+ // Pass the axios instance to the retry function
74
+ axiosRetry(this.axios, {
75
+ // do not use retries for unit tests
76
+ retries: REPORTER_REQUEST_RETRIES.retriesPerRequest, // Number of retries
77
+ shouldResetTimeout: true,
78
+ retryCondition: error => {
79
+ if (!error.response) return false;
80
+ switch (error.response?.status) {
81
+ case 400: // Bad request (probably wrong API key)
82
+ case 404: // Test not matched
83
+ case 429: // Rate limit exceeded
84
+ case 500: // Internal server error
85
+ return false;
86
+ default:
87
+ break;
76
88
  }
77
- }
89
+ return error.response?.status >= 401; // Retry on 401+ and 5xx
90
+ },
91
+ retryDelay: () => REPORTER_REQUEST_RETRIES.retryTimeout, // sum = 15sec
92
+ onRetry: async (retryCount, error) => {
93
+ this.retriesTimestamps.push(Date.now());
94
+
95
+ debug(`${error.message || `Request failed ${error.status}`}. Retry #${retryCount} ...`);
96
+ },
78
97
  });
79
98
 
80
99
  this.isEnabled = true;
@@ -115,15 +134,12 @@ class TestomatioPipe {
115
134
  return;
116
135
  }
117
136
 
118
- const resp = await this.client.request({
119
- method: 'GET',
120
- url: '/api/test_grep',
121
- params: q
122
- });
137
+ const resp = await this.axios.get('/api/test_grep', q);
138
+ const { data } = resp;
123
139
 
124
- if (Array.isArray(resp.data?.tests) && resp.data?.tests?.length > 0) {
125
- foundedTestLog(APP_PREFIX, resp.data.tests);
126
- return resp.data.tests;
140
+ if (Array.isArray(data?.tests) && data?.tests?.length > 0) {
141
+ foundedTestLog(APP_PREFIX, data.tests);
142
+ return data.tests;
127
143
  }
128
144
 
129
145
  console.log(APP_PREFIX, `⛔ No tests found for your --filter --> ${type}=${id}`);
@@ -147,6 +163,7 @@ class TestomatioPipe {
147
163
 
148
164
  // GitHub Actions Url
149
165
  if (!buildUrl && process.env.GITHUB_RUN_ID) {
166
+ // eslint-disable-next-line max-len
150
167
  buildUrl = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`;
151
168
  }
152
169
 
@@ -182,23 +199,16 @@ class TestomatioPipe {
182
199
  if (this.runId) {
183
200
  this.store.runId = this.runId;
184
201
  debug(`Run with id ${this.runId} already created, updating...`);
185
- const resp = await this.client.request({
186
- method: 'PUT',
187
- url: `/api/reporter/${this.runId}`,
188
- data: runParams
189
- });
202
+ const resp = await this.axios.put(`/api/reporter/${this.runId}`, runParams);
190
203
  if (resp.data.artifacts) setS3Credentials(resp.data.artifacts);
191
204
  return;
192
205
  }
193
206
 
194
207
  debug('Creating run...');
195
208
  try {
196
- const resp = await this.client.request({
197
- method: 'POST',
198
- url: '/api/reporter',
199
- data: runParams,
209
+ const resp = await this.axios.post(`/api/reporter`, runParams, {
200
210
  maxContentLength: Infinity,
201
- responseType: 'json'
211
+ maxBodyLength: Infinity,
202
212
  });
203
213
 
204
214
  this.runId = resp.data.uid;
@@ -215,7 +225,6 @@ class TestomatioPipe {
215
225
  debug('Run created', this.runId);
216
226
  } catch (err) {
217
227
  const errorText = err.response?.data?.message || err.message;
218
- debug('Error creating run', err);
219
228
  console.log(errorText || err);
220
229
  if (!this.apiKey) console.error('Testomat.io API key is not set');
221
230
  if (!this.apiKey?.startsWith('tstmt')) console.error('Testomat.io API key is invalid');
@@ -262,15 +271,7 @@ class TestomatioPipe {
262
271
 
263
272
  debug('Adding test', json);
264
273
 
265
- return this.client.request({
266
- method: 'POST',
267
- url: `/api/reporter/${this.runId}/testrun`,
268
- data: json,
269
- headers: {
270
- 'Content-Type': 'application/json',
271
- },
272
- maxContentLength: Infinity
273
- }).catch(err => {
274
+ return this.axios.post(`/api/reporter/${this.runId}/testrun`, json, axiosAddTestrunRequestConfig).catch(err => {
274
275
  this.requestFailures++;
275
276
  this.notReportedTestsCount++;
276
277
  if (err.response) {
@@ -322,43 +323,38 @@ class TestomatioPipe {
322
323
  const testsToSend = this.batch.tests.splice(0);
323
324
  debug('📨 Batch upload', testsToSend.length, 'tests');
324
325
 
325
- return this.client.request({
326
- method: 'POST',
327
- url: `/api/reporter/${this.runId}/testrun`,
328
- data: {
329
- api_key: this.apiKey,
330
- tests: testsToSend,
331
- batch_index: this.batch.batchIndex
332
- },
333
- headers: {
334
- 'Content-Type': 'application/json',
335
- },
336
- maxContentLength: Infinity
337
- }).catch(err => {
338
- this.requestFailures++;
339
- this.notReportedTestsCount += testsToSend.length;
340
- if (err.response) {
341
- if (err.response.status >= 400) {
342
- const responseData = err.response.data || { message: '' };
326
+ return this.axios
327
+ .post(
328
+ `/api/reporter/${this.runId}/testrun`,
329
+ { api_key: this.apiKey, tests: testsToSend, batch_index: this.batch.batchIndex },
330
+ axiosAddTestrunRequestConfig,
331
+ )
332
+ .catch(err => {
333
+ this.requestFailures++;
334
+ this.notReportedTestsCount += testsToSend.length;
335
+ if (err.response) {
336
+ if (err.response.status >= 400) {
337
+ const responseData = err.response.data || { message: '' };
338
+ console.log(
339
+ APP_PREFIX,
340
+ pc.yellow(`Warning: ${responseData.message} (${err.response.status})`),
341
+ // pc.grey(data?.title || ''),
342
+ );
343
+ if (err.response?.data?.message?.includes('could not be matched')) {
344
+ this.hasUnmatchedTests = true;
345
+ }
346
+ return;
347
+ }
343
348
  console.log(
344
349
  APP_PREFIX,
345
- pc.yellow(`Warning: ${responseData.message} (${err.response.status})`),
350
+ pc.yellow(`Warning: (${err.response?.status})`),
351
+ `Report couldn't be processed: ${err?.response?.data?.message}`,
346
352
  );
347
- if (err.response?.data?.message?.includes('could not be matched')) {
348
- this.hasUnmatchedTests = true;
349
- }
350
- return;
353
+ printCreateIssue(err);
354
+ } else {
355
+ console.log(APP_PREFIX, "Report couldn't be processed", err);
351
356
  }
352
- console.log(
353
- APP_PREFIX,
354
- pc.yellow(`Warning: (${err.response?.status})`),
355
- `Report couldn't be processed: ${err?.response?.data?.message}`,
356
- );
357
- printCreateIssue(err);
358
- } else {
359
- console.log(APP_PREFIX, "Report couldn't be processed", err);
360
- }
361
- });
357
+ });
362
358
  };
363
359
 
364
360
  /**
@@ -417,16 +413,12 @@ class TestomatioPipe {
417
413
 
418
414
  try {
419
415
  if (this.runId && !this.proceed) {
420
- await this.client.request({
421
- method: 'PUT',
422
- url: `/api/reporter/${this.runId}`,
423
- data: {
424
- api_key: this.apiKey,
425
- duration: params.duration,
426
- status_event,
427
- detach: params.detach,
428
- tests: params.tests,
429
- }
416
+ await this.axios.put(`/api/reporter/${this.runId}`, {
417
+ api_key: this.apiKey,
418
+ duration: params.duration,
419
+ status_event,
420
+ detach: params.detach,
421
+ tests: params.tests,
430
422
  });
431
423
  if (this.runUrl) {
432
424
  console.log(APP_PREFIX, '📊 Report Saved. Report URL:', pc.magenta(this.runUrl));
@@ -493,4 +485,13 @@ function printCreateIssue(err) {
493
485
  });
494
486
  }
495
487
 
488
+ const axiosAddTestrunRequestConfig = {
489
+ maxContentLength: Infinity,
490
+ maxBodyLength: Infinity,
491
+ headers: {
492
+ // Overwrite Axios's automatically set Content-Type
493
+ 'Content-Type': 'application/json',
494
+ },
495
+ };
496
+
496
497
  export default TestomatioPipe;
@@ -5,9 +5,13 @@ import fs from 'fs';
5
5
  import isValid from 'is-valid-path';
6
6
  import createDebugMessages from 'debug';
7
7
  import os from 'os';
8
+ import { fileURLToPath } from 'url';
8
9
 
9
10
  const debug = createDebugMessages('@testomatio/reporter:util');
10
11
 
12
+ // Use __dirname directly since we're compiling to CommonJS
13
+ const __dirname = path.resolve();
14
+
11
15
  /**
12
16
  * @param {String} testTitle - Test title
13
17
  *
@@ -107,7 +111,7 @@ const fetchSourceCodeFromStackTrace = (stack = '') => {
107
111
  .join('\n');
108
112
  };
109
113
 
110
- const TEST_ID_REGEX = /@T([\w\d]{8})/;
114
+ export const TEST_ID_REGEX = /@T([\w\d]{8})/;
111
115
 
112
116
  const fetchIdFromCode = (code, opts = {}) => {
113
117
  const comments = code
@@ -150,6 +154,9 @@ const fetchSourceCode = (contents, opts = {}) => {
150
154
  if (lineIndex === -1) lineIndex = lines.findIndex(l => l.includes(`@DisplayName("${title}`));
151
155
  if (lineIndex === -1) lineIndex = lines.findIndex(l => l.includes(`public void ${title}`));
152
156
  if (lineIndex === -1) lineIndex = lines.findIndex(l => l.includes(`${title}(`));
157
+ } else if (opts.lang === 'csharp') {
158
+ if (lineIndex === -1) lineIndex = lines.findIndex(l => l.includes(`public void ${title}`));
159
+ if (lineIndex === -1) lineIndex = lines.findIndex(l => l.includes(`${title}(`));
153
160
  } else {
154
161
  lineIndex = lines.findIndex(l => l.includes(title));
155
162
  }
@@ -353,6 +360,12 @@ function formatStep(step, shift = 0) {
353
360
  return lines;
354
361
  }
355
362
 
363
+ export function getPackageVersion() {
364
+ const packageJsonPath = path.resolve(__dirname, '../../package.json');
365
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
366
+ return packageJson.version;
367
+ }
368
+
356
369
  export {
357
370
  ansiRegExp,
358
371
  isSameTest,
package/src/xmlReader.js CHANGED
@@ -13,6 +13,7 @@ import {
13
13
  fetchSourceCodeFromStackTrace,
14
14
  fetchIdFromCode,
15
15
  humanize,
16
+ TEST_ID_REGEX,
16
17
  } from './utils/utils.js';
17
18
  import { pipesFactory } from './pipe/index.js';
18
19
  import adapterFactory from './junit-adapter/index.js';
@@ -26,8 +27,9 @@ const debug = createDebugMessages('@testomatio/reporter:xml');
26
27
  const ridRunId = randomUUID();
27
28
 
28
29
  const TESTOMATIO_URL = process.env.TESTOMATIO_URL || 'https://app.testomat.io';
29
- const { TESTOMATIO_RUNGROUP_TITLE, TESTOMATIO_TITLE, TESTOMATIO_ENV, TESTOMATIO_RUN, TESTOMATIO_MARK_DETACHED } =
30
- process.env;
30
+ const { TESTOMATIO_RUNGROUP_TITLE, TESTOMATIO_SUITE,
31
+ TESTOMATIO_MAX_STACK_TRACE, TESTOMATIO_TITLE, TESTOMATIO_ENV,
32
+ TESTOMATIO_RUN, TESTOMATIO_MARK_DETACHED } = process.env;
31
33
 
32
34
  const options = {
33
35
  ignoreDeclaration: true,
@@ -37,6 +39,8 @@ const options = {
37
39
  parseTagValue: true,
38
40
  };
39
41
 
42
+ const MAX_OUTPUT_LENGTH = parseInt(TESTOMATIO_MAX_STACK_TRACE, 10) || 10000;
43
+
40
44
  const reduceOptions = {};
41
45
 
42
46
  class XmlReader {
@@ -91,7 +95,7 @@ class XmlReader {
91
95
  ];
92
96
 
93
97
  for (const regex of cutRegexes) {
94
- xmlData = xmlData.replace(regex, (_, p1, p2, p3) => `${p1}${p2.substring(0, 5000)}${p3}`);
98
+ xmlData = xmlData.replace(regex, (_, p1, p2, p3) => `${p1}${p2.substring(0, MAX_OUTPUT_LENGTH)}${p3}`);
95
99
  }
96
100
 
97
101
  const jsonResult = this.parser.parse(xmlData);
@@ -341,6 +345,7 @@ class XmlReader {
341
345
  if (file.endsWith('.rb')) this.stats.language = 'ruby';
342
346
  if (file.endsWith('.js')) this.stats.language = 'js';
343
347
  if (file.endsWith('.ts')) this.stats.language = 'ts';
348
+ if (file.endsWith('.cs')) this.stats.language = 'csharp';
344
349
  }
345
350
 
346
351
  if (!fs.existsSync(file)) {
@@ -394,13 +399,14 @@ class XmlReader {
394
399
  async uploadArtifacts() {
395
400
  for (const test of this.tests.filter(t => !!t.stack)) {
396
401
  let files = [];
397
- if (test.files?.length) files = test.files.map(f => path.join(process.cwd(), f));
398
- files = [...files, ...fetchFilesFromStackTrace(test.stack)];
402
+ if (!test.files?.length) continue;
403
+
404
+ files = test.files.map(f => path.isAbsolute(f) ? f : path.join(process.cwd(), f));
399
405
 
400
406
  if (!files.length) continue;
401
407
 
402
408
  const runId = this.runId || this.store.runId || Date.now().toString();
403
- test.artifacts = await Promise.all(files.map(f => this.uploader.uploadFileByPath(f, [runId])));
409
+ test.artifacts = await Promise.all(files.map(f => this.uploader.uploadFileByPath(f, [runId, path.basename(f)])));
404
410
  console.log(APP_PREFIX, `🗄️ Uploaded ${pc.bold(`${files.length} artifacts`)} for test ${test.title}`);
405
411
  }
406
412
  }
@@ -471,7 +477,7 @@ function reduceTestCases(prev, item) {
471
477
  testCases
472
478
  .filter(t => !!t)
473
479
  .forEach(testCaseItem => {
474
- const file = testCaseItem.file || item.filepath || '';
480
+ const file = testCaseItem.file || item.filepath || item.fullname || '';
475
481
 
476
482
  let stack = '';
477
483
  let message = '';
@@ -505,15 +511,38 @@ function reduceTestCases(prev, item) {
505
511
  stack = `${
506
512
  testCaseItem['system-out'] || testCaseItem.output || testCaseItem.log || ''
507
513
  }\n\n${stack}\n\n${suiteOutput}\n\n${suiteErr}`.trim();
508
- const testId = fetchIdFromOutput(stack);
514
+ let testId = fetchIdFromOutput(stack);
515
+
516
+ if (tags?.length && !testId) {
517
+ testId = tags.filter(t => t.startsWith('T')).map(t => `@${t}`).find(t => t.match(TEST_ID_REGEX))?.slice(2);
518
+ }
509
519
 
510
520
  let status = STATUS.PASSED.toString();
511
521
  if ('failure' in testCaseItem || 'error' in testCaseItem) status = STATUS.FAILED;
512
522
  if ('skipped' in testCaseItem) status = STATUS.SKIPPED;
523
+ if (testCaseItem.result && Object.values(STATUS).includes(testCaseItem.result.toLowerCase())) {
524
+ status = testCaseItem.result.toLowerCase();
525
+ }
513
526
 
514
527
  let rid = null;
515
528
  if (testCaseItem.id) rid = `${ridRunId}-${testCaseItem.id}`;
516
529
 
530
+ // Extract attachments
531
+ let files = [];
532
+ if (testCaseItem.attachments) {
533
+ const attachments = Array.isArray(testCaseItem.attachments.attachment)
534
+ ? testCaseItem.attachments.attachment
535
+ : [testCaseItem.attachments.attachment];
536
+
537
+ files = attachments
538
+ .filter(a => a && a.filePath)
539
+ .map(a => a.filePath);
540
+ }
541
+
542
+ // Extract files from stack trace using existing utility
543
+ const stackFiles = fetchFilesFromStackTrace(stack);
544
+ files = [...new Set([...files, ...stackFiles])]; // Remove duplicates
545
+
517
546
  prev.push({
518
547
  rid,
519
548
  file,
@@ -528,7 +557,9 @@ function reduceTestCases(prev, item) {
528
557
  run_time: parseFloat(testCaseItem.time || testCaseItem.duration) * 1000,
529
558
  status,
530
559
  title,
560
+ root_suite_id: TESTOMATIO_SUITE,
531
561
  suite_title: suiteTitle,
562
+ files,
532
563
  });
533
564
  });
534
565
  return prev;
@@ -555,10 +586,15 @@ function fetchProperties(item) {
555
586
 
556
587
  if (!item.properties) return {};
557
588
 
558
- const prop = [item.properties?.property].flat().find(p => p.name === 'Description');
589
+ // Handle both single property and array of properties
590
+ const properties = Array.isArray(item.properties.property)
591
+ ? item.properties.property
592
+ : [item.properties.property].filter(Boolean);
593
+
594
+ const prop = properties.find(p => p.name === 'Description');
559
595
  if (prop) title = prop.value;
560
- [item.properties?.property]
561
- .flat()
596
+
597
+ properties
562
598
  .filter(p => p.name === 'Category')
563
599
  .forEach(p => tags.push(p.value));
564
600
  return { title, tags };