@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/README.md +88 -540
- package/lib/{ArtifactStorage.js → _ArtifactStorageOld.js} +10 -0
- package/lib/adapter/codecept.js +28 -18
- package/lib/adapter/jest.js +15 -2
- package/lib/adapter/mocha.js +1 -1
- package/lib/artifactStorage.js +25 -0
- package/lib/client.js +21 -13
- package/lib/constants.js +5 -0
- package/lib/dataStorage.js +172 -0
- package/lib/logger.js +221 -0
- package/lib/pipe/github.js +2 -2
- package/lib/pipe/testomatio.js +2 -1
- package/lib/reporter.js +8 -2
- package/lib/util.js +53 -33
- package/package.json +1 -1
- package/Changelog.md +0 -257
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)
|
package/lib/pipe/github.js
CHANGED
|
@@ -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.
|
|
203
|
+
if (process.env.GH_KEEP_OUTDATED_REPORTS) return;
|
|
204
204
|
|
|
205
205
|
// get comments
|
|
206
206
|
let comments = [];
|
package/lib/pipe/testomatio.js
CHANGED
|
@@ -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('./
|
|
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)
|
|
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
|
|
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${
|
|
71
|
-
.filter(l => !l?.includes(`node_modules${
|
|
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
|
-
|
|
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) =>
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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()
|
|
153
|
-
|
|
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
|
+
};
|