@testomatio/reporter 1.2.1-beta → 1.2.1-beta.codecept-id
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 +136 -57
- package/lib/adapter/cucumber/current.js +103 -60
- package/lib/adapter/cucumber/legacy.js +27 -12
- 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 -11
- package/lib/adapter/mocha.js +103 -51
- package/lib/adapter/playwright.js +100 -31
- 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 +193 -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 +182 -55
- 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} +145 -12
- package/lib/xmlReader.js +211 -122
- package/package.json +18 -8
- package/lib/_ArtifactStorageOld.js +0 -142
- package/lib/artifactStorage.js +0 -25
- package/lib/dataStorage.js +0 -180
- package/lib/logger.js +0 -278
package/lib/client.js
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
const debug = require('debug')('@testomatio/reporter:client');
|
|
2
2
|
const createCallsiteRecord = require('callsite-record');
|
|
3
3
|
const { sep, join } = require('path');
|
|
4
|
+
const { minimatch } = require('minimatch');
|
|
4
5
|
const fs = require('fs');
|
|
5
6
|
const chalk = require('chalk');
|
|
6
7
|
const { randomUUID } = require('crypto');
|
|
7
8
|
const upload = require('./fileUploader');
|
|
8
9
|
const { APP_PREFIX } = require('./constants');
|
|
9
10
|
const pipesFactory = require('./pipe');
|
|
10
|
-
const
|
|
11
|
+
const { glob } = require('glob');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
|
|
14
|
+
let listOfTestFilesToExcludeFromReport = null;
|
|
11
15
|
|
|
12
16
|
/**
|
|
13
17
|
* @typedef {import('../types').TestData} TestData
|
|
@@ -22,39 +26,86 @@ class Client {
|
|
|
22
26
|
* @param {*} params
|
|
23
27
|
*/
|
|
24
28
|
constructor(params = {}) {
|
|
25
|
-
this.parallel = params.parallel;
|
|
26
29
|
const store = {};
|
|
27
30
|
this.uuid = randomUUID();
|
|
28
31
|
this.pipes = pipesFactory(params, store);
|
|
29
32
|
this.queue = Promise.resolve();
|
|
30
33
|
this.totalUploaded = 0;
|
|
34
|
+
this.failedToUpload = 0;
|
|
31
35
|
this.version = JSON.parse(fs.readFileSync(join(__dirname, '..', 'package.json')).toString()).version;
|
|
36
|
+
this.executionList = Promise.resolve();
|
|
37
|
+
|
|
32
38
|
console.log(APP_PREFIX, `Testomatio Reporter v${this.version}`);
|
|
33
39
|
}
|
|
34
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Asynchronously prepares the execution list for running tests through various pipes.
|
|
43
|
+
* Each pipe in the client is checked for enablement,
|
|
44
|
+
* and if all pipes are disabled, the function returns a resolved Promise.
|
|
45
|
+
* Otherwise, it executes the `prepareRun` method for each enabled pipe and collects the results.
|
|
46
|
+
* The results are then filtered to remove any undefined values.
|
|
47
|
+
* If no valid results are found, the function returns undefined.
|
|
48
|
+
* Otherwise, it returns the first non-empty array from the filtered results.
|
|
49
|
+
*
|
|
50
|
+
* @param {Object} params - The options for preparing the test execution list.
|
|
51
|
+
* @param {string} params.pipe - Name of the executed pipe.
|
|
52
|
+
* @param {string} params.pipeOptions - Filter option.
|
|
53
|
+
* @returns {Promise<any>} - A Promise that resolves to an
|
|
54
|
+
* array containing the prepared execution list,
|
|
55
|
+
* or resolves to undefined if no valid results are found or if all pipes are disabled.
|
|
56
|
+
*/
|
|
57
|
+
async prepareRun(params) {
|
|
58
|
+
const { pipe, pipeOptions } = params;
|
|
59
|
+
// all pipes disabled, skipping
|
|
60
|
+
if (!this.pipes.some(p => p.isEnabled)) {
|
|
61
|
+
return Promise.resolve();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const filterPipe = this.pipes.find(p => p.constructor.name.toLowerCase() === `${pipe.toLowerCase()}pipe`);
|
|
66
|
+
|
|
67
|
+
if (!filterPipe.isEnabled) {
|
|
68
|
+
// TODO:for the future for the another pipes
|
|
69
|
+
console.warn(
|
|
70
|
+
APP_PREFIX,
|
|
71
|
+
`At the moment processing is available only for the "testomatio" key. Example: "testomatio:tag-name=xxx"`,
|
|
72
|
+
);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const results = await Promise.all(
|
|
77
|
+
this.pipes.map(async p => ({ pipe: p.toString(), result: await p.prepareRun(pipeOptions) })),
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const result = results.filter(p => p.pipe.includes('Testomatio'))[0]?.result;
|
|
81
|
+
|
|
82
|
+
if (!result || result.length === 0) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
debug('Execution tests list', result);
|
|
87
|
+
|
|
88
|
+
return result;
|
|
89
|
+
} catch (err) {
|
|
90
|
+
console.error(APP_PREFIX, err);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
35
94
|
/**
|
|
36
95
|
* Used to create a new Test run
|
|
37
96
|
*
|
|
38
97
|
* @returns {Promise<any>} - resolves to Run id which should be used to update / add test
|
|
39
98
|
*/
|
|
40
99
|
createRun() {
|
|
100
|
+
debug('Creating run...');
|
|
41
101
|
// all pipes disabled, skipping
|
|
42
|
-
if (!this.pipes
|
|
43
|
-
|
|
44
|
-
const runParams = {
|
|
45
|
-
title: this.title,
|
|
46
|
-
parallel: this.parallel,
|
|
47
|
-
env: this.env,
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
global.testomatioArtifacts = [];
|
|
51
|
-
// TODO: create global storage
|
|
102
|
+
if (!this.pipes?.filter(p => p.isEnabled).length) return Promise.resolve();
|
|
52
103
|
|
|
53
104
|
this.queue = this.queue
|
|
54
|
-
.then(() => Promise.all(this.pipes.map(p => p.createRun(
|
|
105
|
+
.then(() => Promise.all(this.pipes.map(p => p.createRun())))
|
|
55
106
|
.catch(err => console.log(APP_PREFIX, err))
|
|
56
107
|
.then(() => undefined); // fixes return type
|
|
57
|
-
debug('Run', this.queue);
|
|
108
|
+
// debug('Run', this.queue);
|
|
58
109
|
return this.queue;
|
|
59
110
|
}
|
|
60
111
|
|
|
@@ -63,12 +114,13 @@ class Client {
|
|
|
63
114
|
*
|
|
64
115
|
* @param {string|undefined} status
|
|
65
116
|
* @param {TestData} [testData]
|
|
66
|
-
* @param {string[]} [storeArtifacts]
|
|
67
117
|
* @returns {Promise<PipeResult[]>}
|
|
68
118
|
*/
|
|
69
|
-
async addTestRun(status, testData
|
|
119
|
+
async addTestRun(status, testData) {
|
|
70
120
|
// all pipes disabled, skipping
|
|
71
|
-
if (!this.pipes
|
|
121
|
+
if (!this.pipes?.filter(p => p.isEnabled).length) return [];
|
|
122
|
+
|
|
123
|
+
if (isTestShouldBeExculedFromReport(testData)) return [];
|
|
72
124
|
|
|
73
125
|
if (!testData)
|
|
74
126
|
testData = {
|
|
@@ -85,42 +137,31 @@ class Client {
|
|
|
85
137
|
steps,
|
|
86
138
|
code = null,
|
|
87
139
|
title,
|
|
140
|
+
file,
|
|
88
141
|
suite_title,
|
|
89
142
|
suite_id,
|
|
90
143
|
test_id,
|
|
144
|
+
manuallyAttachedArtifacts,
|
|
145
|
+
meta,
|
|
91
146
|
} = testData;
|
|
92
147
|
let { message = '' } = testData;
|
|
93
148
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
let stack = '';
|
|
97
|
-
|
|
149
|
+
let errorFormatted = '';
|
|
98
150
|
if (error) {
|
|
99
|
-
|
|
151
|
+
errorFormatted += this.formatError(error) || '';
|
|
100
152
|
message = error?.message;
|
|
101
153
|
}
|
|
102
|
-
if (steps) {
|
|
103
|
-
stack = this.formatSteps(stack, steps);
|
|
104
|
-
}
|
|
105
154
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
if (stack) stack += '\n\n';
|
|
109
|
-
stack += testLogs;
|
|
155
|
+
// Attach logs
|
|
156
|
+
const fullLogs = this.formatLogs({ error: errorFormatted, steps, logs: testData.logs });
|
|
110
157
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
files.push(...storeArtifacts);
|
|
114
|
-
}
|
|
158
|
+
// add artifacts
|
|
159
|
+
if (manuallyAttachedArtifacts?.length) files.push(...manuallyAttachedArtifacts);
|
|
115
160
|
|
|
116
|
-
|
|
117
|
-
debug('CLIENT global[testomatioArtifacts]', global.testomatioArtifacts);
|
|
118
|
-
files.push(...global.testomatioArtifacts);
|
|
119
|
-
global.testomatioArtifacts = [];
|
|
120
|
-
}
|
|
161
|
+
const uploadedFiles = [];
|
|
121
162
|
|
|
122
|
-
for (const
|
|
123
|
-
uploadedFiles.push(upload.uploadFileByPath(
|
|
163
|
+
for (const f of files) {
|
|
164
|
+
uploadedFiles.push(upload.uploadFileByPath(f, this.uuid));
|
|
124
165
|
}
|
|
125
166
|
|
|
126
167
|
for (const [idx, buffer] of filesBuffers.entries()) {
|
|
@@ -128,18 +169,22 @@ class Client {
|
|
|
128
169
|
uploadedFiles.push(upload.uploadFileAsBuffer(buffer, fileName, this.uuid));
|
|
129
170
|
}
|
|
130
171
|
|
|
131
|
-
const artifacts = await Promise.all(uploadedFiles);
|
|
172
|
+
const artifacts = (await Promise.all(uploadedFiles)).filter(n => !!n);
|
|
132
173
|
|
|
133
|
-
|
|
174
|
+
if (artifacts.length < uploadedFiles.length) {
|
|
175
|
+
const failedUploading = uploadedFiles.length - artifacts.length;
|
|
176
|
+
this.failedToUpload += failedUploading;
|
|
177
|
+
}
|
|
134
178
|
|
|
135
|
-
this.totalUploaded +=
|
|
179
|
+
this.totalUploaded += artifacts.length;
|
|
136
180
|
|
|
137
181
|
const data = {
|
|
138
182
|
files,
|
|
139
183
|
steps,
|
|
140
184
|
status,
|
|
141
|
-
stack,
|
|
185
|
+
stack: fullLogs,
|
|
142
186
|
example,
|
|
187
|
+
file,
|
|
143
188
|
code,
|
|
144
189
|
title,
|
|
145
190
|
suite_title,
|
|
@@ -148,19 +193,24 @@ class Client {
|
|
|
148
193
|
message,
|
|
149
194
|
run_time: parseFloat(time),
|
|
150
195
|
artifacts,
|
|
196
|
+
meta,
|
|
151
197
|
};
|
|
152
198
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
199
|
+
// debug('Adding test run...', data);
|
|
200
|
+
|
|
201
|
+
this.queue = this.queue.then(() =>
|
|
202
|
+
Promise.all(
|
|
203
|
+
this.pipes.map(async pipe => {
|
|
204
|
+
try {
|
|
205
|
+
const result = await pipe.addTest(data);
|
|
206
|
+
return { pipe: pipe.toString(), result };
|
|
207
|
+
} catch (err) {
|
|
208
|
+
console.log(APP_PREFIX, pipe.toString(), err);
|
|
209
|
+
}
|
|
210
|
+
}),
|
|
211
|
+
),
|
|
212
|
+
);
|
|
213
|
+
|
|
164
214
|
return this.queue;
|
|
165
215
|
}
|
|
166
216
|
|
|
@@ -172,23 +222,36 @@ class Client {
|
|
|
172
222
|
* @returns {Promise<any>} - A Promise that resolves when finishes the run.
|
|
173
223
|
*/
|
|
174
224
|
updateRunStatus(status, isParallel = false) {
|
|
225
|
+
debug('Updating run status...');
|
|
175
226
|
// all pipes disabled, skipping
|
|
176
|
-
if (!this.pipes
|
|
227
|
+
if (!this.pipes?.filter(p => p.isEnabled).length) return Promise.resolve();
|
|
177
228
|
|
|
178
229
|
const runParams = { status, parallel: isParallel };
|
|
179
230
|
|
|
180
231
|
this.queue = this.queue
|
|
181
232
|
.then(() => Promise.all(this.pipes.map(p => p.finishRun(runParams))))
|
|
182
233
|
.then(() => {
|
|
183
|
-
debug('TOTAL
|
|
234
|
+
debug('TOTAL artifacts', this.totalUploaded);
|
|
235
|
+
if (this.totalUploaded && !upload.isArtifactsEnabled())
|
|
236
|
+
debug(`${this.totalUploaded} artifacts are not uploaded, because artifacts uploading is not enabled`);
|
|
184
237
|
|
|
185
|
-
if (
|
|
238
|
+
if (this.totalUploaded && upload.isArtifactsEnabled()) {
|
|
186
239
|
console.log(
|
|
187
240
|
APP_PREFIX,
|
|
188
|
-
`🗄️
|
|
241
|
+
`🗄️ ${this.totalUploaded} artifacts ${
|
|
189
242
|
process.env.TESTOMATIO_PRIVATE_ARTIFACTS ? 'privately' : chalk.bold('publicly')
|
|
190
|
-
} uploaded to S3 bucket
|
|
243
|
+
} uploaded to S3 bucket`,
|
|
191
244
|
);
|
|
245
|
+
|
|
246
|
+
if (this.failedToUpload > 0) {
|
|
247
|
+
console.log(
|
|
248
|
+
APP_PREFIX,
|
|
249
|
+
chalk.yellow(
|
|
250
|
+
`Some artifacts were not uploaded. ${this.failedToUpload} artifacts could not be uploaded.
|
|
251
|
+
Run tests with DEBUG="@testomatio/reporter:file-uploader" to see details"`,
|
|
252
|
+
),
|
|
253
|
+
);
|
|
254
|
+
}
|
|
192
255
|
}
|
|
193
256
|
})
|
|
194
257
|
.catch(err => console.log(APP_PREFIX, err));
|
|
@@ -196,15 +259,27 @@ class Client {
|
|
|
196
259
|
return this.queue;
|
|
197
260
|
}
|
|
198
261
|
|
|
199
|
-
|
|
200
|
-
|
|
262
|
+
/**
|
|
263
|
+
* Returns the formatted stack including the stack trace, steps, and logs.
|
|
264
|
+
* @returns {string}
|
|
265
|
+
*/
|
|
266
|
+
formatLogs({ error, steps, logs }) {
|
|
267
|
+
error = error?.trim();
|
|
268
|
+
steps = steps?.trim();
|
|
269
|
+
logs = logs?.trim();
|
|
270
|
+
|
|
271
|
+
let testLogs = '';
|
|
272
|
+
if (steps) testLogs += `${chalk.bold.blue('################[ Steps ]################')}\n${steps}\n\n`;
|
|
273
|
+
if (logs) testLogs += `${chalk.bold.gray('################[ Logs ]################')}\n${logs}\n\n`;
|
|
274
|
+
if (error) testLogs += `${chalk.bold.red('################[ Failure ]################')}\n${error}`;
|
|
275
|
+
return testLogs;
|
|
201
276
|
}
|
|
202
277
|
|
|
203
278
|
formatError(error, message) {
|
|
204
279
|
if (!message) message = error.message;
|
|
205
280
|
if (error.inspect) message = error.inspect() || '';
|
|
206
281
|
|
|
207
|
-
let stack =
|
|
282
|
+
let stack = `${message}\n`;
|
|
208
283
|
|
|
209
284
|
// diffs for mocha, cypress, codeceptjs style
|
|
210
285
|
if (error.actual && error.expected) {
|
|
@@ -214,17 +289,21 @@ class Client {
|
|
|
214
289
|
stack += '\n\n';
|
|
215
290
|
}
|
|
216
291
|
|
|
292
|
+
const customFilter = process.env.TESTOMATIO_STACK_IGNORE;
|
|
293
|
+
|
|
217
294
|
try {
|
|
295
|
+
let hasFrame = false;
|
|
218
296
|
const record = createCallsiteRecord({
|
|
219
297
|
forError: error,
|
|
298
|
+
isCallsiteFrame: frame => {
|
|
299
|
+
if (customFilter && minimatch(frame.getFileName(), customFilter)) return false;
|
|
300
|
+
if (hasFrame) return false;
|
|
301
|
+
if (isNotInternalFrame(frame)) hasFrame = true;
|
|
302
|
+
return hasFrame;
|
|
303
|
+
},
|
|
220
304
|
});
|
|
221
305
|
if (record && !record.filename.startsWith('http')) {
|
|
222
|
-
stack += record.renderSync({
|
|
223
|
-
stackFilter: frame =>
|
|
224
|
-
frame.getFileName().indexOf(sep) > -1 &&
|
|
225
|
-
frame.getFileName().indexOf('node_modules') < 0 &&
|
|
226
|
-
frame.getFileName().indexOf('internal') < 0,
|
|
227
|
-
});
|
|
306
|
+
stack += record.renderSync({ stackFilter: isNotInternalFrame });
|
|
228
307
|
}
|
|
229
308
|
return stack;
|
|
230
309
|
} catch (e) {
|
|
@@ -233,4 +312,49 @@ class Client {
|
|
|
233
312
|
}
|
|
234
313
|
}
|
|
235
314
|
|
|
315
|
+
function isNotInternalFrame(frame) {
|
|
316
|
+
return (
|
|
317
|
+
frame.getFileName() &&
|
|
318
|
+
frame.getFileName().includes(sep) &&
|
|
319
|
+
!frame.getFileName().includes('node_modules') &&
|
|
320
|
+
!frame.getFileName().includes('internal')
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
*
|
|
326
|
+
* @param {TestData} testData
|
|
327
|
+
* @returns boolean
|
|
328
|
+
*/
|
|
329
|
+
function isTestShouldBeExculedFromReport(testData) {
|
|
330
|
+
// const fileName = path.basename(test.location?.file || '');
|
|
331
|
+
const globExcludeFilesPattern = process.env.TESTOMATIO_EXCLUDE_FILES_FROM_REPORT_GLOB_PATTERN;
|
|
332
|
+
if (!globExcludeFilesPattern) return false;
|
|
333
|
+
|
|
334
|
+
if (!testData.file) {
|
|
335
|
+
debug('No "file" property found for test ', testData.title);
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const excludeParretnsList = globExcludeFilesPattern.split(';');
|
|
340
|
+
|
|
341
|
+
// as scanning files is time consuming operation, just save the result in variable to avoid multiple scans
|
|
342
|
+
if (!listOfTestFilesToExcludeFromReport) {
|
|
343
|
+
// list of files with relative paths
|
|
344
|
+
listOfTestFilesToExcludeFromReport = glob.sync(excludeParretnsList, { ignore: '**/node_modules/**' });
|
|
345
|
+
debug('Tests from next files will not be reported:', listOfTestFilesToExcludeFromReport);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const testFileRelativePath = path.relative(process.cwd(), testData.file);
|
|
349
|
+
|
|
350
|
+
// no files found matching the exclusion pattern
|
|
351
|
+
if (!listOfTestFilesToExcludeFromReport.length) return false;
|
|
352
|
+
|
|
353
|
+
if (listOfTestFilesToExcludeFromReport.includes(testFileRelativePath)) {
|
|
354
|
+
debug(`Excluding test '${testData.title}' <${testFileRelativePath}> from reporting`);
|
|
355
|
+
return true;
|
|
356
|
+
}
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
|
|
236
360
|
module.exports = Client;
|
package/lib/config.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// This file is used to read environment variables from .env file
|
|
2
|
+
|
|
3
|
+
// require('dotenv').config({ path: process.env.TESTOMATIO_ENV_FILE_PATH });
|
|
4
|
+
|
|
5
|
+
const debug = require('debug')('@testomatio/reporter:config');
|
|
6
|
+
|
|
7
|
+
/* for possibility to use multiple env files (reading different paths)
|
|
8
|
+
const dotenv = require('dotenv');
|
|
9
|
+
const envFileVars = dotenv.config({ path: '.env' }).parsed; */
|
|
10
|
+
|
|
11
|
+
if (process.env.TESTOMATIO_API_KEY) {
|
|
12
|
+
process.env.TESTOMATIO = process.env.TESTOMATIO_API_KEY;
|
|
13
|
+
}
|
|
14
|
+
if (process.env.TESTOMATIO_TOKEN) {
|
|
15
|
+
process.env.TESTOMATIO = process.env.TESTOMATIO_TOKEN;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (process.env.TESTOMATIO === 'undefined')
|
|
19
|
+
console.error('TESTOMATIO is "undefined". Something went wrong. Contact dev team.');
|
|
20
|
+
|
|
21
|
+
// select only TESTOMATIO related variables (only to print them in debug)
|
|
22
|
+
const testomatioEnvVars =
|
|
23
|
+
Object.keys(process.env)
|
|
24
|
+
.filter(key => key.startsWith('TESTOMATIO') || key.startsWith('S3_'))
|
|
25
|
+
.reduce((obj, key) => {
|
|
26
|
+
obj[key] = process.env[key];
|
|
27
|
+
return obj;
|
|
28
|
+
}, {}) || {};
|
|
29
|
+
debug('TESTOMATIO variables:', testomatioEnvVars);
|
|
30
|
+
|
|
31
|
+
// includes variables from .env file and process.env
|
|
32
|
+
const config = process.env;
|
|
33
|
+
|
|
34
|
+
module.exports = config;
|
package/lib/constants.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
const chalk = require('chalk');
|
|
2
|
+
const os = require('os');
|
|
3
|
+
const path = require('path');
|
|
2
4
|
|
|
3
5
|
const APP_PREFIX = chalk.gray('[TESTOMATIO]');
|
|
4
|
-
const
|
|
6
|
+
const AXIOS_TIMEOUT = 20 * 1000; // sum = 20sec
|
|
7
|
+
const AXIOS_RETRY_TIMEOUT = 5 * 1000; // sum = 5sec
|
|
5
8
|
|
|
6
|
-
const
|
|
7
|
-
mainDir: "testomatio_tmp",
|
|
8
|
-
}
|
|
9
|
+
const TESTOMAT_TMP_STORAGE_DIR = path.join(os.tmpdir(), 'testomatio_tmp');
|
|
9
10
|
|
|
10
11
|
const CSV_HEADERS = [
|
|
11
12
|
{ id: 'suite_title', title: 'Suite_title' },
|
|
@@ -21,11 +22,22 @@ const STATUS = {
|
|
|
21
22
|
SKIPPED: 'skipped',
|
|
22
23
|
FINISHED: 'finished',
|
|
23
24
|
};
|
|
25
|
+
// html pipe var
|
|
26
|
+
const HTML_REPORT = {
|
|
27
|
+
FOLDER: 'html-report',
|
|
28
|
+
REPORT_DEFAULT_NAME: 'testomatio-report.html',
|
|
29
|
+
TEMPLATE_NAME: 'testomatio.hbs',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const testomatLogoURL = 'https://avatars.githubusercontent.com/u/59105116?s=36&v=4';
|
|
24
33
|
|
|
25
34
|
module.exports = {
|
|
26
35
|
APP_PREFIX,
|
|
27
|
-
|
|
28
|
-
TESTOMAT_TMP_STORAGE,
|
|
36
|
+
TESTOMAT_TMP_STORAGE_DIR,
|
|
29
37
|
CSV_HEADERS,
|
|
30
38
|
STATUS,
|
|
31
|
-
|
|
39
|
+
HTML_REPORT,
|
|
40
|
+
AXIOS_TIMEOUT,
|
|
41
|
+
AXIOS_RETRY_TIMEOUT,
|
|
42
|
+
testomatLogoURL,
|
|
43
|
+
};
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
const debug = require('debug')('@testomatio/reporter:storage');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const { join } = require('path');
|
|
6
|
+
const { TESTOMAT_TMP_STORAGE_DIR } = require('./constants');
|
|
7
|
+
const { fileSystem, testRunnerHelper } = require('./utils/utils');
|
|
8
|
+
const crypto = require('crypto');
|
|
9
|
+
|
|
10
|
+
class DataStorage {
|
|
11
|
+
static #instance;
|
|
12
|
+
|
|
13
|
+
context;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
*
|
|
17
|
+
* @returns {DataStorage}
|
|
18
|
+
*/
|
|
19
|
+
static getInstance() {
|
|
20
|
+
if (!this.#instance) {
|
|
21
|
+
this.#instance = new DataStorage();
|
|
22
|
+
}
|
|
23
|
+
return this.#instance;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
setContext(context) {
|
|
27
|
+
this.context = context;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Creates data storage instance as singleton
|
|
32
|
+
* Stores data to global variable or to file depending on what is applicable for current test runner (adapter)
|
|
33
|
+
* Recommend to use composition while using this class (instead of inheritance).
|
|
34
|
+
* ! Also the class which will use data storage should be singleton (to avoid data loss).
|
|
35
|
+
*/
|
|
36
|
+
constructor() {
|
|
37
|
+
// some frameworks use global variable to store data, some use file storage
|
|
38
|
+
this.isFileStorage = true;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Puts any data to storage (file or global variable).
|
|
43
|
+
* If file: stores data as text, if global variable – stores as array of data.
|
|
44
|
+
* @param {'log' | 'artifact' | 'keyvalue'} dataType
|
|
45
|
+
* @param {*} data anything you want to store (string, object, array, etc)
|
|
46
|
+
* @param {*} context could be testId or any context (test name, suite name, including their IDs etc)
|
|
47
|
+
* suite name + test name is used by default
|
|
48
|
+
* @returns
|
|
49
|
+
*/
|
|
50
|
+
putData(dataType, data, context = null) {
|
|
51
|
+
if (!dataType || !data) return;
|
|
52
|
+
|
|
53
|
+
context = context || this.context || testRunnerHelper.getNameOfCurrentlyRunningTest();
|
|
54
|
+
if (!context) {
|
|
55
|
+
debug(`No context provided for "${dataType}" data:`, data);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const contextHash = stringToMD5Hash(context);
|
|
59
|
+
|
|
60
|
+
if (this.isFileStorage) {
|
|
61
|
+
const dataDirPath = path.join(TESTOMAT_TMP_STORAGE_DIR, dataType);
|
|
62
|
+
fileSystem.createDir(dataDirPath);
|
|
63
|
+
this.#putDataToFile(dataType, data, contextHash);
|
|
64
|
+
} else {
|
|
65
|
+
this.#putDataToGlobalVar(dataType, data, contextHash);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Returns data, stored for specific test/context (or data which was stored without test id specified).
|
|
71
|
+
* This method will get data from global variable and/or from from file (previosly saved with put method).
|
|
72
|
+
*
|
|
73
|
+
* @param {'log' | 'artifact' | 'keyvalue'} dataType
|
|
74
|
+
* @param {string} context
|
|
75
|
+
* @returns {any []} array of data (any type), null (if no data found for context) or string (if data type is log)
|
|
76
|
+
*/
|
|
77
|
+
getData(dataType, context) {
|
|
78
|
+
// TODO: think if it could be useful
|
|
79
|
+
// context = context || this.context || testRunnerHelper.getNameOfCurrentlyRunningTest();
|
|
80
|
+
|
|
81
|
+
if (!context) {
|
|
82
|
+
debug(`Trying to get "${dataType}" data without context`);
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const contextHash = stringToMD5Hash(context);
|
|
87
|
+
|
|
88
|
+
let testDataFromFile = [];
|
|
89
|
+
let testDataFromGlobalVar = [];
|
|
90
|
+
|
|
91
|
+
if (global?.testomatioDataStore) {
|
|
92
|
+
testDataFromGlobalVar = this.#getDataFromGlobalVar(dataType, contextHash);
|
|
93
|
+
if (testDataFromGlobalVar) {
|
|
94
|
+
if (testDataFromGlobalVar.length) return testDataFromGlobalVar;
|
|
95
|
+
}
|
|
96
|
+
// don't return nothing if no data in global variable
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
testDataFromFile = this.#getDataFromFile(dataType, contextHash);
|
|
100
|
+
|
|
101
|
+
if (testDataFromFile.length) {
|
|
102
|
+
return testDataFromFile;
|
|
103
|
+
}
|
|
104
|
+
debug(`No "${dataType}" data for context "${contextHash}" in both file and global variable`);
|
|
105
|
+
|
|
106
|
+
// in case no data found for context
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* @param {'log' | 'artifact' | 'keyvalue'} dataType
|
|
112
|
+
* @param {string} context
|
|
113
|
+
* @returns aray of data (any type)
|
|
114
|
+
*/
|
|
115
|
+
#getDataFromGlobalVar(dataType, context) {
|
|
116
|
+
try {
|
|
117
|
+
if (global?.testomatioDataStore[dataType]) {
|
|
118
|
+
const testData = global.testomatioDataStore[dataType][context];
|
|
119
|
+
if (testData) debug(`"${dataType}" data for constext "${context}":`, testData.join(', '));
|
|
120
|
+
return testData || [];
|
|
121
|
+
}
|
|
122
|
+
// debug(`No ${this.dataType} data for context ${context} in <global> storage`);
|
|
123
|
+
return [];
|
|
124
|
+
} catch (e) {
|
|
125
|
+
// there could be no data, ignore
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* @param {'log' | 'artifact' | 'keyvalue'} dataType
|
|
131
|
+
* @param {*} context
|
|
132
|
+
* @returns array of data (any type)
|
|
133
|
+
*/
|
|
134
|
+
#getDataFromFile(dataType, context) {
|
|
135
|
+
const dataDirPath = path.join(TESTOMAT_TMP_STORAGE_DIR, dataType);
|
|
136
|
+
try {
|
|
137
|
+
const filepath = join(dataDirPath, `${dataType}_${context}`);
|
|
138
|
+
if (fs.existsSync(filepath)) {
|
|
139
|
+
const testDataAsText = fs.readFileSync(filepath, 'utf-8');
|
|
140
|
+
if (testDataAsText) debug(`"${dataType}" data for context "${context}":`, testDataAsText);
|
|
141
|
+
const testDataArr = testDataAsText?.split(os.EOL) || [];
|
|
142
|
+
return testDataArr;
|
|
143
|
+
}
|
|
144
|
+
// debug(`No ${this.dataType} data for ${context} in <file> storage`);
|
|
145
|
+
return [];
|
|
146
|
+
} catch (e) {
|
|
147
|
+
// there could be no data, ignore
|
|
148
|
+
}
|
|
149
|
+
return [];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Puts data to global variable. Unlike the file storage, stores data in array (file storage just append as string).
|
|
154
|
+
* @param {'log' | 'artifact' | 'keyvalue'} dataType
|
|
155
|
+
* @param {*} data
|
|
156
|
+
* @param {*} context
|
|
157
|
+
*/
|
|
158
|
+
#putDataToGlobalVar(dataType, data, context) {
|
|
159
|
+
debug('Saving data to global variable for ', context, ':', data);
|
|
160
|
+
if (!global.testomatioDataStore) global.testomatioDataStore = {};
|
|
161
|
+
if (!global.testomatioDataStore?.[dataType]) global.testomatioDataStore[dataType] = {};
|
|
162
|
+
|
|
163
|
+
if (!global.testomatioDataStore?.[dataType][context]) global.testomatioDataStore[dataType][context] = [];
|
|
164
|
+
global.testomatioDataStore[dataType][context].push(data);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Puts data to file. Unlike the global variable storage, stores data as string
|
|
169
|
+
* @param {'log' | 'artifact' | 'keyvalue'} dataType
|
|
170
|
+
* @param {*} data
|
|
171
|
+
* @param {string} context
|
|
172
|
+
* @returns
|
|
173
|
+
*/
|
|
174
|
+
#putDataToFile(dataType, data, context) {
|
|
175
|
+
const dataDirPath = path.join(TESTOMAT_TMP_STORAGE_DIR, dataType);
|
|
176
|
+
if (typeof data !== 'string') data = JSON.stringify(data);
|
|
177
|
+
const filename = `${dataType}_${context}`;
|
|
178
|
+
const filepath = join(dataDirPath, filename);
|
|
179
|
+
if (!fs.existsSync(dataDirPath)) fileSystem.createDir(dataDirPath);
|
|
180
|
+
debug(`Saving data to file for context "${context}" to ${filepath}. Data: ${JSON.stringify(data)}`);
|
|
181
|
+
|
|
182
|
+
// append new line if file already exists (in this case its definitely includes some data)
|
|
183
|
+
if (fs.existsSync(filepath)) {
|
|
184
|
+
fs.appendFileSync(filepath, os.EOL + data, 'utf-8');
|
|
185
|
+
} else {
|
|
186
|
+
fs.writeFileSync(filepath, data, 'utf-8');
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function stringToMD5Hash(str) {
|
|
192
|
+
const md5 = crypto.createHash('md5');
|
|
193
|
+
md5.update(str);
|
|
194
|
+
const hash = md5.digest('hex');
|
|
195
|
+
|
|
196
|
+
return hash;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
module.exports.dataStorage = DataStorage.getInstance();
|
|
200
|
+
module.exports.stringToMD5Hash = stringToMD5Hash;
|
|
201
|
+
|
|
202
|
+
// TODO: consider using fs promises instead of writeSync/appendFileSync to
|
|
203
|
+
// prevent blocking and improve performance (probably queue usage will be required)
|