@testomatio/reporter 1.6.0-beta-6-artifacts → 1.6.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.
@@ -24,7 +24,6 @@ function MochaReporter(runner, opts) {
24
24
  let passes = 0;
25
25
  let failures = 0;
26
26
  let skipped = 0;
27
- // let artifactStore;
28
27
 
29
28
  const apiKey = opts?.reporterOptions?.apiKey || config.TESTOMATIO;
30
29
 
@@ -33,6 +32,7 @@ function MochaReporter(runner, opts) {
33
32
  runner.on(EVENT_RUN_BEGIN, () => {
34
33
  client.createRun();
35
34
 
35
+ // clear dir with artifacts/logs
36
36
  fileSystem.clearDir(TESTOMAT_TMP_STORAGE_DIR);
37
37
  });
38
38
 
@@ -66,7 +66,7 @@ function MochaReporter(runner, opts) {
66
66
  test_id: testId,
67
67
  suite_title: getSuiteTitle(test),
68
68
  title: getTestName(test),
69
- code: test.body.toString(),
69
+ code: process.env.TESTOMATIO_UPDATE_CODE ? test.body.toString() : '',
70
70
  file: getFile(test),
71
71
  time: test.duration,
72
72
  logs,
@@ -82,7 +82,7 @@ function MochaReporter(runner, opts) {
82
82
  client.addTestRun(STATUS.SKIPPED, {
83
83
  title: getTestName(test),
84
84
  suite_title: getSuiteTitle(test),
85
- code: test.body.toString(),
85
+ code: process.env.TESTOMATIO_UPDATE_CODE ? test.body.toString() : '',
86
86
  file: getFile(test),
87
87
  test_id: testId,
88
88
  time: test.duration,
@@ -102,7 +102,7 @@ function MochaReporter(runner, opts) {
102
102
  file: getFile(test),
103
103
  test_id: testId,
104
104
  title: getTestName(test),
105
- code: test.body.toString(),
105
+ code: process.env.TESTOMATIO_UPDATE_CODE ? test.body.toString() : '',
106
106
  time: test.duration,
107
107
  logs,
108
108
  });
@@ -111,6 +111,7 @@ class PlaywrightReporter {
111
111
 
112
112
  const promises = [];
113
113
 
114
+ // ? possible move to addTestRun (needs investigation if files are ready)
114
115
  for (const upload of this.uploads) {
115
116
  const { rid, file, title } = upload;
116
117
 
@@ -1,7 +1,9 @@
1
1
  // eslint-disable-next-line global-require, import/no-extraneous-dependencies
2
2
  const WDIOReporter = require('@wdio/reporter').default;
3
3
  const TestomatClient = require('../client');
4
- const { getTestomatIdFromTestTitle } = require('../utils/utils');
4
+ const { getTestomatIdFromTestTitle, fileSystem } = require('../utils/utils');
5
+ const { services } = require('../services');
6
+ const { TESTOMAT_TMP_STORAGE_DIR } = require('../constants');
5
7
 
6
8
  class WebdriverReporter extends WDIOReporter {
7
9
  constructor(options) {
@@ -15,6 +17,15 @@ class WebdriverReporter extends WDIOReporter {
15
17
  this._isSynchronising = false;
16
18
  }
17
19
 
20
+ onRunnerStart() {
21
+ // clear dir with artifacts/logs
22
+ fileSystem.clearDir(TESTOMAT_TMP_STORAGE_DIR);
23
+ }
24
+
25
+ onTestStart(test) {
26
+ services.setContext(test.fullTitle);
27
+ }
28
+
18
29
  get isSynchronised() {
19
30
  return this._isSynchronising === false;
20
31
  }
@@ -28,15 +39,18 @@ class WebdriverReporter extends WDIOReporter {
28
39
  }
29
40
 
30
41
  onTestEnd(test) {
42
+ test.suite = test.parent;
43
+ const logs = getTestLogs(test.fullTitle);
44
+ // TODO: FIX: artifacts for some reason leads to empty report on Testomat.io
45
+ // const artifacts = services.artifacts.get(test.fullTitle);
46
+ // const keyValues = services.keyValues.get(test.fullTitle);
47
+ test.logs = logs;
48
+ // test.artifacts = artifacts;
49
+ // test.meta = keyValues;
31
50
  this._addTestPromises.push(this.addTest(test));
32
51
  }
33
52
 
34
53
  // wdio-cucumber does not trigger onTestEnd hook, thus, using this one
35
- /**
36
- *
37
- * @param {} scerario
38
- * @returns
39
- */
40
54
  onSuiteEnd(scerario) {
41
55
  if (scerario.type === 'scenario') {
42
56
  this._addTestPromises.push(this.addBddScenario(scerario));
@@ -56,7 +70,10 @@ class WebdriverReporter extends WDIOReporter {
56
70
  .map(el => Buffer.from(el.result.value, 'base64'));
57
71
 
58
72
  await this.client.addTestRun(state, {
73
+ manuallyAttachedArtifacts: test.artifacts,
59
74
  error,
75
+ logs: test.logs,
76
+ meta: test.meta,
60
77
  title,
61
78
  test_id: testId,
62
79
  time: duration,
@@ -98,4 +115,16 @@ class WebdriverReporter extends WDIOReporter {
98
115
  }
99
116
  }
100
117
 
118
+ /**
119
+ *
120
+ * @param {*} fullTestTitle
121
+ * @returns string
122
+ */
123
+ function getTestLogs(fullTestTitle) {
124
+ const logsArr = services.logger.getLogs(fullTestTitle);
125
+ // remove duplicates (for some reason, logs are duplicated several times)
126
+ const logs = logsArr ? Array.from(new Set(logsArr)).join('\n').trim() : '';
127
+ return logs;
128
+ }
129
+
101
130
  module.exports = WebdriverReporter;
package/lib/bin/cli.js CHANGED
@@ -10,6 +10,7 @@ const { APP_PREFIX, STATUS } = require('../constants');
10
10
  const { version } = require('../../package.json');
11
11
  const config = require('../config');
12
12
  const { readLatestRunId } = require('../utils/utils');
13
+ const { filesize: prettyBytes } = require('filesize');
13
14
 
14
15
  console.log(chalk.cyan.bold(` 🤩 Testomat.io Reporter v${version}`));
15
16
 
@@ -185,7 +186,7 @@ program
185
186
  isBatchEnabled: false,
186
187
  });
187
188
 
188
- let testruns = client.uploader.readUploadedFiles(process.env.TESTOMATIO_RUN);
189
+ let testruns = client.uploader.readUploadedFiles(runId);
189
190
  const numTotalArtifacts = testruns.length;
190
191
 
191
192
  debug('Found testruns:', testruns);
@@ -193,7 +194,7 @@ program
193
194
  if (!opts.force) testruns = testruns.filter(tr => !tr.uploaded);
194
195
 
195
196
  if (!testruns.length) {
196
- console.log(APP_PREFIX, 'Total artifacts:', numTotalArtifacts);
197
+ console.log(APP_PREFIX, '🗄️ Total artifacts:', numTotalArtifacts);
197
198
  if (numTotalArtifacts) {
198
199
  console.log(APP_PREFIX, 'No new artifacts to upload');
199
200
  console.log(APP_PREFIX, 'To re-upload artifacts run this command with --force flag');
@@ -211,16 +212,38 @@ program
211
212
 
212
213
  await client.createRun();
213
214
  client.uploader.checkEnabled();
214
- client.uploader.disbleLogStorage();
215
+ client.uploader.disableLogStorage();
215
216
 
216
217
  for (const rid in testrunsByRid) {
217
218
  const files = testrunsByRid[rid];
218
219
  await client.addTestRun(undefined, { rid, files });
219
220
  }
220
221
 
221
- console.log(APP_PREFIX, client.uploader.totalUploadsCount, 'artifacts uploaded');
222
- if (client.uploader.failedUploadsCount) {
223
- console.log(APP_PREFIX, client.uploader.failedUploadsCount, 'artifacts failed to upload');
222
+ console.log(APP_PREFIX, '🗄️', client.uploader.totalSuccessfulUploadsCount, 'artifacts 🟢uploaded');
223
+ const filesizeStrMaxLength = 7;
224
+
225
+ if (client.uploader.failedUploads.length) {
226
+ console.log(
227
+ '\n',
228
+ APP_PREFIX,
229
+ '🗄️',
230
+ client.uploader.failedUploads.length,
231
+ `artifacts 🔴${chalk.bold('failed')} to upload`,
232
+ );
233
+
234
+ const failedUploads = client.uploader.failedUploads.map(({ path, size }) => ({
235
+ relativePath: path.replace(process.cwd(), ''),
236
+ sizePretty: prettyBytes(size, { round: 0 }).toString(),
237
+ }));
238
+
239
+ const pathPadding = Math.max(...failedUploads.map(upload => upload.relativePath.length)) + 1;
240
+ failedUploads.forEach(upload => {
241
+ console.log(
242
+ ` ${chalk.gray('|')} 🔴 ${upload.relativePath.padEnd(pathPadding)} ${chalk.gray(
243
+ `| ${upload.sizePretty.padStart(filesizeStrMaxLength)} |`,
244
+ )}`,
245
+ );
246
+ });
224
247
  }
225
248
  });
226
249
 
@@ -64,7 +64,7 @@ program
64
64
  await client.createRun();
65
65
 
66
66
  client.uploader.checkEnabled();
67
- client.uploader.disbleLogStorage();
67
+ client.uploader.disableLogStorage();
68
68
 
69
69
  for (const rid in testrunsByRid) {
70
70
  const files = testrunsByRid[rid];
@@ -74,9 +74,9 @@ program
74
74
  });
75
75
  }
76
76
 
77
- console.log(APP_PREFIX, client.uploader.totalUploadsCount, 'artifacts uploaded');
78
- if (client.uploader.failedUploadsCount) {
79
- console.log(APP_PREFIX, client.uploader.failedUploadsCount, 'artifacts failed to upload');
77
+ console.log(APP_PREFIX, client.uploader.totalSuccessfulUploadsCount, 'artifacts uploaded');
78
+ if (client.uploader.failedUploads.length) {
79
+ console.log(APP_PREFIX, client.uploader.failedUploads.length, 'artifacts failed to upload');
80
80
  }
81
81
  });
82
82
 
package/lib/client.js CHANGED
@@ -10,7 +10,8 @@ const { APP_PREFIX, STATUS } = require('./constants');
10
10
  const pipesFactory = require('./pipe');
11
11
  const { glob } = require('glob');
12
12
  const path = require('path');
13
- const { storeRunId } = require('./utils/utils');
13
+ const { storeRunId, formatStep } = require('./utils/utils');
14
+ const { filesize: prettyBytes } = require('filesize');
14
15
 
15
16
  let listOfTestFilesToExcludeFromReport = null;
16
17
 
@@ -250,32 +251,65 @@ class Client {
250
251
  this.queue = this.queue
251
252
  .then(() => Promise.all(this.pipes.map(p => p.finishRun(runParams))))
252
253
  .then(() => {
253
- debug('TOTAL artifacts', this.uploader.totalUploadsCount);
254
- debug(`${this.uploader.skippedUploadsCount} artifacts skipped`);
255
-
256
- if (this.uploader.totalUploadsCount && this.uploader.isEnabled) {
254
+ if (this.uploader.totalSuccessfulUploadsCount && this.uploader.isEnabled) {
257
255
  console.log(
258
256
  APP_PREFIX,
259
- `🗄️ ${this.uploader.totalUploadsCount} artifacts ${
257
+ `🗄️ ${this.uploader.totalSuccessfulUploadsCount} artifacts ${
260
258
  process.env.TESTOMATIO_PRIVATE_ARTIFACTS ? 'privately' : chalk.bold('publicly')
261
- } uploaded to S3 bucket`,
259
+ } 🟢 uploaded to S3 bucket`,
262
260
  );
263
261
  }
264
262
 
265
- if (this.uploader.failedUploadsCount) {
263
+ const filesizeStrMaxLength = 7;
264
+
265
+ if (this.uploader.failedUploads.length) {
266
266
  console.log(
267
+ '\n',
267
268
  APP_PREFIX,
268
- chalk.gray('[CLIENT]'),
269
- `${this.uploader.failedUploadsCount} artifacts failed to upload`,
269
+ `🗄️ ${this.uploader.failedUploads.length} artifacts 🔴${chalk.bold('failed')} to upload`,
270
270
  );
271
+ const failedUploads = this.uploader.failedUploads.map(file => ({
272
+ relativePath: file.path.replace(process.cwd(), ''),
273
+ sizePretty: prettyBytes(file.size, { round: 0 }).toString(),
274
+ }));
275
+
276
+ const pathPadding = Math.max(...failedUploads.map(upload => upload.relativePath.length)) + 1;
277
+
278
+ failedUploads.forEach(upload => {
279
+ console.log(
280
+ ` ${chalk.gray('|')} 🔴 ${upload.relativePath.padEnd(pathPadding)} ${chalk.gray(
281
+ `| ${upload.sizePretty.padStart(filesizeStrMaxLength)} |`,
282
+ )}`,
283
+ );
284
+ });
271
285
  }
272
286
 
273
- if (this.uploader.isEnabled && this.uploader.skippedUploadsCount) {
274
- console.log(APP_PREFIX, `${chalk.bold(this.uploader.skippedUploadsCount)} artifacts skipped to upload`);
287
+ if (this.uploader.isEnabled && this.uploader.skippedUploads.length) {
288
+ console.log(
289
+ '\n',
290
+ APP_PREFIX,
291
+ `🗄️ ${chalk.bold(this.uploader.skippedUploads.length)} artifacts uploading 🟡${chalk.bold(
292
+ 'skipped',
293
+ )} (due to large size)`,
294
+ );
295
+ const skippedUploads = this.uploader.skippedUploads.map(file => ({
296
+ relativePath: file.path.replace(process.cwd(), ''),
297
+ sizePretty: file.size === null ? 'unknown' : prettyBytes(file.size, { round: 0 }).toString(),
298
+ }));
299
+ const pathPadding = Math.max(...skippedUploads.map(upload => upload.relativePath.length)) + 1;
300
+ skippedUploads.forEach(upload => {
301
+ console.log(
302
+ ` ${chalk.gray('|')} 🟡 ${upload.relativePath.padEnd(pathPadding)} ${chalk.gray(
303
+ `| ${upload.sizePretty.padStart(filesizeStrMaxLength)} |`,
304
+ )}`,
305
+ );
306
+ });
275
307
  }
276
308
 
277
- if (this.uploader.skippedUploadsCount || this.uploader.failedUploadsCount) {
278
- const command = `TESTOMATIO_RUN=${this.runId} npx @testomatio/reporter upload-artifacts`;
309
+ if (this.uploader.skippedUploads.length || this.uploader.failedUploads.length) {
310
+ const command = `TESTOMATIO=<your_api_key> TESTOMATIO_RUN=${
311
+ this.runId
312
+ } npx @testomatio/reporter upload-artifacts`;
279
313
  console.log(
280
314
  APP_PREFIX,
281
315
  `Run "${chalk.magenta(command)}" with valid S3 credentials to upload skipped & failed artifacts`,
@@ -365,24 +399,6 @@ function isNotInternalFrame(frame) {
365
399
  );
366
400
  }
367
401
 
368
- function formatStep(step, shift = 0) {
369
- const prefix = ' '.repeat(shift);
370
-
371
- const lines = [];
372
-
373
- if (step.error) {
374
- lines.push(`${prefix}${chalk.red(step.title)} ${chalk.gray(`${step.duration}ms`)}`);
375
- } else {
376
- lines.push(`${prefix}${step.title} ${chalk.gray(`${step.duration}ms`)}`);
377
- }
378
-
379
- for (const child of step.steps || []) {
380
- lines.push(...formatStep(child, shift + 2));
381
- }
382
-
383
- return lines;
384
- }
385
-
386
402
  /**
387
403
  *
388
404
  * @param {TestData} testData
package/lib/constants.js CHANGED
@@ -3,7 +3,11 @@ const os = require('os');
3
3
  const path = require('path');
4
4
 
5
5
  const APP_PREFIX = chalk.gray('[TESTOMATIO]');
6
- const AXIOS_TIMEOUT = 20 * 1000; // sum = 20sec
6
+ const TESTOMATIO_REQUEST_TIMEOUT = parseInt(process.env.TESTOMATIO_REQUEST_TIMEOUT, 10);
7
+ if (TESTOMATIO_REQUEST_TIMEOUT) {
8
+ console.log(`${APP_PREFIX} Request timeout is set to ${TESTOMATIO_REQUEST_TIMEOUT / 1000}s`);
9
+ }
10
+ const AXIOS_TIMEOUT = TESTOMATIO_REQUEST_TIMEOUT || 20 * 1000;
7
11
 
8
12
  const TESTOMAT_TMP_STORAGE_DIR = path.join(os.tmpdir(), 'testomatio_tmp');
9
13
 
@@ -0,0 +1,102 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+ const { APP_PREFIX } = require('../constants');
5
+ const debug = require('debug')('@testomatio/reporter:pipe:debug');
6
+ // upgrade to latest for ESM
7
+ const prettyMs = require('pretty-ms');
8
+
9
+ class DebugPipe {
10
+ constructor(params, store) {
11
+ this.isEnabled = !!process.env.TESTOMATIO_DEBUG;
12
+ if (this.isEnabled) {
13
+ this.batch = {
14
+ isEnabled: params.isBatchEnabled ?? !process.env.TESTOMATIO_DISABLE_BATCH_UPLOAD ?? true,
15
+ intervalFunction: null,
16
+ intervalTime: 5000,
17
+ tests: [],
18
+ batchIndex: 0,
19
+ };
20
+ this.logFilePath = path.join(os.tmpdir(), `testomatio.debug.${Date.now()}.json`);
21
+ this.store = store || {};
22
+
23
+ debug('Creating debug file:', this.logFilePath);
24
+ fs.writeFileSync(this.logFilePath, '');
25
+ console.log(APP_PREFIX, '🪲. Debug created:');
26
+ this.testomatioEnvVars = Object.keys(process.env)
27
+ .filter(key => key.startsWith('TESTOMATIO_'))
28
+ .reduce((acc, key) => {
29
+ acc[key] = process.env[key];
30
+ return acc;
31
+ }, {});
32
+ this.logToFile({ datetime: new Date().toISOString(), timestamp: Date.now() });
33
+ this.logToFile({ data: 'variables', testomatioEnvVars: this.testomatioEnvVars });
34
+ this.logToFile({ data: 'store', store: this.store || {} });
35
+ // Bind batchUpload to the instance
36
+ this.batchUpload = this.batchUpload.bind(this);
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Logs data to a file if logging is enabled.
42
+ *
43
+ * @param {Object} logData - The data to be logged.
44
+ * @returns {Promise<void>} A promise that resolves when the log data has been appended to the file.
45
+ */
46
+ logToFile(logData) {
47
+ if (!this.isEnabled) return;
48
+ const timePassedFromLastAction = Date.now() - (this.lastActionTimestamp || Date.now());
49
+ this.lastActionTimestamp = Date.now();
50
+
51
+ const logLine = JSON.stringify({ t: `+${prettyMs(timePassedFromLastAction)}`, ...logData });
52
+ fs.appendFileSync(this.logFilePath, `${logLine}\n`);
53
+ }
54
+
55
+ async prepareRun(opts) {
56
+ if (!this.isEnabled) return [];
57
+
58
+ this.logToFile({ action: 'prepareRun', data: opts });
59
+ }
60
+
61
+ async createRun(params = {}) {
62
+ if (params.isBatchEnabled === true || params.isBatchEnabled === false) this.batch.isEnabled = params.isBatchEnabled;
63
+
64
+ if (!this.isEnabled) return {};
65
+ if (this.batch.isEnabled) this.batch.intervalFunction = setInterval(this.batchUpload, this.batch.intervalTime);
66
+
67
+ this.logToFile({ action: 'createRun', params });
68
+ }
69
+
70
+ async addTest(data) {
71
+ if (!this.isEnabled) return;
72
+
73
+ if (!this.batch.isEnabled) this.logToFile({ action: 'addTest', testId: data });
74
+ else this.batch.tests.push(data);
75
+
76
+ if (!this.batch.intervalFunction) await this.batchUpload();
77
+ }
78
+
79
+ async batchUpload() {
80
+ this.batch.batchIndex++;
81
+ if (!this.batch.isEnabled) return;
82
+ if (!this.batch.tests.length) return;
83
+
84
+ const testsToSend = this.batch.tests.splice(0);
85
+
86
+ this.logToFile({ action: 'addTestsBatch', tests: testsToSend });
87
+ }
88
+
89
+ async finishRun(params) {
90
+ if (!this.isEnabled) return;
91
+ this.logToFile({ actions: 'finishRun', params });
92
+ await this.batchUpload();
93
+ if (this.batch.intervalFunction) clearInterval(this.batch.intervalFunction);
94
+ console.log(APP_PREFIX, '🪲. Debug Saved to', this.logFilePath);
95
+ }
96
+
97
+ toString() {
98
+ return 'Debug Reporter';
99
+ }
100
+ }
101
+
102
+ module.exports = DebugPipe;
package/lib/pipe/html.js CHANGED
@@ -5,8 +5,7 @@ const path = require('path');
5
5
  const chalk = require('chalk');
6
6
  const handlebars = require('handlebars');
7
7
  const fileUrl = require('file-url');
8
-
9
- const { fileSystem, isSameTest, ansiRegExp } = require('../utils/utils');
8
+ const { fileSystem, isSameTest, ansiRegExp, formatStep } = require('../utils/utils');
10
9
  const { HTML_REPORT } = require('../constants');
11
10
 
12
11
  class HtmlPipe {
@@ -123,6 +122,14 @@ class HtmlPipe {
123
122
  }
124
123
 
125
124
  tests.forEach(test => {
125
+ // steps could be an array or a string
126
+ test.steps = Array.isArray(test.steps)
127
+ ? (test.steps = test.steps
128
+ .map(step => formatStep(step))
129
+ .flat()
130
+ .join('\n'))
131
+ : test.steps;
132
+
126
133
  if (!test.message?.trim()) {
127
134
  test.message = "This test has no 'message' code";
128
135
  }
package/lib/pipe/index.js CHANGED
@@ -8,6 +8,7 @@ const GitLabPipe = require('./gitlab');
8
8
  const CsvPipe = require('./csv');
9
9
  const HtmlPipe = require('./html');
10
10
  const BitbucketPipe = require('./bitbucket');
11
+ const DebugPipe = require('./debug');
11
12
 
12
13
  function PipeFactory(params, opts) {
13
14
  const extraPipes = [];
@@ -47,6 +48,7 @@ function PipeFactory(params, opts) {
47
48
  new CsvPipe(params, opts),
48
49
  new HtmlPipe(params, opts),
49
50
  new BitbucketPipe(params, opts),
51
+ new DebugPipe(params, opts),
50
52
  ...extraPipes,
51
53
  ];
52
54
 
@@ -98,6 +98,7 @@ class TestomatioPipe {
98
98
  this.runId = params.runId || process.env.runId;
99
99
  this.createNewTests = params.createNewTests ?? !!process.env.TESTOMATIO_CREATE;
100
100
  this.hasUnmatchedTests = false;
101
+ this.requestFailures = 0;
101
102
 
102
103
  if (!isValidUrl(this.url.trim())) {
103
104
  this.isEnabled = false;
@@ -217,10 +218,13 @@ class TestomatioPipe {
217
218
  process.env.runId = this.runId;
218
219
  debug('Run created', this.runId);
219
220
  } catch (err) {
221
+ const errorText = err.response?.data?.message || err.message;
222
+ console.log(errorText || err);
223
+ if (!this.apiKey) console.error('Testomat.io API key is not set');
224
+ if (!this.apiKey?.startsWith('tstmt')) console.error('Testomat.io API key is invalid');
220
225
  console.error(
221
226
  APP_PREFIX,
222
- 'Error creating Testomat.io report, please check if your API key is valid. Skipping report | ',
223
- err?.response?.statusText || err?.status || err.message,
227
+ 'Error creating Testomat.io report (see details above), please check if your API key is valid. Skipping report'
224
228
  );
225
229
  printCreateIssue(err);
226
230
  }
@@ -229,37 +233,25 @@ class TestomatioPipe {
229
233
 
230
234
  /**
231
235
  * Decides whether to skip test reporting in case of too many request failures
232
- * @param {TestData} testData
233
236
  * @returns {boolean}
234
237
  */
235
- #cancelTestReportingInCaseOfTooManyReqFailures(testData) {
236
- if (this.reportingCanceledDueToReqFailures) return true;
237
-
238
- const retriesCountWithinTime = this.retriesTimestamps.filter(
239
- timestamp => Date.now() - timestamp < REPORTER_REQUEST_RETRIES.withinTimeSeconds * 1000,
240
- ).length;
241
- debug(`${retriesCountWithinTime} failed requests within ${REPORTER_REQUEST_RETRIES.withinTimeSeconds}s`);
242
-
243
- if (retriesCountWithinTime > REPORTER_REQUEST_RETRIES.maxTotalRetries) {
244
- const errorMessage = chalk.yellow(
245
- `${retriesCountWithinTime} requests were failed within ${REPORTER_REQUEST_RETRIES.withinTimeSeconds}s,\
246
- reporting for test "${testData.title}" to Testomat is skipped`,
247
- );
248
- console.warn(`${APP_PREFIX} ${errorMessage}`);
238
+ #cancelTestReportingInCaseOfTooManyReqFailures() {
239
+ if (!process.env.TESTOMATIO_MAX_REQUEST_FAILURES) return;
249
240
 
241
+ const cancelReporting = this.requestFailures >= parseInt(process.env.TESTOMATIO_MAX_REQUEST_FAILURES, 10);
242
+ if (cancelReporting) {
250
243
  this.reportingCanceledDueToReqFailures = true;
251
- this.notReportedTestsCount++;
252
-
253
- return true;
244
+ const errorMessage =
245
+ `⚠️ ${process.env.TESTOMATIO_MAX_REQUEST_FAILURES} requests were failed, reporting to Testomat aborted.`;
246
+ console.warn(`${APP_PREFIX} ${chalk.yellow(errorMessage)}`);
254
247
  }
255
-
256
- return false;
248
+ return cancelReporting;
257
249
  }
258
250
 
259
251
  #uploadSingleTest = async data => {
260
252
  if (!this.isEnabled) return;
261
253
  if (!this.runId) return;
262
- if (this.#cancelTestReportingInCaseOfTooManyReqFailures(data)) return;
254
+ if (this.#cancelTestReportingInCaseOfTooManyReqFailures()) return;
263
255
 
264
256
  data.api_key = this.apiKey;
265
257
  data.create = this.createNewTests;
@@ -275,6 +267,8 @@ class TestomatioPipe {
275
267
  return this.axios
276
268
  .post(`/api/reporter/${this.runId}/testrun`, json, axiosAddTestrunRequestConfig)
277
269
  .catch(err => {
270
+ this.requestFailures++;
271
+ this.notReportedTestsCount++;
278
272
  if (err.response) {
279
273
  if (err.response.status >= 400) {
280
274
  const responseData = err.response.data || { message: '' };
@@ -308,6 +302,7 @@ class TestomatioPipe {
308
302
  this.batch.batchIndex++;
309
303
  if (!this.batch.isEnabled) return;
310
304
  if (!this.batch.tests.length) return;
305
+ if (this.#cancelTestReportingInCaseOfTooManyReqFailures()) return;
311
306
 
312
307
  // get tests from batch and clear batch
313
308
  const testsToSend = this.batch.tests.splice(0);
@@ -322,6 +317,8 @@ class TestomatioPipe {
322
317
  axiosAddTestrunRequestConfig,
323
318
  )
324
319
  .catch(err => {
320
+ this.requestFailures++;
321
+ this.notReportedTestsCount += testsToSend.length;
325
322
  if (err.response) {
326
323
  if (err.response.status >= 400) {
327
324
  const responseData = err.response.data || { message: '' };
@@ -399,6 +396,7 @@ class TestomatioPipe {
399
396
  await this.axios.put(`/api/reporter/${this.runId}`, {
400
397
  api_key: this.apiKey,
401
398
  status_event,
399
+ duration: params.duration,
402
400
  tests: params.tests,
403
401
  });
404
402
  if (this.runUrl) {
package/lib/reporter.js CHANGED
@@ -4,7 +4,7 @@ const { services } = require('./services');
4
4
 
5
5
  const reporterFunctions = require('./reporter-functions');
6
6
 
7
- module.exports = {
7
+ const testomat = {
8
8
  // TODO: deprecate in future; use log or testomat.log
9
9
  testomatioLogger: services.logger,
10
10
 
@@ -17,3 +17,14 @@ module.exports = {
17
17
  TestomatClient,
18
18
  TRConstants,
19
19
  };
20
+
21
+ module.exports = testomat;
22
+
23
+ module.exports.testomatioLogger = testomat;
24
+ module.exports.artifact = reporterFunctions.artifact;
25
+ module.exports.log = reporterFunctions.log;
26
+ module.exports.logger = services.logger;
27
+ module.exports.meta = reporterFunctions.keyValue;
28
+ module.exports.step = reporterFunctions.step;
29
+ module.exports.TestomatClient = TestomatClient;
30
+ module.exports.TRConstants = TRConstants;
package/lib/uploader.js CHANGED
@@ -7,6 +7,7 @@ const path = require('path');
7
7
  const promiseRetry = require('promise-retry');
8
8
  const chalk = require('chalk');
9
9
  const { APP_PREFIX } = require('./constants');
10
+ const { filesize: prettyBytes } = require('filesize');
10
11
 
11
12
  class S3Uploader {
12
13
  constructor() {
@@ -15,9 +16,12 @@ class S3Uploader {
15
16
  this.config = undefined;
16
17
 
17
18
  // counters
18
- this.skippedUploadsCount = 0;
19
- this.failedUploadsCount = 0;
20
- this.totalUploadsCount = 0;
19
+ /**
20
+ * @type {{path: string, size: number}[]}
21
+ */
22
+ this.skippedUploads = [];
23
+ this.failedUploads = [];
24
+ this.totalSuccessfulUploadsCount = 0;
21
25
 
22
26
  this.succesfulUploads = {};
23
27
 
@@ -75,11 +79,18 @@ class S3Uploader {
75
79
  this.storeEnabled = true;
76
80
  }
77
81
 
78
- disbleLogStorage() {
82
+ disableLogStorage() {
79
83
  this.storeEnabled = false;
80
84
  }
81
85
 
82
- async uploadToS3(Body, Key) {
86
+ /**
87
+ *
88
+ * @param {*} Body
89
+ * @param {*} Key
90
+ * @param {{path: string, size?: number}} file
91
+ * @returns
92
+ */
93
+ async #uploadToS3(Body, Key, file) {
83
94
  const { S3_BUCKET, TESTOMATIO_PRIVATE_ARTIFACTS } = this.getConfig();
84
95
  const ACL = TESTOMATIO_PRIVATE_ARTIFACTS ? 'private' : 'public-read';
85
96
 
@@ -94,7 +105,7 @@ class S3Uploader {
94
105
 
95
106
  debug('Uploading to S3:', Key);
96
107
 
97
- const s3 = new S3(this.getS3Config());
108
+ const s3 = new S3(this.#getS3Config());
98
109
 
99
110
  try {
100
111
  const upload = new Upload({
@@ -108,18 +119,23 @@ class S3Uploader {
108
119
  });
109
120
 
110
121
  const link = await this.getS3LocationLink(upload);
111
- this.totalUploadsCount++;
122
+ this.totalSuccessfulUploadsCount++;
112
123
  this.succesfulUploads[Key] = link;
113
124
  return link;
114
125
  } catch (e) {
115
- this.failedUploadsCount++;
126
+ this.failedUploads.push({ path: file.path, size: file.size });
116
127
  debug('S3 uploading error:', e);
117
- console.log(APP_PREFIX, 'Upload failed:', e.message, this.getMaskedConfig());
128
+ console.log(APP_PREFIX, 'Upload failed:', e.message, '\nConfig:\n', this.getMaskedConfig());
118
129
  }
119
130
  }
120
131
 
132
+ /**
133
+ * Returns an array of uploaded files
134
+ *
135
+ * @returns {{rid: string, file: string, uploaded: boolean}[]}
136
+ */
121
137
  readUploadedFiles(runId) {
122
- const tempFilePath = this.#getUploadFilePath(runId);
138
+ const tempFilePath = this.#getFilePathWithUploadsList(runId);
123
139
 
124
140
  debug('Reading file', tempFilePath);
125
141
 
@@ -146,9 +162,9 @@ class S3Uploader {
146
162
  return lines.map(line => JSON.parse(line));
147
163
  }
148
164
 
149
- #getUploadFilePath(runId, forceCreate = false) {
150
- const tempFilePath = path.join(os.tmpdir(), `testomatio.run.${runId}.jsonl`);
151
- if (!fs.existsSync(tempFilePath) || forceCreate) {
165
+ #getFilePathWithUploadsList(runId) {
166
+ const tempFilePath = path.join(os.tmpdir(), `testomatio.run.${runId}.json`);
167
+ if (!fs.existsSync(tempFilePath)) {
152
168
  debug('Creating artifacts file:', tempFilePath);
153
169
  fs.writeFileSync(tempFilePath, '');
154
170
  }
@@ -160,7 +176,11 @@ class S3Uploader {
160
176
 
161
177
  if (!filePath || !runId || !rid) return;
162
178
 
163
- const tempFilePath = this.#getUploadFilePath(runId);
179
+ const tempFilePath = this.#getFilePathWithUploadsList(runId);
180
+
181
+ if (typeof filePath === 'object') {
182
+ filePath = filePath.path;
183
+ }
164
184
 
165
185
  if (typeof filePath === 'string' && !path.isAbsolute(filePath)) {
166
186
  filePath = path.join(process.cwd(), filePath);
@@ -168,22 +188,34 @@ class S3Uploader {
168
188
 
169
189
  const data = { rid, file: filePath, uploaded };
170
190
  const jsonLine = `${JSON.stringify(data)}\n`;
171
-
172
191
  fs.appendFileSync(tempFilePath, jsonLine);
173
192
  }
174
193
 
175
- getskippedUpload() {
176
- return this.skippedUploadsCount;
177
- }
178
-
194
+ /**
195
+ *
196
+ * @param {*} filePath
197
+ * @param {*} pathInS3 contains runId, rid and filename
198
+ * @returns
199
+ */
179
200
  async uploadFileByPath(filePath, pathInS3) {
180
201
  const [runId, rid] = pathInS3;
181
202
 
182
203
  if (!filePath) return;
183
204
 
205
+ let fileSize = null;
206
+ let fileSizeInMb = null;
207
+
208
+ try {
209
+ // file may not exist
210
+ fileSize = fs.statSync(filePath).size;
211
+ fileSizeInMb = Number((fileSize / (1024 * 1024)).toFixed(2));
212
+ } catch (e) {
213
+ debug(`File ${filePath} does not exist`);
214
+ }
215
+
184
216
  if (!this.isEnabled) {
185
217
  this.storeUploadedFile(filePath, runId, rid, false);
186
- this.skippedUploadsCount++;
218
+ this.skippedUploads.push({ path: filePath, size: fileSize });
187
219
  return;
188
220
  }
189
221
 
@@ -191,33 +223,44 @@ class S3Uploader {
191
223
 
192
224
  debug('Started upload', filePath, 'to', S3_BUCKET);
193
225
 
194
- const isFileExist = await this.checkFileExists(filePath, 20, 500);
226
+ const isFileExist = await this.checkArtifactExistsInFileSystem(filePath, 20, 500);
195
227
 
196
228
  if (!isFileExist) {
197
229
  console.error(chalk.yellow(`Artifacts file ${filePath} does not exist. Skipping...`));
198
230
  return;
199
231
  }
200
232
 
201
- const fileSize = fs.statSync(filePath).size;
202
- const fileSizeInMb = fileSize / (1024 * 1024);
203
-
204
- if (TESTOMATIO_ARTIFACT_MAX_SIZE_MB && fileSizeInMb > parseInt(TESTOMATIO_ARTIFACT_MAX_SIZE_MB, 10)) {
205
- this.skippedUploadsCount++;
206
- console.error(chalk.yellow(`Artifacts file ${filePath} exceeds the maximum allowed size. Skipping...`));
233
+ // skipping artifact only if: 1. storing to file is enabled, 2. max size is set and 3. file size exceeds the limit
234
+ if (
235
+ this.storeEnabled &&
236
+ TESTOMATIO_ARTIFACT_MAX_SIZE_MB &&
237
+ fileSizeInMb > parseFloat(TESTOMATIO_ARTIFACT_MAX_SIZE_MB)
238
+ ) {
239
+ const skippedArtifact = { path: filePath, size: fileSize };
240
+ this.storeUploadedFile(filePath, runId, rid, false);
241
+ this.skippedUploads.push(skippedArtifact);
242
+ debug(
243
+ chalk.yellow(`Artifacts file ${JSON.stringify(skippedArtifact)} exceeds the maximum allowed size. Skipping.`),
244
+ );
207
245
  return;
208
246
  }
209
- debug('File:', filePath, 'exists, size:', fileSizeInMb.toFixed(2), 'MB');
247
+ debug('File:', filePath, 'exists, size:', prettyBytes(fileSize));
210
248
 
211
249
  const fileStream = fs.createReadStream(filePath);
212
250
  const Key = pathInS3.join('/');
213
251
 
214
- const link = await this.uploadToS3(fileStream, Key);
252
+ const link = await this.#uploadToS3(fileStream, Key, { path: filePath, size: fileSize });
215
253
 
216
254
  this.storeUploadedFile(filePath, runId, rid, !!link);
217
255
 
218
256
  return link;
219
257
  }
220
258
 
259
+ /**
260
+ * @param {Buffer} buffer
261
+ * @param {string[]} pathInS3
262
+ * @returns
263
+ */
221
264
  async uploadFileAsBuffer(buffer, pathInS3) {
222
265
  if (!this.isEnabled) return;
223
266
 
@@ -228,10 +271,10 @@ class S3Uploader {
228
271
  Key = `${Key}.${ext}`;
229
272
  }
230
273
 
231
- return this.uploadToS3(buffer, Key);
274
+ return this.#uploadToS3(buffer, Key, { path: Key });
232
275
  }
233
276
 
234
- async checkFileExists(filePath, attempts = 5, intervalMs = 500) {
277
+ async checkArtifactExistsInFileSystem(filePath, attempts = 5, intervalMs = 500) {
235
278
  return promiseRetry(
236
279
  async (retry, number) => {
237
280
  try {
@@ -286,7 +329,7 @@ class S3Uploader {
286
329
  );
287
330
  }
288
331
 
289
- getS3Config() {
332
+ #getS3Config() {
290
333
  const { S3_REGION, S3_SESSION_TOKEN, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, S3_FORCE_PATH_STYLE, S3_ENDPOINT } =
291
334
  this.getConfig();
292
335
 
@@ -321,6 +321,7 @@ const testRunnerHelper = {
321
321
  };
322
322
 
323
323
  function storeRunId(runId) {
324
+ if (!runId || runId === 'undefined') return;
324
325
  const filePath = path.join(os.tmpdir(), `testomatio.latest.run`);
325
326
  fs.writeFileSync(filePath, runId);
326
327
  }
@@ -339,6 +340,24 @@ function readLatestRunId() {
339
340
  }
340
341
  }
341
342
 
343
+ function formatStep(step, shift = 0) {
344
+ const prefix = ' '.repeat(shift);
345
+
346
+ const lines = [];
347
+
348
+ if (step.error) {
349
+ lines.push(`${prefix}${chalk.red(step.title)} ${chalk.gray(`${step.duration}ms`)}`);
350
+ } else {
351
+ lines.push(`${prefix}${step.title} ${chalk.gray(`${step.duration}ms`)}`);
352
+ }
353
+
354
+ for (const child of step.steps || []) {
355
+ lines.push(...formatStep(child, shift + 2));
356
+ }
357
+
358
+ return lines;
359
+ }
360
+
342
361
  module.exports = {
343
362
  storeRunId,
344
363
  readLatestRunId,
@@ -349,6 +368,7 @@ module.exports = {
349
368
  fetchIdFromOutput,
350
369
  fetchFilesFromStackTrace,
351
370
  fileSystem,
371
+ formatStep,
352
372
  getCurrentDateTime,
353
373
  specificTestInfo,
354
374
  isValidUrl,
package/lib/xmlReader.js CHANGED
@@ -107,7 +107,7 @@ class XmlReader {
107
107
  }
108
108
 
109
109
  processJUnit(jsonSuite) {
110
- const { testsuite, name, tests, failures, errors } = jsonSuite;
110
+ const { testsuite, name, tests, failures, errors, time } = jsonSuite;
111
111
 
112
112
  reduceOptions.preferClassname = this.stats.language === 'python';
113
113
  const resultTests = processTestSuite(testsuite);
@@ -115,12 +115,18 @@ class XmlReader {
115
115
  const hasFailures = resultTests.filter(t => t.status === 'failed').length > 0;
116
116
  const status = failures > 0 || errors > 0 || hasFailures ? 'failed' : 'passed';
117
117
 
118
+ if (time) {
119
+ if (!this.stats.duration) this.stats.duration = 0;
120
+ this.stats.duration += parseFloat(time);
121
+ }
122
+
118
123
  this.tests = this.tests.concat(resultTests);
119
124
 
120
125
  return {
121
126
  status,
122
127
  create_tests: true,
123
128
  name,
129
+ duration: parseFloat(time),
124
130
  tests_count: parseInt(tests, 10),
125
131
  passed_count: parseInt(tests, 10) - parseInt(failures, 10),
126
132
  failed_count: parseInt(failures, 10),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testomatio/reporter",
3
- "version": "1.6.0-beta-6-artifacts",
3
+ "version": "1.6.0",
4
4
  "description": "Testomatio Reporter Client",
5
5
  "main": "./lib/reporter.js",
6
6
  "typings": "typings/index.d.ts",
@@ -23,6 +23,7 @@
23
23
  "dotenv": "^16.0.1",
24
24
  "fast-xml-parser": "^4.4.1",
25
25
  "file-url": "3.0.0",
26
+ "filesize": "^10.1.6",
26
27
  "glob": "^10.3",
27
28
  "handlebars": "^4.7.8",
28
29
  "has-flag": "^5.0.1",
@@ -32,6 +33,7 @@
32
33
  "lodash.memoize": "^4.1.2",
33
34
  "lodash.merge": "^4.6.2",
34
35
  "minimatch": "^9.0.3",
36
+ "pretty-ms": "^7.0.1",
35
37
  "promise-retry": "^2.0.1",
36
38
  "strip-ansi": "^7.1.0",
37
39
  "uuid": "^9.0.0"