@testomatio/reporter 1.3.1-beta → 1.3.1-beta.1-exclude-skipped-tests
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 +19 -12
- package/lib/adapter/cucumber/legacy.js +7 -6
- 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 +50 -17
- package/lib/adapter/mocha.js +110 -58
- package/lib/adapter/playwright.js +95 -33
- package/lib/adapter/webdriver.js +47 -2
- package/lib/bin/reportXml.js +22 -16
- package/lib/bin/startTest.js +27 -6
- package/lib/client.js +185 -62
- package/lib/config.js +34 -0
- package/lib/constants.js +32 -7
- package/lib/data-storage.js +203 -0
- package/lib/fileUploader.js +135 -54
- 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 +37 -31
- package/lib/pipe/github.js +27 -39
- package/lib/pipe/gitlab.js +22 -26
- package/lib/pipe/html.js +361 -0
- package/lib/pipe/index.js +3 -1
- package/lib/pipe/testomatio.js +337 -54
- 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/emptyData.svg +23 -0
- package/lib/template/testomatio.hbs +1421 -0
- package/lib/utils/pipe_utils.js +129 -0
- package/lib/{util.js → utils/utils.js} +147 -17
- package/lib/xmlReader.js +218 -122
- package/package.json +17 -9
- package/lib/_ArtifactStorageOld.js +0 -142
- package/lib/artifactStorage.js +0 -25
- package/lib/dataStorage.js +0 -244
- package/lib/logger.js +0 -301
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
|
-
const { APP_PREFIX, STATUS } = require('../constants');
|
|
6
|
-
const { isValidUrl } = require('../util');
|
|
7
|
-
const { resetConfig } = require('../fileUploader');
|
|
8
8
|
|
|
9
|
-
const {
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
const { APP_PREFIX, STATUS, AXIOS_TIMEOUT, REPORTER_REQUEST_RETRIES } = require('../constants');
|
|
10
|
+
const { isValidUrl, foundedTestLog } = require('../utils/utils');
|
|
11
|
+
const { parseFilterParams, generateFilterRequestParams, setS3Credentials } = require('../utils/pipe_utils');
|
|
12
|
+
const config = require('../config');
|
|
13
|
+
|
|
14
|
+
if (process.env.TESTOMATIO_RUN) {
|
|
15
|
+
process.env.runId = process.env.TESTOMATIO_RUN;
|
|
12
16
|
}
|
|
13
17
|
|
|
14
18
|
/**
|
|
@@ -19,25 +23,71 @@ 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;
|
|
28
43
|
}
|
|
29
44
|
debug('Testomatio Pipe: Enabled');
|
|
45
|
+
this.parallel = params.parallel;
|
|
30
46
|
this.store = store || {};
|
|
31
47
|
this.title = params.title || process.env.TESTOMATIO_TITLE;
|
|
32
48
|
this.sharedRun = !!process.env.TESTOMATIO_SHARED_RUN;
|
|
33
49
|
this.groupTitle = params.groupTitle || process.env.TESTOMATIO_RUNGROUP_TITLE;
|
|
34
50
|
this.env = process.env.TESTOMATIO_ENV;
|
|
35
|
-
this.
|
|
51
|
+
this.label = process.env.TESTOMATIO_LABEL;
|
|
52
|
+
// Create a new instance of axios with a custom config
|
|
53
|
+
this.axios = axios.create({
|
|
54
|
+
baseURL: `${this.url.trim()}`,
|
|
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
|
+
},
|
|
82
|
+
});
|
|
83
|
+
|
|
36
84
|
this.isEnabled = true;
|
|
37
85
|
// do not finish this run (for parallel testing)
|
|
38
86
|
this.proceed = process.env.TESTOMATIO_PROCEED;
|
|
87
|
+
this.jiraId = process.env.TESTOMATIO_JIRA_ID;
|
|
39
88
|
this.runId = params.runId || process.env.runId;
|
|
40
|
-
this.createNewTests = !!process.env.TESTOMATIO_CREATE;
|
|
89
|
+
this.createNewTests = params.createNewTests ?? !!process.env.TESTOMATIO_CREATE;
|
|
90
|
+
this.hasUnmatchedTests = false;
|
|
41
91
|
|
|
42
92
|
if (!isValidUrl(this.url.trim())) {
|
|
43
93
|
this.isEnabled = false;
|
|
@@ -46,91 +96,277 @@ class TestomatioPipe {
|
|
|
46
96
|
}
|
|
47
97
|
|
|
48
98
|
/**
|
|
99
|
+
* Asynchronously prepares and retrieves the Testomat.io test grepList based on the provided options.
|
|
100
|
+
* @param {Object} opts - The options for preparing the test grepList.
|
|
101
|
+
* @returns {Promise<string[]>} - An array containing the retrieved
|
|
102
|
+
* test grepList, or an empty array if no tests are found or the request is disabled.
|
|
103
|
+
* @throws {Error} - Throws an error if there was a problem while making the request.
|
|
104
|
+
*/
|
|
105
|
+
async prepareRun(opts) {
|
|
106
|
+
if (!this.isEnabled) return [];
|
|
107
|
+
|
|
108
|
+
const { type, id } = parseFilterParams(opts);
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
const q = generateFilterRequestParams({
|
|
112
|
+
type,
|
|
113
|
+
id,
|
|
114
|
+
apiKey: this.apiKey.trim(),
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
if (!q) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const resp = await this.axios.get('/api/test_grep', q);
|
|
122
|
+
const { data } = resp;
|
|
123
|
+
|
|
124
|
+
if (Array.isArray(data?.tests) && data?.tests?.length > 0) {
|
|
125
|
+
foundedTestLog(APP_PREFIX, data.tests);
|
|
126
|
+
return data.tests;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
console.log(APP_PREFIX, `⛔ No tests found for your --filter --> ${type}=${id}`);
|
|
130
|
+
} catch (err) {
|
|
131
|
+
console.error(APP_PREFIX, `🚩 Error getting Testomat.io test grepList: ${err}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Creates a new run on Testomat.io
|
|
137
|
+
* @param {{isBatchEnabled?: boolean}} params
|
|
49
138
|
* @returns Promise<void>
|
|
50
139
|
*/
|
|
51
|
-
async createRun() {
|
|
140
|
+
async createRun(params = {}) {
|
|
141
|
+
this.batch.isEnabled = params.isBatchEnabled ?? this.batch.isEnabled;
|
|
142
|
+
debug('Creating run...');
|
|
52
143
|
if (!this.isEnabled) return;
|
|
144
|
+
if (this.batch.isEnabled) this.batch.intervalFunction = setInterval(this.#batchUpload, this.batch.intervalTime);
|
|
145
|
+
|
|
146
|
+
let buildUrl = process.env.BUILD_URL || process.env.CI_JOB_URL || process.env.CIRCLE_BUILD_URL;
|
|
147
|
+
|
|
148
|
+
// GitHub Actions Url
|
|
149
|
+
if (!buildUrl && process.env.GITHUB_RUN_ID) {
|
|
150
|
+
// eslint-disable-next-line max-len
|
|
151
|
+
buildUrl = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Azure DevOps Url
|
|
155
|
+
if (!buildUrl && process.env.SYSTEM_TEAMFOUNDATIONCOLLECTIONURI) {
|
|
156
|
+
const collectionUri = process.env.SYSTEM_TEAMFOUNDATIONCOLLECTIONURI;
|
|
157
|
+
const project = process.env.SYSTEM_TEAMPROJECT;
|
|
158
|
+
const buildId = process.env.BUILD_BUILDID;
|
|
159
|
+
buildUrl = `${collectionUri}/${project}/_build/results?buildId=${buildId}`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (buildUrl && !buildUrl.startsWith('http')) buildUrl = undefined;
|
|
163
|
+
|
|
164
|
+
const accessEvent = process.env.TESTOMATIO_PUBLISH ? 'publish' : null;
|
|
53
165
|
|
|
54
166
|
const runParams = Object.fromEntries(
|
|
55
167
|
Object.entries({
|
|
168
|
+
ci_build_url: buildUrl,
|
|
169
|
+
parallel: this.parallel,
|
|
56
170
|
api_key: this.apiKey.trim(),
|
|
57
171
|
group_title: this.groupTitle,
|
|
172
|
+
access_event: accessEvent,
|
|
173
|
+
jira_id: this.jiraId,
|
|
58
174
|
env: this.env,
|
|
59
175
|
title: this.title,
|
|
176
|
+
label: this.label,
|
|
60
177
|
shared_run: this.sharedRun,
|
|
61
|
-
}).filter(([, value]) => !!value)
|
|
178
|
+
}).filter(([, value]) => !!value),
|
|
62
179
|
);
|
|
180
|
+
debug('Run params', JSON.stringify(runParams, null, 2));
|
|
63
181
|
|
|
64
182
|
if (this.runId) {
|
|
65
|
-
|
|
183
|
+
debug(`Run with id ${this.runId} already created, updating...`);
|
|
184
|
+
const resp = await this.axios.put(`/api/reporter/${this.runId}`, runParams);
|
|
66
185
|
if (resp.data.artifacts) setS3Credentials(resp.data.artifacts);
|
|
67
186
|
return;
|
|
68
187
|
}
|
|
69
188
|
|
|
70
189
|
try {
|
|
71
|
-
const resp = await this.axios.post(
|
|
190
|
+
const resp = await this.axios.post(`/api/reporter`, runParams, {
|
|
72
191
|
maxContentLength: Infinity,
|
|
73
192
|
maxBodyLength: Infinity,
|
|
74
193
|
});
|
|
194
|
+
|
|
75
195
|
this.runId = resp.data.uid;
|
|
76
196
|
this.runUrl = `${this.url}/${resp.data.url.split('/').splice(3).join('/')}`;
|
|
197
|
+
this.runPublicUrl = resp.data.public_url;
|
|
198
|
+
|
|
77
199
|
if (resp.data.artifacts) setS3Credentials(resp.data.artifacts);
|
|
200
|
+
|
|
78
201
|
this.store.runUrl = this.runUrl;
|
|
202
|
+
this.store.runPublicUrl = this.runPublicUrl;
|
|
79
203
|
this.store.runId = this.runId;
|
|
80
204
|
console.log(APP_PREFIX, '📊 Report created. Report ID:', this.runId);
|
|
81
205
|
process.env.runId = this.runId;
|
|
206
|
+
debug('Run created', this.runId);
|
|
82
207
|
} catch (err) {
|
|
83
208
|
console.error(
|
|
84
209
|
APP_PREFIX,
|
|
85
|
-
'Error creating Testomat.io report, please check if your API key is valid. Skipping report',
|
|
210
|
+
'Error creating Testomat.io report, please check if your API key is valid. Skipping report | ',
|
|
211
|
+
err?.response?.statusText || err?.status || err.message,
|
|
86
212
|
);
|
|
213
|
+
printCreateIssue(err);
|
|
87
214
|
}
|
|
215
|
+
debug('"createRun" function finished');
|
|
88
216
|
}
|
|
89
217
|
|
|
90
218
|
/**
|
|
91
|
-
*
|
|
92
|
-
* @param testData
|
|
93
|
-
* @returns
|
|
219
|
+
* Decides whether to skip test reporting in case of too many request failures
|
|
220
|
+
* @param {TestData} testData
|
|
221
|
+
* @returns {boolean}
|
|
94
222
|
*/
|
|
95
|
-
|
|
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 => {
|
|
96
248
|
if (!this.isEnabled) return;
|
|
97
249
|
if (!this.runId) return;
|
|
250
|
+
if (this.#cancelTestReportingInCaseOfTooManyReqFailures(data)) return;
|
|
251
|
+
|
|
98
252
|
data.api_key = this.apiKey;
|
|
99
253
|
data.create = this.createNewTests;
|
|
254
|
+
|
|
255
|
+
if (!process.env.TESTOMATIO_STACK_PASSED && data.status === STATUS.PASSED) {
|
|
256
|
+
data.stack = null;
|
|
257
|
+
}
|
|
258
|
+
|
|
100
259
|
const json = JsonCycle.stringify(data);
|
|
101
260
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
+
}
|
|
114
279
|
console.log(
|
|
115
280
|
APP_PREFIX,
|
|
116
|
-
chalk.
|
|
117
|
-
`Report couldn't be processed:
|
|
281
|
+
chalk.yellow(`Warning: ${data?.title || ''} (${err.response?.status})`),
|
|
282
|
+
`Report couldn't be processed: ${err?.response?.data?.message}`,
|
|
118
283
|
);
|
|
119
|
-
|
|
284
|
+
printCreateIssue(err);
|
|
285
|
+
} else {
|
|
286
|
+
console.log(APP_PREFIX, chalk.blue(data?.title || ''), "Report couldn't be processed", err);
|
|
120
287
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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;
|
|
323
|
+
}
|
|
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);
|
|
332
|
+
}
|
|
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();
|
|
126
350
|
}
|
|
127
351
|
|
|
128
352
|
/**
|
|
129
|
-
* @param {import('../../types').RunData} params
|
|
130
|
-
* @returns
|
|
353
|
+
* @param {import('../../types').RunData} params
|
|
354
|
+
* @returns
|
|
131
355
|
*/
|
|
132
356
|
async finishRun(params) {
|
|
357
|
+
this.#batchUpload();
|
|
358
|
+
if (this.batch.intervalFunction) clearInterval(this.batch.intervalFunction);
|
|
359
|
+
|
|
360
|
+
debug('Finishing run...');
|
|
133
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
|
+
}
|
|
134
370
|
|
|
135
371
|
const { status, parallel } = params;
|
|
136
372
|
|
|
@@ -143,7 +379,7 @@ class TestomatioPipe {
|
|
|
143
379
|
|
|
144
380
|
try {
|
|
145
381
|
if (this.runId && !this.proceed) {
|
|
146
|
-
await this.axios.put(
|
|
382
|
+
await this.axios.put(`/api/reporter/${this.runId}`, {
|
|
147
383
|
api_key: this.apiKey,
|
|
148
384
|
status_event,
|
|
149
385
|
tests: params.tests,
|
|
@@ -151,15 +387,45 @@ class TestomatioPipe {
|
|
|
151
387
|
if (this.runUrl) {
|
|
152
388
|
console.log(APP_PREFIX, '📊 Report Saved. Report URL:', chalk.magenta(this.runUrl));
|
|
153
389
|
}
|
|
390
|
+
if (this.runPublicUrl) {
|
|
391
|
+
console.log(APP_PREFIX, '🌟 Public URL:', chalk.magenta(this.runPublicUrl));
|
|
392
|
+
}
|
|
154
393
|
}
|
|
155
394
|
if (this.runUrl && this.proceed) {
|
|
156
395
|
const notFinishedMessage = chalk.yellow.bold('Run was not finished because of $TESTOMATIO_PROCEED');
|
|
157
396
|
console.log(APP_PREFIX, `📊 ${notFinishedMessage}. Report URL: ${chalk.magenta(this.runUrl)}`);
|
|
158
397
|
console.log(APP_PREFIX, `🛬 Run to finish it: TESTOMATIO_RUN=${this.runId} npx start-test-run --finish`);
|
|
159
398
|
}
|
|
399
|
+
if (this.hasUnmatchedTests) {
|
|
400
|
+
console.log('');
|
|
401
|
+
// eslint-disable-next-line max-len
|
|
402
|
+
console.log(APP_PREFIX, chalk.yellow.bold('⚠️ Some reported tests were not found in Testomat.io project'));
|
|
403
|
+
// eslint-disable-next-line max-len
|
|
404
|
+
console.log(
|
|
405
|
+
APP_PREFIX,
|
|
406
|
+
`If you use Testomat.io as a reporter only, please re-run tests using ${chalk.bold('TESTOMATIO_CREATE=1')}`,
|
|
407
|
+
);
|
|
408
|
+
// eslint-disable-next-line max-len
|
|
409
|
+
console.log(
|
|
410
|
+
APP_PREFIX,
|
|
411
|
+
`But to keep your tests consistent it is recommended to ${chalk.bold('import tests first')}`,
|
|
412
|
+
);
|
|
413
|
+
console.log(APP_PREFIX, 'If tests were imported but still not matched, assign test IDs to your tests.');
|
|
414
|
+
console.log(APP_PREFIX, 'You can do that automatically via command line tools:');
|
|
415
|
+
console.log(APP_PREFIX, chalk.bold('npx check-tests ... --update-ids'), 'See: https://bit.ly/js-update-ids');
|
|
416
|
+
console.log(APP_PREFIX, 'or for Cucumber:');
|
|
417
|
+
// eslint-disable-next-line max-len
|
|
418
|
+
console.log(
|
|
419
|
+
APP_PREFIX,
|
|
420
|
+
chalk.bold('npx check-cucumber ... --update-ids'),
|
|
421
|
+
'See: https://bit.ly/bdd-update-ids',
|
|
422
|
+
);
|
|
423
|
+
}
|
|
160
424
|
} catch (err) {
|
|
161
425
|
console.log(APP_PREFIX, 'Error updating status, skipping...', err);
|
|
426
|
+
printCreateIssue(err);
|
|
162
427
|
}
|
|
428
|
+
debug('Run finished');
|
|
163
429
|
}
|
|
164
430
|
|
|
165
431
|
toString() {
|
|
@@ -167,19 +433,36 @@ class TestomatioPipe {
|
|
|
167
433
|
}
|
|
168
434
|
}
|
|
169
435
|
|
|
170
|
-
|
|
171
|
-
|
|
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;
|
|
172
450
|
|
|
173
|
-
|
|
174
|
-
|
|
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
|
+
}
|
|
175
458
|
|
|
176
|
-
|
|
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
|
+
};
|
|
177
467
|
|
|
178
|
-
|
|
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
|
-
}
|
|
468
|
+
module.exports = TestomatioPipe;
|
|
@@ -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
|
+
};
|