@testomatio/reporter 1.3.0-ignore-stack-for-passed.1 → 1.3.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.
@@ -44,10 +44,15 @@ program
44
44
 
45
45
  let timeoutTimer;
46
46
  if (opts.timelimit) {
47
- timeoutTimer = setTimeout(() => {
48
- console.log(`⚠️ Reached timeout of ${opts.timelimit}s. Exiting... (Exit code is 0 to not fail the pipeline)`);
49
- process.exit(0);
50
- }, parseInt(opts.timelimit, 10) * 1000);
47
+ timeoutTimer = setTimeout(
48
+ () => {
49
+ console.log(
50
+ `⚠️ Reached timeout of ${opts.timelimit}s. Exiting... (Exit code is 0 to not fail the pipeline)`,
51
+ );
52
+ process.exit(0);
53
+ },
54
+ parseInt(opts.timelimit, 10) * 1000,
55
+ );
51
56
  }
52
57
 
53
58
  try {
package/lib/constants.js CHANGED
@@ -4,7 +4,6 @@ const path = require('path');
4
4
 
5
5
  const APP_PREFIX = chalk.gray('[TESTOMATIO]');
6
6
  const AXIOS_TIMEOUT = 20 * 1000; // sum = 20sec
7
- const AXIOS_RETRY_TIMEOUT = 5 * 1000; // sum = 5sec
8
7
 
9
8
  const TESTOMAT_TMP_STORAGE_DIR = path.join(os.tmpdir(), 'testomatio_tmp');
10
9
 
@@ -31,6 +30,19 @@ const HTML_REPORT = {
31
30
 
32
31
  const testomatLogoURL = 'https://avatars.githubusercontent.com/u/59105116?s=36&v=4';
33
32
 
33
+ const REPORTER_REQUEST_RETRIES = {
34
+ retryTimeout: 5 * 1000, // sum = 5sec
35
+ retriesPerRequest: 2,
36
+ maxTotalRetries: Number(process.env.TESTOMATIO_MAX_REQUEST_FAILURES_COUNT) || 10,
37
+ withinTimeSeconds: Number(process.env.TESTOMATIO_MAX_REQUEST_RETRIES_WITHIN_TIME_SECONDS) || 60,
38
+ };
39
+
40
+ const RunStatus = {
41
+ Passed: 'passed',
42
+ Failed: 'failed',
43
+ Finished: 'finished',
44
+ };
45
+
34
46
  module.exports = {
35
47
  APP_PREFIX,
36
48
  TESTOMAT_TMP_STORAGE_DIR,
@@ -38,6 +50,7 @@ module.exports = {
38
50
  STATUS,
39
51
  HTML_REPORT,
40
52
  AXIOS_TIMEOUT,
41
- AXIOS_RETRY_TIMEOUT,
42
53
  testomatLogoURL,
54
+ REPORTER_REQUEST_RETRIES,
55
+ RunStatus,
43
56
  };
@@ -20,6 +20,7 @@ const keys = [
20
20
  'S3_BUCKET',
21
21
  'S3_ACCESS_KEY_ID',
22
22
  'S3_SECRET_ACCESS_KEY',
23
+ 'S3_SESSION_TOKEN',
23
24
  'TESTOMATIO_DISABLE_ARTIFACTS',
24
25
  'TESTOMATIO_PRIVATE_ARTIFACTS',
25
26
  'S3_FORCE_PATH_STYLE',
@@ -74,7 +75,8 @@ const _getFileExtBase64 = str => {
74
75
  };
75
76
 
76
77
  const _getS3Config = () => {
77
- const { S3_REGION, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, S3_FORCE_PATH_STYLE, S3_ENDPOINT } = getConfig();
78
+ const { S3_REGION, S3_SESSION_TOKEN, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, S3_FORCE_PATH_STYLE, S3_ENDPOINT } =
79
+ getConfig();
78
80
 
79
81
  const cfg = {
80
82
  region: S3_REGION,
@@ -85,6 +87,10 @@ const _getS3Config = () => {
85
87
  },
86
88
  };
87
89
 
90
+ if (S3_SESSION_TOKEN) {
91
+ cfg.credentials.sessionToken = S3_SESSION_TOKEN;
92
+ }
93
+
88
94
  if (S3_ENDPOINT) {
89
95
  cfg.endpoint = S3_ENDPOINT;
90
96
  }
@@ -79,13 +79,13 @@ class GitLabPipe {
79
79
  let summary = `${this.hiddenCommentData}
80
80
 
81
81
  | [![Testomat.io Report](${testomatLogoURL})](https://testomat.io) | ${statusEmoji(
82
- runParams.status,
83
- )} ${runParams.status.toUpperCase()} ${statusEmoji(runParams.status)} |
82
+ runParams.status,
83
+ )} ${runParams.status.toUpperCase()} ${statusEmoji(runParams.status)} |
84
84
  | --- | --- |
85
85
  | Tests | ✔️ **${this.tests.length}** tests run |
86
86
  | Summary | ${statusEmoji('failed')} **${failedCount}** failed; ${statusEmoji(
87
- 'passed',
88
- )} **${passedCount}** passed; **${statusEmoji('skipped')}** ${skippedCount} skipped |
87
+ 'passed',
88
+ )} **${passedCount}** passed; **${statusEmoji('skipped')}** ${skippedCount} skipped |
89
89
  | Duration | 🕐 **${humanizeDuration(
90
90
  parseInt(
91
91
  this.tests.reduce((a, t) => a + (t.run_time || 0), 0),
package/lib/pipe/html.js CHANGED
@@ -91,7 +91,7 @@ class HtmlPipe {
91
91
  // GENERATE HTML reports based on the results data
92
92
  this.buildReport({
93
93
  runParams,
94
- // TODO: this.tests=[] in case of Mocha
94
+ // TODO: this.tests=[] in case of Mocha, need retest by Vitalii
95
95
  tests: this.tests,
96
96
  outputPath: this.htmlOutputPath,
97
97
  templatePath: this.templateHtmlPath,
@@ -123,30 +123,31 @@ class HtmlPipe {
123
123
  }
124
124
 
125
125
  tests.forEach(test => {
126
- if (!test.message || test.message.trim() === '') {
126
+ if (!test.message?.trim()) {
127
127
  test.message = "This test has no 'message' code";
128
128
  }
129
129
 
130
- if (!test.suite_title || test.suite_title.trim() === '') {
130
+ if (!test.suite_title?.trim()) {
131
131
  test.suite_title = 'Unknown suite';
132
132
  }
133
133
 
134
- if (!test.title || test.title.trim() === '') {
134
+ if (!test.title?.trim()) {
135
135
  test.title = 'Unknown test title';
136
136
  }
137
137
 
138
- if (!test.files || test.files.length === 0) {
138
+ if (!test.files?.length) {
139
139
  test.files = 'This test has no files';
140
140
  }
141
141
 
142
- if (test.steps) {
143
- if (!test.steps || test.steps.trim() === '') {
144
- test.steps = "This test has no 'steps' code";
145
- } else {
146
- test.steps = removeAnsiColorCodes(test.steps);
147
- }
142
+ if (!test.steps?.trim()) {
143
+ test.steps = "This test has no 'steps' code";
144
+ } else {
145
+ test.steps = removeAnsiColorCodes(test.steps);
148
146
  }
149
147
 
148
+ // TODO: future-proof: currently there is no need to display Artifacts and Metadata in HTML
149
+ test.artifacts = test.artifacts || [];
150
+ test.meta = test.meta || {};
150
151
  // TODO: u can added an additional test values to this checks in the future
151
152
  });
152
153
 
@@ -199,7 +200,8 @@ class HtmlPipe {
199
200
 
200
201
  return template(data);
201
202
  } catch (e) {
202
- console.log('Unknown HTML report generation error: ', e);
203
+ console.log(chalk.red(' Oops! An unknown error occurred when generating an HTML report'));
204
+ console.log(chalk.red(e));
203
205
  }
204
206
  }
205
207
 
@@ -209,42 +211,77 @@ class HtmlPipe {
209
211
  (tests, status) => tests.filter(test => test.status.toLowerCase() === status.toLowerCase()).length,
210
212
  );
211
213
 
212
- handlebars.registerHelper('json', tests => {
213
- // TODO: please refactor the code below, add meaningful variable names, separate into functions
214
- function replaceScriptTagsInArray(array) {
215
- return array.map(obj => {
216
- const keysToCheck = ['steps', 'stack', 'title', 'suite_title', 'message', 'code'];
217
- const newObj = {};
218
-
219
- for (const key in obj) {
220
- if (Object.prototype.hasOwnProperty.call(obj, key)) {
221
- if (key === 'example') {
222
- newObj[key] = {};
223
- for (const subKey in obj[key]) {
224
- if (Object.prototype.hasOwnProperty.call(obj[key], subKey)) {
225
- newObj[key][subKey] =
226
- typeof obj[key][subKey] === 'string'
227
- ? obj[key][subKey].replace(/<script>/g, '<$cript>').replace(/<\/script>/g, '</$cript>')
228
- : obj[key][subKey];
229
- }
230
- }
231
- } else if (keysToCheck.includes(key)) {
232
- newObj[key] =
233
- typeof obj[key] === 'string'
234
- ? obj[key].replace(/<script>/g, '<$cript>').replace(/<\/script>/g, '</$cript>')
235
- : obj[key];
236
- } else {
237
- newObj[key] = obj[key];
238
- }
214
+ handlebars.registerHelper(
215
+ 'selectComponent',
216
+ () =>
217
+ new handlebars.SafeString(
218
+ `<select style="width: 70px;height: 38px;" class="form-select" aria-label="Tests counter on page">
219
+ <option value="0">10</option>
220
+ <option value="1">25</option>
221
+ <option value="2">50</option>
222
+ </select>`,
223
+ ),
224
+ );
225
+
226
+ handlebars.registerHelper(
227
+ 'emptyDataComponent',
228
+ () =>
229
+ new handlebars.SafeString(
230
+ `<div class="noData m-0">
231
+ NO MATCHING TESTS
232
+ </div>`,
233
+ ),
234
+ );
235
+
236
+ handlebars.registerHelper('pageDispleyElements', tests => {
237
+ // We wrapp the lines to the HTML format we need
238
+ const totalTests = JSON.parse(
239
+ JSON.stringify(tests)
240
+ .replace(/<script>/g, '&lt;script&gt;')
241
+ .replace(/<\/script>/g, '&lt;/script&gt;'), // eslint-disable-line
242
+ );
243
+
244
+ const paginationOptions = {
245
+ 0: 10,
246
+ 1: 25,
247
+ 2: 50,
248
+ };
249
+ const statuses = ['all', 'passed', 'failed', 'skipped'];
250
+ const pageItemGroups = {
251
+ all: {},
252
+ passed: {},
253
+ failed: {},
254
+ skipped: {},
255
+ totalTests,
256
+ };
257
+
258
+ function paginateItems(items, pageSize) {
259
+ const paginatedItems = [];
260
+ for (let i = 0; i < items.length; i += pageSize) {
261
+ paginatedItems.push(items.slice(i, i + pageSize));
262
+ }
263
+ return paginatedItems;
264
+ }
265
+
266
+ statuses.forEach(status => {
267
+ for (const option in paginationOptions) {
268
+ // eslint-disable-next-line no-prototype-builtins
269
+ if (paginationOptions.hasOwnProperty(option)) {
270
+ const pageSize = paginationOptions[option];
271
+ let filteredItems = totalTests;
272
+
273
+ if (status !== 'all') {
274
+ filteredItems = totalTests.filter(item => item.status === status);
239
275
  }
276
+
277
+ pageItemGroups[status][option] = paginateItems(filteredItems, pageSize);
240
278
  }
279
+ }
280
+ });
241
281
 
242
- return newObj;
243
- });
244
- }
282
+ pageItemGroups.totalTests = totalTests;
245
283
 
246
- // Remove ANSI escape codes
247
- return JSON.stringify(replaceScriptTagsInArray(tests));
284
+ return JSON.stringify(pageItemGroups);
248
285
  });
249
286
  }
250
287
 
@@ -261,7 +298,7 @@ class HtmlPipe {
261
298
  */
262
299
  function testExecutionSumTime(tests) {
263
300
  const totalMilliseconds = tests.reduce((sum, test) => {
264
- if (typeof test.run_time === 'number') {
301
+ if (typeof test.run_time === 'number' && !Number.isNaN(test.run_time)) {
265
302
  return sum + test.run_time;
266
303
  }
267
304
  return sum;
@@ -6,7 +6,7 @@ const axiosRetry = require('axios-retry');
6
6
  const axios = require('axios');
7
7
  const JsonCycle = require('json-cycle');
8
8
 
9
- const { APP_PREFIX, STATUS, AXIOS_TIMEOUT, AXIOS_RETRY_TIMEOUT } = require('../constants');
9
+ const { APP_PREFIX, STATUS, AXIOS_TIMEOUT, REPORTER_REQUEST_RETRIES } = require('../constants');
10
10
  const { isValidUrl, foundedTestLog } = require('../utils/utils');
11
11
  const { parseFilterParams, generateFilterRequestParams, setS3Credentials } = require('../utils/pipe_utils');
12
12
  const config = require('../config');
@@ -23,6 +23,10 @@ if (process.env.TESTOMATIO_RUN) {
23
23
  */
24
24
  class TestomatioPipe {
25
25
  constructor(params, store) {
26
+ this.retriesTimestamps = [];
27
+ this.reportingCanceledDueToReqFailures = false;
28
+ this.notReportedTestsCount = 0;
29
+
26
30
  this.isEnabled = false;
27
31
  this.url = params.testomatioUrl || process.env.TESTOMATIO_URL || 'https://app.testomat.io';
28
32
  this.apiKey = params.apiKey || config.TESTOMATIO;
@@ -43,25 +47,30 @@ class TestomatioPipe {
43
47
  baseURL: `${this.url.trim()}`,
44
48
  timeout: AXIOS_TIMEOUT,
45
49
  });
50
+
46
51
  // Pass the axios instance to the retry function
47
52
  axiosRetry(this.axios, {
48
- retries: 3, // Number of retries (Defaults to 3)
53
+ // do not use retries for unit tests
54
+ retries: REPORTER_REQUEST_RETRIES.retriesPerRequest, // Number of retries
49
55
  shouldResetTimeout: true,
50
56
  retryCondition: error => {
51
- // Conditional check the error status code
52
- switch (error.response.status) {
53
- case 409:
54
- case 429:
55
- case 502:
56
- case 503:
57
- return true; // Retry request with response status code 409, 429, 502, 503
57
+ if (!error.response) return false;
58
+ switch (error.response?.status) {
59
+ case 400: // Bad request (probably wrong API key)
60
+ case 404: // Test not matched
61
+ case 429: // Rate limit exceeded
62
+ case 500: // Internal server error
63
+ return false;
58
64
  default:
59
- return false; // Do not retry the others
65
+ break;
60
66
  }
67
+ return error.response?.status >= 401; // Retry on 401+ and 5xx
61
68
  },
62
- retryDelay: retryCount => retryCount * AXIOS_RETRY_TIMEOUT, // sum = 15sec
63
- onRetry: retryCount => {
64
- debug(`Retry attempt #${retryCount} failed. Retrying again...`);
69
+ retryDelay: () => REPORTER_REQUEST_RETRIES.retryTimeout, // sum = 15sec
70
+ onRetry: async (retryCount, error) => {
71
+ this.retriesTimestamps.push(Date.now());
72
+
73
+ debug(`${error.message || `Request failed ${error.status}`}. Retry #${retryCount} ...`);
65
74
  },
66
75
  });
67
76
 
@@ -190,13 +199,45 @@ class TestomatioPipe {
190
199
  'Error creating Testomat.io report, please check if your API key is valid. Skipping report | ',
191
200
  err?.response?.statusText || err?.status || err.message,
192
201
  );
202
+ printCreateIssue(err);
193
203
  }
194
204
  debug('"createRun" function finished');
195
205
  }
196
206
 
207
+ /**
208
+ * Decides whether to skip test reporting in case of too many request failures
209
+ * @param {TestData} testData
210
+ * @returns {boolean}
211
+ */
212
+ #cancelTestReportingInCaseOfTooManyReqFailures(testData) {
213
+ if (this.reportingCanceledDueToReqFailures) return true;
214
+
215
+ const retriesCountWithinTime = this.retriesTimestamps.filter(
216
+ timestamp => Date.now() - timestamp < REPORTER_REQUEST_RETRIES.withinTimeSeconds * 1000,
217
+ ).length;
218
+ debug(`${retriesCountWithinTime} failed requests within ${REPORTER_REQUEST_RETRIES.withinTimeSeconds}s`);
219
+
220
+ if (retriesCountWithinTime > REPORTER_REQUEST_RETRIES.maxTotalRetries) {
221
+ const errorMessage = chalk.yellow(
222
+ `${retriesCountWithinTime} requests were failed within ${REPORTER_REQUEST_RETRIES.withinTimeSeconds}s,\
223
+ reporting for test "${testData.title}" to Testomat is skipped`,
224
+ );
225
+ console.warn(`${APP_PREFIX} ${errorMessage}`);
226
+
227
+ this.reportingCanceledDueToReqFailures = true;
228
+ this.notReportedTestsCount++;
229
+
230
+ return true;
231
+ }
232
+
233
+ return false;
234
+ }
235
+
197
236
  addTest(data) {
198
237
  if (!this.isEnabled) return;
199
238
  if (!this.runId) return;
239
+ if (this.#cancelTestReportingInCaseOfTooManyReqFailures(data)) return;
240
+
200
241
  data.api_key = this.apiKey;
201
242
  data.create = this.createNewTests;
202
243
 
@@ -227,7 +268,7 @@ class TestomatioPipe {
227
268
  chalk.yellow(`Warning: ${responseData.message} (${err.response.status})`),
228
269
  chalk.grey(data?.title || ''),
229
270
  );
230
- if (err.response.data.message.includes('could not be matched')) {
271
+ if (err.response?.data?.message?.includes('could not be matched')) {
231
272
  this.hasUnmatchedTests = true;
232
273
  }
233
274
  return;
@@ -237,6 +278,7 @@ class TestomatioPipe {
237
278
  chalk.yellow(`Warning: ${data?.title || ''} (${err.response?.status})`),
238
279
  `Report couldn't be processed: ${err?.response?.data?.message}`,
239
280
  );
281
+ printCreateIssue(err);
240
282
  } else {
241
283
  console.log(APP_PREFIX, chalk.blue(data?.title || ''), "Report couldn't be processed", err);
242
284
  }
@@ -248,8 +290,15 @@ class TestomatioPipe {
248
290
  * @returns
249
291
  */
250
292
  async finishRun(params) {
251
- debug('Finishing run...');
252
293
  if (!this.isEnabled) return;
294
+ debug('Finishing run...');
295
+
296
+ if (this.reportingCanceledDueToReqFailures) {
297
+ const errorMessage = chalk.red(
298
+ `⚠️ Due to request failures, ${this.notReportedTestsCount} test(s) were not reported to Testomat.io`,
299
+ );
300
+ console.warn(`${APP_PREFIX} ${errorMessage}`);
301
+ }
253
302
 
254
303
  const { status, parallel } = params;
255
304
 
@@ -306,6 +355,7 @@ class TestomatioPipe {
306
355
  }
307
356
  } catch (err) {
308
357
  console.log(APP_PREFIX, 'Error updating status, skipping...', err);
358
+ printCreateIssue(err);
309
359
  }
310
360
  debug('Run finished');
311
361
  }
@@ -315,4 +365,27 @@ class TestomatioPipe {
315
365
  }
316
366
  }
317
367
 
368
+ let registeredErrorHints = false;
369
+ function printCreateIssue(err) {
370
+ if (registeredErrorHints) return;
371
+ registeredErrorHints = true;
372
+ process.on('exit', () => {
373
+ console.log();
374
+ console.log(APP_PREFIX, 'There was an error reporting to Testomat.io:');
375
+ console.log(
376
+ APP_PREFIX,
377
+ 'If you think this is a bug please create an issue: https://github.com/testomatio/reporter/issues/new',
378
+ ); // eslint-disable-line max-len
379
+ console.log(APP_PREFIX, 'Provide this information:');
380
+ console.log('Error:', err.message || err.code);
381
+ if (!err.config) return;
382
+
383
+ const time = new Date().toUTCString();
384
+ const { data, url, baseURL, method } = err?.config || {};
385
+ console.log('```js');
386
+ console.log({ data: data?.replace(/"(tstmt_[^"]+)"/g, 'tstmt_*'), url, baseURL, method, time });
387
+ console.log('```');
388
+ });
389
+ }
390
+
318
391
  module.exports = TestomatioPipe;