@testomatio/reporter 1.1.1 → 1.1.2-beta-fix-config

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.
@@ -55,8 +55,8 @@ class JestReporter {
55
55
  if (!fullSuiteTitle && testResult.testFilePath) fullSuiteTitle = path.basename(testResult.testFilePath);
56
56
 
57
57
  const logs = getTestLogs(result);
58
- const artifacts = services.artifacts.get(testResult.fullName);
59
- const keyValues = services.keyValues.get(testResult.fullName);
58
+ const artifacts = services.artifacts.get(result.fullName);
59
+ const keyValues = services.keyValues.get(result.fullName);
60
60
 
61
61
  const deducedStatus = status === 'pending' ? 'skipped' : status;
62
62
  // In jest if test is not matched with test name pattern it is considered as skipped.
@@ -42,9 +42,7 @@ class PlaywrightReporter {
42
42
 
43
43
  const { error, duration } = result;
44
44
 
45
- const suite_title = test.parent
46
- ? test.parent?.title
47
- : path.basename(test?.location?.file);
45
+ const suite_title = test.parent ? test.parent?.title : path.basename(test?.location?.file);
48
46
 
49
47
  const steps = [];
50
48
  for (const step of result.steps) {
@@ -87,12 +85,11 @@ class PlaywrightReporter {
87
85
  reportTestPromises.push(reportTestPromise);
88
86
  }
89
87
 
90
-
91
88
  #getArtifactPath(artifact) {
92
89
  if (artifact.path) {
93
90
  if (path.isAbsolute(artifact.path)) return artifact.path;
94
91
 
95
- return path.join(this.config.outputDir || this.config.projects[0].outputDir, artifact.path);
92
+ return path.join(this.config.outputDir || this.config.projects[0].outputDir, artifact.path);
96
93
  }
97
94
 
98
95
  if (artifact.body) {
@@ -104,7 +101,7 @@ class PlaywrightReporter {
104
101
  return null;
105
102
  }
106
103
 
107
- async onEnd(result, config) {
104
+ async onEnd(result) {
108
105
  if (!this.client) return;
109
106
 
110
107
  await Promise.all(reportTestPromises);
@@ -117,9 +114,11 @@ class PlaywrightReporter {
117
114
  for (const upload of this.uploads) {
118
115
  const { title, testId, suite_title } = upload;
119
116
 
120
- const files = upload.files.map(attachment => {
121
- return { path: this.#getArtifactPath(attachment), title, type: attachment.contentType };
122
- });
117
+ const files = upload.files.map(attachment => ({
118
+ path: this.#getArtifactPath(attachment),
119
+ title,
120
+ type: attachment.contentType
121
+ }));
123
122
 
124
123
  promises.push(
125
124
  this.client.addTestRun(undefined, {
@@ -169,7 +168,7 @@ function tmpFile(prefix = 'tmp.') {
169
168
  /**
170
169
  * Returns filename + test title
171
170
  * @param {*} test - testInfo object from Playwright
172
- * @returns
171
+ * @returns
173
172
  */
174
173
  function getTestContextName(test) {
175
174
  return `${test._requireFile || ''}_${test.title}`;
@@ -186,7 +185,7 @@ function initPlaywrightForStorage() {
186
185
  });
187
186
  } catch (e) {
188
187
  // ignore
189
- }
188
+ }
190
189
  }
191
190
 
192
191
  module.exports = PlaywrightReporter;
package/lib/client.js CHANGED
@@ -140,7 +140,6 @@ class Client {
140
140
  } = testData;
141
141
  let { message = '' } = testData;
142
142
 
143
-
144
143
  let errorFormatted = '';
145
144
  if (error) {
146
145
  errorFormatted += this.formatError(error) || '';
@@ -242,11 +241,11 @@ class Client {
242
241
  console.log(
243
242
  APP_PREFIX,
244
243
  chalk.yellow(
245
- `Some artifacts were not uploaded. ${this.failedToUpload} artifacts could not be uploaded. Run tests with DEBUG="@testomatio/reporter:file-uploader" to see details"`,
244
+ `Some artifacts were not uploaded. ${this.failedToUpload} artifacts could not be uploaded.
245
+ Run tests with DEBUG="@testomatio/reporter:file-uploader" to see details"`,
246
246
  ),
247
247
  );
248
248
  }
249
-
250
249
  }
251
250
  })
252
251
  .catch(err => console.log(APP_PREFIX, err));
package/lib/constants.js CHANGED
@@ -3,6 +3,8 @@ 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
7
+ const AXIOS_RETRY_TIMEOUT = 5 * 1000; // sum = 5sec
6
8
 
7
9
  const TESTOMAT_TMP_STORAGE_DIR = path.join(os.tmpdir(), 'testomatio_tmp');
8
10
 
@@ -32,5 +34,7 @@ module.exports = {
32
34
  TESTOMAT_TMP_STORAGE_DIR,
33
35
  CSV_HEADERS,
34
36
  STATUS,
35
- HTML_REPORT
37
+ HTML_REPORT,
38
+ AXIOS_TIMEOUT,
39
+ AXIOS_RETRY_TIMEOUT
36
40
  }
@@ -4,7 +4,8 @@ const path = require('path');
4
4
  const os = require('os');
5
5
  const { join } = require('path');
6
6
  const { TESTOMAT_TMP_STORAGE_DIR } = require('./constants');
7
- const { fileSystem, jestHelpers } = require('./utils/utils');
7
+ const { fileSystem, testRunnerHelper } = require('./utils/utils');
8
+ const crypto = require('crypto');
8
9
 
9
10
  class DataStorage {
10
11
  static #instance;
@@ -37,16 +38,6 @@ class DataStorage {
37
38
  this.isFileStorage = true;
38
39
  }
39
40
 
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
41
  /**
51
42
  * Puts any data to storage (file or global variable).
52
43
  * If file: stores data as text, if global variable – stores as array of data.
@@ -59,19 +50,19 @@ class DataStorage {
59
50
  putData(dataType, data, context = null) {
60
51
  if (!dataType || !data) return;
61
52
 
62
- context = context || this.context || global.testomatioTestTitle || jestHelpers.getNameOfCurrentlyRunningTest();
53
+ context = context || this.context || testRunnerHelper.getNameOfCurrentlyRunningTest();
63
54
  if (!context) {
64
55
  debug(`No context provided for "${dataType}" data:`, data);
65
56
  return;
66
57
  }
67
- context = this.#stringToFilename(context);
58
+ const contextHash = stringToMD5Hash(context);
68
59
 
69
60
  if (this.isFileStorage) {
70
61
  const dataDirPath = path.join(TESTOMAT_TMP_STORAGE_DIR, dataType);
71
62
  fileSystem.createDir(dataDirPath);
72
- this.#putDataToFile(dataType, data, context);
63
+ this.#putDataToFile(dataType, data, contextHash);
73
64
  } else {
74
- this.#putDataToGlobalVar(dataType, data, context);
65
+ this.#putDataToGlobalVar(dataType, data, contextHash);
75
66
  }
76
67
  }
77
68
 
@@ -85,32 +76,32 @@ class DataStorage {
85
76
  */
86
77
  getData(dataType, context) {
87
78
  // TODO: think if it could be useful
88
- // context = context || this.context || global.testomatioTestTitle || jestHelpers.getNameOfCurrentlyRunningTest();
79
+ // context = context || this.context || testRunnerHelper.getNameOfCurrentlyRunningTest();
89
80
 
90
81
  if (!context) {
91
82
  debug(`Trying to get "${dataType}" data without context`);
92
83
  return null;
93
84
  }
94
85
 
95
- context = this.#stringToFilename(context);
86
+ const contextHash = stringToMD5Hash(context);
96
87
 
97
88
  let testDataFromFile = [];
98
89
  let testDataFromGlobalVar = [];
99
90
 
100
91
  if (global?.testomatioDataStore) {
101
- testDataFromGlobalVar = this.#getDataFromGlobalVar(dataType, context);
92
+ testDataFromGlobalVar = this.#getDataFromGlobalVar(dataType, contextHash);
102
93
  if (testDataFromGlobalVar) {
103
94
  if (testDataFromGlobalVar.length) return testDataFromGlobalVar;
104
95
  }
105
96
  // don't return nothing if no data in global variable
106
97
  }
107
98
 
108
- testDataFromFile = this.#getDataFromFile(dataType, context);
99
+ testDataFromFile = this.#getDataFromFile(dataType, contextHash);
109
100
 
110
101
  if (testDataFromFile.length) {
111
102
  return testDataFromFile;
112
103
  }
113
- debug(`No "${dataType}" data for context "${context}" in both file and global variable`);
104
+ debug(`No "${dataType}" data for context "${contextHash}" in both file and global variable`);
114
105
 
115
106
  // in case no data found for context
116
107
  return null;
@@ -197,7 +188,16 @@ class DataStorage {
197
188
  }
198
189
  }
199
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
+
200
199
  module.exports.dataStorage = DataStorage.getInstance();
200
+ module.exports.stringToMD5Hash = stringToMD5Hash;
201
201
 
202
202
  // TODO: consider using fs promises instead of writeSync/appendFileSync to
203
203
  // prevent blocking and improve performance (probably queue usage will be required)
@@ -1,8 +1,12 @@
1
1
  const debug = require('debug')('@testomatio/reporter:pipe:testomatio');
2
2
  const chalk = require('chalk');
3
+ // Retry interceptor function
4
+ const axiosRetry = require('axios-retry');
5
+ // Default axios instance
3
6
  const axios = require('axios');
4
7
  const JsonCycle = require('json-cycle');
5
- const { APP_PREFIX, STATUS } = require('../constants');
8
+
9
+ const { APP_PREFIX, STATUS, AXIOS_TIMEOUT, AXIOS_RETRY_TIMEOUT } = require('../constants');
6
10
  const { isValidUrl, foundedTestLog } = require('../utils/utils');
7
11
  const { parseFilterParams, generateFilterRequestParams, setS3Credentials, } = require('../utils/pipe_utils');
8
12
  const { TESTOMATIO } = require('../config');
@@ -34,10 +38,31 @@ class TestomatioPipe {
34
38
  this.groupTitle = params.groupTitle || process.env.TESTOMATIO_RUNGROUP_TITLE;
35
39
  this.env = process.env.TESTOMATIO_ENV;
36
40
  this.label = process.env.TESTOMATIO_LABEL;
37
-
41
+ // Create a new instance of axios with a custom config
38
42
  this.axios = axios.create({
39
43
  baseURL: `${this.url.trim()}`,
40
- timeout: 30000
44
+ timeout: AXIOS_TIMEOUT,
45
+ });
46
+ // Pass the axios instance to the retry function
47
+ axiosRetry(this.axios, {
48
+ retries: 3, // Number of retries (Defaults to 3)
49
+ shouldResetTimeout: true,
50
+ retryCondition: (error) => {
51
+ // Conditional check the error status code
52
+ switch (error.response.status) {
53
+ case 409:
54
+ case 429:
55
+ case 502:
56
+ case 503:
57
+ return true; // Retry request with response status code 409, 429, 502, 503
58
+ default:
59
+ return false; // Do not retry the others
60
+ }
61
+ },
62
+ retryDelay: (retryCount) => retryCount * AXIOS_RETRY_TIMEOUT, // sum = 15sec
63
+ onRetry: (retryCount) => {
64
+ debug(`Retry attempt #${retryCount} failed. Retrying again...`);
65
+ },
41
66
  });
42
67
 
43
68
  this.isEnabled = true;
@@ -150,10 +175,13 @@ class TestomatioPipe {
150
175
  maxContentLength: Infinity,
151
176
  maxBodyLength: Infinity,
152
177
  });
178
+
153
179
  this.runId = resp.data.uid;
154
180
  this.runUrl = `${this.url}/${resp.data.url.split('/').splice(3).join('/')}`;
155
181
  this.runPublicUrl = resp.data.public_url;
182
+
156
183
  if (resp.data.artifacts) setS3Credentials(resp.data.artifacts);
184
+
157
185
  this.store.runUrl = this.runUrl;
158
186
  this.store.runPublicUrl = this.runPublicUrl;
159
187
  this.store.runId = this.runId;
@@ -26,7 +26,7 @@ class Logger {
26
26
  // set default logger to be used in log, warn, error, etc methods
27
27
  #originalUserLogger = { ...console };
28
28
 
29
- #isConsoleIntercepted = false;
29
+ #userLoggerWithOverridenMethods;
30
30
 
31
31
  static #instance;
32
32
 
@@ -197,16 +197,18 @@ class Logger {
197
197
  * @param {*} userLogger
198
198
  */
199
199
  intercept(userLogger) {
200
- /* prevent multiple console interceptions (cause of infinite loop)
201
- actual only for "console", because its used as default output and is intercepted by default */
202
- const isUserLoggerConsole = userLogger.toString?.().toLowerCase() === '[object console]';
203
- if (isUserLoggerConsole && this.#isConsoleIntercepted) {
204
- debug(`Try to intercept console, but it is already intercepted`);
205
- return;
200
+ // STEP 1: reset previously intercepted logger methods to original
201
+ if (this.#userLoggerWithOverridenMethods) {
202
+ for (const method of LOG_METHODS) {
203
+ this.#userLoggerWithOverridenMethods[method] = this.#originalUserLogger[method];
204
+ }
206
205
  }
207
206
 
208
- global.TESTOMATIO_LOGGER_CONSOLE_INTERCEPTED = true;
209
- debug(isUserLoggerConsole ? 'console intercepted' : 'User logger intercepted');
207
+ // STEP 2: intercept new logger
208
+ this.#originalUserLogger = { ...userLogger };
209
+
210
+ const isUserLoggerConsole = userLogger.toString?.().toLowerCase() === '[object console]';
211
+ debug(`Intercepting ${isUserLoggerConsole ? 'console' : 'some user'} logger}`);
210
212
 
211
213
  // override user logger (any, e.g. console) methods to intercept log messages
212
214
  for (const method of LOG_METHODS) {
@@ -219,10 +221,7 @@ class Logger {
219
221
  userLogger[method] = (...args) => this[method](...args);
220
222
  }
221
223
 
222
- /* Playwright
223
- Playwright intercepts console messages by default. Thus when we intercept any logger and provide output by console,
224
- messages are intercepted by Playwright.
225
- */
224
+ this.#userLoggerWithOverridenMethods = userLogger;
226
225
 
227
226
  /*
228
227
  Initial idea was to intercept any logger (tracer, pino, etc),
@@ -232,11 +231,22 @@ class Logger {
232
231
  Also its difficult to understand (actually did not find the way to do it) if logger was already intercepted or not.
233
232
  Thus, decided to intercept only console by default and provide output by default console.
234
233
  It means, if user uses his own logger, its messages will be intercepted,
235
- but the output will be provided by console.
234
+ but the output will be always provided by console.
236
235
  TODO: try to implement the providing output to terminal by user logger
237
236
  */
238
237
  }
239
238
 
239
+ stopInterception() {
240
+ debug('Stop ntercepting logs');
241
+
242
+ // restore original user logger
243
+ if (this.#userLoggerWithOverridenMethods) {
244
+ for (const method of LOG_METHODS) {
245
+ this.#userLoggerWithOverridenMethods[method] = this.#originalUserLogger[method];
246
+ }
247
+ }
248
+ }
249
+
240
250
  /**
241
251
  * Allows to configure logger. Make sure you do it before the logger usage in your code.
242
252
  *
@@ -269,7 +279,6 @@ I found the only ability to get any logs using .task('log', 'message') (this is
269
279
  and then intercept it with:
270
280
  on('task', {
271
281
  log (message) {
272
- console.log(message)
273
282
  return null
274
283
  }
275
284
  })
@@ -298,8 +298,11 @@ function removeColorCodes(input) {
298
298
  return input.replace(/\x1b\[[0-9;]*m/g, '');
299
299
  }
300
300
 
301
- const jestHelpers = {
301
+ const testRunnerHelper = {
302
+ // for Jest
302
303
  getNameOfCurrentlyRunningTest: () => {
304
+ if (global.testomatioTestTitle) return global.testomatioTestTitle;
305
+
303
306
  if (!process.env.JEST_WORKER_ID) return null;
304
307
  try {
305
308
  // TODO: expect?.getState()?.testPath + ' ' + expect?.getState()?.currentTestName
@@ -329,5 +332,5 @@ module.exports = {
329
332
  humanize,
330
333
  removeColorCodes,
331
334
  foundedTestLog,
332
- jestHelpers,
335
+ testRunnerHelper,
333
336
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testomatio/reporter",
3
- "version": "1.1.1",
3
+ "version": "1.1.2-beta-fix-config",
4
4
  "description": "Testomatio Reporter Client",
5
5
  "main": "./lib/reporter.js",
6
6
  "typings": "typings/index.d.ts",
@@ -12,7 +12,8 @@
12
12
  "@aws-sdk/lib-storage": "^3.279.0",
13
13
  "@octokit/rest": "^19.0.5",
14
14
  "aws-sdk": "^2.1072.0",
15
- "axios": "^0.25.0",
15
+ "axios": "^1.6.2",
16
+ "axios-retry": "^3.9.1",
16
17
  "callsite-record": "^4.1.4",
17
18
  "chalk": "^4.1.0",
18
19
  "commander": "^4.1.1",
@@ -30,8 +31,8 @@
30
31
  "lodash.memoize": "^4.1.2",
31
32
  "lodash.merge": "^4.6.2",
32
33
  "minimatch": "^9.0.3",
33
- "uuid": "^9.0.0",
34
- "promise-retry": "^2.0.1"
34
+ "promise-retry": "^2.0.1",
35
+ "uuid": "^9.0.0"
35
36
  },
36
37
  "files": [
37
38
  "bin",
@@ -52,7 +53,7 @@
52
53
  "test:adapter:jasmine:example": "./tests/adapter/examples/jasmine/passReporterOpts.sh && jasmine './tests/adapter/examples/jasmine/index.test.js' --reporter=./../../../lib/adapter/jasmine.js",
53
54
  "test:adapter:codecept:example": "codeceptjs run --config='./tests/adapter/examples/codecept/codecept.conf.js'",
54
55
  "test:adapter:cucumber:example": "cd ./tests/adapter/examples/cucumber && npx cucumber-js",
55
- "test:storage": "npx mocha tests-storage/artifact-storage.test.js && npx mocha tests-storage/data-storage.test.js && TESTOMATIO_INTERCEPT_CONSOLE_LOGS=true npx mocha tests-storage/logger.test.js && npx mocha tests-storage/reporter-functions.test.js"
56
+ "test:storage": "npx mocha tests-storage/artifact-storage.test.js && npx mocha tests-storage/data-storage.test.js && TESTOMATIO_INTERCEPT_CONSOLE_LOGS=true npx mocha tests-storage/logger.test.js && npx mocha tests-storage/logger-2.test.js && npx mocha tests-storage/reporter-functions.test.js"
56
57
  },
57
58
  "devDependencies": {
58
59
  "@cucumber/cucumber": "^9.3.0",