@testomatio/reporter 2.0.0-beta.4-gaxios → 2.0.0

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,31 +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
- httpMethodsToRetry: ['GET','PUT','HEAD','OPTIONS','DELETE','POST'],
65
- shouldRetry: (error) => {
66
- if (!error.response) return false;
67
- switch (error.response?.status) {
68
- case 400: // Bad request (probably wrong API key)
69
- case 404: // Test not matched
70
- case 429: // Rate limit exceeded
71
- case 500: // Internal server error
72
- return false;
73
- default:
74
- break;
64
+ proxy: proxy
65
+ ? {
66
+ host: proxy.hostname,
67
+ port: parseInt(proxy.port, 10),
68
+ protocol: proxy.protocol,
75
69
  }
76
- 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;
77
88
  }
78
- }
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
+ },
79
97
  });
80
98
 
81
99
  this.isEnabled = true;
@@ -116,15 +134,12 @@ class TestomatioPipe {
116
134
  return;
117
135
  }
118
136
 
119
- const resp = await this.client.request({
120
- method: 'GET',
121
- url: '/api/test_grep',
122
- params: q
123
- });
137
+ const resp = await this.axios.get('/api/test_grep', q);
138
+ const { data } = resp;
124
139
 
125
- if (Array.isArray(resp.data?.tests) && resp.data?.tests?.length > 0) {
126
- foundedTestLog(APP_PREFIX, resp.data.tests);
127
- return resp.data.tests;
140
+ if (Array.isArray(data?.tests) && data?.tests?.length > 0) {
141
+ foundedTestLog(APP_PREFIX, data.tests);
142
+ return data.tests;
128
143
  }
129
144
 
130
145
  console.log(APP_PREFIX, `⛔ No tests found for your --filter --> ${type}=${id}`);
@@ -148,6 +163,7 @@ class TestomatioPipe {
148
163
 
149
164
  // GitHub Actions Url
150
165
  if (!buildUrl && process.env.GITHUB_RUN_ID) {
166
+ // eslint-disable-next-line max-len
151
167
  buildUrl = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`;
152
168
  }
153
169
 
@@ -183,23 +199,16 @@ class TestomatioPipe {
183
199
  if (this.runId) {
184
200
  this.store.runId = this.runId;
185
201
  debug(`Run with id ${this.runId} already created, updating...`);
186
- const resp = await this.client.request({
187
- method: 'PUT',
188
- url: `/api/reporter/${this.runId}`,
189
- data: runParams
190
- });
202
+ const resp = await this.axios.put(`/api/reporter/${this.runId}`, runParams);
191
203
  if (resp.data.artifacts) setS3Credentials(resp.data.artifacts);
192
204
  return;
193
205
  }
194
206
 
195
207
  debug('Creating run...');
196
208
  try {
197
- const resp = await this.client.request({
198
- method: 'POST',
199
- url: '/api/reporter',
200
- data: runParams,
209
+ const resp = await this.axios.post(`/api/reporter`, runParams, {
201
210
  maxContentLength: Infinity,
202
- responseType: 'json'
211
+ maxBodyLength: Infinity,
203
212
  });
204
213
 
205
214
  this.runId = resp.data.uid;
@@ -216,7 +225,6 @@ class TestomatioPipe {
216
225
  debug('Run created', this.runId);
217
226
  } catch (err) {
218
227
  const errorText = err.response?.data?.message || err.message;
219
- debug('Error creating run', err);
220
228
  console.log(errorText || err);
221
229
  if (!this.apiKey) console.error('Testomat.io API key is not set');
222
230
  if (!this.apiKey?.startsWith('tstmt')) console.error('Testomat.io API key is invalid');
@@ -263,15 +271,7 @@ class TestomatioPipe {
263
271
 
264
272
  debug('Adding test', json);
265
273
 
266
- return this.client.request({
267
- method: 'POST',
268
- url: `/api/reporter/${this.runId}/testrun`,
269
- data: json,
270
- headers: {
271
- 'Content-Type': 'application/json',
272
- },
273
- maxContentLength: Infinity
274
- }).catch(err => {
274
+ return this.axios.post(`/api/reporter/${this.runId}/testrun`, json, axiosAddTestrunRequestConfig).catch(err => {
275
275
  this.requestFailures++;
276
276
  this.notReportedTestsCount++;
277
277
  if (err.response) {
@@ -323,43 +323,38 @@ class TestomatioPipe {
323
323
  const testsToSend = this.batch.tests.splice(0);
324
324
  debug('📨 Batch upload', testsToSend.length, 'tests');
325
325
 
326
- return this.client.request({
327
- method: 'POST',
328
- url: `/api/reporter/${this.runId}/testrun`,
329
- data: {
330
- api_key: this.apiKey,
331
- tests: testsToSend,
332
- batch_index: this.batch.batchIndex
333
- },
334
- headers: {
335
- 'Content-Type': 'application/json',
336
- },
337
- maxContentLength: Infinity
338
- }).catch(err => {
339
- this.requestFailures++;
340
- this.notReportedTestsCount += testsToSend.length;
341
- if (err.response) {
342
- if (err.response.status >= 400) {
343
- 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
+ }
344
348
  console.log(
345
349
  APP_PREFIX,
346
- 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}`,
347
352
  );
348
- if (err.response?.data?.message?.includes('could not be matched')) {
349
- this.hasUnmatchedTests = true;
350
- }
351
- return;
353
+ printCreateIssue(err);
354
+ } else {
355
+ console.log(APP_PREFIX, "Report couldn't be processed", err);
352
356
  }
353
- console.log(
354
- APP_PREFIX,
355
- pc.yellow(`Warning: (${err.response?.status})`),
356
- `Report couldn't be processed: ${err?.response?.data?.message}`,
357
- );
358
- printCreateIssue(err);
359
- } else {
360
- console.log(APP_PREFIX, "Report couldn't be processed", err);
361
- }
362
- });
357
+ });
363
358
  };
364
359
 
365
360
  /**
@@ -418,16 +413,12 @@ class TestomatioPipe {
418
413
 
419
414
  try {
420
415
  if (this.runId && !this.proceed) {
421
- await this.client.request({
422
- method: 'PUT',
423
- url: `/api/reporter/${this.runId}`,
424
- data: {
425
- api_key: this.apiKey,
426
- duration: params.duration,
427
- status_event,
428
- detach: params.detach,
429
- tests: params.tests,
430
- }
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,
431
422
  });
432
423
  if (this.runUrl) {
433
424
  console.log(APP_PREFIX, '📊 Report Saved. Report URL:', pc.magenta(this.runUrl));
@@ -487,11 +478,20 @@ function printCreateIssue(err) {
487
478
  if (!err.config) return;
488
479
 
489
480
  const time = new Date().toUTCString();
490
- const { body, url, baseURL, method } = err?.config || {};
481
+ const { data, url, baseURL, method } = err?.config || {};
491
482
  console.log('```js');
492
- console.log({ body: body?.replace(/"(tstmt_[^"]+)"/g, 'tstmt_*'), url, baseURL, method, time });
483
+ console.log({ data: data?.replace(/"(tstmt_[^"]+)"/g, 'tstmt_*'), url, baseURL, method, time });
493
484
  console.log('```');
494
485
  });
495
486
  }
496
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
+
497
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 };