@testomatio/reporter 1.0.0-beta.4 → 1.1.0-beta

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,221 @@
1
+ const debug = require('debug')('@testomatio/reporter:logger');
2
+ const { DataStorage } = require('./dataStorage');
3
+
4
+ const LOG_METHODS = ['assert', 'debug', 'error', 'info', 'log', 'trace', 'warn'];
5
+
6
+ /**
7
+ * Logger allows to:
8
+ * 1. Intercept logs from user logger (console.log, etc) and store them.
9
+ * 2. Output logs to console (actually, logger functionality).
10
+ * 3. Varied syntax.
11
+ */
12
+ class Logger {
13
+ // _originalUserLogger used to output logs to console by the user logger
14
+ // _loggerToIntercept intercepted and reassigned immediately when added
15
+
16
+ constructor(params = {}) {
17
+ this.dataStorage = new DataStorage('log', params);
18
+
19
+ // commented because prefer to use "intercept" method
20
+ // if (params?.logger) this._loggerToIntercept = params.logger;
21
+
22
+ this.intercept(this._loggerToIntercept);
23
+
24
+ // singleton
25
+ if (!Logger.instance) {
26
+ Logger.instance = this;
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Returns the string (logs) and the context
32
+ * This is required because loggers take multiple arguments
33
+ * @param {...any} args
34
+ */
35
+ /*
36
+ TODO: its difficult to distinguish context from log artument if its not a string,
37
+ e.g. user can use something like console.log('some log', { key: 'value' });
38
+ consider not to use this method at all and don't expect context from user inside default log methods;
39
+ instead, use custom method like $`some log` and parse it
40
+ */
41
+ // _getLogStringAndContextFromArgs(...args) {
42
+ // let context = '';
43
+ // if (args.length > 1 && typeof args[args.length - 1] !== 'string') {
44
+ // context = args.pop();
45
+ // }
46
+ // const logs = args.join(' ');
47
+ // return { logs, context };
48
+ // }
49
+
50
+ /**
51
+ * Tagget template literal. Allows to use different syntaxes:
52
+ * 1. Tagget template: $`text ${someVar}`
53
+ * 2. Standard: $(`text ${someVar}`)
54
+ * 3. Standard with multiple arguments: $('text', someVar)
55
+ */
56
+ _log(strings, ...args) {
57
+ let logs;
58
+ // this block means tagged template is used (syntax like $`text ${someVar}`)
59
+ if (Array.isArray(strings) && strings.length === args.length + 1) {
60
+ logs = strings.reduce(
61
+ (result, current, index) =>
62
+ result +
63
+ current +
64
+ // strings are splitted by args when use tagged template, thus we add arg after each string
65
+ // it looks like: `string1 arg1 string2 arg2 string3`
66
+ (args[index] !== undefined // eslint-disable-line no-nested-ternary
67
+ ? typeof args[index] === 'string'
68
+ ? // add arg as it is
69
+ args[index]
70
+ : // stringify arg
71
+ this._strinfifyLogs(args[index])
72
+ : // add space if no arg after string
73
+ ' '),
74
+ // initial accumulator value
75
+ '',
76
+ );
77
+ } else {
78
+ // this block means arguments syntax is used (syntax like $('text', someVar))
79
+ logs = this._strinfifyLogs(strings, ...args);
80
+ }
81
+ this.dataStorage.putData(logs);
82
+ }
83
+
84
+ /**
85
+ *
86
+ * @param {*} context testId or test context from test runner
87
+ * @returns
88
+ */
89
+ getLogs(context) {
90
+ return this.dataStorage.getData(context);
91
+ }
92
+
93
+ _strinfifyLogs(...args) {
94
+ const logs = [];
95
+ // stringify everything except strings
96
+ for (const arg of args) {
97
+ if (typeof arg === 'string') {
98
+ logs.push(arg);
99
+ } else {
100
+ logs.push(JSON.stringify(arg));
101
+ }
102
+ }
103
+ return logs.join(' ');
104
+ }
105
+
106
+ assert(...args) {
107
+ const logs = this._strinfifyLogs(...args);
108
+ this.dataStorage.putData(logs);
109
+ try {
110
+ this._originalUserLogger.log(`Assertion result: `, ...args);
111
+ } catch (e) {
112
+ // method could be unexisting, ignore error
113
+ }
114
+ }
115
+
116
+ debug(...args) {
117
+ const logs = this._strinfifyLogs(...args);
118
+ this.dataStorage.putData(logs);
119
+ try {
120
+ this._originalUserLogger.debug(...args);
121
+ } catch (e) {
122
+ // method could be unexisting, ignore error
123
+ }
124
+ }
125
+
126
+ error(...args) {
127
+ const logs = this._strinfifyLogs(...args);
128
+ this.dataStorage.putData(logs);
129
+ try {
130
+ this._originalUserLogger.error(...args);
131
+ } catch (e) {
132
+ // method could be unexisting, ignore error
133
+ }
134
+ }
135
+
136
+ info(...args) {
137
+ const logs = this._strinfifyLogs(...args);
138
+ this.dataStorage.putData(logs);
139
+ try {
140
+ this._originalUserLogger.info(...args);
141
+ } catch (e) {
142
+ // method could be unexisting, ignore error
143
+ }
144
+ }
145
+
146
+ log(...args) {
147
+ const logs = this._strinfifyLogs(...args);
148
+ this.dataStorage.putData(logs);
149
+ try {
150
+ this._originalUserLogger.log(...args);
151
+ } catch (e) {
152
+ // method could be unexisting, ignore error
153
+ }
154
+ }
155
+
156
+ trace(...args) {
157
+ const logs = this._strinfifyLogs(...args);
158
+ this.dataStorage.putData(logs);
159
+ try {
160
+ this._originalUserLogger.trace(...args);
161
+ } catch (e) {
162
+ // method could be unexisting, ignore error
163
+ }
164
+ }
165
+
166
+ warn(...args) {
167
+ const logs = this._strinfifyLogs(...args);
168
+ this.dataStorage.putData(logs);
169
+ try {
170
+ this._originalUserLogger.warn(...args);
171
+ } catch (e) {
172
+ // method could be unexisting, ignore error
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Intercepts user logger messages.
178
+ * When call this method, Logger start to control the user logger,
179
+ * but almost nothing is changed for user ragarding the console output (like log level set by user)
180
+ * (until multiple loggers are intercepted,
181
+ * in this case only the last intercepted logger will be used as user console output).
182
+ * @param {*} userLogger
183
+ */
184
+ intercept(userLogger) {
185
+ if (!userLogger) return;
186
+ debug(`Intercepting user logger`);
187
+
188
+ // save original user logger to use it for logging (last intercepted will be used for console output)
189
+ this._originalUserLogger = { ...userLogger };
190
+ this._loggerToIntercept = userLogger;
191
+
192
+ /*
193
+ override user logger (any, e.g. console) methods to intercept log messages
194
+ this._loggerToIntercept = this; could be used, but decided to override only output methods
195
+ */
196
+ for (const method of LOG_METHODS) {
197
+ /*
198
+ decided to comment next code line because its better to create method even if it does not exist in user logger;
199
+ on method invocation, we will store the data anyway and catch block will prevent potential errors
200
+ */
201
+ // if (!this._loggerToIntercept[method]) continue;
202
+ this._loggerToIntercept[method] = (...args) => this[method](...args);
203
+ }
204
+ }
205
+ }
206
+
207
+ Logger.instance = null;
208
+
209
+ // module.exports.Logger = Logger;
210
+ module.exports = new Logger();
211
+
212
+ // TODO: consider using fs promises instead of writeSync/appendFileSync to prevent blocking and improve performance
213
+ // TODO: try to use method generator *
214
+ // TODO: understand captureStackTrace in output (check on .error) (NOT SUCH IMPORTANT)
215
+ // TODO: handle log levels to colorize logs later on UI
216
+ // TODO: parse passed arguments as {level: 'str', message: 'str'} because some loggers use such syntax
217
+ // TODO: add setLevel method - allows skip messages with the lower level
218
+ // TODO: in case of unset _originalUserLogger, logger.{method} will not provide console output,
219
+ // need to add some logger by default
220
+
221
+ // TODO step method (blue color)
@@ -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,15 @@
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);
4
7
 
5
8
  module.exports = {
9
+ logger,
10
+ log,
6
11
  TestomatClient,
7
12
  TRConstants,
8
- TRArtifacts
13
+ TRArtifacts,
14
+ addArtifact: TRArtifacts.artifact,
9
15
  };
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
@@ -32,7 +33,6 @@ const parseSuite = suiteTitle => {
32
33
  return null;
33
34
  };
34
35
 
35
-
36
36
  const ansiRegExp = () => {
37
37
  const pattern = [
38
38
  '[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)',
@@ -54,11 +54,14 @@ const isValidUrl = s => {
54
54
 
55
55
  const fetchFilesFromStackTrace = (stack = '') => {
56
56
  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
- }
57
+ return Array.from(files)
58
+ .map(f => f[1])
59
+ .filter(f => fs.existsSync(f));
60
+ };
59
61
 
60
62
  const fetchSourceCodeFromStackTrace = (stack = '') => {
61
- const stackLines = stack.split('\n')
63
+ const stackLines = stack
64
+ .split('\n')
62
65
  .filter(l => l.includes(':'))
63
66
  // .map(l => l.match(/\[(.*?)\]/)?.[1] || l) // minitest format
64
67
  // .map(l => l.split(':')[0])
@@ -67,17 +70,17 @@ const fetchSourceCodeFromStackTrace = (stack = '') => {
67
70
  .filter(l => isValid(l?.split(':')[0]))
68
71
 
69
72
  // // filter out 3rd party libs
70
- .filter(l => !l?.includes(`vendor${ sep}`))
71
- .filter(l => !l?.includes(`node_modules${ sep}`))
73
+ .filter(l => !l?.includes(`vendor${sep}`))
74
+ .filter(l => !l?.includes(`node_modules${sep}`))
72
75
  .filter(l => fs.existsSync(l.split(':')[0]))
73
- .filter(l => fs.lstatSync(l.split(':')[0]).isFile())
76
+ .filter(l => fs.lstatSync(l.split(':')[0]).isFile());
74
77
 
75
78
  if (!stackLines.length) return '';
76
79
 
77
80
  const [file, line] = stackLines[0].split(':');
78
81
 
79
82
  const prepend = 3;
80
- const source = fetchSourceCode(fs.readFileSync(file).toString(), { line, prepend, limit: 7 })
83
+ const source = fetchSourceCode(fs.readFileSync(file).toString(), { line, prepend, limit: 7 });
81
84
 
82
85
  if (!source) return '';
83
86
 
@@ -85,22 +88,22 @@ const fetchSourceCodeFromStackTrace = (stack = '') => {
85
88
  .map((l, i) => {
86
89
  if (i === prepend) return `${line} > ${chalk.bold(l)}`;
87
90
  return `${line - prepend + i} | ${l}`
88
- }).join('\n')
89
- }
91
+ }).join('\n');
92
+ };
90
93
 
91
94
  const fetchSourceCode = (contents, opts = {}) => {
92
95
  if (!opts.title && !opts.line) return '';
93
96
 
94
- // code fragment is 20 lines
97
+ // code fragment is 20 lines
95
98
  const limit = opts.limit || 50;
96
99
  let lineIndex;
97
100
  if (opts.line) lineIndex = opts.line - 1;
98
- const lines = contents.split('\n')
101
+ const lines = contents.split('\n');
99
102
 
100
103
  // remove special chars from title
101
104
  if (!lineIndex && opts.title) {
102
- const title = opts.title.replace(/[([@].*/g, '')
103
- lineIndex = lines.findIndex(l => l.includes(title))
105
+ const title = opts.title.replace(/[([@].*/g, '');
106
+ lineIndex = lines.findIndex(l => l.includes(title));
104
107
  }
105
108
 
106
109
  if (opts.prepend) {
@@ -133,52 +136,69 @@ const fetchSourceCode = (contents, opts = {}) => {
133
136
  if (opts.lang === 'java' && lines[i].includes(' public void ')) break;
134
137
  if (opts.lang === 'java' && lines[i].includes(' class ')) break;
135
138
  }
136
- result.push(lines[i])
139
+ result.push(lines[i]);
137
140
  }
138
141
  return result.join('\n');
139
142
  }
140
- }
143
+ };
141
144
 
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;
145
+ const isSameTest = (test, t) =>
146
+ typeof t === 'object' &&
147
+ typeof test === 'object' &&
148
+ t.title === test.title &&
149
+ t.suite_title === test.suite_title &&
150
+ Object.values(t.example || {}) === Object.values(test.example || {}) &&
151
+ t.test_id === test.test_id;
148
152
 
149
153
  const getCurrentDateTime = () => {
150
154
  const today = new Date();
151
-
152
- return `${today.getFullYear() }_${ today.getMonth() + 1 }_${ today.getDate() }_${
153
- today.getHours() }_${ today.getMinutes() }_${ today.getSeconds()}`;
154
- }
155
+
156
+ return `${today.getFullYear()}_${
157
+ today.getMonth() + 1
158
+ }_${today.getDate()}_${today.getHours()}_${today.getMinutes()}_${today.getSeconds()}`;
159
+ };
155
160
 
156
161
  /**
157
162
  * @param {Object} test - Test adapter object
158
163
  *
159
- * @returns {String|null} testInfo as one string
164
+ * @returns {String|null} testInfo as one string
160
165
  */
161
166
  const specificTestInfo = test => {
162
167
  // TODO: afterEach has another context.... need to add specific handler, maybe...
163
168
  if (test?.title && test?.file) {
164
-
165
- return `${basename(test.file).split(".").join("#")
166
- }#${
167
- test.title.split(" ").join("#")}`;
169
+ return `${basename(test.file).split('.').join('#')}#${test.title.split(' ').join('#')}`;
168
170
  }
169
171
 
170
172
  return null;
171
173
  };
172
174
 
175
+ const fileSystem = {
176
+ createDir(dirPath) {
177
+ if (!fs.existsSync(dirPath)) {
178
+ fs.mkdirSync(dirPath, { recursive: true });
179
+ debug('Created dir: ', dirPath);
180
+ }
181
+ },
182
+ clearDir(dirPath) {
183
+ if (fs.existsSync(dirPath)) {
184
+ fs.rmSync(dirPath, { recursive: true });
185
+ debug(`Dir ${dirPath} was deleted`);
186
+ } else {
187
+ debug(`Trying to delete ${dirPath} but it doesn't exist`);
188
+ }
189
+ }
190
+ };
191
+
173
192
  module.exports = {
174
193
  isSameTest,
175
194
  fetchSourceCode,
176
195
  fetchSourceCodeFromStackTrace,
177
196
  fetchFilesFromStackTrace,
197
+ fileSystem,
178
198
  getCurrentDateTime,
179
199
  specificTestInfo,
180
200
  isValidUrl,
181
201
  ansiRegExp,
182
202
  parseTest,
183
- parseSuite
184
- }
203
+ parseSuite,
204
+ };
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.1.0-beta",
4
4
  "description": "Testomatio Reporter Client",
5
5
  "main": "./lib/reporter.js",
6
6
  "typings": "typings/index.d.ts",