@testomatio/reporter 1.0.0-beta.4 → 1.0.1

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/logger.js ADDED
@@ -0,0 +1,278 @@
1
+ const chalk = require('chalk');
2
+ const debug = require('debug')('@testomatio/reporter:logger');
3
+ const { DataStorage } = require('./dataStorage');
4
+
5
+ const LOG_METHODS = ['assert', 'debug', 'error', 'info', 'log', 'trace', 'warn'];
6
+ const LEVELS = {
7
+ ALL: { severity: 1, color: '' },
8
+ VERBOSE: { severity: 3, color: 'grey' },
9
+ TRACE: { severity: 5, color: 'grey' },
10
+ DEBUG: { severity: 7, color: 'cyan' },
11
+ INFO: { severity: 9, color: 'black' },
12
+ LOG: { severity: 11, color: 'black' },
13
+ WARN: { severity: 13, color: 'yellow' },
14
+ ERROR: { severity: 15, color: 'red' },
15
+ };
16
+
17
+ /**
18
+ * Logger allows to:
19
+ * 1. Intercept logs from user logger (console.log, etc) and store them.
20
+ * 2. Output logs to console (actually, logger functionality).
21
+ * 3. Varied syntax.
22
+ */
23
+ class Logger {
24
+ // _originalUserLogger used to output logs to console by the user logger
25
+ // _loggerToIntercept intercepted and reassigned immediately when added
26
+
27
+ constructor(params = {}) {
28
+ // set default logger to be used in log, warn, error, etc methods
29
+ this._originalUserLogger = { ...console };
30
+
31
+ this.dataStorage = new DataStorage('log', params);
32
+ this.logLevel = process?.env?.LOG_LEVEL?.toUpperCase() || 'ALL';
33
+
34
+ // commented because prefer to use "intercept" method
35
+ // if (params?.logger) this._loggerToIntercept = params.logger;
36
+
37
+ this.intercept(this._loggerToIntercept);
38
+
39
+ // singleton
40
+ if (!Logger.instance) {
41
+ Logger.instance = this;
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Tagget template literal. Allows to use different syntaxes:
47
+ * 1. Tagget template: $`text ${someVar}`
48
+ * 2. Standard: $(`text ${someVar}`)
49
+ * 3. Standard with multiple arguments: $('text', someVar)
50
+ */
51
+ _log(strings, ...args) {
52
+ let logs;
53
+ // this block means tagged template is used (syntax like $`text ${someVar}`)
54
+ if (Array.isArray(strings) && strings.length === args.length + 1) {
55
+ logs = strings.reduce(
56
+ (result, current, index) =>
57
+ result +
58
+ current +
59
+ // strings are splitted by args when use tagged template, thus we add arg after each string
60
+ // it looks like: `string1 arg1 string2 arg2 string3`
61
+ (args[index] !== undefined // eslint-disable-line no-nested-ternary
62
+ ? typeof args[index] === 'string'
63
+ ? args[index] // add arg as it is
64
+ : this._strinfifyLogs(args[index]) // stringify arg
65
+ : ' '), // add space if no arg after string
66
+ // initial accumulator value
67
+ '',
68
+ );
69
+ } else {
70
+ // this block means arguments syntax is used (syntax like $('text', someVar))
71
+ // in this case strings represents just a first argument
72
+ logs = this._strinfifyLogs(strings, ...args);
73
+ }
74
+ this.dataStorage.putData(logs);
75
+ }
76
+
77
+ /**
78
+ * Allows you to define a step inside a test. Step name is attached to the report and
79
+ * helps to understand the test flow.
80
+ * @param {*} strings
81
+ * @param {...any} values
82
+ */
83
+ step(strings, ...values) {
84
+ let logs = '';
85
+ for (let i = 0; i < strings.length; i++) {
86
+ logs += strings[i];
87
+ if (i < values.length) {
88
+ logs += values[i];
89
+ }
90
+ }
91
+ logs = chalk.blue(`> ${logs}`);
92
+ this.dataStorage.putData(logs);
93
+ }
94
+
95
+ /**
96
+ *
97
+ * @param {*} context testId or test context from test runner
98
+ * @returns
99
+ */
100
+ getLogs(context) {
101
+ return this.dataStorage.getData(context);
102
+ }
103
+
104
+ _strinfifyLogs(...args) {
105
+ const logs = [];
106
+ // stringify everything except strings
107
+ for (const arg of args) {
108
+ if (typeof arg === 'string') {
109
+ logs.push(arg);
110
+ } else {
111
+ try {
112
+ // eslint-disable-next-line no-unused-expressions
113
+ this.prettyObjects ? logs.push(JSON.stringify(arg, null, 2)) : logs.push(JSON.stringify(arg));
114
+ } catch (e) {
115
+ debug('Error while stringify object', e);
116
+ logs.push(arg);
117
+ }
118
+ }
119
+ }
120
+ return logs.join(' ');
121
+ }
122
+
123
+ assert(...args) {
124
+ const level = 'ERROR';
125
+ const severity = LEVELS[level].severity;
126
+ if (severity < LEVELS[this.logLevel]?.severity) return;
127
+
128
+ const logs = this._strinfifyLogs(...args);
129
+ this.dataStorage.putData(logs);
130
+ try {
131
+ this._originalUserLogger.log(`Assertion result: `, ...args);
132
+ } catch (e) {
133
+ // method could be unexisting, ignore error
134
+ }
135
+ }
136
+
137
+ debug(...args) {
138
+ const level = 'DEBUG';
139
+ const severity = LEVELS[level].severity;
140
+ if (severity < LEVELS[this.logLevel]?.severity) return;
141
+
142
+ if (this.logLevel === 'error' || this.logLevel === 'warn') return;
143
+ const logs = this._strinfifyLogs(...args);
144
+ const colorizedLogs = chalk[LEVELS[level].color](logs);
145
+ this.dataStorage.putData(colorizedLogs);
146
+ try {
147
+ this._originalUserLogger.debug(...args);
148
+ } catch (e) {
149
+ // method could be unexisting, ignore error
150
+ }
151
+ }
152
+
153
+ error(...args) {
154
+ const level = 'ERROR';
155
+ const severity = LEVELS[level].severity;
156
+ if (severity < LEVELS[this.logLevel]?.severity) return;
157
+
158
+ const logs = this._strinfifyLogs(...args);
159
+ const colorizedLogs = chalk[LEVELS[level].color](logs);
160
+ this.dataStorage.putData(colorizedLogs);
161
+ try {
162
+ this._originalUserLogger.error(...args);
163
+ } catch (e) {
164
+ // method could be unexisting, ignore error
165
+ }
166
+ }
167
+
168
+ info(...args) {
169
+ const level = 'INFO';
170
+ const severity = LEVELS[level].severity;
171
+ if (severity < LEVELS[this.logLevel]?.severity) return;
172
+
173
+ const logs = this._strinfifyLogs(...args);
174
+ this.dataStorage.putData(logs);
175
+ try {
176
+ this._originalUserLogger.info(...args);
177
+ } catch (e) {
178
+ // method could be unexisting, ignore error
179
+ }
180
+ }
181
+
182
+ log(...args) {
183
+ const level = 'INFO';
184
+ const severity = LEVELS[level].severity;
185
+ if (severity < LEVELS[this.logLevel]?.severity) return;
186
+
187
+ const logs = this._strinfifyLogs(...args);
188
+ this.dataStorage.putData(logs);
189
+ try {
190
+ this._originalUserLogger.log(...args);
191
+ } catch (e) {
192
+ // method could be unexisting, ignore error
193
+ }
194
+ }
195
+
196
+ trace(...args) {
197
+ const level = 'TRACE';
198
+ const severity = LEVELS[level].severity;
199
+ if (severity < LEVELS[this.logLevel]?.severity) return;
200
+
201
+ const logs = this._strinfifyLogs(...args);
202
+ const colorizedLogs = chalk[LEVELS[level].color](logs);
203
+ this.dataStorage.putData(colorizedLogs);
204
+ try {
205
+ this._originalUserLogger.trace(...args);
206
+ } catch (e) {
207
+ // method could be unexisting, ignore error
208
+ }
209
+ }
210
+
211
+ warn(...args) {
212
+ const level = 'WARN';
213
+ const severity = LEVELS[level].severity;
214
+ if (severity < LEVELS[this.logLevel]?.severity) return;
215
+
216
+ const logs = this._strinfifyLogs(...args);
217
+ const colorizedLogs = chalk[LEVELS[level].color](logs);
218
+ this.dataStorage.putData(colorizedLogs);
219
+ try {
220
+ this._originalUserLogger.warn(...args);
221
+ } catch (e) {
222
+ // method could be unexisting, ignore error
223
+ }
224
+ }
225
+
226
+ /**
227
+ * Intercepts user logger messages.
228
+ * When call this method, Logger start to control the user logger,
229
+ * but almost nothing is changed for user ragarding the console output (like log level set by user)
230
+ * (until multiple loggers are intercepted,
231
+ * in this case only the last intercepted logger will be used as user console output).
232
+ * @param {*} userLogger
233
+ */
234
+ intercept(userLogger) {
235
+ if (!userLogger) return;
236
+ debug(`Intercepting user logger`);
237
+
238
+ // save original user logger to use it for logging (last intercepted will be used for console output)
239
+ this._originalUserLogger = { ...userLogger };
240
+ this._loggerToIntercept = userLogger;
241
+
242
+ /*
243
+ override user logger (any, e.g. console) methods to intercept log messages
244
+ this._loggerToIntercept = this; could be used, but decided to override only output methods
245
+ */
246
+ for (const method of LOG_METHODS) {
247
+ /*
248
+ decided to comment next code line because its better to create method even if it does not exist in user logger;
249
+ on method invocation, we will store the data anyway and catch block will prevent potential errors
250
+ */
251
+ // if (!this._loggerToIntercept[method]) continue;
252
+ this._loggerToIntercept[method] = (...args) => this[method](...args);
253
+ }
254
+ }
255
+
256
+ /**
257
+ * Allows to configure logger. Make sure you do it before the logger usage in your code.
258
+ *
259
+ * @param {Object} [config={}] - The configuration object.
260
+ * @param {string} [config.logLevel] - The desired log level. Valid values are 'DEBUG', 'INFO', 'WARN', and 'ERROR'.
261
+ * @param {boolean} [config.prettyObjects] - Specifies whether to enable pretty printing of objects.
262
+ * @returns {void}
263
+ */
264
+ configure(config = {}) {
265
+ if (!config) return;
266
+ if (config.prettyObjects) this.prettyObjects = config.prettyObjects;
267
+ if (config.logLevel) this.logLevel = config.logLevel.toUpperCase();
268
+ }
269
+ }
270
+
271
+ Logger.instance = null;
272
+
273
+ // module.exports.Logger = Logger;
274
+ module.exports = new Logger();
275
+
276
+ // TODO: parse passed arguments as {level: 'str', message: 'str'} because some loggers use such syntax;
277
+ // upd: did not face such loggers, but still could be useful
278
+
@@ -39,8 +39,8 @@ class GitHubPipe {
39
39
  async createRun() {}
40
40
 
41
41
  addTest(test) {
42
- debug('Adding test:', test);
43
42
  if (!this.isEnabled) return;
43
+ debug('Adding test:', test);
44
44
 
45
45
  const index = this.tests.findIndex(t => isSameTest(t, test));
46
46
  // update if they were already added
@@ -200,7 +200,7 @@ function fullName(t) {
200
200
  }
201
201
 
202
202
  async function deletePreviousReport(octokit, owner, repo, issue, hiddenCommentData) {
203
- if (process.env.GITHUB_KEEP_OUTDATED_REPORTS) return;
203
+ if (process.env.GH_KEEP_OUTDATED_REPORTS) return;
204
204
 
205
205
  // get comments
206
206
  let comments = [];
@@ -169,6 +169,7 @@ class TestomatioPipe {
169
169
 
170
170
  module.exports = TestomatioPipe;
171
171
 
172
+
172
173
  function setS3Credentials(artifacts) {
173
174
  if (!Object.keys(artifacts).length) return;
174
175
 
@@ -181,4 +182,4 @@ function setS3Credentials(artifacts) {
181
182
  if (artifacts.ENDPOINT) process.env.S3_ENDPOINT = artifacts.ENDPOINT;
182
183
  if (artifacts.presign) process.env.TESTOMATIO_PRIVATE_ARTIFACTS = '1';
183
184
  resetConfig();
184
- }
185
+ }
package/lib/reporter.js CHANGED
@@ -1,9 +1,17 @@
1
+ const logger = require('./logger');
1
2
  const TestomatClient = require('./client');
2
3
  const TRConstants = require('./constants');
3
- const TRArtifacts = require('./ArtifactStorage');
4
+ const TRArtifacts = require('./_ArtifactStorageOld');
5
+
6
+ const log = logger._log.bind(logger);
7
+ const step = logger.step.bind(logger);
4
8
 
5
9
  module.exports = {
10
+ logger,
11
+ log,
12
+ step,
6
13
  TestomatClient,
7
14
  TRConstants,
8
- TRArtifacts
15
+ TRArtifacts,
16
+ addArtifact: TRArtifacts.artifact,
9
17
  };
package/lib/util.js CHANGED
@@ -3,6 +3,7 @@ const { sep, basename } = require('path');
3
3
  const chalk = require('chalk');
4
4
  const fs = require('fs');
5
5
  const isValid = require('is-valid-path');
6
+ const debug = require('debug')('@testomatio/reporter:util');
6
7
 
7
8
  /**
8
9
  * @param {String} testTitle - Test title
@@ -10,6 +11,7 @@ const isValid = require('is-valid-path');
10
11
  * @returns {String|null} testId
11
12
  */
12
13
  const parseTest = testTitle => {
14
+ if (!testTitle) return null;
13
15
  const captures = testTitle.match(/@T([\w\d]+)/);
14
16
  if (captures) {
15
17
  return captures[1];
@@ -32,7 +34,6 @@ const parseSuite = suiteTitle => {
32
34
  return null;
33
35
  };
34
36
 
35
-
36
37
  const ansiRegExp = () => {
37
38
  const pattern = [
38
39
  '[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)',
@@ -54,11 +55,14 @@ const isValidUrl = s => {
54
55
 
55
56
  const fetchFilesFromStackTrace = (stack = '') => {
56
57
  const files = stack.matchAll(/file:?\/(\/.*?\.(png|avi|webm|jpg|html|txt))/g);
57
- return Array.from(files).map(f => f[1]).filter(f => fs.existsSync(f));
58
- }
58
+ return Array.from(files)
59
+ .map(f => f[1])
60
+ .filter(f => fs.existsSync(f));
61
+ };
59
62
 
60
63
  const fetchSourceCodeFromStackTrace = (stack = '') => {
61
- const stackLines = stack.split('\n')
64
+ const stackLines = stack
65
+ .split('\n')
62
66
  .filter(l => l.includes(':'))
63
67
  // .map(l => l.match(/\[(.*?)\]/)?.[1] || l) // minitest format
64
68
  // .map(l => l.split(':')[0])
@@ -67,17 +71,17 @@ const fetchSourceCodeFromStackTrace = (stack = '') => {
67
71
  .filter(l => isValid(l?.split(':')[0]))
68
72
 
69
73
  // // filter out 3rd party libs
70
- .filter(l => !l?.includes(`vendor${ sep}`))
71
- .filter(l => !l?.includes(`node_modules${ sep}`))
74
+ .filter(l => !l?.includes(`vendor${sep}`))
75
+ .filter(l => !l?.includes(`node_modules${sep}`))
72
76
  .filter(l => fs.existsSync(l.split(':')[0]))
73
- .filter(l => fs.lstatSync(l.split(':')[0]).isFile())
77
+ .filter(l => fs.lstatSync(l.split(':')[0]).isFile());
74
78
 
75
79
  if (!stackLines.length) return '';
76
80
 
77
81
  const [file, line] = stackLines[0].split(':');
78
82
 
79
83
  const prepend = 3;
80
- const source = fetchSourceCode(fs.readFileSync(file).toString(), { line, prepend, limit: 7 })
84
+ const source = fetchSourceCode(fs.readFileSync(file).toString(), { line, prepend, limit: 7 });
81
85
 
82
86
  if (!source) return '';
83
87
 
@@ -85,22 +89,22 @@ const fetchSourceCodeFromStackTrace = (stack = '') => {
85
89
  .map((l, i) => {
86
90
  if (i === prepend) return `${line} > ${chalk.bold(l)}`;
87
91
  return `${line - prepend + i} | ${l}`
88
- }).join('\n')
89
- }
92
+ }).join('\n');
93
+ };
90
94
 
91
95
  const fetchSourceCode = (contents, opts = {}) => {
92
96
  if (!opts.title && !opts.line) return '';
93
97
 
94
- // code fragment is 20 lines
98
+ // code fragment is 20 lines
95
99
  const limit = opts.limit || 50;
96
100
  let lineIndex;
97
101
  if (opts.line) lineIndex = opts.line - 1;
98
- const lines = contents.split('\n')
102
+ const lines = contents.split('\n');
99
103
 
100
104
  // remove special chars from title
101
105
  if (!lineIndex && opts.title) {
102
- const title = opts.title.replace(/[([@].*/g, '')
103
- lineIndex = lines.findIndex(l => l.includes(title))
106
+ const title = opts.title.replace(/[([@].*/g, '');
107
+ lineIndex = lines.findIndex(l => l.includes(title));
104
108
  }
105
109
 
106
110
  if (opts.prepend) {
@@ -133,52 +137,69 @@ const fetchSourceCode = (contents, opts = {}) => {
133
137
  if (opts.lang === 'java' && lines[i].includes(' public void ')) break;
134
138
  if (opts.lang === 'java' && lines[i].includes(' class ')) break;
135
139
  }
136
- result.push(lines[i])
140
+ result.push(lines[i]);
137
141
  }
138
142
  return result.join('\n');
139
143
  }
140
- }
144
+ };
141
145
 
142
- const isSameTest = (test, t) => (typeof t === 'object')
143
- && (typeof test === 'object')
144
- && t.title === test.title
145
- && t.suite_title === test.suite_title
146
- && Object.values(t.example || {}) === Object.values(test.example || {})
147
- && t.test_id === test.test_id;
146
+ const isSameTest = (test, t) =>
147
+ typeof t === 'object' &&
148
+ typeof test === 'object' &&
149
+ t.title === test.title &&
150
+ t.suite_title === test.suite_title &&
151
+ Object.values(t.example || {}) === Object.values(test.example || {}) &&
152
+ t.test_id === test.test_id;
148
153
 
149
154
  const getCurrentDateTime = () => {
150
155
  const today = new Date();
151
-
152
- return `${today.getFullYear() }_${ today.getMonth() + 1 }_${ today.getDate() }_${
153
- today.getHours() }_${ today.getMinutes() }_${ today.getSeconds()}`;
154
- }
156
+
157
+ return `${today.getFullYear()}_${
158
+ today.getMonth() + 1
159
+ }_${today.getDate()}_${today.getHours()}_${today.getMinutes()}_${today.getSeconds()}`;
160
+ };
155
161
 
156
162
  /**
157
163
  * @param {Object} test - Test adapter object
158
164
  *
159
- * @returns {String|null} testInfo as one string
165
+ * @returns {String|null} testInfo as one string
160
166
  */
161
167
  const specificTestInfo = test => {
162
168
  // TODO: afterEach has another context.... need to add specific handler, maybe...
163
169
  if (test?.title && test?.file) {
164
-
165
- return `${basename(test.file).split(".").join("#")
166
- }#${
167
- test.title.split(" ").join("#")}`;
170
+ return `${basename(test.file).split('.').join('#')}#${test.title.split(' ').join('#')}`;
168
171
  }
169
172
 
170
173
  return null;
171
174
  };
172
175
 
176
+ const fileSystem = {
177
+ createDir(dirPath) {
178
+ if (!fs.existsSync(dirPath)) {
179
+ fs.mkdirSync(dirPath, { recursive: true });
180
+ debug('Created dir: ', dirPath);
181
+ }
182
+ },
183
+ clearDir(dirPath) {
184
+ if (fs.existsSync(dirPath)) {
185
+ fs.rmSync(dirPath, { recursive: true });
186
+ debug(`Dir ${dirPath} was deleted`);
187
+ } else {
188
+ debug(`Trying to delete ${dirPath} but it doesn't exist`);
189
+ }
190
+ }
191
+ };
192
+
173
193
  module.exports = {
174
194
  isSameTest,
175
195
  fetchSourceCode,
176
196
  fetchSourceCodeFromStackTrace,
177
197
  fetchFilesFromStackTrace,
198
+ fileSystem,
178
199
  getCurrentDateTime,
179
200
  specificTestInfo,
180
201
  isValidUrl,
181
202
  ansiRegExp,
182
203
  parseTest,
183
- parseSuite
184
- }
204
+ parseSuite,
205
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testomatio/reporter",
3
- "version": "1.0.0-beta.4",
3
+ "version": "1.0.1",
4
4
  "description": "Testomatio Reporter Client",
5
5
  "main": "./lib/reporter.js",
6
6
  "typings": "typings/index.d.ts",