@testomatio/reporter 1.6.0-beta-6-artifacts → 1.6.1-beta-artifacts
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/lib/adapter/mocha.js +1 -1
- package/lib/adapter/playwright.js +1 -0
- package/lib/adapter/webdriver.js +35 -6
- package/lib/bin/cli.js +29 -6
- package/lib/bin/uploadArtifacts.js +4 -4
- package/lib/client.js +48 -32
- package/lib/constants.js +5 -1
- package/lib/pipe/debug.js +102 -0
- package/lib/pipe/html.js +9 -2
- package/lib/pipe/index.js +2 -0
- package/lib/pipe/testomatio.js +21 -23
- package/lib/reporter.js +12 -1
- package/lib/uploader.js +75 -32
- package/lib/utils/utils.js +20 -0
- package/lib/xmlReader.js +7 -1
- package/package.json +3 -1
package/lib/adapter/mocha.js
CHANGED
|
@@ -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
|
|
package/lib/adapter/webdriver.js
CHANGED
|
@@ -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(
|
|
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.
|
|
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.
|
|
222
|
-
|
|
223
|
-
|
|
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.
|
|
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.
|
|
78
|
-
if (client.uploader.
|
|
79
|
-
console.log(APP_PREFIX, client.uploader.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
263
|
+
const filesizeStrMaxLength = 7;
|
|
264
|
+
|
|
265
|
+
if (this.uploader.failedUploads.length) {
|
|
266
266
|
console.log(
|
|
267
|
+
'\n',
|
|
267
268
|
APP_PREFIX,
|
|
268
|
-
chalk.
|
|
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.
|
|
274
|
-
console.log(
|
|
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.
|
|
278
|
-
const command = `TESTOMATIO_RUN=${
|
|
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
|
|
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
|
|
package/lib/pipe/testomatio.js
CHANGED
|
@@ -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(
|
|
236
|
-
if (
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
82
|
+
disableLogStorage() {
|
|
79
83
|
this.storeEnabled = false;
|
|
80
84
|
}
|
|
81
85
|
|
|
82
|
-
|
|
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
|
|
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.
|
|
122
|
+
this.totalSuccessfulUploadsCount++;
|
|
112
123
|
this.succesfulUploads[Key] = link;
|
|
113
124
|
return link;
|
|
114
125
|
} catch (e) {
|
|
115
|
-
this.
|
|
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.#
|
|
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
|
-
#
|
|
150
|
-
const tempFilePath = path.join(os.tmpdir(), `testomatio.run.${runId}.
|
|
151
|
-
if (!fs.existsSync(tempFilePath)
|
|
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.#
|
|
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
|
-
|
|
176
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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:',
|
|
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
|
|
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
|
|
274
|
+
return this.#uploadToS3(buffer, Key, { path: Key });
|
|
232
275
|
}
|
|
233
276
|
|
|
234
|
-
async
|
|
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
|
|
package/lib/utils/utils.js
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "1.6.1-beta-artifacts",
|
|
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"
|