@testomatio/reporter 1.2.3-beta → 1.2.3-beta-batch-upload
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 +61 -54
- package/lib/adapter/codecept.js +96 -35
- package/lib/adapter/cucumber/current.js +23 -16
- package/lib/adapter/cucumber/legacy.js +14 -13
- package/lib/adapter/cucumber.js +2 -2
- package/lib/adapter/cypress-plugin/index.js +52 -25
- package/lib/adapter/jasmine.js +1 -1
- package/lib/adapter/jest.js +49 -16
- package/lib/adapter/mocha.js +103 -51
- package/lib/adapter/playwright.js +80 -27
- package/lib/adapter/webdriver.js +1 -1
- package/lib/bin/reportXml.js +14 -13
- package/lib/bin/startTest.js +27 -6
- package/lib/client.js +192 -69
- package/lib/config.js +34 -0
- package/lib/constants.js +19 -7
- package/lib/data-storage.js +203 -0
- package/lib/fileUploader.js +128 -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 +35 -17
- 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 +5 -3
- package/lib/pipe/github.js +27 -39
- package/lib/pipe/gitlab.js +20 -24
- package/lib/pipe/html.js +317 -0
- package/lib/pipe/index.js +3 -1
- package/lib/pipe/testomatio.js +220 -62
- package/lib/reporter-functions.js +46 -0
- package/lib/reporter.js +11 -9
- package/lib/services/artifacts.js +57 -0
- package/lib/services/index.js +13 -0
- package/lib/services/key-values.js +58 -0
- package/lib/services/logger.js +311 -0
- package/lib/template/template-draft.hbs +249 -0
- package/lib/template/testomatio.hbs +388 -0
- package/lib/utils/pipe_utils.js +128 -0
- package/lib/{util.js → utils/utils.js} +144 -12
- package/lib/xmlReader.js +215 -122
- package/package.json +18 -7
- package/lib/_ArtifactStorageOld.js +0 -142
- package/lib/artifactStorage.js +0 -25
- package/lib/dataStorage.js +0 -241
- package/lib/helpers.js +0 -34
- package/lib/logger.js +0 -293
package/lib/pipe/testomatio.js
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
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
|
-
|
|
5
|
-
const { APP_PREFIX, STATUS } = require('../constants');
|
|
6
|
-
const { isValidUrl } = require('../
|
|
7
|
-
const {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
if (TESTOMATIO_RUN) {
|
|
11
|
-
process.env.runId = TESTOMATIO_RUN;
|
|
7
|
+
|
|
8
|
+
const { APP_PREFIX, STATUS, AXIOS_TIMEOUT, AXIOS_RETRY_TIMEOUT } = require('../constants');
|
|
9
|
+
const { isValidUrl, foundedTestLog } = require('../utils/utils');
|
|
10
|
+
const { parseFilterParams, generateFilterRequestParams, setS3Credentials } = require('../utils/pipe_utils');
|
|
11
|
+
const config = require('../config');
|
|
12
|
+
|
|
13
|
+
if (process.env.TESTOMATIO_RUN) {
|
|
14
|
+
process.env.runId = process.env.TESTOMATIO_RUN;
|
|
12
15
|
}
|
|
13
16
|
|
|
14
17
|
/**
|
|
@@ -18,26 +21,62 @@ if (TESTOMATIO_RUN) {
|
|
|
18
21
|
* @implements {Pipe}
|
|
19
22
|
*/
|
|
20
23
|
class TestomatioPipe {
|
|
24
|
+
batch = {
|
|
25
|
+
tests: [],
|
|
26
|
+
intervalFunction: null,
|
|
27
|
+
intervalTime: 5000, // how often tests are sent
|
|
28
|
+
};
|
|
29
|
+
|
|
21
30
|
constructor(params, store) {
|
|
22
31
|
this.isEnabled = false;
|
|
23
32
|
this.url = params.testomatioUrl || process.env.TESTOMATIO_URL || 'https://app.testomat.io';
|
|
24
|
-
this.apiKey = params.apiKey ||
|
|
33
|
+
this.apiKey = params.apiKey || config.TESTOMATIO;
|
|
25
34
|
debug('Testomatio Pipe: ', this.apiKey ? 'API KEY' : '*no api key*');
|
|
26
35
|
if (!this.apiKey) {
|
|
27
36
|
return;
|
|
28
37
|
}
|
|
29
38
|
debug('Testomatio Pipe: Enabled');
|
|
39
|
+
this.parallel = params.parallel;
|
|
30
40
|
this.store = store || {};
|
|
31
41
|
this.title = params.title || process.env.TESTOMATIO_TITLE;
|
|
32
42
|
this.sharedRun = !!process.env.TESTOMATIO_SHARED_RUN;
|
|
33
43
|
this.groupTitle = params.groupTitle || process.env.TESTOMATIO_RUNGROUP_TITLE;
|
|
34
44
|
this.env = process.env.TESTOMATIO_ENV;
|
|
35
|
-
this.
|
|
45
|
+
this.label = process.env.TESTOMATIO_LABEL;
|
|
46
|
+
// Create a new instance of axios with a custom config
|
|
47
|
+
this.axios = axios.create({
|
|
48
|
+
baseURL: `${this.url.trim()}`,
|
|
49
|
+
timeout: AXIOS_TIMEOUT,
|
|
50
|
+
});
|
|
51
|
+
// Pass the axios instance to the retry function
|
|
52
|
+
axiosRetry(this.axios, {
|
|
53
|
+
retries: 3, // Number of retries (Defaults to 3)
|
|
54
|
+
shouldResetTimeout: true,
|
|
55
|
+
retryCondition: error => {
|
|
56
|
+
// Conditional check the error status code
|
|
57
|
+
switch (error.response.status) {
|
|
58
|
+
case 409:
|
|
59
|
+
case 429:
|
|
60
|
+
case 502:
|
|
61
|
+
case 503:
|
|
62
|
+
return true; // Retry request with response status code 409, 429, 502, 503
|
|
63
|
+
default:
|
|
64
|
+
return false; // Do not retry the others
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
retryDelay: retryCount => retryCount * AXIOS_RETRY_TIMEOUT, // sum = 15sec
|
|
68
|
+
onRetry: retryCount => {
|
|
69
|
+
debug(`Retry attempt #${retryCount} failed. Retrying again...`);
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
|
|
36
73
|
this.isEnabled = true;
|
|
37
74
|
// do not finish this run (for parallel testing)
|
|
38
75
|
this.proceed = process.env.TESTOMATIO_PROCEED;
|
|
76
|
+
this.jiraId = process.env.TESTOMATIO_JIRA_ID;
|
|
39
77
|
this.runId = params.runId || process.env.runId;
|
|
40
|
-
this.createNewTests = !!process.env.TESTOMATIO_CREATE;
|
|
78
|
+
this.createNewTests = params.createNewTests ?? !!process.env.TESTOMATIO_CREATE;
|
|
79
|
+
this.hasUnmatchedTests = false;
|
|
41
80
|
|
|
42
81
|
if (!isValidUrl(this.url.trim())) {
|
|
43
82
|
this.isEnabled = false;
|
|
@@ -45,91 +84,196 @@ class TestomatioPipe {
|
|
|
45
84
|
}
|
|
46
85
|
}
|
|
47
86
|
|
|
87
|
+
/**
|
|
88
|
+
* Asynchronously prepares and retrieves the Testomat.io test grepList based on the provided options.
|
|
89
|
+
* @param {Object} opts - The options for preparing the test grepList.
|
|
90
|
+
* @returns {Promise<string[]>} - An array containing the retrieved
|
|
91
|
+
* test grepList, or an empty array if no tests are found or the request is disabled.
|
|
92
|
+
* @throws {Error} - Throws an error if there was a problem while making the request.
|
|
93
|
+
*/
|
|
94
|
+
async prepareRun(opts) {
|
|
95
|
+
if (!this.isEnabled) return [];
|
|
96
|
+
|
|
97
|
+
const { type, id } = parseFilterParams(opts);
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const q = generateFilterRequestParams({
|
|
101
|
+
type,
|
|
102
|
+
id,
|
|
103
|
+
apiKey: this.apiKey.trim(),
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
if (!q) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const resp = await this.axios.get('/api/test_grep', q);
|
|
111
|
+
const { data } = resp;
|
|
112
|
+
|
|
113
|
+
if (Array.isArray(data?.tests) && data?.tests?.length > 0) {
|
|
114
|
+
foundedTestLog(APP_PREFIX, data.tests);
|
|
115
|
+
return data.tests;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
console.log(APP_PREFIX, `⛔ No tests found for your --filter --> ${type}=${id}`);
|
|
119
|
+
} catch (err) {
|
|
120
|
+
console.error(APP_PREFIX, `🚩 Error getting Testomat.io test grepList: ${err}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
48
124
|
/**
|
|
49
125
|
* @returns Promise<void>
|
|
50
126
|
*/
|
|
51
127
|
async createRun() {
|
|
128
|
+
debug('Creating run...');
|
|
52
129
|
if (!this.isEnabled) return;
|
|
130
|
+
this.batch.intervalFunction = setInterval(this.#batchUpload, this.batch.intervalTime);
|
|
131
|
+
|
|
132
|
+
let buildUrl = process.env.BUILD_URL || process.env.CI_JOB_URL || process.env.CIRCLE_BUILD_URL;
|
|
133
|
+
|
|
134
|
+
// GitHub Actions Url
|
|
135
|
+
if (!buildUrl && process.env.GITHUB_RUN_ID) {
|
|
136
|
+
// eslint-disable-next-line max-len
|
|
137
|
+
buildUrl = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Azure DevOps Url
|
|
141
|
+
if (!buildUrl && process.env.SYSTEM_TEAMFOUNDATIONCOLLECTIONURI) {
|
|
142
|
+
const collectionUri = process.env.SYSTEM_TEAMFOUNDATIONCOLLECTIONURI;
|
|
143
|
+
const project = process.env.SYSTEM_TEAMPROJECT;
|
|
144
|
+
const buildId = process.env.BUILD_BUILDID;
|
|
145
|
+
buildUrl = `${collectionUri}/${project}/_build/results?buildId=${buildId}`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (buildUrl && !buildUrl.startsWith('http')) buildUrl = undefined;
|
|
149
|
+
|
|
150
|
+
const accessEvent = process.env.TESTOMATIO_PUBLISH ? 'publish' : null;
|
|
53
151
|
|
|
54
152
|
const runParams = Object.fromEntries(
|
|
55
153
|
Object.entries({
|
|
154
|
+
ci_build_url: buildUrl,
|
|
155
|
+
parallel: this.parallel,
|
|
56
156
|
api_key: this.apiKey.trim(),
|
|
57
157
|
group_title: this.groupTitle,
|
|
158
|
+
access_event: accessEvent,
|
|
159
|
+
jira_id: this.jiraId,
|
|
58
160
|
env: this.env,
|
|
59
161
|
title: this.title,
|
|
162
|
+
label: this.label,
|
|
60
163
|
shared_run: this.sharedRun,
|
|
61
|
-
}).filter(([, value]) => !!value)
|
|
164
|
+
}).filter(([, value]) => !!value),
|
|
62
165
|
);
|
|
166
|
+
debug('Run params', JSON.stringify(runParams, null, 2));
|
|
63
167
|
|
|
64
168
|
if (this.runId) {
|
|
65
|
-
|
|
169
|
+
debug(`Run with id ${this.runId} already created, updating...`);
|
|
170
|
+
const resp = await this.axios.put(`/api/reporter/${this.runId}`, runParams);
|
|
66
171
|
if (resp.data.artifacts) setS3Credentials(resp.data.artifacts);
|
|
67
172
|
return;
|
|
68
173
|
}
|
|
69
174
|
|
|
70
175
|
try {
|
|
71
|
-
const resp = await this.axios.post(
|
|
176
|
+
const resp = await this.axios.post(`/api/reporter`, runParams, {
|
|
72
177
|
maxContentLength: Infinity,
|
|
73
178
|
maxBodyLength: Infinity,
|
|
74
179
|
});
|
|
180
|
+
|
|
75
181
|
this.runId = resp.data.uid;
|
|
76
182
|
this.runUrl = `${this.url}/${resp.data.url.split('/').splice(3).join('/')}`;
|
|
183
|
+
this.runPublicUrl = resp.data.public_url;
|
|
184
|
+
|
|
77
185
|
if (resp.data.artifacts) setS3Credentials(resp.data.artifacts);
|
|
186
|
+
|
|
78
187
|
this.store.runUrl = this.runUrl;
|
|
188
|
+
this.store.runPublicUrl = this.runPublicUrl;
|
|
79
189
|
this.store.runId = this.runId;
|
|
80
190
|
console.log(APP_PREFIX, '📊 Report created. Report ID:', this.runId);
|
|
81
191
|
process.env.runId = this.runId;
|
|
192
|
+
debug('Run created', this.runId);
|
|
82
193
|
} catch (err) {
|
|
83
194
|
console.error(
|
|
84
195
|
APP_PREFIX,
|
|
85
|
-
'Error creating Testomat.io report, please check if your API key is valid. Skipping report',
|
|
196
|
+
'Error creating Testomat.io report, please check if your API key is valid. Skipping report | ',
|
|
197
|
+
err?.response?.statusText || err?.status || err.message,
|
|
86
198
|
);
|
|
87
199
|
}
|
|
200
|
+
debug('"createRun" function finished');
|
|
88
201
|
}
|
|
89
202
|
|
|
90
203
|
/**
|
|
91
|
-
*
|
|
92
|
-
|
|
93
|
-
|
|
204
|
+
* Uploads tests as a batch (multiple tests at once). Intended to be used with a setInterval
|
|
205
|
+
*/
|
|
206
|
+
#batchUpload = async () => {
|
|
207
|
+
if (!this.batch.tests.length) return;
|
|
208
|
+
|
|
209
|
+
// get tests from batch and clear batch
|
|
210
|
+
const testsToSend = this.batch.tests.splice(0);
|
|
211
|
+
debug('📨 Batch upload', testsToSend.length, 'tests');
|
|
212
|
+
|
|
213
|
+
return this.axios
|
|
214
|
+
.post(
|
|
215
|
+
`/api/reporter/${this.runId}/testrun`,
|
|
216
|
+
{ api_key: this.apiKey, tests: testsToSend },
|
|
217
|
+
{
|
|
218
|
+
maxContentLength: Infinity,
|
|
219
|
+
maxBodyLength: Infinity,
|
|
220
|
+
headers: {
|
|
221
|
+
// Overwrite Axios's automatically set Content-Type
|
|
222
|
+
'Content-Type': 'application/json',
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
)
|
|
226
|
+
.catch(err => {
|
|
227
|
+
if (err.response) {
|
|
228
|
+
if (err.response.status >= 400) {
|
|
229
|
+
const responseData = err.response.data || { message: '' };
|
|
230
|
+
console.log(
|
|
231
|
+
APP_PREFIX,
|
|
232
|
+
chalk.yellow(`Warning: ${responseData.message} (${err.response.status})`),
|
|
233
|
+
// chalk.grey(data?.title || ''),
|
|
234
|
+
);
|
|
235
|
+
if (err.response.data.message.includes('could not be matched')) {
|
|
236
|
+
this.hasUnmatchedTests = true;
|
|
237
|
+
}
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
console.log(
|
|
241
|
+
APP_PREFIX,
|
|
242
|
+
chalk.yellow(`Warning: (${err.response?.status})`),
|
|
243
|
+
`Report couldn't be processed: ${err?.response?.data?.message}`,
|
|
244
|
+
);
|
|
245
|
+
} else {
|
|
246
|
+
console.log(APP_PREFIX, "Report couldn't be processed", err);
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Adds a test to the batch uploader
|
|
94
253
|
*/
|
|
95
254
|
addTest(data) {
|
|
96
255
|
if (!this.isEnabled) return;
|
|
97
256
|
if (!this.runId) return;
|
|
98
257
|
data.api_key = this.apiKey;
|
|
99
258
|
data.create = this.createNewTests;
|
|
100
|
-
const json = JsonCycle.stringify(data);
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
'Content-Type': 'application/json',
|
|
108
|
-
},
|
|
109
|
-
})
|
|
110
|
-
.catch((err) => {
|
|
111
|
-
if (err.response) {
|
|
112
|
-
if (err.response.status >= 400) {
|
|
113
|
-
const responseData = err.response.data || { message: '' };
|
|
114
|
-
console.log(
|
|
115
|
-
APP_PREFIX,
|
|
116
|
-
chalk.blue(this.title),
|
|
117
|
-
`Report couldn't be processed: (${err.response.status}) ${responseData.message}`,
|
|
118
|
-
);
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
console.log(APP_PREFIX, chalk.blue(this.title), `Report couldn't be processed: ${err.response.data.message}`);
|
|
122
|
-
} else {
|
|
123
|
-
console.log(APP_PREFIX, chalk.blue(this.title), "Report couldn't be processed", err);
|
|
124
|
-
}
|
|
125
|
-
});
|
|
259
|
+
// const json = JsonCycle.stringify(data);
|
|
260
|
+
// debug('Adding test', JSON.stringify(data));
|
|
261
|
+
|
|
262
|
+
this.batch.tests.push(data);
|
|
263
|
+
|
|
264
|
+
// if test is added after run already finished
|
|
265
|
+
if (!this.batch.intervalFunction) this.#batchUpload();
|
|
126
266
|
}
|
|
127
267
|
|
|
128
268
|
/**
|
|
129
|
-
* @param {import('../../types').RunData} params
|
|
130
|
-
* @returns
|
|
269
|
+
* @param {import('../../types').RunData} params
|
|
270
|
+
* @returns
|
|
131
271
|
*/
|
|
132
272
|
async finishRun(params) {
|
|
273
|
+
this.#batchUpload();
|
|
274
|
+
if (this.batch.intervalFunction) clearInterval(this.batch.intervalFunction);
|
|
275
|
+
|
|
276
|
+
debug('Finishing run...');
|
|
133
277
|
if (!this.isEnabled) return;
|
|
134
278
|
|
|
135
279
|
const { status, parallel } = params;
|
|
@@ -143,7 +287,7 @@ class TestomatioPipe {
|
|
|
143
287
|
|
|
144
288
|
try {
|
|
145
289
|
if (this.runId && !this.proceed) {
|
|
146
|
-
await this.axios.put(
|
|
290
|
+
await this.axios.put(`/api/reporter/${this.runId}`, {
|
|
147
291
|
api_key: this.apiKey,
|
|
148
292
|
status_event,
|
|
149
293
|
tests: params.tests,
|
|
@@ -151,15 +295,44 @@ class TestomatioPipe {
|
|
|
151
295
|
if (this.runUrl) {
|
|
152
296
|
console.log(APP_PREFIX, '📊 Report Saved. Report URL:', chalk.magenta(this.runUrl));
|
|
153
297
|
}
|
|
298
|
+
if (this.runPublicUrl) {
|
|
299
|
+
console.log(APP_PREFIX, '🌟 Public URL:', chalk.magenta(this.runPublicUrl));
|
|
300
|
+
}
|
|
154
301
|
}
|
|
155
302
|
if (this.runUrl && this.proceed) {
|
|
156
303
|
const notFinishedMessage = chalk.yellow.bold('Run was not finished because of $TESTOMATIO_PROCEED');
|
|
157
304
|
console.log(APP_PREFIX, `📊 ${notFinishedMessage}. Report URL: ${chalk.magenta(this.runUrl)}`);
|
|
158
305
|
console.log(APP_PREFIX, `🛬 Run to finish it: TESTOMATIO_RUN=${this.runId} npx start-test-run --finish`);
|
|
159
306
|
}
|
|
307
|
+
if (this.hasUnmatchedTests) {
|
|
308
|
+
console.log('');
|
|
309
|
+
// eslint-disable-next-line max-len
|
|
310
|
+
console.log(APP_PREFIX, chalk.yellow.bold('⚠️ Some reported tests were not found in Testomat.io project'));
|
|
311
|
+
// eslint-disable-next-line max-len
|
|
312
|
+
console.log(
|
|
313
|
+
APP_PREFIX,
|
|
314
|
+
`If you use Testomat.io as a reporter only, please re-run tests using ${chalk.bold('TESTOMATIO_CREATE=1')}`,
|
|
315
|
+
);
|
|
316
|
+
// eslint-disable-next-line max-len
|
|
317
|
+
console.log(
|
|
318
|
+
APP_PREFIX,
|
|
319
|
+
`But to keep your tests consistent it is recommended to ${chalk.bold('import tests first')}`,
|
|
320
|
+
);
|
|
321
|
+
console.log(APP_PREFIX, 'If tests were imported but still not matched, assign test IDs to your tests.');
|
|
322
|
+
console.log(APP_PREFIX, 'You can do that automatically via command line tools:');
|
|
323
|
+
console.log(APP_PREFIX, chalk.bold('npx check-tests ... --update-ids'), 'See: https://bit.ly/js-update-ids');
|
|
324
|
+
console.log(APP_PREFIX, 'or for Cucumber:');
|
|
325
|
+
// eslint-disable-next-line max-len
|
|
326
|
+
console.log(
|
|
327
|
+
APP_PREFIX,
|
|
328
|
+
chalk.bold('npx check-cucumber ... --update-ids'),
|
|
329
|
+
'See: https://bit.ly/bdd-update-ids',
|
|
330
|
+
);
|
|
331
|
+
}
|
|
160
332
|
} catch (err) {
|
|
161
333
|
console.log(APP_PREFIX, 'Error updating status, skipping...', err);
|
|
162
334
|
}
|
|
335
|
+
debug('Run finished');
|
|
163
336
|
}
|
|
164
337
|
|
|
165
338
|
toString() {
|
|
@@ -168,18 +341,3 @@ class TestomatioPipe {
|
|
|
168
341
|
}
|
|
169
342
|
|
|
170
343
|
module.exports = TestomatioPipe;
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
function setS3Credentials(artifacts) {
|
|
174
|
-
if (!Object.keys(artifacts).length) return;
|
|
175
|
-
|
|
176
|
-
console.log(APP_PREFIX, 'S3 were credentials obtained from Testomat.io...');
|
|
177
|
-
|
|
178
|
-
if (artifacts.ACCESS_KEY_ID) process.env.S3_ACCESS_KEY_ID = artifacts.ACCESS_KEY_ID;
|
|
179
|
-
if (artifacts.SECRET_ACCESS_KEY) process.env.S3_SECRET_ACCESS_KEY = artifacts.SECRET_ACCESS_KEY;
|
|
180
|
-
if (artifacts.REGION) process.env.S3_REGION = artifacts.REGION;
|
|
181
|
-
if (artifacts.BUCKET) process.env.S3_BUCKET = artifacts.BUCKET;
|
|
182
|
-
if (artifacts.ENDPOINT) process.env.S3_ENDPOINT = artifacts.ENDPOINT;
|
|
183
|
-
if (artifacts.presign) process.env.TESTOMATIO_PRIVATE_ARTIFACTS = '1';
|
|
184
|
-
resetConfig();
|
|
185
|
-
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const { services } = require('./services');
|
|
2
|
+
const { initPlaywrightForStorage } = require('./adapter/playwright');
|
|
3
|
+
|
|
4
|
+
if (process.env.PLAYWRIGHT_TEST_BASE_URL) {
|
|
5
|
+
initPlaywrightForStorage();
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Stores path to file as artifact and uploads it to the S3 storage
|
|
10
|
+
* @param {string | {path: string, type: string, name: string}} data - path to file or object with path, type and name
|
|
11
|
+
*/
|
|
12
|
+
function saveArtifact(data, context = null) {
|
|
13
|
+
if (!data) return;
|
|
14
|
+
services.artifacts.put(data, context);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Attach log message(s) to the test report
|
|
19
|
+
* @param {...any} args
|
|
20
|
+
*/
|
|
21
|
+
function logMessage(...args) {
|
|
22
|
+
services.logger._templateLiteralLog(...args);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Similar to "log" function but marks message in report as a step
|
|
27
|
+
* @param {*} message
|
|
28
|
+
*/
|
|
29
|
+
function addStep(message) {
|
|
30
|
+
services.logger.step(message);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Add key-value pair(s) to the test report
|
|
35
|
+
* @param {*} keyValue
|
|
36
|
+
*/
|
|
37
|
+
function setKeyValue(keyValue) {
|
|
38
|
+
services.keyValues.put(keyValue);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = {
|
|
42
|
+
artifact: saveArtifact,
|
|
43
|
+
log: logMessage,
|
|
44
|
+
step: addStep,
|
|
45
|
+
keyValue: setKeyValue,
|
|
46
|
+
};
|
package/lib/reporter.js
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
|
-
const logger = require('./logger');
|
|
2
1
|
const TestomatClient = require('./client');
|
|
3
2
|
const TRConstants = require('./constants');
|
|
4
|
-
const
|
|
3
|
+
const { services } = require('./services');
|
|
5
4
|
|
|
6
|
-
const
|
|
7
|
-
const step = logger.step.bind(logger);
|
|
5
|
+
const reporterFunctions = require('./reporter-functions');
|
|
8
6
|
|
|
9
7
|
module.exports = {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
// TODO: deprecate in future; use log or testomat.log
|
|
9
|
+
testomatioLogger: services.logger,
|
|
10
|
+
|
|
11
|
+
artifact: reporterFunctions.artifact,
|
|
12
|
+
log: reporterFunctions.log,
|
|
13
|
+
logger: services.logger,
|
|
14
|
+
meta: reporterFunctions.keyValue,
|
|
15
|
+
step: reporterFunctions.step,
|
|
16
|
+
|
|
13
17
|
TestomatClient,
|
|
14
18
|
TRConstants,
|
|
15
|
-
TRArtifacts,
|
|
16
|
-
addArtifact: TRArtifacts.artifact,
|
|
17
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
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
const debug = require('debug')('@testomatio/reporter:services-key-value');
|
|
2
|
+
const { dataStorage } = require('../data-storage');
|
|
3
|
+
|
|
4
|
+
class KeyValueStorage {
|
|
5
|
+
static #instance;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
*
|
|
9
|
+
* @returns {KeyValueStorage}
|
|
10
|
+
*/
|
|
11
|
+
static getInstance() {
|
|
12
|
+
if (!this.#instance) {
|
|
13
|
+
this.#instance = new KeyValueStorage();
|
|
14
|
+
}
|
|
15
|
+
return this.#instance;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Stores key-value pair and passes it to reporter
|
|
20
|
+
* @param {{key: string}} keyValue - key-value pair(s) as object
|
|
21
|
+
* @param {*} context - full test title
|
|
22
|
+
*/
|
|
23
|
+
put(keyValue, context = null) {
|
|
24
|
+
if (!keyValue) return;
|
|
25
|
+
dataStorage.putData('keyvalue', keyValue, context);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
#isKeyValueObject(smth) {
|
|
29
|
+
return smth && typeof smth === 'object' && !Array.isArray(smth) && smth !== null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Returns key-values pairs for the test as object
|
|
34
|
+
* @param {*} context testId or test context from test runner
|
|
35
|
+
* @returns {{[key: string]: string} | {}} key-values pairs as object, e.g. {priority: 'high', browser: 'chrome'}
|
|
36
|
+
*/
|
|
37
|
+
get(context = null) {
|
|
38
|
+
const keyValuesList = dataStorage.getData('keyvalue', context);
|
|
39
|
+
if (!keyValuesList || !keyValuesList?.length) return {};
|
|
40
|
+
|
|
41
|
+
const keyValues = {};
|
|
42
|
+
for (const keyValue of keyValuesList) {
|
|
43
|
+
if (this.#isKeyValueObject(keyValue)) {
|
|
44
|
+
Object.assign(keyValues, keyValue);
|
|
45
|
+
} else if (typeof keyValue === 'string') {
|
|
46
|
+
try {
|
|
47
|
+
Object.assign(keyValues, JSON.parse(keyValue));
|
|
48
|
+
} catch (e) {
|
|
49
|
+
debug(`Error parsing key-values for test ${context}`, keyValue);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return keyValues;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports.keyValueStorage = KeyValueStorage.getInstance();
|