@testomatio/reporter 1.2.2-beta → 1.2.2-beta-html-pagination-feature-v2

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.
Files changed (46) hide show
  1. package/README.md +61 -54
  2. package/lib/adapter/codecept.js +140 -61
  3. package/lib/adapter/cucumber/current.js +103 -60
  4. package/lib/adapter/cucumber/legacy.js +27 -12
  5. package/lib/adapter/cucumber.js +2 -2
  6. package/lib/adapter/cypress-plugin/index.js +52 -25
  7. package/lib/adapter/jasmine.js +1 -1
  8. package/lib/adapter/jest.js +49 -11
  9. package/lib/adapter/mocha.js +103 -51
  10. package/lib/adapter/playwright.js +100 -31
  11. package/lib/adapter/webdriver.js +1 -1
  12. package/lib/bin/reportXml.js +14 -13
  13. package/lib/bin/startTest.js +27 -6
  14. package/lib/client.js +193 -69
  15. package/lib/config.js +34 -0
  16. package/lib/constants.js +19 -7
  17. package/lib/data-storage.js +203 -0
  18. package/lib/fileUploader.js +128 -53
  19. package/lib/junit-adapter/adapter.js +0 -2
  20. package/lib/junit-adapter/csharp.js +3 -4
  21. package/lib/junit-adapter/index.js +3 -3
  22. package/lib/junit-adapter/java.js +35 -17
  23. package/lib/junit-adapter/javascript.js +1 -2
  24. package/lib/junit-adapter/python.js +12 -14
  25. package/lib/junit-adapter/ruby.js +1 -2
  26. package/lib/pipe/csv.js +5 -3
  27. package/lib/pipe/github.js +27 -39
  28. package/lib/pipe/gitlab.js +20 -24
  29. package/lib/pipe/html.js +363 -0
  30. package/lib/pipe/index.js +3 -1
  31. package/lib/pipe/testomatio.js +183 -56
  32. package/lib/reporter-functions.js +46 -0
  33. package/lib/reporter.js +11 -9
  34. package/lib/services/artifacts.js +57 -0
  35. package/lib/services/index.js +13 -0
  36. package/lib/services/key-values.js +58 -0
  37. package/lib/services/logger.js +311 -0
  38. package/lib/template/testomatio.hbs +1233 -0
  39. package/lib/utils/pipe_utils.js +128 -0
  40. package/lib/{util.js → utils/utils.js} +144 -12
  41. package/lib/xmlReader.js +211 -122
  42. package/package.json +18 -8
  43. package/lib/_ArtifactStorageOld.js +0 -142
  44. package/lib/artifactStorage.js +0 -25
  45. package/lib/dataStorage.js +0 -180
  46. 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 logger = require('./logger');
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.filter(p => p.isEnabled).length) return Promise.resolve();
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(runParams))))
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, storeArtifacts = []) {
119
+ async addTestRun(status, testData) {
70
120
  // all pipes disabled, skipping
71
- if (!this.pipes.filter(p => p.isEnabled).length) return;
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
- const uploadedFiles = [];
95
-
96
- let stack = '';
97
-
149
+ let errorFormatted = '';
98
150
  if (error) {
99
- stack = this.formatError(error) || '';
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
- const testLogs = logger.getLogs(test_id);
107
- debug(`Test logs for ${test_id}:\n`, testLogs);
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
- if (Array.isArray(storeArtifacts) && storeArtifacts.length) {
112
- debug('CLIENT storeArtifact', storeArtifacts);
113
- files.push(...storeArtifacts);
114
- }
158
+ // add artifacts
159
+ if (manuallyAttachedArtifacts?.length) files.push(...manuallyAttachedArtifacts);
115
160
 
116
- if (Array.isArray(global.testomatioArtifacts)) {
117
- debug('CLIENT global[testomatioArtifacts]', global.testomatioArtifacts);
118
- files.push(...global.testomatioArtifacts);
119
- global.testomatioArtifacts = [];
120
- }
161
+ const uploadedFiles = [];
121
162
 
122
- for (const file of files) {
123
- uploadedFiles.push(upload.uploadFileByPath(file, this.uuid));
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
- global.testomatioArtifacts = [];
174
+ if (artifacts.length < uploadedFiles.length) {
175
+ const failedUploading = uploadedFiles.length - artifacts.length;
176
+ this.failedToUpload += failedUploading;
177
+ }
134
178
 
135
- this.totalUploaded += uploadedFiles.filter(n => n).length;
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
- this.queue = this.queue
154
- .then(() => Promise.all(this.pipes.map(async p => {
155
- try {
156
- const result = await p.addTest(data);
157
- return { pipe: p.toString(), result };
158
- } catch (err) {
159
- console.log(APP_PREFIX, p.toString(), err);
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.filter(p => p.isEnabled).length) return Promise.resolve();
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 uploaded files', this.totalUploaded);
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 (upload.isArtifactsEnabled() && this.totalUploaded > 0) {
238
+ if (this.totalUploaded && upload.isArtifactsEnabled()) {
186
239
  console.log(
187
240
  APP_PREFIX,
188
- `🗄️ Total ${this.totalUploaded} artifacts ${
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
- formatSteps(stack, steps) {
200
- return stack ? `${steps}\n\n${chalk.bold.red('################[ Failure ]################')}\n${stack}` : steps;
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 = `\n${chalk.bold(message)}\n`;
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 TESTOMAT_ARTIFACT_SUFFIX = "testomatio_artifact_";
6
+ const AXIOS_TIMEOUT = 20 * 1000; // sum = 20sec
7
+ const AXIOS_RETRY_TIMEOUT = 5 * 1000; // sum = 5sec
5
8
 
6
- const TESTOMAT_TMP_STORAGE = {
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
- TESTOMAT_ARTIFACT_SUFFIX,
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)