@testomatio/reporter 1.2.0-beta-3 → 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/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(this.pipes.map(async p =>
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, storeArtifacts = []) {
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
- const uploadedFiles = [];
145
-
146
- let stack = '';
147
-
149
+ let errorFormatted = '';
148
150
  if (error) {
149
- stack = this.formatError(error) || '';
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
- // in some cases (e.g. using cucumber) logger instance becomes empty object, if import at the top of the file
159
- const logger = require('./logger');
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
- if (Array.isArray(storeArtifacts) && storeArtifacts.length) {
166
- debug('CLIENT storeArtifact', storeArtifacts);
167
- files.push(...storeArtifacts);
168
- }
158
+ // add artifacts
159
+ if (manuallyAttachedArtifacts?.length) files.push(...manuallyAttachedArtifacts);
169
160
 
170
- if (Array.isArray(global.testomatioArtifacts)) {
171
- debug('CLIENT global[testomatioArtifacts]', global.testomatioArtifacts);
172
- files.push(...global.testomatioArtifacts);
173
- global.testomatioArtifacts = [];
174
- }
161
+ const uploadedFiles = [];
175
162
 
176
- for (const file of files) {
177
- uploadedFiles.push(upload.uploadFileByPath(file, this.uuid));
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
- global.testomatioArtifacts = [];
174
+ if (artifacts.length < uploadedFiles.length) {
175
+ const failedUploading = uploadedFiles.length - artifacts.length;
176
+ this.failedToUpload += failedUploading;
177
+ }
188
178
 
189
- this.totalUploaded += uploadedFiles.filter(n => n).length;
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 p => {
203
+ this.pipes.map(async pipe => {
210
204
  try {
211
- const result = await p.addTest(data);
212
- return { pipe: p.toString(), result };
205
+ const result = await pipe.addTest(data);
206
+ return { pipe: pipe.toString(), result };
213
207
  } catch (err) {
214
- console.log(APP_PREFIX, p.toString(), err);
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 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`);
241
237
 
242
- if (upload.isArtifactsEnabled() && this.totalUploaded > 0) {
238
+ if (this.totalUploaded && upload.isArtifactsEnabled()) {
243
239
  console.log(
244
240
  APP_PREFIX,
245
- `🗄️ Total ${this.totalUploaded} artifacts ${
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
- formatSteps(stack, steps) {
257
- 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;
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 = `\n${chalk.bold(message)}\n`;
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 frame.getFileName().includes(sep) &&
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 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' },
@@ -30,9 +31,10 @@ const HTML_REPORT = {
30
31
 
31
32
  module.exports = {
32
33
  APP_PREFIX,
33
- TESTOMAT_ARTIFACT_SUFFIX,
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)