@testomatio/reporter 1.4.1-beta-1 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -57
- package/lib/adapter/codecept.js +86 -27
- package/lib/adapter/cucumber/current.js +17 -10
- package/lib/adapter/cucumber/legacy.js +5 -4
- package/lib/adapter/cucumber.js +2 -2
- package/lib/adapter/cypress-plugin/index.js +53 -26
- package/lib/adapter/jasmine.js +2 -2
- package/lib/adapter/jest.js +48 -10
- package/lib/adapter/mocha.js +90 -10
- package/lib/adapter/playwright.js +68 -18
- package/lib/adapter/webdriver.js +47 -2
- package/lib/bin/reportXml.js +21 -16
- package/lib/bin/startTest.js +12 -12
- package/lib/client.js +112 -52
- package/lib/config.js +34 -0
- package/lib/constants.js +28 -1
- package/lib/data-storage.js +203 -0
- package/lib/fileUploader.js +66 -53
- package/lib/junit-adapter/adapter.js +0 -2
- package/lib/junit-adapter/csharp.js +3 -4
- package/lib/junit-adapter/index.js +3 -3
- package/lib/junit-adapter/java.js +11 -11
- package/lib/junit-adapter/javascript.js +1 -2
- package/lib/junit-adapter/python.js +12 -14
- package/lib/junit-adapter/ruby.js +1 -2
- package/lib/pipe/csv.js +35 -32
- package/lib/pipe/github.js +4 -3
- package/lib/pipe/gitlab.js +17 -10
- package/lib/pipe/html.js +361 -0
- package/lib/pipe/index.js +3 -1
- package/lib/pipe/testomatio.js +230 -51
- package/lib/reporter-functions.js +12 -9
- package/lib/reporter.js +8 -12
- package/lib/services/artifacts.js +57 -0
- package/lib/services/index.js +13 -0
- package/lib/{storages/key-value-storage.js → services/key-values.js} +19 -19
- package/lib/{storages → services}/logger.js +58 -37
- package/lib/template/emptyData.svg +23 -0
- package/lib/template/testomatio.hbs +1421 -0
- package/lib/utils/pipe_utils.js +73 -79
- package/lib/utils/utils.js +53 -40
- package/lib/xmlReader.js +113 -105
- package/package.json +13 -7
- package/lib/storages/artifact-storage.js +0 -70
- package/lib/storages/data-storage.js +0 -307
package/lib/client.js
CHANGED
|
@@ -1,14 +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
|
|
11
|
+
const { glob } = require('glob');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
|
|
14
|
+
let listOfTestFilesToExcludeFromReport = null;
|
|
12
15
|
|
|
13
16
|
/**
|
|
14
17
|
* @typedef {import('../types').TestData} TestData
|
|
@@ -28,6 +31,7 @@ class Client {
|
|
|
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;
|
|
32
36
|
this.executionList = Promise.resolve();
|
|
33
37
|
|
|
@@ -36,7 +40,7 @@ class Client {
|
|
|
36
40
|
|
|
37
41
|
/**
|
|
38
42
|
* Asynchronously prepares the execution list for running tests through various pipes.
|
|
39
|
-
* Each pipe in the client is checked for enablement,
|
|
43
|
+
* Each pipe in the client is checked for enablement,
|
|
40
44
|
* and if all pipes are disabled, the function returns a resolved Promise.
|
|
41
45
|
* Otherwise, it executes the `prepareRun` method for each enabled pipe and collects the results.
|
|
42
46
|
* The results are then filtered to remove any undefined values.
|
|
@@ -46,7 +50,7 @@ class Client {
|
|
|
46
50
|
* @param {Object} params - The options for preparing the test execution list.
|
|
47
51
|
* @param {string} params.pipe - Name of the executed pipe.
|
|
48
52
|
* @param {string} params.pipeOptions - Filter option.
|
|
49
|
-
* @returns {Promise<any>} - A Promise that resolves to an
|
|
53
|
+
* @returns {Promise<any>} - A Promise that resolves to an
|
|
50
54
|
* array containing the prepared execution list,
|
|
51
55
|
* or resolves to undefined if no valid results are found or if all pipes are disabled.
|
|
52
56
|
*/
|
|
@@ -63,22 +67,22 @@ class Client {
|
|
|
63
67
|
if (!filterPipe.isEnabled) {
|
|
64
68
|
// TODO:for the future for the another pipes
|
|
65
69
|
console.warn(
|
|
66
|
-
APP_PREFIX,
|
|
67
|
-
`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"`,
|
|
68
72
|
);
|
|
69
73
|
return;
|
|
70
74
|
}
|
|
71
75
|
|
|
72
|
-
const results = await Promise.all(
|
|
73
|
-
({ pipe: p.toString(), result: await p.prepareRun(pipeOptions) })
|
|
74
|
-
)
|
|
75
|
-
|
|
76
|
+
const results = await Promise.all(
|
|
77
|
+
this.pipes.map(async p => ({ pipe: p.toString(), result: await p.prepareRun(pipeOptions) })),
|
|
78
|
+
);
|
|
79
|
+
|
|
76
80
|
const result = results.filter(p => p.pipe.includes('Testomatio'))[0]?.result;
|
|
77
81
|
|
|
78
82
|
if (!result || result.length === 0) {
|
|
79
83
|
return;
|
|
80
|
-
}
|
|
81
|
-
|
|
84
|
+
}
|
|
85
|
+
|
|
82
86
|
debug('Execution tests list', result);
|
|
83
87
|
|
|
84
88
|
return result;
|
|
@@ -116,6 +120,8 @@ class Client {
|
|
|
116
120
|
// all pipes disabled, skipping
|
|
117
121
|
if (!this.pipes?.filter(p => p.isEnabled).length) return [];
|
|
118
122
|
|
|
123
|
+
if (isTestShouldBeExculedFromReport(testData)) return [];
|
|
124
|
+
|
|
119
125
|
if (!testData)
|
|
120
126
|
testData = {
|
|
121
127
|
title: 'Unknown test',
|
|
@@ -131,44 +137,31 @@ class Client {
|
|
|
131
137
|
steps,
|
|
132
138
|
code = null,
|
|
133
139
|
title,
|
|
140
|
+
file,
|
|
134
141
|
suite_title,
|
|
135
142
|
suite_id,
|
|
136
143
|
test_id,
|
|
144
|
+
manuallyAttachedArtifacts,
|
|
145
|
+
meta,
|
|
137
146
|
} = testData;
|
|
138
147
|
let { message = '' } = testData;
|
|
139
148
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
let stack = '';
|
|
143
|
-
|
|
149
|
+
let errorFormatted = '';
|
|
144
150
|
if (error) {
|
|
145
|
-
|
|
151
|
+
errorFormatted += this.formatError(error) || '';
|
|
146
152
|
message = error?.message;
|
|
147
153
|
}
|
|
148
|
-
if (steps) {
|
|
149
|
-
stack = this.formatSteps(stack, steps);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
stack += testData.stack || '';
|
|
153
154
|
|
|
154
|
-
//
|
|
155
|
-
|
|
156
|
-
const logger = require('./storages/logger');
|
|
157
|
-
const testLogs = logger.getLogs(test_id);
|
|
158
|
-
// debug(`Test logs for ${test_id}:\n`, testLogs);
|
|
159
|
-
if (stack) stack += '\n\n';
|
|
160
|
-
stack += testLogs ? `${chalk.bold('Logs:')}\n${testLogs}` : '';
|
|
155
|
+
// Attach logs
|
|
156
|
+
const fullLogs = this.formatLogs({ error: errorFormatted, steps, logs: testData.logs });
|
|
161
157
|
|
|
162
|
-
//
|
|
163
|
-
|
|
164
|
-
if (artifactFiles) files.push(...artifactFiles);
|
|
158
|
+
// add artifacts
|
|
159
|
+
if (manuallyAttachedArtifacts?.length) files.push(...manuallyAttachedArtifacts);
|
|
165
160
|
|
|
166
|
-
|
|
167
|
-
const keyValueStorage = require('./storages/key-value-storage');
|
|
168
|
-
const keyValues = keyValueStorage.get(test_id);
|
|
161
|
+
const uploadedFiles = [];
|
|
169
162
|
|
|
170
|
-
for (const
|
|
171
|
-
uploadedFiles.push(upload.uploadFileByPath(
|
|
163
|
+
for (const f of files) {
|
|
164
|
+
uploadedFiles.push(upload.uploadFileByPath(f, this.uuid));
|
|
172
165
|
}
|
|
173
166
|
|
|
174
167
|
for (const [idx, buffer] of filesBuffers.entries()) {
|
|
@@ -176,18 +169,22 @@ class Client {
|
|
|
176
169
|
uploadedFiles.push(upload.uploadFileAsBuffer(buffer, fileName, this.uuid));
|
|
177
170
|
}
|
|
178
171
|
|
|
179
|
-
const artifacts = await Promise.all(uploadedFiles);
|
|
172
|
+
const artifacts = (await Promise.all(uploadedFiles)).filter(n => !!n);
|
|
180
173
|
|
|
181
|
-
|
|
174
|
+
if (artifacts.length < uploadedFiles.length) {
|
|
175
|
+
const failedUploading = uploadedFiles.length - artifacts.length;
|
|
176
|
+
this.failedToUpload += failedUploading;
|
|
177
|
+
}
|
|
182
178
|
|
|
183
|
-
this.totalUploaded +=
|
|
179
|
+
this.totalUploaded += artifacts.length;
|
|
184
180
|
|
|
185
181
|
const data = {
|
|
186
182
|
files,
|
|
187
183
|
steps,
|
|
188
184
|
status,
|
|
189
|
-
stack,
|
|
185
|
+
stack: fullLogs,
|
|
190
186
|
example,
|
|
187
|
+
file,
|
|
191
188
|
code,
|
|
192
189
|
title,
|
|
193
190
|
suite_title,
|
|
@@ -196,17 +193,19 @@ class Client {
|
|
|
196
193
|
message,
|
|
197
194
|
run_time: parseFloat(time),
|
|
198
195
|
artifacts,
|
|
199
|
-
meta
|
|
196
|
+
meta,
|
|
200
197
|
};
|
|
201
198
|
|
|
199
|
+
// debug('Adding test run...', data);
|
|
200
|
+
|
|
202
201
|
this.queue = this.queue.then(() =>
|
|
203
202
|
Promise.all(
|
|
204
|
-
this.pipes.map(async
|
|
203
|
+
this.pipes.map(async pipe => {
|
|
205
204
|
try {
|
|
206
|
-
const result = await
|
|
207
|
-
return { pipe:
|
|
205
|
+
const result = await pipe.addTest(data);
|
|
206
|
+
return { pipe: pipe.toString(), result };
|
|
208
207
|
} catch (err) {
|
|
209
|
-
console.log(APP_PREFIX,
|
|
208
|
+
console.log(APP_PREFIX, pipe.toString(), err);
|
|
210
209
|
}
|
|
211
210
|
}),
|
|
212
211
|
),
|
|
@@ -243,6 +242,16 @@ class Client {
|
|
|
243
242
|
process.env.TESTOMATIO_PRIVATE_ARTIFACTS ? 'privately' : chalk.bold('publicly')
|
|
244
243
|
} uploaded to S3 bucket`,
|
|
245
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
|
+
}
|
|
246
255
|
}
|
|
247
256
|
})
|
|
248
257
|
.catch(err => console.log(APP_PREFIX, err));
|
|
@@ -250,15 +259,27 @@ class Client {
|
|
|
250
259
|
return this.queue;
|
|
251
260
|
}
|
|
252
261
|
|
|
253
|
-
|
|
254
|
-
|
|
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;
|
|
255
276
|
}
|
|
256
277
|
|
|
257
278
|
formatError(error, message) {
|
|
258
279
|
if (!message) message = error.message;
|
|
259
280
|
if (error.inspect) message = error.inspect() || '';
|
|
260
281
|
|
|
261
|
-
let stack =
|
|
282
|
+
let stack = `${message}\n`;
|
|
262
283
|
|
|
263
284
|
// diffs for mocha, cypress, codeceptjs style
|
|
264
285
|
if (error.actual && error.expected) {
|
|
@@ -275,11 +296,11 @@ class Client {
|
|
|
275
296
|
const record = createCallsiteRecord({
|
|
276
297
|
forError: error,
|
|
277
298
|
isCallsiteFrame: frame => {
|
|
278
|
-
if (customFilter && minimatch(frame.getFileName(), customFilter)) return false;
|
|
299
|
+
if (customFilter && minimatch(frame.getFileName(), customFilter)) return false;
|
|
279
300
|
if (hasFrame) return false;
|
|
280
301
|
if (isNotInternalFrame(frame)) hasFrame = true;
|
|
281
302
|
return hasFrame;
|
|
282
|
-
}
|
|
303
|
+
},
|
|
283
304
|
});
|
|
284
305
|
if (record && !record.filename.startsWith('http')) {
|
|
285
306
|
stack += record.renderSync({ stackFilter: isNotInternalFrame });
|
|
@@ -292,9 +313,48 @@ class Client {
|
|
|
292
313
|
}
|
|
293
314
|
|
|
294
315
|
function isNotInternalFrame(frame) {
|
|
295
|
-
return
|
|
316
|
+
return (
|
|
317
|
+
frame.getFileName() &&
|
|
318
|
+
frame.getFileName().includes(sep) &&
|
|
296
319
|
!frame.getFileName().includes('node_modules') &&
|
|
297
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;
|
|
298
358
|
}
|
|
299
359
|
|
|
300
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
|
@@ -3,6 +3,7 @@ const os = require('os');
|
|
|
3
3
|
const path = require('path');
|
|
4
4
|
|
|
5
5
|
const APP_PREFIX = chalk.gray('[TESTOMATIO]');
|
|
6
|
+
const AXIOS_TIMEOUT = 20 * 1000; // sum = 20sec
|
|
6
7
|
|
|
7
8
|
const TESTOMAT_TMP_STORAGE_DIR = path.join(os.tmpdir(), 'testomatio_tmp');
|
|
8
9
|
|
|
@@ -20,10 +21,36 @@ const STATUS = {
|
|
|
20
21
|
SKIPPED: 'skipped',
|
|
21
22
|
FINISHED: 'finished',
|
|
22
23
|
};
|
|
24
|
+
// html pipe var
|
|
25
|
+
const HTML_REPORT = {
|
|
26
|
+
FOLDER: 'html-report',
|
|
27
|
+
REPORT_DEFAULT_NAME: 'testomatio-report.html',
|
|
28
|
+
TEMPLATE_NAME: 'testomatio.hbs',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const testomatLogoURL = 'https://avatars.githubusercontent.com/u/59105116?s=36&v=4';
|
|
32
|
+
|
|
33
|
+
const REPORTER_REQUEST_RETRIES = {
|
|
34
|
+
retryTimeout: 5 * 1000, // sum = 5sec
|
|
35
|
+
retriesPerRequest: 2,
|
|
36
|
+
maxTotalRetries: Number(process.env.TESTOMATIO_MAX_REQUEST_FAILURES_COUNT) || 10,
|
|
37
|
+
withinTimeSeconds: Number(process.env.TESTOMATIO_MAX_REQUEST_RETRIES_WITHIN_TIME_SECONDS) || 60,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const RunStatus = {
|
|
41
|
+
Passed: 'passed',
|
|
42
|
+
Failed: 'failed',
|
|
43
|
+
Finished: 'finished',
|
|
44
|
+
};
|
|
23
45
|
|
|
24
46
|
module.exports = {
|
|
25
47
|
APP_PREFIX,
|
|
26
48
|
TESTOMAT_TMP_STORAGE_DIR,
|
|
27
49
|
CSV_HEADERS,
|
|
28
50
|
STATUS,
|
|
29
|
-
|
|
51
|
+
HTML_REPORT,
|
|
52
|
+
AXIOS_TIMEOUT,
|
|
53
|
+
testomatLogoURL,
|
|
54
|
+
REPORTER_REQUEST_RETRIES,
|
|
55
|
+
RunStatus,
|
|
56
|
+
};
|
|
@@ -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)
|