@testomatio/reporter 1.1.0-beta.mocha-create.9 → 1.1.0-rc.2

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,14 +1,13 @@
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 artifactStorage = require('./storages/artifact-storage');
12
11
 
13
12
  /**
14
13
  * @typedef {import('../types').TestData} TestData
@@ -36,7 +35,7 @@ class Client {
36
35
 
37
36
  /**
38
37
  * Asynchronously prepares the execution list for running tests through various pipes.
39
- * Each pipe in the client is checked for enablement,
38
+ * Each pipe in the client is checked for enablement,
40
39
  * and if all pipes are disabled, the function returns a resolved Promise.
41
40
  * Otherwise, it executes the `prepareRun` method for each enabled pipe and collects the results.
42
41
  * The results are then filtered to remove any undefined values.
@@ -46,7 +45,7 @@ class Client {
46
45
  * @param {Object} params - The options for preparing the test execution list.
47
46
  * @param {string} params.pipe - Name of the executed pipe.
48
47
  * @param {string} params.pipeOptions - Filter option.
49
- * @returns {Promise<any>} - A Promise that resolves to an
48
+ * @returns {Promise<any>} - A Promise that resolves to an
50
49
  * array containing the prepared execution list,
51
50
  * or resolves to undefined if no valid results are found or if all pipes are disabled.
52
51
  */
@@ -63,22 +62,22 @@ class Client {
63
62
  if (!filterPipe.isEnabled) {
64
63
  // TODO:for the future for the another pipes
65
64
  console.warn(
66
- APP_PREFIX,
67
- `At the moment processing is available only for the "testomatio" key. Example: "testomatio:tag-name=xxx"`
65
+ APP_PREFIX,
66
+ `At the moment processing is available only for the "testomatio" key. Example: "testomatio:tag-name=xxx"`,
68
67
  );
69
68
  return;
70
69
  }
71
70
 
72
- const results = await Promise.all(this.pipes.map(async p =>
73
- ({ pipe: p.toString(), result: await p.prepareRun(pipeOptions) })
74
- ));
75
-
71
+ const results = await Promise.all(
72
+ this.pipes.map(async p => ({ pipe: p.toString(), result: await p.prepareRun(pipeOptions) })),
73
+ );
74
+
76
75
  const result = results.filter(p => p.pipe.includes('Testomatio'))[0]?.result;
77
76
 
78
77
  if (!result || result.length === 0) {
79
78
  return;
80
- }
81
-
79
+ }
80
+
82
81
  debug('Execution tests list', result);
83
82
 
84
83
  return result;
@@ -135,41 +134,28 @@ class Client {
135
134
  suite_title,
136
135
  suite_id,
137
136
  test_id,
137
+ manuallyAttachedArtifacts,
138
+ meta,
138
139
  } = testData;
139
140
  let { message = '' } = testData;
140
141
 
141
- const uploadedFiles = [];
142
-
143
- let stack = '';
144
142
 
143
+ let errorFormatted = '';
145
144
  if (error) {
146
- stack = this.formatError(error) || '';
145
+ errorFormatted += this.formatError(error) || '';
147
146
  message = error?.message;
148
147
  }
149
- if (steps) {
150
- stack = this.formatSteps(stack, steps);
151
- }
152
-
153
- stack += testData.stack || '';
154
148
 
155
- // ATTACH LOGS from storage
156
- // in some cases (e.g. using cucumber) logger instance becomes empty object, if import at the top of the file
157
- const logger = require('./storages/logger');
158
- const testLogs = logger.getLogs(test_id);
159
- // debug(`Test logs for ${test_id}:\n`, testLogs);
160
- if (stack) stack += '\n\n';
161
- stack += testLogs ? `${chalk.bold('Logs:')}\n${testLogs}` : '';
149
+ // Attach logs
150
+ const fullLogs = this.formatLogs({ error: errorFormatted, steps, logs: testData.logs });
162
151
 
163
- // GET ARTIFACTS from storage
164
- const artifactFiles = artifactStorage.get(test_id);
165
- if (artifactFiles) files.push(...artifactFiles);
152
+ // add artifacts
153
+ if (manuallyAttachedArtifacts?.length) files.push(...manuallyAttachedArtifacts);
166
154
 
167
- // GET KEY-VALUEs from storage
168
- const keyValueStorage = require('./storages/key-value-storage');
169
- const keyValues = keyValueStorage.get(test_id);
155
+ const uploadedFiles = [];
170
156
 
171
- for (const file of files) {
172
- uploadedFiles.push(upload.uploadFileByPath(file, this.uuid));
157
+ for (const f of files) {
158
+ uploadedFiles.push(upload.uploadFileByPath(f, this.uuid));
173
159
  }
174
160
 
175
161
  for (const [idx, buffer] of filesBuffers.entries()) {
@@ -177,9 +163,16 @@ class Client {
177
163
  uploadedFiles.push(upload.uploadFileAsBuffer(buffer, fileName, this.uuid));
178
164
  }
179
165
 
180
- const artifacts = await Promise.all(uploadedFiles);
166
+ const artifacts = (await Promise.all(uploadedFiles)).filter(n => !!n);
181
167
 
182
- // global.testomatioArtifacts = [];
168
+ if (artifacts.length < uploadedFiles.length) {
169
+ console.log(
170
+ APP_PREFIX,
171
+ chalk.yellow(
172
+ `Some artifacts were not uploaded. ${artifacts.length}/${uploadedFiles.length} artifacts uploaded.`,
173
+ ),
174
+ );
175
+ }
183
176
 
184
177
  this.totalUploaded += uploadedFiles.filter(n => n).length;
185
178
 
@@ -187,7 +180,7 @@ class Client {
187
180
  files,
188
181
  steps,
189
182
  status,
190
- stack,
183
+ stack: fullLogs,
191
184
  example,
192
185
  file,
193
186
  code,
@@ -198,9 +191,11 @@ class Client {
198
191
  message,
199
192
  run_time: parseFloat(time),
200
193
  artifacts,
201
- meta: keyValues,
194
+ meta,
202
195
  };
203
196
 
197
+ debug('Adding test run...', data);
198
+
204
199
  this.queue = this.queue.then(() =>
205
200
  Promise.all(
206
201
  this.pipes.map(async p => {
@@ -252,15 +247,27 @@ class Client {
252
247
  return this.queue;
253
248
  }
254
249
 
255
- formatSteps(stack, steps) {
256
- return stack ? `${steps}\n\n${chalk.bold.red('################[ Failure ]################')}\n${stack}` : steps;
250
+ /**
251
+ * Returns the formatted stack including the stack trace, steps, and logs.
252
+ * @returns {string}
253
+ */
254
+ formatLogs({ error, steps, logs }) {
255
+ error = error?.trim();
256
+ steps = steps?.trim();
257
+ logs = logs?.trim();
258
+
259
+ let testLogs = '';
260
+ if (steps) testLogs += `${chalk.bold.blue('################[ Steps ]################')}\n${steps}\n\n`;
261
+ if (logs) testLogs += `${chalk.bold.gray('################[ Logs ]################')}\n${logs}\n\n`;
262
+ if (error) testLogs += `${chalk.bold.red('################[ Failure ]################')}\n${error}`;
263
+ return testLogs;
257
264
  }
258
265
 
259
266
  formatError(error, message) {
260
267
  if (!message) message = error.message;
261
268
  if (error.inspect) message = error.inspect() || '';
262
269
 
263
- let stack = `\n${chalk.bold(message)}\n`;
270
+ let stack = `${message}\n`;
264
271
 
265
272
  // diffs for mocha, cypress, codeceptjs style
266
273
  if (error.actual && error.expected) {
@@ -277,11 +284,11 @@ class Client {
277
284
  const record = createCallsiteRecord({
278
285
  forError: error,
279
286
  isCallsiteFrame: frame => {
280
- if (customFilter && minimatch(frame.getFileName(), customFilter)) return false;
287
+ if (customFilter && minimatch(frame.getFileName(), customFilter)) return false;
281
288
  if (hasFrame) return false;
282
289
  if (isNotInternalFrame(frame)) hasFrame = true;
283
290
  return hasFrame;
284
- }
291
+ },
285
292
  });
286
293
  if (record && !record.filename.startsWith('http')) {
287
294
  stack += record.renderSync({ stackFilter: isNotInternalFrame });
@@ -294,9 +301,11 @@ class Client {
294
301
  }
295
302
 
296
303
  function isNotInternalFrame(frame) {
297
- return frame.getFileName().includes(sep) &&
304
+ return (
305
+ frame.getFileName().includes(sep) &&
298
306
  !frame.getFileName().includes('node_modules') &&
299
307
  !frame.getFileName().includes('internal')
308
+ );
300
309
  }
301
310
 
302
311
  module.exports = Client;
package/lib/config.js CHANGED
@@ -8,6 +8,10 @@ const debug = require('debug')('@testomatio/reporter:config');
8
8
  const dotenv = require('dotenv');
9
9
  const envFileVars = dotenv.config({ path: '.env' }).parsed; */
10
10
 
11
+ const TESTOMATIO = process.env.TESTOMATIO || process.env.TESTOMATIO_API_KEY || process.env.TESTOMATIO_TOKEN || '';
12
+ if (TESTOMATIO === 'undefined') console.error('TESTOMATIO is "undefined". Something went wrong. Contact dev team.');
13
+ process.env.TESTOMATIO = TESTOMATIO;
14
+
11
15
  // select only TESTOMATIO related variables (only to print them in debug)
12
16
  const testomatioEnvVars =
13
17
  Object.keys(process.env)
@@ -22,3 +26,4 @@ debug('TESTOMATIO variables:', testomatioEnvVars);
22
26
  const config = process.env;
23
27
 
24
28
  module.exports = config;
29
+ module.exports.TESTOMATIO = TESTOMATIO;
@@ -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, jestHelpers } = require('./utils/utils');
8
+
9
+ class DataStorage {
10
+ static #instance;
11
+
12
+ context;
13
+
14
+ /**
15
+ *
16
+ * @returns {DataStorage}
17
+ */
18
+ static getInstance() {
19
+ if (!this.#instance) {
20
+ this.#instance = new DataStorage();
21
+ }
22
+ return this.#instance;
23
+ }
24
+
25
+ setContext(context) {
26
+ this.context = context;
27
+ }
28
+
29
+ /**
30
+ * Creates data storage instance as singleton
31
+ * Stores data to global variable or to file depending on what is applicable for current test runner (adapter)
32
+ * Recommend to use composition while using this class (instead of inheritance).
33
+ * ! Also the class which will use data storage should be singleton (to avoid data loss).
34
+ */
35
+ constructor() {
36
+ // some frameworks use global variable to store data, some use file storage
37
+ this.isFileStorage = true;
38
+ }
39
+
40
+ #stringToFilename(str) {
41
+ // TODO: use md5 hash later
42
+ const validFilenameRegex = /[^a-zA-Z0-9_.-]/g;
43
+ // replace all characters not in the regex above with underscore, then duplicate underscores removed
44
+ let filename = str.replace(validFilenameRegex, '_').replace(/_{2,}/g, '_').substring(0, 255); // max filename length
45
+ // remove leading and trailing underscores
46
+ filename = filename.replace(/^_+|_+$/g, '');
47
+ return filename;
48
+ }
49
+
50
+ /**
51
+ * Puts any data to storage (file or global variable).
52
+ * If file: stores data as text, if global variable – stores as array of data.
53
+ * @param {'log' | 'artifact' | 'keyvalue'} dataType
54
+ * @param {*} data anything you want to store (string, object, array, etc)
55
+ * @param {*} context could be testId or any context (test name, suite name, including their IDs etc)
56
+ * suite name + test name is used by default
57
+ * @returns
58
+ */
59
+ putData(dataType, data, context = null) {
60
+ if (!dataType || !data) return;
61
+
62
+ context = context || this.context || global.testomatioTestTitle || jestHelpers.getIdOfCurrentlyRunningTest();
63
+ if (!context) {
64
+ debug(`No context provided for "${dataType}" data:`, data);
65
+ return;
66
+ }
67
+ context = this.#stringToFilename(context);
68
+
69
+ if (this.isFileStorage) {
70
+ const dataDirPath = path.join(TESTOMAT_TMP_STORAGE_DIR, dataType);
71
+ fileSystem.createDir(dataDirPath);
72
+ this.#putDataToFile(dataType, data, context);
73
+ } else {
74
+ this.#putDataToGlobalVar(dataType, data, context);
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Returns data, stored for specific test/context (or data which was stored without test id specified).
80
+ * This method will get data from global variable and/or from from file (previosly saved with put method).
81
+ *
82
+ * @param {'log' | 'artifact' | 'keyvalue'} dataType
83
+ * @param {string} context
84
+ * @returns {any []} array of data (any type), null (if no data found for context) or string (if data type is log)
85
+ */
86
+ getData(dataType, context) {
87
+ // TODO: think if it could be useful
88
+ // context = context || this.context || global.testomatioTestTitle || jestHelpers.getIdOfCurrentlyRunningTest();
89
+
90
+ if (!context) {
91
+ debug(`Trying to get "${dataType}" data without context`);
92
+ return null;
93
+ }
94
+
95
+ context = this.#stringToFilename(context);
96
+
97
+ let testDataFromFile = [];
98
+ let testDataFromGlobalVar = [];
99
+
100
+ if (global?.testomatioDataStore) {
101
+ testDataFromGlobalVar = this.#getDataFromGlobalVar(dataType, context);
102
+ if (testDataFromGlobalVar) {
103
+ if (testDataFromGlobalVar.length) return testDataFromGlobalVar;
104
+ }
105
+ // don't return nothing if no data in global variable
106
+ }
107
+
108
+ testDataFromFile = this.#getDataFromFile(dataType, context);
109
+
110
+ if (testDataFromFile.length) {
111
+ return testDataFromFile;
112
+ }
113
+ debug(`No "${dataType}" data for context "${context}" in both file and global variable`);
114
+
115
+ // in case no data found for context
116
+ return null;
117
+ }
118
+
119
+ /**
120
+ * @param {'log' | 'artifact' | 'keyvalue'} dataType
121
+ * @param {string} context
122
+ * @returns aray of data (any type)
123
+ */
124
+ #getDataFromGlobalVar(dataType, context) {
125
+ try {
126
+ if (global?.testomatioDataStore[dataType]) {
127
+ const testData = global.testomatioDataStore[dataType][context];
128
+ if (testData) debug(`"${dataType}" data for constext "${context}":`, testData.join(', '));
129
+ return testData || [];
130
+ }
131
+ // debug(`No ${this.dataType} data for context ${context} in <global> storage`);
132
+ return [];
133
+ } catch (e) {
134
+ // there could be no data, ignore
135
+ }
136
+ }
137
+
138
+ /**
139
+ * @param {'log' | 'artifact' | 'keyvalue'} dataType
140
+ * @param {*} context
141
+ * @returns array of data (any type)
142
+ */
143
+ #getDataFromFile(dataType, context) {
144
+ const dataDirPath = path.join(TESTOMAT_TMP_STORAGE_DIR, dataType);
145
+ try {
146
+ const filepath = join(dataDirPath, `${dataType}_${context}`);
147
+ if (fs.existsSync(filepath)) {
148
+ const testDataAsText = fs.readFileSync(filepath, 'utf-8');
149
+ if (testDataAsText) debug(`"${dataType}" data for context "${context}":`, testDataAsText);
150
+ const testDataArr = testDataAsText?.split(os.EOL) || [];
151
+ return testDataArr;
152
+ }
153
+ // debug(`No ${this.dataType} data for ${context} in <file> storage`);
154
+ return [];
155
+ } catch (e) {
156
+ // there could be no data, ignore
157
+ }
158
+ return [];
159
+ }
160
+
161
+ /**
162
+ * Puts data to global variable. Unlike the file storage, stores data in array (file storage just append as string).
163
+ * @param {'log' | 'artifact' | 'keyvalue'} dataType
164
+ * @param {*} data
165
+ * @param {*} context
166
+ */
167
+ #putDataToGlobalVar(dataType, data, context) {
168
+ debug('Saving data to global variable for ', context, ':', data);
169
+ if (!global.testomatioDataStore) global.testomatioDataStore = {};
170
+ if (!global.testomatioDataStore?.[dataType]) global.testomatioDataStore[dataType] = {};
171
+
172
+ if (!global.testomatioDataStore?.[dataType][context]) global.testomatioDataStore[dataType][context] = [];
173
+ global.testomatioDataStore[dataType][context].push(data);
174
+ }
175
+
176
+ /**
177
+ * Puts data to file. Unlike the global variable storage, stores data as string
178
+ * @param {'log' | 'artifact' | 'keyvalue'} dataType
179
+ * @param {*} data
180
+ * @param {string} context
181
+ * @returns
182
+ */
183
+ #putDataToFile(dataType, data, context) {
184
+ const dataDirPath = path.join(TESTOMAT_TMP_STORAGE_DIR, dataType);
185
+ if (typeof data !== 'string') data = JSON.stringify(data);
186
+ const filename = `${dataType}_${context}`;
187
+ const filepath = join(dataDirPath, filename);
188
+ if (!fs.existsSync(dataDirPath)) fileSystem.createDir(dataDirPath);
189
+ debug(`Saving data to file for context "${context}" to ${filepath}. Data: ${JSON.stringify(data)}`);
190
+
191
+ // append new line if file already exists (in this case its definitely includes some data)
192
+ if (fs.existsSync(filepath)) {
193
+ fs.appendFileSync(filepath, os.EOL + data, 'utf-8');
194
+ } else {
195
+ fs.writeFileSync(filepath, data, 'utf-8');
196
+ }
197
+ }
198
+ }
199
+
200
+ module.exports.dataStorage = DataStorage.getInstance();
201
+
202
+ // TODO: consider using fs promises instead of writeSync/appendFileSync to
203
+ // prevent blocking and improve performance (probably queue usage will be required)
@@ -146,8 +146,13 @@ const uploadUsingS3 = async (filePath, runId) => {
146
146
  params
147
147
  });
148
148
 
149
- return await getS3LocationLink(out);
150
- }
149
+
150
+ const link = await getS3LocationLink(out);
151
+
152
+ debug(`Succesfully uploaded ${filePath} => ${S3_BUCKET}/${Key} | URL: ${link}`);
153
+
154
+ return link;
155
+ }
151
156
  catch (e) {
152
157
  debug('S3 file uploading error: ', e);
153
158
 
@@ -279,8 +284,6 @@ const getS3LocationLink = async (out) => {
279
284
 
280
285
  let s3Location = response?.Location;
281
286
 
282
- debug('Uploaded response.Location', s3Location);
283
-
284
287
  if (!s3Location) {
285
288
  // TODO: out: a fallback case - remove after deeper testing
286
289
  s3Location = out?.singleUploadResult?.Location;
@@ -6,8 +6,7 @@ const RubyAdapter = require('./ruby');
6
6
  const CSharpAdapter = require('./csharp');
7
7
 
8
8
  function AdapterFactory(lang, opts) {
9
- if (!lang) return new Adapter(opts);
10
-
9
+
11
10
  if (lang === 'java') {
12
11
  return new JavaAdapter(opts);
13
12
  }
@@ -23,6 +22,8 @@ function AdapterFactory(lang, opts) {
23
22
  if (lang === 'c#' || lang === 'csharp') {
24
23
  return new CSharpAdapter(opts);
25
24
  }
25
+
26
+ return new Adapter(opts);
26
27
  }
27
28
 
28
29
  module.exports = AdapterFactory;
@@ -5,6 +5,7 @@ const JsonCycle = require('json-cycle');
5
5
  const { APP_PREFIX, STATUS } = require('../constants');
6
6
  const { isValidUrl, foundedTestLog } = require('../utils/utils');
7
7
  const { parseFilterParams, generateFilterRequestParams, setS3Credentials, } = require('../utils/pipe_utils');
8
+ const { TESTOMATIO } = require('../config');
8
9
 
9
10
  if (process.env.TESTOMATIO_RUN) {
10
11
  process.env.runId = process.env.TESTOMATIO_RUN;
@@ -20,7 +21,7 @@ class TestomatioPipe {
20
21
  constructor(params, store) {
21
22
  this.isEnabled = false;
22
23
  this.url = params.testomatioUrl || process.env.TESTOMATIO_URL || 'https://app.testomat.io';
23
- this.apiKey = params.apiKey || process.env.TESTOMATIO;
24
+ this.apiKey = params.apiKey || TESTOMATIO;
24
25
  debug('Testomatio Pipe: ', this.apiKey ? 'API KEY' : '*no api key*');
25
26
  if (!this.apiKey) {
26
27
  return;
@@ -1,6 +1,9 @@
1
- const logger = require('./storages/logger');
2
- const artifactStorage = require('./storages/artifact-storage');
3
- const keyValueStorage = require('./storages/key-value-storage');
1
+ const { services } = require('./services');
2
+ const { initPlaywrightForStorage } = require('./adapter/playwright');
3
+
4
+ if (process.env.PLAYWRIGHT_TEST_BASE_URL) {
5
+ initPlaywrightForStorage();
6
+ }
4
7
 
5
8
  /**
6
9
  * Stores path to file as artifact and uploads it to the S3 storage
@@ -8,7 +11,7 @@ const keyValueStorage = require('./storages/key-value-storage');
8
11
  */
9
12
  function saveArtifact(data, context = null) {
10
13
  if (!data) return;
11
- artifactStorage.put(data, context);
14
+ services.artifacts.put(data, context);
12
15
  }
13
16
 
14
17
  /**
@@ -16,23 +19,23 @@ function saveArtifact(data, context = null) {
16
19
  * @param {...any} args
17
20
  */
18
21
  function logMessage(...args) {
19
- logger.log(...args);
22
+ services.logger.templateLiteralLog(...args);
20
23
  }
21
24
 
22
25
  /**
23
26
  * Similar to "log" function but marks message in report as a step
24
- * @param {*} message
27
+ * @param {*} message
25
28
  */
26
29
  function addStep(message) {
27
- logger.step(message);
30
+ services.logger.step(message);
28
31
  }
29
32
 
30
33
  /**
31
34
  * Add key-value pair(s) to the test report
32
- * @param {*} keyValue
35
+ * @param {*} keyValue
33
36
  */
34
37
  function setKeyValue(keyValue) {
35
- keyValueStorage.put(keyValue);
38
+ services.keyValues.put(keyValue);
36
39
  }
37
40
 
38
41
  module.exports = {
package/lib/reporter.js CHANGED
@@ -1,23 +1,19 @@
1
- const logger = require('./storages/logger');
2
1
  const TestomatClient = require('./client');
3
2
  const TRConstants = require('./constants');
4
3
 
5
- const log = logger.templateLiteralLog.bind(logger);
6
- const step = logger.step.bind(logger);
7
4
  const reporterFunctions = require('./reporter-functions');
8
5
 
9
6
  module.exports = {
10
- logger,
11
- testomatioLogger: logger,
12
- log,
13
- step,
14
- TestomatClient,
15
- TRConstants,
16
- };
7
+ // TODO: deprecate in future; use log or testomat.log
8
+ logger: reporterFunctions.log,
9
+ testomatioLogger: reporterFunctions.log,
17
10
 
18
- module.exports.testomat = {
19
11
  artifact: reporterFunctions.artifact,
20
12
  log: reporterFunctions.log,
21
- step: reporterFunctions.step,
22
13
  meta: reporterFunctions.keyValue,
14
+ step: reporterFunctions.step,
15
+
16
+ TestomatClient,
17
+ TRConstants,
23
18
  };
19
+
@@ -0,0 +1,57 @@
1
+ const debug = require('debug')('@testomatio/reporter:services-artifacts');
2
+ const { dataStorage } = require('../data-storage');
3
+
4
+ /**
5
+ * Artifact storage is supposed to store file paths
6
+ */
7
+ class ArtifactStorage {
8
+ static #instance;
9
+
10
+ /**
11
+ * Singleton
12
+ * @returns {ArtifactStorage}
13
+ */
14
+ static getInstance() {
15
+ if (!this.#instance) {
16
+ this.#instance = new ArtifactStorage();
17
+ }
18
+ return this.#instance;
19
+ }
20
+
21
+ /**
22
+ * Stores path to file as artifact and uploads it to the S3 storage
23
+ * @param {string | {path: string, type: string, name: string}} data - path to file or object with path, type and name
24
+ * @param {*} context testId or test title
25
+ */
26
+ put(data, context = null) {
27
+ if (!data) return;
28
+ debug('Save artifact:', data);
29
+ dataStorage.putData('artifact', data, context);
30
+ }
31
+
32
+ /**
33
+ * Returns list of artifacts to upload
34
+ * @param {*} context testId or test context from test runner
35
+ * @returns {(string | {path: string, type: string, name: string})[]}
36
+ */
37
+ get(context) {
38
+ let artifacts = dataStorage.getData('artifact', context);
39
+ if (!artifacts || !artifacts.length) return [];
40
+
41
+ artifacts = artifacts.map(artifactData => {
42
+ // artifact could be an object ({type, path, name} props) or string (just path)
43
+ let artifact;
44
+ try {
45
+ artifact = JSON.parse(artifactData);
46
+ } catch (e) {
47
+ artifact = artifactData;
48
+ }
49
+ return artifact;
50
+ });
51
+ artifacts = artifacts.filter(artifact => !!artifact);
52
+ debug(`Artifacts for test ${context}:`, artifacts);
53
+ return artifacts.length ? artifacts : [];
54
+ }
55
+ }
56
+
57
+ module.exports.artifactStorage = ArtifactStorage.getInstance();
@@ -0,0 +1,13 @@
1
+ const { logger } = require('./logger');
2
+ const { artifactStorage } = require('./artifacts');
3
+ const { keyValueStorage } = require('./key-values');
4
+ const { dataStorage } = require('../data-storage');
5
+
6
+ module.exports.services = {
7
+ logger,
8
+ artifacts: artifactStorage,
9
+ keyValues: keyValueStorage,
10
+ setContext: context => {
11
+ dataStorage.setContext(context);
12
+ },
13
+ };