@testomatio/reporter 1.2.0-beta-4 → 1.2.0
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 +27 -18
- package/lib/adapter/codecept.js +83 -11
- package/lib/adapter/cucumber/current.js +17 -10
- package/lib/adapter/cucumber/legacy.js +4 -3
- package/lib/adapter/cypress-plugin/index.js +51 -24
- package/lib/adapter/jest.js +47 -23
- package/lib/adapter/mocha.js +99 -45
- package/lib/adapter/playwright.js +94 -32
- package/lib/bin/startTest.js +8 -7
- package/lib/client.js +119 -62
- package/lib/config.js +34 -0
- package/lib/constants.js +9 -7
- package/lib/data-storage.js +203 -0
- package/lib/fileUploader.js +125 -46
- package/lib/junit-adapter/index.js +3 -2
- package/lib/pipe/gitlab.js +1 -1
- package/lib/pipe/html.js +248 -246
- package/lib/pipe/testomatio.js +73 -50
- package/lib/reporter-functions.js +46 -0
- package/lib/reporter.js +11 -10
- package/lib/services/artifacts.js +57 -0
- package/lib/services/index.js +13 -0
- package/lib/services/key-values.js +58 -0
- package/lib/{logger.js → services/logger.js} +58 -66
- package/lib/utils/utils.js +28 -2
- package/lib/xmlReader.js +106 -105
- package/package.json +5 -3
- package/lib/_ArtifactStorageOld.js +0 -142
- package/lib/artifactStorage.js +0 -25
- package/lib/dataStorage.js +0 -232
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
|
+
const { minimatch } = require('minimatch');
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const chalk = require('chalk');
|
|
7
7
|
const { randomUUID } = require('crypto');
|
|
8
8
|
const upload = require('./fileUploader');
|
|
9
9
|
const { APP_PREFIX } = require('./constants');
|
|
10
10
|
const pipesFactory = require('./pipe');
|
|
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
|
|
@@ -27,6 +31,7 @@ class Client {
|
|
|
27
31
|
this.pipes = pipesFactory(params, store);
|
|
28
32
|
this.queue = Promise.resolve();
|
|
29
33
|
this.totalUploaded = 0;
|
|
34
|
+
this.failedToUpload = 0;
|
|
30
35
|
this.version = JSON.parse(fs.readFileSync(join(__dirname, '..', 'package.json')).toString()).version;
|
|
31
36
|
this.executionList = Promise.resolve();
|
|
32
37
|
|
|
@@ -35,7 +40,7 @@ class Client {
|
|
|
35
40
|
|
|
36
41
|
/**
|
|
37
42
|
* Asynchronously prepares the execution list for running tests through various pipes.
|
|
38
|
-
* Each pipe in the client is checked for enablement,
|
|
43
|
+
* Each pipe in the client is checked for enablement,
|
|
39
44
|
* and if all pipes are disabled, the function returns a resolved Promise.
|
|
40
45
|
* Otherwise, it executes the `prepareRun` method for each enabled pipe and collects the results.
|
|
41
46
|
* The results are then filtered to remove any undefined values.
|
|
@@ -45,7 +50,7 @@ class Client {
|
|
|
45
50
|
* @param {Object} params - The options for preparing the test execution list.
|
|
46
51
|
* @param {string} params.pipe - Name of the executed pipe.
|
|
47
52
|
* @param {string} params.pipeOptions - Filter option.
|
|
48
|
-
* @returns {Promise<any>} - A Promise that resolves to an
|
|
53
|
+
* @returns {Promise<any>} - A Promise that resolves to an
|
|
49
54
|
* array containing the prepared execution list,
|
|
50
55
|
* or resolves to undefined if no valid results are found or if all pipes are disabled.
|
|
51
56
|
*/
|
|
@@ -62,22 +67,22 @@ class Client {
|
|
|
62
67
|
if (!filterPipe.isEnabled) {
|
|
63
68
|
// TODO:for the future for the another pipes
|
|
64
69
|
console.warn(
|
|
65
|
-
APP_PREFIX,
|
|
66
|
-
`At the moment processing is available only for the "testomatio" key. Example: "testomatio:tag-name=xxx"
|
|
70
|
+
APP_PREFIX,
|
|
71
|
+
`At the moment processing is available only for the "testomatio" key. Example: "testomatio:tag-name=xxx"`,
|
|
67
72
|
);
|
|
68
73
|
return;
|
|
69
74
|
}
|
|
70
75
|
|
|
71
|
-
const results = await Promise.all(
|
|
72
|
-
({ pipe: p.toString(), result: await p.prepareRun(pipeOptions) })
|
|
73
|
-
)
|
|
74
|
-
|
|
76
|
+
const results = await Promise.all(
|
|
77
|
+
this.pipes.map(async p => ({ pipe: p.toString(), result: await p.prepareRun(pipeOptions) })),
|
|
78
|
+
);
|
|
79
|
+
|
|
75
80
|
const result = results.filter(p => p.pipe.includes('Testomatio'))[0]?.result;
|
|
76
81
|
|
|
77
82
|
if (!result || result.length === 0) {
|
|
78
83
|
return;
|
|
79
|
-
}
|
|
80
|
-
|
|
84
|
+
}
|
|
85
|
+
|
|
81
86
|
debug('Execution tests list', result);
|
|
82
87
|
|
|
83
88
|
return result;
|
|
@@ -96,9 +101,6 @@ class Client {
|
|
|
96
101
|
// all pipes disabled, skipping
|
|
97
102
|
if (!this.pipes?.filter(p => p.isEnabled).length) return Promise.resolve();
|
|
98
103
|
|
|
99
|
-
global.testomatioArtifacts = [];
|
|
100
|
-
if (!global.testomatioDataStore) global.testomatioDataStore = {};
|
|
101
|
-
|
|
102
104
|
this.queue = this.queue
|
|
103
105
|
.then(() => Promise.all(this.pipes.map(p => p.createRun())))
|
|
104
106
|
.catch(err => console.log(APP_PREFIX, err))
|
|
@@ -112,14 +114,14 @@ class Client {
|
|
|
112
114
|
*
|
|
113
115
|
* @param {string|undefined} status
|
|
114
116
|
* @param {TestData} [testData]
|
|
115
|
-
* @param {string[]} [storeArtifacts]
|
|
116
117
|
* @returns {Promise<PipeResult[]>}
|
|
117
118
|
*/
|
|
118
|
-
async addTestRun(status, testData
|
|
119
|
-
debug('Adding test run for test', testData?.test_id || 'unknown test');
|
|
119
|
+
async addTestRun(status, testData) {
|
|
120
120
|
// all pipes disabled, skipping
|
|
121
121
|
if (!this.pipes?.filter(p => p.isEnabled).length) return [];
|
|
122
122
|
|
|
123
|
+
if (isTestShouldBeExculedFromReport(testData)) return [];
|
|
124
|
+
|
|
123
125
|
if (!testData)
|
|
124
126
|
testData = {
|
|
125
127
|
title: 'Unknown test',
|
|
@@ -135,46 +137,31 @@ class Client {
|
|
|
135
137
|
steps,
|
|
136
138
|
code = null,
|
|
137
139
|
title,
|
|
140
|
+
file,
|
|
138
141
|
suite_title,
|
|
139
142
|
suite_id,
|
|
140
143
|
test_id,
|
|
144
|
+
manuallyAttachedArtifacts,
|
|
145
|
+
meta,
|
|
141
146
|
} = testData;
|
|
142
147
|
let { message = '' } = testData;
|
|
143
148
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
let stack = '';
|
|
147
|
-
|
|
149
|
+
let errorFormatted = '';
|
|
148
150
|
if (error) {
|
|
149
|
-
|
|
151
|
+
errorFormatted += this.formatError(error) || '';
|
|
150
152
|
message = error?.message;
|
|
151
153
|
}
|
|
152
|
-
if (steps) {
|
|
153
|
-
stack = this.formatSteps(stack, steps);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
stack += testData.stack || '';
|
|
157
154
|
|
|
158
|
-
//
|
|
159
|
-
const
|
|
160
|
-
const testLogs = logger.getLogs(test_id);
|
|
161
|
-
// debug(`Test logs for ${test_id}:\n`, testLogs);
|
|
162
|
-
if (stack) stack += '\n\n';
|
|
163
|
-
stack += testLogs ? `${chalk.bold('Logs:')}\n${testLogs}` : '';
|
|
155
|
+
// Attach logs
|
|
156
|
+
const fullLogs = this.formatLogs({ error: errorFormatted, steps, logs: testData.logs });
|
|
164
157
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
files.push(...storeArtifacts);
|
|
168
|
-
}
|
|
158
|
+
// add artifacts
|
|
159
|
+
if (manuallyAttachedArtifacts?.length) files.push(...manuallyAttachedArtifacts);
|
|
169
160
|
|
|
170
|
-
|
|
171
|
-
debug('CLIENT global[testomatioArtifacts]', global.testomatioArtifacts);
|
|
172
|
-
files.push(...global.testomatioArtifacts);
|
|
173
|
-
global.testomatioArtifacts = [];
|
|
174
|
-
}
|
|
161
|
+
const uploadedFiles = [];
|
|
175
162
|
|
|
176
|
-
for (const
|
|
177
|
-
uploadedFiles.push(upload.uploadFileByPath(
|
|
163
|
+
for (const f of files) {
|
|
164
|
+
uploadedFiles.push(upload.uploadFileByPath(f, this.uuid));
|
|
178
165
|
}
|
|
179
166
|
|
|
180
167
|
for (const [idx, buffer] of filesBuffers.entries()) {
|
|
@@ -182,18 +169,22 @@ class Client {
|
|
|
182
169
|
uploadedFiles.push(upload.uploadFileAsBuffer(buffer, fileName, this.uuid));
|
|
183
170
|
}
|
|
184
171
|
|
|
185
|
-
const artifacts = await Promise.all(uploadedFiles);
|
|
172
|
+
const artifacts = (await Promise.all(uploadedFiles)).filter(n => !!n);
|
|
186
173
|
|
|
187
|
-
|
|
174
|
+
if (artifacts.length < uploadedFiles.length) {
|
|
175
|
+
const failedUploading = uploadedFiles.length - artifacts.length;
|
|
176
|
+
this.failedToUpload += failedUploading;
|
|
177
|
+
}
|
|
188
178
|
|
|
189
|
-
this.totalUploaded +=
|
|
179
|
+
this.totalUploaded += artifacts.length;
|
|
190
180
|
|
|
191
181
|
const data = {
|
|
192
182
|
files,
|
|
193
183
|
steps,
|
|
194
184
|
status,
|
|
195
|
-
stack,
|
|
185
|
+
stack: fullLogs,
|
|
196
186
|
example,
|
|
187
|
+
file,
|
|
197
188
|
code,
|
|
198
189
|
title,
|
|
199
190
|
suite_title,
|
|
@@ -202,16 +193,19 @@ class Client {
|
|
|
202
193
|
message,
|
|
203
194
|
run_time: parseFloat(time),
|
|
204
195
|
artifacts,
|
|
196
|
+
meta,
|
|
205
197
|
};
|
|
206
198
|
|
|
199
|
+
// debug('Adding test run...', data);
|
|
200
|
+
|
|
207
201
|
this.queue = this.queue.then(() =>
|
|
208
202
|
Promise.all(
|
|
209
|
-
this.pipes.map(async
|
|
203
|
+
this.pipes.map(async pipe => {
|
|
210
204
|
try {
|
|
211
|
-
const result = await
|
|
212
|
-
return { pipe:
|
|
205
|
+
const result = await pipe.addTest(data);
|
|
206
|
+
return { pipe: pipe.toString(), result };
|
|
213
207
|
} catch (err) {
|
|
214
|
-
console.log(APP_PREFIX,
|
|
208
|
+
console.log(APP_PREFIX, pipe.toString(), err);
|
|
215
209
|
}
|
|
216
210
|
}),
|
|
217
211
|
),
|
|
@@ -237,15 +231,27 @@ class Client {
|
|
|
237
231
|
this.queue = this.queue
|
|
238
232
|
.then(() => Promise.all(this.pipes.map(p => p.finishRun(runParams))))
|
|
239
233
|
.then(() => {
|
|
240
|
-
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`);
|
|
241
237
|
|
|
242
|
-
if (
|
|
238
|
+
if (this.totalUploaded && upload.isArtifactsEnabled()) {
|
|
243
239
|
console.log(
|
|
244
240
|
APP_PREFIX,
|
|
245
|
-
`🗄️
|
|
241
|
+
`🗄️ ${this.totalUploaded} artifacts ${
|
|
246
242
|
process.env.TESTOMATIO_PRIVATE_ARTIFACTS ? 'privately' : chalk.bold('publicly')
|
|
247
|
-
} uploaded to S3 bucket
|
|
243
|
+
} uploaded to S3 bucket`,
|
|
248
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
|
+
}
|
|
249
255
|
}
|
|
250
256
|
})
|
|
251
257
|
.catch(err => console.log(APP_PREFIX, err));
|
|
@@ -253,15 +259,27 @@ class Client {
|
|
|
253
259
|
return this.queue;
|
|
254
260
|
}
|
|
255
261
|
|
|
256
|
-
|
|
257
|
-
|
|
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;
|
|
258
276
|
}
|
|
259
277
|
|
|
260
278
|
formatError(error, message) {
|
|
261
279
|
if (!message) message = error.message;
|
|
262
280
|
if (error.inspect) message = error.inspect() || '';
|
|
263
281
|
|
|
264
|
-
let stack =
|
|
282
|
+
let stack = `${message}\n`;
|
|
265
283
|
|
|
266
284
|
// diffs for mocha, cypress, codeceptjs style
|
|
267
285
|
if (error.actual && error.expected) {
|
|
@@ -278,11 +296,11 @@ class Client {
|
|
|
278
296
|
const record = createCallsiteRecord({
|
|
279
297
|
forError: error,
|
|
280
298
|
isCallsiteFrame: frame => {
|
|
281
|
-
if (customFilter && minimatch(frame.getFileName(), customFilter)) return false;
|
|
299
|
+
if (customFilter && minimatch(frame.getFileName(), customFilter)) return false;
|
|
282
300
|
if (hasFrame) return false;
|
|
283
301
|
if (isNotInternalFrame(frame)) hasFrame = true;
|
|
284
302
|
return hasFrame;
|
|
285
|
-
}
|
|
303
|
+
},
|
|
286
304
|
});
|
|
287
305
|
if (record && !record.filename.startsWith('http')) {
|
|
288
306
|
stack += record.renderSync({ stackFilter: isNotInternalFrame });
|
|
@@ -295,9 +313,48 @@ class Client {
|
|
|
295
313
|
}
|
|
296
314
|
|
|
297
315
|
function isNotInternalFrame(frame) {
|
|
298
|
-
return
|
|
316
|
+
return (
|
|
317
|
+
frame.getFileName() &&
|
|
318
|
+
frame.getFileName().includes(sep) &&
|
|
299
319
|
!frame.getFileName().includes('node_modules') &&
|
|
300
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;
|
|
301
358
|
}
|
|
302
359
|
|
|
303
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' },
|
|
@@ -30,9 +31,10 @@ const HTML_REPORT = {
|
|
|
30
31
|
|
|
31
32
|
module.exports = {
|
|
32
33
|
APP_PREFIX,
|
|
33
|
-
|
|
34
|
-
TESTOMAT_TMP_STORAGE,
|
|
34
|
+
TESTOMAT_TMP_STORAGE_DIR,
|
|
35
35
|
CSV_HEADERS,
|
|
36
36
|
STATUS,
|
|
37
|
-
HTML_REPORT
|
|
37
|
+
HTML_REPORT,
|
|
38
|
+
AXIOS_TIMEOUT,
|
|
39
|
+
AXIOS_RETRY_TIMEOUT
|
|
38
40
|
}
|
|
@@ -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)
|