@testomatio/reporter 1.4.1-beta-1 → 1.4.1
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/README.md +60 -57
- package/lib/adapter/codecept.js +86 -27
- package/lib/adapter/cucumber/current.js +17 -10
- package/lib/adapter/cucumber/legacy.js +5 -4
- package/lib/adapter/cucumber.js +2 -2
- package/lib/adapter/cypress-plugin/index.js +53 -26
- package/lib/adapter/jasmine.js +2 -2
- package/lib/adapter/jest.js +48 -10
- package/lib/adapter/mocha.js +90 -10
- package/lib/adapter/playwright.js +68 -18
- package/lib/adapter/webdriver.js +47 -2
- package/lib/bin/reportXml.js +21 -16
- package/lib/bin/startTest.js +12 -12
- package/lib/client.js +112 -52
- package/lib/config.js +34 -0
- package/lib/constants.js +28 -1
- package/lib/data-storage.js +203 -0
- package/lib/fileUploader.js +66 -53
- package/lib/junit-adapter/adapter.js +0 -2
- package/lib/junit-adapter/csharp.js +3 -4
- package/lib/junit-adapter/index.js +3 -3
- package/lib/junit-adapter/java.js +11 -11
- package/lib/junit-adapter/javascript.js +1 -2
- package/lib/junit-adapter/python.js +12 -14
- package/lib/junit-adapter/ruby.js +1 -2
- package/lib/pipe/csv.js +35 -32
- package/lib/pipe/github.js +4 -3
- package/lib/pipe/gitlab.js +17 -10
- package/lib/pipe/html.js +361 -0
- package/lib/pipe/index.js +3 -1
- package/lib/pipe/testomatio.js +230 -51
- package/lib/reporter-functions.js +12 -9
- package/lib/reporter.js +8 -12
- package/lib/services/artifacts.js +57 -0
- package/lib/services/index.js +13 -0
- package/lib/{storages/key-value-storage.js → services/key-values.js} +19 -19
- package/lib/{storages → services}/logger.js +58 -37
- package/lib/template/emptyData.svg +23 -0
- package/lib/template/testomatio.hbs +1421 -0
- package/lib/utils/pipe_utils.js +73 -79
- package/lib/utils/utils.js +53 -40
- package/lib/xmlReader.js +113 -105
- package/package.json +13 -7
- package/lib/storages/artifact-storage.js +0 -70
- package/lib/storages/data-storage.js +0 -307
package/lib/pipe/testomatio.js
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
const debug = require('debug')('@testomatio/reporter:pipe:testomatio');
|
|
2
2
|
const chalk = require('chalk');
|
|
3
|
+
// Retry interceptor function
|
|
4
|
+
const axiosRetry = require('axios-retry');
|
|
5
|
+
// Default axios instance
|
|
3
6
|
const axios = require('axios');
|
|
4
7
|
const JsonCycle = require('json-cycle');
|
|
5
|
-
|
|
8
|
+
|
|
9
|
+
const { APP_PREFIX, STATUS, AXIOS_TIMEOUT, REPORTER_REQUEST_RETRIES } = require('../constants');
|
|
6
10
|
const { isValidUrl, foundedTestLog } = require('../utils/utils');
|
|
7
|
-
const { parseFilterParams, generateFilterRequestParams, setS3Credentials
|
|
11
|
+
const { parseFilterParams, generateFilterRequestParams, setS3Credentials } = require('../utils/pipe_utils');
|
|
12
|
+
const config = require('../config');
|
|
8
13
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
process.env.runId = TESTOMATIO_RUN;
|
|
14
|
+
if (process.env.TESTOMATIO_RUN) {
|
|
15
|
+
process.env.runId = process.env.TESTOMATIO_RUN;
|
|
12
16
|
}
|
|
13
17
|
|
|
14
18
|
/**
|
|
@@ -19,9 +23,20 @@ if (TESTOMATIO_RUN) {
|
|
|
19
23
|
*/
|
|
20
24
|
class TestomatioPipe {
|
|
21
25
|
constructor(params, store) {
|
|
26
|
+
this.batch = {
|
|
27
|
+
isEnabled: params.isBatchEnabled ?? !process.env.TESTOMATIO_DISABLE_BATCH_UPLOAD ?? true,
|
|
28
|
+
intervalFunction: null, // will be created in createRun by setInterval function
|
|
29
|
+
intervalTime: 5000, // how often tests are sent
|
|
30
|
+
tests: [], // array of tests in batch
|
|
31
|
+
batchIndex: 0, // represents the current batch index (starts from 1 and increments by 1 for each batch)
|
|
32
|
+
};
|
|
33
|
+
this.retriesTimestamps = [];
|
|
34
|
+
this.reportingCanceledDueToReqFailures = false;
|
|
35
|
+
this.notReportedTestsCount = 0;
|
|
36
|
+
|
|
22
37
|
this.isEnabled = false;
|
|
23
38
|
this.url = params.testomatioUrl || process.env.TESTOMATIO_URL || 'https://app.testomat.io';
|
|
24
|
-
this.apiKey = params.apiKey ||
|
|
39
|
+
this.apiKey = params.apiKey || config.TESTOMATIO;
|
|
25
40
|
debug('Testomatio Pipe: ', this.apiKey ? 'API KEY' : '*no api key*');
|
|
26
41
|
if (!this.apiKey) {
|
|
27
42
|
return;
|
|
@@ -33,10 +48,37 @@ class TestomatioPipe {
|
|
|
33
48
|
this.sharedRun = !!process.env.TESTOMATIO_SHARED_RUN;
|
|
34
49
|
this.groupTitle = params.groupTitle || process.env.TESTOMATIO_RUNGROUP_TITLE;
|
|
35
50
|
this.env = process.env.TESTOMATIO_ENV;
|
|
36
|
-
|
|
51
|
+
this.label = process.env.TESTOMATIO_LABEL;
|
|
52
|
+
// Create a new instance of axios with a custom config
|
|
37
53
|
this.axios = axios.create({
|
|
38
54
|
baseURL: `${this.url.trim()}`,
|
|
39
|
-
timeout:
|
|
55
|
+
timeout: AXIOS_TIMEOUT,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Pass the axios instance to the retry function
|
|
59
|
+
axiosRetry(this.axios, {
|
|
60
|
+
// do not use retries for unit tests
|
|
61
|
+
retries: REPORTER_REQUEST_RETRIES.retriesPerRequest, // Number of retries
|
|
62
|
+
shouldResetTimeout: true,
|
|
63
|
+
retryCondition: error => {
|
|
64
|
+
if (!error.response) return false;
|
|
65
|
+
switch (error.response?.status) {
|
|
66
|
+
case 400: // Bad request (probably wrong API key)
|
|
67
|
+
case 404: // Test not matched
|
|
68
|
+
case 429: // Rate limit exceeded
|
|
69
|
+
case 500: // Internal server error
|
|
70
|
+
return false;
|
|
71
|
+
default:
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
return error.response?.status >= 401; // Retry on 401+ and 5xx
|
|
75
|
+
},
|
|
76
|
+
retryDelay: () => REPORTER_REQUEST_RETRIES.retryTimeout, // sum = 15sec
|
|
77
|
+
onRetry: async (retryCount, error) => {
|
|
78
|
+
this.retriesTimestamps.push(Date.now());
|
|
79
|
+
|
|
80
|
+
debug(`${error.message || `Request failed ${error.status}`}. Retry #${retryCount} ...`);
|
|
81
|
+
},
|
|
40
82
|
});
|
|
41
83
|
|
|
42
84
|
this.isEnabled = true;
|
|
@@ -56,7 +98,7 @@ class TestomatioPipe {
|
|
|
56
98
|
/**
|
|
57
99
|
* Asynchronously prepares and retrieves the Testomat.io test grepList based on the provided options.
|
|
58
100
|
* @param {Object} opts - The options for preparing the test grepList.
|
|
59
|
-
* @returns {Promise<string[]>} - An array containing the retrieved
|
|
101
|
+
* @returns {Promise<string[]>} - An array containing the retrieved
|
|
60
102
|
* test grepList, or an empty array if no tests are found or the request is disabled.
|
|
61
103
|
* @throws {Error} - Throws an error if there was a problem while making the request.
|
|
62
104
|
*/
|
|
@@ -69,7 +111,7 @@ class TestomatioPipe {
|
|
|
69
111
|
const q = generateFilterRequestParams({
|
|
70
112
|
type,
|
|
71
113
|
id,
|
|
72
|
-
apiKey: this.apiKey.trim()
|
|
114
|
+
apiKey: this.apiKey.trim(),
|
|
73
115
|
});
|
|
74
116
|
|
|
75
117
|
if (!q) {
|
|
@@ -83,23 +125,23 @@ class TestomatioPipe {
|
|
|
83
125
|
foundedTestLog(APP_PREFIX, data.tests);
|
|
84
126
|
return data.tests;
|
|
85
127
|
}
|
|
86
|
-
|
|
128
|
+
|
|
87
129
|
console.log(APP_PREFIX, `⛔ No tests found for your --filter --> ${type}=${id}`);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
console.error(
|
|
91
|
-
APP_PREFIX,
|
|
92
|
-
`🚩 Error getting Testomat.io test grepList: ${err}`,
|
|
93
|
-
);
|
|
130
|
+
} catch (err) {
|
|
131
|
+
console.error(APP_PREFIX, `🚩 Error getting Testomat.io test grepList: ${err}`);
|
|
94
132
|
}
|
|
95
133
|
}
|
|
96
134
|
|
|
97
135
|
/**
|
|
136
|
+
* Creates a new run on Testomat.io
|
|
137
|
+
* @param {{isBatchEnabled?: boolean}} params
|
|
98
138
|
* @returns Promise<void>
|
|
99
139
|
*/
|
|
100
|
-
async createRun() {
|
|
140
|
+
async createRun(params = {}) {
|
|
141
|
+
this.batch.isEnabled = params.isBatchEnabled ?? this.batch.isEnabled;
|
|
101
142
|
debug('Creating run...');
|
|
102
143
|
if (!this.isEnabled) return;
|
|
144
|
+
if (this.batch.isEnabled) this.batch.intervalFunction = setInterval(this.#batchUpload, this.batch.intervalTime);
|
|
103
145
|
|
|
104
146
|
let buildUrl = process.env.BUILD_URL || process.env.CI_JOB_URL || process.env.CIRCLE_BUILD_URL;
|
|
105
147
|
|
|
@@ -131,6 +173,7 @@ class TestomatioPipe {
|
|
|
131
173
|
jira_id: this.jiraId,
|
|
132
174
|
env: this.env,
|
|
133
175
|
title: this.title,
|
|
176
|
+
label: this.label,
|
|
134
177
|
shared_run: this.sharedRun,
|
|
135
178
|
}).filter(([, value]) => !!value),
|
|
136
179
|
);
|
|
@@ -148,10 +191,13 @@ class TestomatioPipe {
|
|
|
148
191
|
maxContentLength: Infinity,
|
|
149
192
|
maxBodyLength: Infinity,
|
|
150
193
|
});
|
|
194
|
+
|
|
151
195
|
this.runId = resp.data.uid;
|
|
152
196
|
this.runUrl = `${this.url}/${resp.data.url.split('/').splice(3).join('/')}`;
|
|
153
197
|
this.runPublicUrl = resp.data.public_url;
|
|
198
|
+
|
|
154
199
|
if (resp.data.artifacts) setS3Credentials(resp.data.artifacts);
|
|
200
|
+
|
|
155
201
|
this.store.runUrl = this.runUrl;
|
|
156
202
|
this.store.runPublicUrl = this.runPublicUrl;
|
|
157
203
|
this.store.runId = this.runId;
|
|
@@ -161,57 +207,146 @@ class TestomatioPipe {
|
|
|
161
207
|
} catch (err) {
|
|
162
208
|
console.error(
|
|
163
209
|
APP_PREFIX,
|
|
164
|
-
'Error creating Testomat.io report, please check if your API key is valid. Skipping report',
|
|
165
|
-
err,
|
|
210
|
+
'Error creating Testomat.io report, please check if your API key is valid. Skipping report | ',
|
|
211
|
+
err?.response?.statusText || err?.status || err.message,
|
|
166
212
|
);
|
|
213
|
+
printCreateIssue(err);
|
|
167
214
|
}
|
|
168
215
|
debug('"createRun" function finished');
|
|
169
216
|
}
|
|
170
217
|
|
|
171
218
|
/**
|
|
172
|
-
*
|
|
173
|
-
* @param testData
|
|
174
|
-
* @returns
|
|
219
|
+
* Decides whether to skip test reporting in case of too many request failures
|
|
220
|
+
* @param {TestData} testData
|
|
221
|
+
* @returns {boolean}
|
|
175
222
|
*/
|
|
176
|
-
|
|
177
|
-
|
|
223
|
+
#cancelTestReportingInCaseOfTooManyReqFailures(testData) {
|
|
224
|
+
if (this.reportingCanceledDueToReqFailures) return true;
|
|
225
|
+
|
|
226
|
+
const retriesCountWithinTime = this.retriesTimestamps.filter(
|
|
227
|
+
timestamp => Date.now() - timestamp < REPORTER_REQUEST_RETRIES.withinTimeSeconds * 1000,
|
|
228
|
+
).length;
|
|
229
|
+
debug(`${retriesCountWithinTime} failed requests within ${REPORTER_REQUEST_RETRIES.withinTimeSeconds}s`);
|
|
230
|
+
|
|
231
|
+
if (retriesCountWithinTime > REPORTER_REQUEST_RETRIES.maxTotalRetries) {
|
|
232
|
+
const errorMessage = chalk.yellow(
|
|
233
|
+
`${retriesCountWithinTime} requests were failed within ${REPORTER_REQUEST_RETRIES.withinTimeSeconds}s,\
|
|
234
|
+
reporting for test "${testData.title}" to Testomat is skipped`,
|
|
235
|
+
);
|
|
236
|
+
console.warn(`${APP_PREFIX} ${errorMessage}`);
|
|
237
|
+
|
|
238
|
+
this.reportingCanceledDueToReqFailures = true;
|
|
239
|
+
this.notReportedTestsCount++;
|
|
240
|
+
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
#uploadSingleTest = async data => {
|
|
178
248
|
if (!this.isEnabled) return;
|
|
179
249
|
if (!this.runId) return;
|
|
250
|
+
if (this.#cancelTestReportingInCaseOfTooManyReqFailures(data)) return;
|
|
251
|
+
|
|
180
252
|
data.api_key = this.apiKey;
|
|
181
253
|
data.create = this.createNewTests;
|
|
254
|
+
|
|
255
|
+
if (!process.env.TESTOMATIO_STACK_PASSED && data.status === STATUS.PASSED) {
|
|
256
|
+
data.stack = null;
|
|
257
|
+
}
|
|
258
|
+
|
|
182
259
|
const json = JsonCycle.stringify(data);
|
|
183
260
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
261
|
+
debug('Adding test', json);
|
|
262
|
+
|
|
263
|
+
return this.axios
|
|
264
|
+
.post(`/api/reporter/${this.runId}/testrun`, json, axiosAddTestrunRequestConfig)
|
|
265
|
+
.catch(err => {
|
|
266
|
+
if (err.response) {
|
|
267
|
+
if (err.response.status >= 400) {
|
|
268
|
+
const responseData = err.response.data || { message: '' };
|
|
269
|
+
console.log(
|
|
270
|
+
APP_PREFIX,
|
|
271
|
+
chalk.yellow(`Warning: ${responseData.message} (${err.response.status})`),
|
|
272
|
+
chalk.grey(data?.title || ''),
|
|
273
|
+
);
|
|
274
|
+
if (err.response?.data?.message?.includes('could not be matched')) {
|
|
275
|
+
this.hasUnmatchedTests = true;
|
|
276
|
+
}
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
196
279
|
console.log(
|
|
197
280
|
APP_PREFIX,
|
|
198
|
-
chalk.yellow(`Warning: ${
|
|
199
|
-
|
|
281
|
+
chalk.yellow(`Warning: ${data?.title || ''} (${err.response?.status})`),
|
|
282
|
+
`Report couldn't be processed: ${err?.response?.data?.message}`,
|
|
200
283
|
);
|
|
201
|
-
|
|
202
|
-
|
|
284
|
+
printCreateIssue(err);
|
|
285
|
+
} else {
|
|
286
|
+
console.log(APP_PREFIX, chalk.blue(data?.title || ''), "Report couldn't be processed", err);
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Uploads tests as a batch (multiple tests at once). Intended to be used with a setInterval
|
|
294
|
+
*/
|
|
295
|
+
#batchUpload = async () => {
|
|
296
|
+
this.batch.batchIndex++;
|
|
297
|
+
if (!this.batch.isEnabled) return;
|
|
298
|
+
if (!this.batch.tests.length) return;
|
|
299
|
+
|
|
300
|
+
// get tests from batch and clear batch
|
|
301
|
+
const testsToSend = this.batch.tests.splice(0);
|
|
302
|
+
debug('📨 Batch upload', testsToSend.length, 'tests');
|
|
303
|
+
|
|
304
|
+
return this.axios
|
|
305
|
+
.post(
|
|
306
|
+
`/api/reporter/${this.runId}/testrun`,
|
|
307
|
+
{ api_key: this.apiKey, tests: testsToSend, batch_index: this.batch.batchIndex },
|
|
308
|
+
axiosAddTestrunRequestConfig,
|
|
309
|
+
)
|
|
310
|
+
.catch(err => {
|
|
311
|
+
if (err.response) {
|
|
312
|
+
if (err.response.status >= 400) {
|
|
313
|
+
const responseData = err.response.data || { message: '' };
|
|
314
|
+
console.log(
|
|
315
|
+
APP_PREFIX,
|
|
316
|
+
chalk.yellow(`Warning: ${responseData.message} (${err.response.status})`),
|
|
317
|
+
// chalk.grey(data?.title || ''),
|
|
318
|
+
);
|
|
319
|
+
if (err.response?.data?.message?.includes('could not be matched')) {
|
|
320
|
+
this.hasUnmatchedTests = true;
|
|
321
|
+
}
|
|
322
|
+
return;
|
|
203
323
|
}
|
|
204
|
-
|
|
324
|
+
console.log(
|
|
325
|
+
APP_PREFIX,
|
|
326
|
+
chalk.yellow(`Warning: (${err.response?.status})`),
|
|
327
|
+
`Report couldn't be processed: ${err?.response?.data?.message}`,
|
|
328
|
+
);
|
|
329
|
+
printCreateIssue(err);
|
|
330
|
+
} else {
|
|
331
|
+
console.log(APP_PREFIX, "Report couldn't be processed", err);
|
|
205
332
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
333
|
+
});
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Adds a test to the batch uploader (or reports a single test if batch uploading is disabled)
|
|
338
|
+
*/
|
|
339
|
+
addTest(data) {
|
|
340
|
+
if (!this.isEnabled) return;
|
|
341
|
+
if (!this.runId) return;
|
|
342
|
+
data.api_key = this.apiKey;
|
|
343
|
+
data.create = this.createNewTests;
|
|
344
|
+
|
|
345
|
+
if (!this.batch.isEnabled) this.#uploadSingleTest(data);
|
|
346
|
+
else this.batch.tests.push(data);
|
|
347
|
+
|
|
348
|
+
// if test is added after run already finished
|
|
349
|
+
if (!this.batch.intervalFunction) this.#batchUpload();
|
|
215
350
|
}
|
|
216
351
|
|
|
217
352
|
/**
|
|
@@ -219,8 +354,19 @@ class TestomatioPipe {
|
|
|
219
354
|
* @returns
|
|
220
355
|
*/
|
|
221
356
|
async finishRun(params) {
|
|
357
|
+
this.#batchUpload();
|
|
358
|
+
if (this.batch.intervalFunction) clearInterval(this.batch.intervalFunction);
|
|
359
|
+
|
|
222
360
|
debug('Finishing run...');
|
|
223
361
|
if (!this.isEnabled) return;
|
|
362
|
+
debug('Finishing run...');
|
|
363
|
+
|
|
364
|
+
if (this.reportingCanceledDueToReqFailures) {
|
|
365
|
+
const errorMessage = chalk.red(
|
|
366
|
+
`⚠️ Due to request failures, ${this.notReportedTestsCount} test(s) were not reported to Testomat.io`,
|
|
367
|
+
);
|
|
368
|
+
console.warn(`${APP_PREFIX} ${errorMessage}`);
|
|
369
|
+
}
|
|
224
370
|
|
|
225
371
|
const { status, parallel } = params;
|
|
226
372
|
|
|
@@ -277,6 +423,7 @@ class TestomatioPipe {
|
|
|
277
423
|
}
|
|
278
424
|
} catch (err) {
|
|
279
425
|
console.log(APP_PREFIX, 'Error updating status, skipping...', err);
|
|
426
|
+
printCreateIssue(err);
|
|
280
427
|
}
|
|
281
428
|
debug('Run finished');
|
|
282
429
|
}
|
|
@@ -286,4 +433,36 @@ class TestomatioPipe {
|
|
|
286
433
|
}
|
|
287
434
|
}
|
|
288
435
|
|
|
436
|
+
let registeredErrorHints = false;
|
|
437
|
+
function printCreateIssue(err) {
|
|
438
|
+
if (registeredErrorHints) return;
|
|
439
|
+
registeredErrorHints = true;
|
|
440
|
+
process.on('exit', () => {
|
|
441
|
+
console.log();
|
|
442
|
+
console.log(APP_PREFIX, 'There was an error reporting to Testomat.io:');
|
|
443
|
+
console.log(
|
|
444
|
+
APP_PREFIX,
|
|
445
|
+
'If you think this is a bug please create an issue: https://github.com/testomatio/reporter/issues/new',
|
|
446
|
+
); // eslint-disable-line max-len
|
|
447
|
+
console.log(APP_PREFIX, 'Provide this information:');
|
|
448
|
+
console.log('Error:', err.message || err.code);
|
|
449
|
+
if (!err.config) return;
|
|
450
|
+
|
|
451
|
+
const time = new Date().toUTCString();
|
|
452
|
+
const { data, url, baseURL, method } = err?.config || {};
|
|
453
|
+
console.log('```js');
|
|
454
|
+
console.log({ data: data?.replace(/"(tstmt_[^"]+)"/g, 'tstmt_*'), url, baseURL, method, time });
|
|
455
|
+
console.log('```');
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const axiosAddTestrunRequestConfig = {
|
|
460
|
+
maxContentLength: Infinity,
|
|
461
|
+
maxBodyLength: Infinity,
|
|
462
|
+
headers: {
|
|
463
|
+
// Overwrite Axios's automatically set Content-Type
|
|
464
|
+
'Content-Type': 'application/json',
|
|
465
|
+
},
|
|
466
|
+
};
|
|
467
|
+
|
|
289
468
|
module.exports = TestomatioPipe;
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
const
|
|
2
|
-
const
|
|
3
|
-
|
|
1
|
+
const { services } = require('./services');
|
|
2
|
+
const { initPlaywrightForStorage } = require('./adapter/playwright');
|
|
3
|
+
|
|
4
|
+
if (process.env.PLAYWRIGHT_TEST_BASE_URL) {
|
|
5
|
+
initPlaywrightForStorage();
|
|
6
|
+
}
|
|
4
7
|
|
|
5
8
|
/**
|
|
6
9
|
* Stores path to file as artifact and uploads it to the S3 storage
|
|
@@ -8,7 +11,7 @@ const keyValueStorage = require('./storages/key-value-storage');
|
|
|
8
11
|
*/
|
|
9
12
|
function saveArtifact(data, context = null) {
|
|
10
13
|
if (!data) return;
|
|
11
|
-
|
|
14
|
+
services.artifacts.put(data, context);
|
|
12
15
|
}
|
|
13
16
|
|
|
14
17
|
/**
|
|
@@ -16,23 +19,23 @@ function saveArtifact(data, context = null) {
|
|
|
16
19
|
* @param {...any} args
|
|
17
20
|
*/
|
|
18
21
|
function logMessage(...args) {
|
|
19
|
-
logger.
|
|
22
|
+
services.logger._templateLiteralLog(...args);
|
|
20
23
|
}
|
|
21
24
|
|
|
22
25
|
/**
|
|
23
26
|
* Similar to "log" function but marks message in report as a step
|
|
24
|
-
* @param {*} message
|
|
27
|
+
* @param {*} message
|
|
25
28
|
*/
|
|
26
29
|
function addStep(message) {
|
|
27
|
-
logger.step(message);
|
|
30
|
+
services.logger.step(message);
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
/**
|
|
31
34
|
* Add key-value pair(s) to the test report
|
|
32
|
-
* @param {*} keyValue
|
|
35
|
+
* @param {*} keyValue
|
|
33
36
|
*/
|
|
34
37
|
function setKeyValue(keyValue) {
|
|
35
|
-
|
|
38
|
+
services.keyValues.put(keyValue);
|
|
36
39
|
}
|
|
37
40
|
|
|
38
41
|
module.exports = {
|
package/lib/reporter.js
CHANGED
|
@@ -1,23 +1,19 @@
|
|
|
1
|
-
const logger = require('./storages/logger');
|
|
2
1
|
const TestomatClient = require('./client');
|
|
3
2
|
const TRConstants = require('./constants');
|
|
3
|
+
const { services } = require('./services');
|
|
4
4
|
|
|
5
|
-
const log = logger.templateLiteralLog.bind(logger);
|
|
6
|
-
const step = logger.step.bind(logger);
|
|
7
5
|
const reporterFunctions = require('./reporter-functions');
|
|
8
6
|
|
|
9
7
|
module.exports = {
|
|
10
|
-
|
|
11
|
-
testomatioLogger: logger,
|
|
12
|
-
log,
|
|
13
|
-
step,
|
|
14
|
-
TestomatClient,
|
|
15
|
-
TRConstants,
|
|
16
|
-
};
|
|
8
|
+
// TODO: deprecate in future; use log or testomat.log
|
|
9
|
+
testomatioLogger: services.logger,
|
|
17
10
|
|
|
18
|
-
module.exports.testomat = {
|
|
19
11
|
artifact: reporterFunctions.artifact,
|
|
20
12
|
log: reporterFunctions.log,
|
|
21
|
-
|
|
13
|
+
logger: services.logger,
|
|
22
14
|
meta: reporterFunctions.keyValue,
|
|
15
|
+
step: reporterFunctions.step,
|
|
16
|
+
|
|
17
|
+
TestomatClient,
|
|
18
|
+
TRConstants,
|
|
23
19
|
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const debug = require('debug')('@testomatio/reporter:services-artifacts');
|
|
2
|
+
const { dataStorage } = require('../data-storage');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Artifact storage is supposed to store file paths
|
|
6
|
+
*/
|
|
7
|
+
class ArtifactStorage {
|
|
8
|
+
static #instance;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Singleton
|
|
12
|
+
* @returns {ArtifactStorage}
|
|
13
|
+
*/
|
|
14
|
+
static getInstance() {
|
|
15
|
+
if (!this.#instance) {
|
|
16
|
+
this.#instance = new ArtifactStorage();
|
|
17
|
+
}
|
|
18
|
+
return this.#instance;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Stores path to file as artifact and uploads it to the S3 storage
|
|
23
|
+
* @param {string | {path: string, type: string, name: string}} data - path to file or object with path, type and name
|
|
24
|
+
* @param {*} context testId or test title
|
|
25
|
+
*/
|
|
26
|
+
put(data, context = null) {
|
|
27
|
+
if (!data) return;
|
|
28
|
+
debug('Save artifact:', data);
|
|
29
|
+
dataStorage.putData('artifact', data, context);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Returns list of artifacts to upload
|
|
34
|
+
* @param {*} context testId or test context from test runner
|
|
35
|
+
* @returns {(string | {path: string, type: string, name: string})[]}
|
|
36
|
+
*/
|
|
37
|
+
get(context) {
|
|
38
|
+
let artifacts = dataStorage.getData('artifact', context);
|
|
39
|
+
if (!artifacts || !artifacts.length) return [];
|
|
40
|
+
|
|
41
|
+
artifacts = artifacts.map(artifactData => {
|
|
42
|
+
// artifact could be an object ({type, path, name} props) or string (just path)
|
|
43
|
+
let artifact;
|
|
44
|
+
try {
|
|
45
|
+
artifact = JSON.parse(artifactData);
|
|
46
|
+
} catch (e) {
|
|
47
|
+
artifact = artifactData;
|
|
48
|
+
}
|
|
49
|
+
return artifact;
|
|
50
|
+
});
|
|
51
|
+
artifacts = artifacts.filter(artifact => !!artifact);
|
|
52
|
+
debug(`Artifacts for test ${context}:`, artifacts);
|
|
53
|
+
return artifacts.length ? artifacts : [];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports.artifactStorage = ArtifactStorage.getInstance();
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const { logger } = require('./logger');
|
|
2
|
+
const { artifactStorage } = require('./artifacts');
|
|
3
|
+
const { keyValueStorage } = require('./key-values');
|
|
4
|
+
const { dataStorage } = require('../data-storage');
|
|
5
|
+
|
|
6
|
+
module.exports.services = {
|
|
7
|
+
logger,
|
|
8
|
+
artifacts: artifactStorage,
|
|
9
|
+
keyValues: keyValueStorage,
|
|
10
|
+
setContext: context => {
|
|
11
|
+
dataStorage.setContext(context);
|
|
12
|
+
},
|
|
13
|
+
};
|
|
@@ -1,24 +1,28 @@
|
|
|
1
|
-
const debug = require('debug')('@testomatio/reporter:key-value
|
|
2
|
-
const
|
|
1
|
+
const debug = require('debug')('@testomatio/reporter:services-key-value');
|
|
2
|
+
const { dataStorage } = require('../data-storage');
|
|
3
3
|
|
|
4
4
|
class KeyValueStorage {
|
|
5
|
-
|
|
6
|
-
this.dataStorage = new DataStorage('keyvalue');
|
|
5
|
+
static #instance;
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
/**
|
|
8
|
+
*
|
|
9
|
+
* @returns {KeyValueStorage}
|
|
10
|
+
*/
|
|
11
|
+
static getInstance() {
|
|
12
|
+
if (!this.#instance) {
|
|
13
|
+
this.#instance = new KeyValueStorage();
|
|
11
14
|
}
|
|
15
|
+
return this.#instance;
|
|
12
16
|
}
|
|
13
17
|
|
|
14
18
|
/**
|
|
15
19
|
* Stores key-value pair and passes it to reporter
|
|
16
20
|
* @param {{key: string}} keyValue - key-value pair(s) as object
|
|
17
|
-
* @param {*} context
|
|
21
|
+
* @param {*} context - full test title
|
|
18
22
|
*/
|
|
19
23
|
put(keyValue, context = null) {
|
|
20
24
|
if (!keyValue) return;
|
|
21
|
-
|
|
25
|
+
dataStorage.putData('keyvalue', keyValue, context);
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
#isKeyValueObject(smth) {
|
|
@@ -28,13 +32,11 @@ class KeyValueStorage {
|
|
|
28
32
|
/**
|
|
29
33
|
* Returns key-values pairs for the test as object
|
|
30
34
|
* @param {*} context testId or test context from test runner
|
|
31
|
-
* @returns {
|
|
35
|
+
* @returns {{[key: string]: string} | {}} key-values pairs as object, e.g. {priority: 'high', browser: 'chrome'}
|
|
32
36
|
*/
|
|
33
|
-
get(context) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const keyValuesList = this.dataStorage.getData(context);
|
|
37
|
-
if (!keyValuesList || !keyValuesList?.length) return null;
|
|
37
|
+
get(context = null) {
|
|
38
|
+
const keyValuesList = dataStorage.getData('keyvalue', context);
|
|
39
|
+
if (!keyValuesList || !keyValuesList?.length) return {};
|
|
38
40
|
|
|
39
41
|
const keyValues = {};
|
|
40
42
|
for (const keyValue of keyValuesList) {
|
|
@@ -49,10 +51,8 @@ class KeyValueStorage {
|
|
|
49
51
|
}
|
|
50
52
|
}
|
|
51
53
|
|
|
52
|
-
return
|
|
54
|
+
return keyValues;
|
|
53
55
|
}
|
|
54
56
|
}
|
|
55
57
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
module.exports = new KeyValueStorage();
|
|
58
|
+
module.exports.keyValueStorage = KeyValueStorage.getInstance();
|