@testomatio/reporter 1.6.0-beta-6-artifacts → 1.6.0-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 -2
- package/lib/adapter/webdriver.js +35 -6
- package/lib/bin/cli.js +1 -1
- package/lib/pipe/testomatio.js +19 -23
- package/lib/reporter.js +12 -1
- package/lib/uploader.js +27 -17
- package/lib/utils/utils.js +1 -0
- package/lib/xmlReader.js +7 -1
- package/package.json +1 -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
|
|
|
@@ -104,6 +104,7 @@ class PlaywrightReporter {
|
|
|
104
104
|
if (!this.client) return;
|
|
105
105
|
|
|
106
106
|
await Promise.all(reportTestPromises);
|
|
107
|
+
await this.client.updateRunStatus(checkStatus(result.status));
|
|
107
108
|
|
|
108
109
|
if (!this.uploads.length) return;
|
|
109
110
|
|
|
@@ -135,8 +136,6 @@ class PlaywrightReporter {
|
|
|
135
136
|
);
|
|
136
137
|
}
|
|
137
138
|
await Promise.all(promises);
|
|
138
|
-
|
|
139
|
-
await this.client.updateRunStatus(checkStatus(result.status));
|
|
140
139
|
}
|
|
141
140
|
}
|
|
142
141
|
|
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
|
@@ -185,7 +185,7 @@ program
|
|
|
185
185
|
isBatchEnabled: false,
|
|
186
186
|
});
|
|
187
187
|
|
|
188
|
-
let testruns = client.uploader.readUploadedFiles(
|
|
188
|
+
let testruns = client.uploader.readUploadedFiles(runId);
|
|
189
189
|
const numTotalArtifacts = testruns.length;
|
|
190
190
|
|
|
191
191
|
debug('Found testruns:', testruns);
|
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,11 @@ class TestomatioPipe {
|
|
|
217
218
|
process.env.runId = this.runId;
|
|
218
219
|
debug('Run created', this.runId);
|
|
219
220
|
} catch (err) {
|
|
221
|
+
const errorInfo = `${err?.response?.statusText} ${chalk.red(err?.status)} ${err.message}`;
|
|
220
222
|
console.error(
|
|
221
223
|
APP_PREFIX,
|
|
222
|
-
'Error creating Testomat.io report, please check if your API key is valid. Skipping report
|
|
223
|
-
|
|
224
|
+
'Error creating Testomat.io report, please check if your API key is valid. Skipping report\n',
|
|
225
|
+
errorInfo || JSON.stringify(err),
|
|
224
226
|
);
|
|
225
227
|
printCreateIssue(err);
|
|
226
228
|
}
|
|
@@ -229,37 +231,25 @@ class TestomatioPipe {
|
|
|
229
231
|
|
|
230
232
|
/**
|
|
231
233
|
* Decides whether to skip test reporting in case of too many request failures
|
|
232
|
-
* @param {TestData} testData
|
|
233
234
|
* @returns {boolean}
|
|
234
235
|
*/
|
|
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}`);
|
|
236
|
+
#cancelTestReportingInCaseOfTooManyReqFailures() {
|
|
237
|
+
if (!process.env.TESTOMATIO_MAX_REQUEST_FAILURES) return;
|
|
249
238
|
|
|
239
|
+
const cancelReporting = this.requestFailures >= parseInt(process.env.TESTOMATIO_MAX_REQUEST_FAILURES, 10);
|
|
240
|
+
if (cancelReporting) {
|
|
250
241
|
this.reportingCanceledDueToReqFailures = true;
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
242
|
+
const errorMessage =
|
|
243
|
+
`⚠️ ${process.env.TESTOMATIO_MAX_REQUEST_FAILURES} requests were failed, reporting to Testomat aborted.`;
|
|
244
|
+
console.warn(`${APP_PREFIX} ${chalk.yellow(errorMessage)}`);
|
|
254
245
|
}
|
|
255
|
-
|
|
256
|
-
return false;
|
|
246
|
+
return cancelReporting;
|
|
257
247
|
}
|
|
258
248
|
|
|
259
249
|
#uploadSingleTest = async data => {
|
|
260
250
|
if (!this.isEnabled) return;
|
|
261
251
|
if (!this.runId) return;
|
|
262
|
-
if (this.#cancelTestReportingInCaseOfTooManyReqFailures(
|
|
252
|
+
if (this.#cancelTestReportingInCaseOfTooManyReqFailures()) return;
|
|
263
253
|
|
|
264
254
|
data.api_key = this.apiKey;
|
|
265
255
|
data.create = this.createNewTests;
|
|
@@ -275,6 +265,8 @@ class TestomatioPipe {
|
|
|
275
265
|
return this.axios
|
|
276
266
|
.post(`/api/reporter/${this.runId}/testrun`, json, axiosAddTestrunRequestConfig)
|
|
277
267
|
.catch(err => {
|
|
268
|
+
this.requestFailures++;
|
|
269
|
+
this.notReportedTestsCount++;
|
|
278
270
|
if (err.response) {
|
|
279
271
|
if (err.response.status >= 400) {
|
|
280
272
|
const responseData = err.response.data || { message: '' };
|
|
@@ -308,6 +300,7 @@ class TestomatioPipe {
|
|
|
308
300
|
this.batch.batchIndex++;
|
|
309
301
|
if (!this.batch.isEnabled) return;
|
|
310
302
|
if (!this.batch.tests.length) return;
|
|
303
|
+
if (this.#cancelTestReportingInCaseOfTooManyReqFailures()) return;
|
|
311
304
|
|
|
312
305
|
// get tests from batch and clear batch
|
|
313
306
|
const testsToSend = this.batch.tests.splice(0);
|
|
@@ -322,6 +315,8 @@ class TestomatioPipe {
|
|
|
322
315
|
axiosAddTestrunRequestConfig,
|
|
323
316
|
)
|
|
324
317
|
.catch(err => {
|
|
318
|
+
this.requestFailures++;
|
|
319
|
+
this.notReportedTestsCount += testsToSend.length;
|
|
325
320
|
if (err.response) {
|
|
326
321
|
if (err.response.status >= 400) {
|
|
327
322
|
const responseData = err.response.data || { message: '' };
|
|
@@ -399,6 +394,7 @@ class TestomatioPipe {
|
|
|
399
394
|
await this.axios.put(`/api/reporter/${this.runId}`, {
|
|
400
395
|
api_key: this.apiKey,
|
|
401
396
|
status_event,
|
|
397
|
+
duration: params.duration,
|
|
402
398
|
tests: params.tests,
|
|
403
399
|
});
|
|
404
400
|
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
|
@@ -79,7 +79,7 @@ class S3Uploader {
|
|
|
79
79
|
this.storeEnabled = false;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
async uploadToS3(Body, Key) {
|
|
82
|
+
async #uploadToS3(Body, Key) {
|
|
83
83
|
const { S3_BUCKET, TESTOMATIO_PRIVATE_ARTIFACTS } = this.getConfig();
|
|
84
84
|
const ACL = TESTOMATIO_PRIVATE_ARTIFACTS ? 'private' : 'public-read';
|
|
85
85
|
|
|
@@ -94,7 +94,7 @@ class S3Uploader {
|
|
|
94
94
|
|
|
95
95
|
debug('Uploading to S3:', Key);
|
|
96
96
|
|
|
97
|
-
const s3 = new S3(this
|
|
97
|
+
const s3 = new S3(this.#getS3Config());
|
|
98
98
|
|
|
99
99
|
try {
|
|
100
100
|
const upload = new Upload({
|
|
@@ -118,8 +118,13 @@ class S3Uploader {
|
|
|
118
118
|
}
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Returns an array of uploaded files list
|
|
123
|
+
*
|
|
124
|
+
* @returns {{rid: string, file: string, uploaded: boolean}[]}
|
|
125
|
+
*/
|
|
121
126
|
readUploadedFiles(runId) {
|
|
122
|
-
const tempFilePath = this.#
|
|
127
|
+
const tempFilePath = this.#getFilePathWithUploadsList(runId);
|
|
123
128
|
|
|
124
129
|
debug('Reading file', tempFilePath);
|
|
125
130
|
|
|
@@ -146,9 +151,9 @@ class S3Uploader {
|
|
|
146
151
|
return lines.map(line => JSON.parse(line));
|
|
147
152
|
}
|
|
148
153
|
|
|
149
|
-
#
|
|
150
|
-
const tempFilePath = path.join(os.tmpdir(), `testomatio.run.${runId}.
|
|
151
|
-
if (!fs.existsSync(tempFilePath)
|
|
154
|
+
#getFilePathWithUploadsList(runId) {
|
|
155
|
+
const tempFilePath = path.join(os.tmpdir(), `testomatio.run.${runId}.json`);
|
|
156
|
+
if (!fs.existsSync(tempFilePath)) {
|
|
152
157
|
debug('Creating artifacts file:', tempFilePath);
|
|
153
158
|
fs.writeFileSync(tempFilePath, '');
|
|
154
159
|
}
|
|
@@ -160,7 +165,11 @@ class S3Uploader {
|
|
|
160
165
|
|
|
161
166
|
if (!filePath || !runId || !rid) return;
|
|
162
167
|
|
|
163
|
-
const tempFilePath = this.#
|
|
168
|
+
const tempFilePath = this.#getFilePathWithUploadsList(runId);
|
|
169
|
+
|
|
170
|
+
if (typeof filePath === 'object') {
|
|
171
|
+
filePath = filePath.path;
|
|
172
|
+
}
|
|
164
173
|
|
|
165
174
|
if (typeof filePath === 'string' && !path.isAbsolute(filePath)) {
|
|
166
175
|
filePath = path.join(process.cwd(), filePath);
|
|
@@ -168,14 +177,15 @@ class S3Uploader {
|
|
|
168
177
|
|
|
169
178
|
const data = { rid, file: filePath, uploaded };
|
|
170
179
|
const jsonLine = `${JSON.stringify(data)}\n`;
|
|
171
|
-
|
|
172
180
|
fs.appendFileSync(tempFilePath, jsonLine);
|
|
173
181
|
}
|
|
174
182
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
183
|
+
/**
|
|
184
|
+
*
|
|
185
|
+
* @param {*} filePath
|
|
186
|
+
* @param {*} pathInS3 contains runId, rid and filename
|
|
187
|
+
* @returns
|
|
188
|
+
*/
|
|
179
189
|
async uploadFileByPath(filePath, pathInS3) {
|
|
180
190
|
const [runId, rid] = pathInS3;
|
|
181
191
|
|
|
@@ -191,7 +201,7 @@ class S3Uploader {
|
|
|
191
201
|
|
|
192
202
|
debug('Started upload', filePath, 'to', S3_BUCKET);
|
|
193
203
|
|
|
194
|
-
const isFileExist = await this.
|
|
204
|
+
const isFileExist = await this.checkArtifactExistsInFileSystem(filePath, 20, 500);
|
|
195
205
|
|
|
196
206
|
if (!isFileExist) {
|
|
197
207
|
console.error(chalk.yellow(`Artifacts file ${filePath} does not exist. Skipping...`));
|
|
@@ -211,7 +221,7 @@ class S3Uploader {
|
|
|
211
221
|
const fileStream = fs.createReadStream(filePath);
|
|
212
222
|
const Key = pathInS3.join('/');
|
|
213
223
|
|
|
214
|
-
const link = await this
|
|
224
|
+
const link = await this.#uploadToS3(fileStream, Key);
|
|
215
225
|
|
|
216
226
|
this.storeUploadedFile(filePath, runId, rid, !!link);
|
|
217
227
|
|
|
@@ -228,10 +238,10 @@ class S3Uploader {
|
|
|
228
238
|
Key = `${Key}.${ext}`;
|
|
229
239
|
}
|
|
230
240
|
|
|
231
|
-
return this
|
|
241
|
+
return this.#uploadToS3(buffer, Key);
|
|
232
242
|
}
|
|
233
243
|
|
|
234
|
-
async
|
|
244
|
+
async checkArtifactExistsInFileSystem(filePath, attempts = 5, intervalMs = 500) {
|
|
235
245
|
return promiseRetry(
|
|
236
246
|
async (retry, number) => {
|
|
237
247
|
try {
|
|
@@ -286,7 +296,7 @@ class S3Uploader {
|
|
|
286
296
|
);
|
|
287
297
|
}
|
|
288
298
|
|
|
289
|
-
getS3Config() {
|
|
299
|
+
#getS3Config() {
|
|
290
300
|
const { S3_REGION, S3_SESSION_TOKEN, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, S3_FORCE_PATH_STYLE, S3_ENDPOINT } =
|
|
291
301
|
this.getConfig();
|
|
292
302
|
|
package/lib/utils/utils.js
CHANGED
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),
|