@testomatio/reporter 1.1.1-file-upload.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.
@@ -7,6 +7,8 @@ const { APP_PREFIX, STATUS: Status, TESTOMAT_TMP_STORAGE_DIR } = require('../con
7
7
  const TestomatioClient = require('../client');
8
8
  const { isArtifactsEnabled } = require('../fileUploader');
9
9
  const { parseTest, fileSystem } = require('../utils/utils');
10
+ const { services } = require('../services');
11
+ const { dataStorage } = require('../data-storage');
10
12
 
11
13
  const reportTestPromises = [];
12
14
 
@@ -26,6 +28,11 @@ class PlaywrightReporter {
26
28
  this.client.createRun();
27
29
  }
28
30
 
31
+ onTestBegin(testInfo) {
32
+ const fullTestTitle = getTestContextName(testInfo);
33
+ dataStorage.setContext(fullTestTitle);
34
+ }
35
+
29
36
  onTestEnd(test, result) {
30
37
  if (!this.client) return;
31
38
 
@@ -35,17 +42,20 @@ class PlaywrightReporter {
35
42
 
36
43
  const { error, duration } = result;
37
44
 
38
- const suite_title = test.parent ? test.parent.title : null;
45
+ const suite_title = test.parent ? test.parent?.title : path.basename(test?.location?.file);
39
46
 
40
47
  const steps = [];
41
48
  for (const step of result.steps) {
42
49
  appendStep(step, steps);
43
50
  }
44
51
 
52
+ const fullTestTitle = getTestContextName(test);
45
53
  let logs = '';
46
54
  if (result.stderr.length || result.stdout.length) {
47
55
  logs = `\n\n${chalk.bold('Logs:')}\n${chalk.red(result.stderr.join(''))}\n${result.stdout.join('')}`;
48
56
  }
57
+ const manuallyAttachedArtifacts = services.artifacts.get(fullTestTitle);
58
+ const keyValues = services.keyValues.get(fullTestTitle);
49
59
 
50
60
  const reportTestPromise = this.client
51
61
  .addTestRun(checkStatus(result.status), {
@@ -56,7 +66,8 @@ class PlaywrightReporter {
56
66
  steps: steps.join('\n'),
57
67
  time: duration,
58
68
  logs,
59
- // TODO: attach artifacts
69
+ manuallyAttachedArtifacts,
70
+ meta: keyValues,
60
71
  })
61
72
  .then(pipes => {
62
73
  testId = pipes?.filter(p => p.pipe.includes('Testomatio'))[0]?.result?.data?.test_id;
@@ -74,12 +85,11 @@ class PlaywrightReporter {
74
85
  reportTestPromises.push(reportTestPromise);
75
86
  }
76
87
 
77
-
78
88
  #getArtifactPath(artifact) {
79
89
  if (artifact.path) {
80
90
  if (path.isAbsolute(artifact.path)) return artifact.path;
81
91
 
82
- 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);
83
93
  }
84
94
 
85
95
  if (artifact.body) {
@@ -91,7 +101,7 @@ class PlaywrightReporter {
91
101
  return null;
92
102
  }
93
103
 
94
- async onEnd(result, config) {
104
+ async onEnd(result) {
95
105
  if (!this.client) return;
96
106
 
97
107
  await Promise.all(reportTestPromises);
@@ -104,9 +114,11 @@ class PlaywrightReporter {
104
114
  for (const upload of this.uploads) {
105
115
  const { title, testId, suite_title } = upload;
106
116
 
107
- const files = upload.files.map(attachment => {
108
- return { path: this.#getArtifactPath(attachment), title, type: attachment.contentType };
109
- });
117
+ const files = upload.files.map(attachment => ({
118
+ path: this.#getArtifactPath(attachment),
119
+ title,
120
+ type: attachment.contentType
121
+ }));
110
122
 
111
123
  promises.push(
112
124
  this.client.addTestRun(undefined, {
@@ -153,6 +165,15 @@ function tmpFile(prefix = 'tmp.') {
153
165
  return path.join(tmpdir, prefix + crypto.randomBytes(16).toString('hex'));
154
166
  }
155
167
 
168
+ /**
169
+ * Returns filename + test title
170
+ * @param {*} test - testInfo object from Playwright
171
+ * @returns
172
+ */
173
+ function getTestContextName(test) {
174
+ return `${test._requireFile || ''}_${test.title}`;
175
+ }
176
+
156
177
  function initPlaywrightForStorage() {
157
178
  try {
158
179
  // @ts-ignore-next-line
@@ -160,12 +181,11 @@ function initPlaywrightForStorage() {
160
181
  const { test } = require('@playwright/test');
161
182
  // eslint-disable-next-line no-empty-pattern
162
183
  test.beforeEach(async ({}, testInfo) => {
163
- const fullTestTitle = `${testInfo.file}_${testInfo.title}`;
164
- global.testomatioTestTitle = fullTestTitle;
184
+ global.testomatioTestTitle = `${testInfo.file || ''}_${testInfo.title}`;
165
185
  });
166
186
  } catch (e) {
167
187
  // ignore
168
- }
188
+ }
169
189
  }
170
190
 
171
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.getIdOfCurrentlyRunningTest();
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.getIdOfCurrentlyRunningTest();
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;
@@ -19,7 +19,7 @@ function saveArtifact(data, context = null) {
19
19
  * @param {...any} args
20
20
  */
21
21
  function logMessage(...args) {
22
- services.logger.templateLiteralLog(...args);
22
+ services.logger._templateLiteralLog(...args);
23
23
  }
24
24
 
25
25
  /**
package/lib/reporter.js CHANGED
@@ -1,19 +1,19 @@
1
1
  const TestomatClient = require('./client');
2
2
  const TRConstants = require('./constants');
3
+ const { services } = require('./services');
3
4
 
4
5
  const reporterFunctions = require('./reporter-functions');
5
6
 
6
7
  module.exports = {
7
8
  // TODO: deprecate in future; use log or testomat.log
8
- logger: reporterFunctions.log,
9
- testomatioLogger: reporterFunctions.log,
9
+ testomatioLogger: services.logger,
10
10
 
11
11
  artifact: reporterFunctions.artifact,
12
12
  log: reporterFunctions.log,
13
+ logger: services.logger,
13
14
  meta: reporterFunctions.keyValue,
14
15
  step: reporterFunctions.step,
15
16
 
16
17
  TestomatClient,
17
18
  TRConstants,
18
19
  };
19
-
@@ -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
 
@@ -44,8 +44,7 @@ class Logger {
44
44
  logLevel = process?.env?.LOG_LEVEL?.toUpperCase() || 'ALL';
45
45
 
46
46
  constructor() {
47
- // intercept console by default
48
- this.intercept(console);
47
+ if (!dataStorage.isFileStorage || process.env.TESTOMATIO_INTERCEPT_CONSOLE_LOGS) this.intercept(console);
49
48
  }
50
49
 
51
50
  /**
@@ -106,7 +105,7 @@ class Logger {
106
105
  * 2. Standard: log(`text ${someVar}`)
107
106
  * 3. Standard with multiple arguments: log('text', someVar)
108
107
  */
109
- templateLiteralLog(strings, ...args) {
108
+ _templateLiteralLog(strings, ...args) {
110
109
  if (Array.isArray(strings)) strings = strings.filter(item => item !== '').map(item => item.trim());
111
110
  if (Array.isArray(args)) args = args.filter(item => item !== '');
112
111
 
@@ -198,16 +197,18 @@ class Logger {
198
197
  * @param {*} userLogger
199
198
  */
200
199
  intercept(userLogger) {
201
- /* prevent multiple console interceptions (cause of infinite loop)
202
- actual only for "console", because its used as default output and is intercepted by default */
203
- const isUserLoggerConsole = userLogger.toString?.().toLowerCase() === '[object console]';
204
- if (isUserLoggerConsole && this.#isConsoleIntercepted) {
205
- debug(`Try to intercept console, but it is already intercepted`);
206
- 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
+ }
207
205
  }
208
206
 
209
- global.TESTOMATIO_LOGGER_CONSOLE_INTERCEPTED = true;
210
- 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}`);
211
212
 
212
213
  // override user logger (any, e.g. console) methods to intercept log messages
213
214
  for (const method of LOG_METHODS) {
@@ -220,10 +221,7 @@ class Logger {
220
221
  userLogger[method] = (...args) => this[method](...args);
221
222
  }
222
223
 
223
- /* Playwright
224
- Playwright intercepts console messages by default. Thus when we intercept any logger and provide output by console,
225
- messages are intercepted by Playwright.
226
- */
224
+ this.#userLoggerWithOverridenMethods = userLogger;
227
225
 
228
226
  /*
229
227
  Initial idea was to intercept any logger (tracer, pino, etc),
@@ -233,11 +231,22 @@ class Logger {
233
231
  Also its difficult to understand (actually did not find the way to do it) if logger was already intercepted or not.
234
232
  Thus, decided to intercept only console by default and provide output by default console.
235
233
  It means, if user uses his own logger, its messages will be intercepted,
236
- but the output will be provided by console.
234
+ but the output will be always provided by console.
237
235
  TODO: try to implement the providing output to terminal by user logger
238
236
  */
239
237
  }
240
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
+
241
250
  /**
242
251
  * Allows to configure logger. Make sure you do it before the logger usage in your code.
243
252
  *
@@ -270,7 +279,6 @@ I found the only ability to get any logs using .task('log', 'message') (this is
270
279
  and then intercept it with:
271
280
  on('task', {
272
281
  log (message) {
273
- console.log(message)
274
282
  return null
275
283
  }
276
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 = {
302
- getIdOfCurrentlyRunningTest: () => {
301
+ const testRunnerHelper = {
302
+ // for Jest
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-file-upload.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 && 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",