@testomatio/reporter 0.5.8 → 0.6.0-beta.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/Changelog.md +9 -0
- package/README.md +18 -0
- package/lib/adapter/cypress-plugin/index.js +15 -7
- package/lib/bin/reportXml.js +50 -0
- package/lib/client.js +3 -2
- package/lib/junit-adapter/adapter.js +25 -0
- package/lib/junit-adapter/java.js +40 -0
- package/lib/junit-adapter/javascript.js +31 -0
- package/lib/junit-adapter/python.js +42 -0
- package/lib/junit-adapter/ruby.js +13 -0
- package/lib/util.js +95 -0
- package/lib/xmlReader.js +348 -0
- package/package.json +8 -3
- package/tests/adapter/config/index.js +1 -1
- package/tests/data/artifacts/failed_test.png +0 -0
- package/tests/data/artifacts/screenshot1.png +0 -0
- package/tests/data/cli/RunCest.php +1024 -0
- package/tests/data/cli/SnapshotCest.php +97 -0
- package/tests/data/codecept.xml +150 -0
- package/tests/data/codecept2.xml +25 -0
- package/tests/data/java.xml +25 -0
- package/tests/data/junit1.xml +70 -0
- package/tests/data/minitest.xml +25 -0
- package/tests/data/phpunit.xml +65 -0
- package/tests/data/pytest.xml +13 -0
- package/tests/data/src/services/search_service_test.rb +146 -0
- package/tests/util_test.js +51 -0
- package/tests/xmlReader_test.js +232 -0
package/Changelog.md
CHANGED
package/README.md
CHANGED
|
@@ -205,6 +205,24 @@ Run the following command from you project folder:
|
|
|
205
205
|
TESTOMATIO={API_KEY} npx @testomatio/reporter -c 'npx wdio wdio.conf.js'
|
|
206
206
|
```
|
|
207
207
|
|
|
208
|
+
## JUnit Reports
|
|
209
|
+
|
|
210
|
+
Other frameworks and languages are supported via JUnit reports.
|
|
211
|
+
|
|
212
|
+
JUnit XML format is standard among test runners on various platforms. Testomat.io can load XML reports from test runners and create tests in a project from it. If your framework is not supported yet, generate JUnit report and upload it into Testomat.io
|
|
213
|
+
|
|
214
|
+
```
|
|
215
|
+
TESTOMATIO={API_KEY} npx @testomatio/reporter -c 'npx wdio wdio.conf.js'
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Tested Frameworks:
|
|
219
|
+
|
|
220
|
+
* JUnit (JUnit)
|
|
221
|
+
* Python (Pytest)
|
|
222
|
+
* Minitest (Ruby)
|
|
223
|
+
* PHPUnit (PHP)
|
|
224
|
+
|
|
225
|
+
|
|
208
226
|
## Advanced Usage
|
|
209
227
|
|
|
210
228
|
### Create Unmatched Tests
|
|
@@ -3,13 +3,13 @@ const { parseTest, parseSuite } = require('../../util');
|
|
|
3
3
|
const TestomatClient = require('../../client');
|
|
4
4
|
|
|
5
5
|
const testomatioReporter = on => {
|
|
6
|
+
if (!process.env.TESTOMATIO) {
|
|
7
|
+
console.log('TESTOMATIO key is empty, ignoring reports');
|
|
8
|
+
return
|
|
9
|
+
}
|
|
6
10
|
const client = new TestomatClient({ apiKey: process.env.TESTOMATIO });
|
|
7
11
|
|
|
8
12
|
on('before:run', async (run) => {
|
|
9
|
-
if (!client.title) {
|
|
10
|
-
client.title = `${run.config.projectName} tests`
|
|
11
|
-
if (client.title.includes('undefined')) delete client.title;
|
|
12
|
-
}
|
|
13
13
|
if (!client.env) {
|
|
14
14
|
client.env = `${run.browser.displayName},${run.system.osName}`
|
|
15
15
|
}
|
|
@@ -28,7 +28,12 @@ const testomatioReporter = on => {
|
|
|
28
28
|
const time = latestAttempt.duration;
|
|
29
29
|
const error = latestAttempt.error;
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
let title = test.title.pop();
|
|
32
|
+
let examples = title.match(/\(example (#\d+)\)/);
|
|
33
|
+
let example = null;
|
|
34
|
+
if (examples && examples[1]) example = { example: examples[1] };
|
|
35
|
+
title = title.replace(/\(example #\d+\)/, '').trim();
|
|
36
|
+
|
|
32
37
|
const suiteTitle = test.title.pop();
|
|
33
38
|
|
|
34
39
|
const testId = parseTest(title);
|
|
@@ -36,7 +41,10 @@ const testomatioReporter = on => {
|
|
|
36
41
|
|
|
37
42
|
if (error) {
|
|
38
43
|
error.inspect = function() {
|
|
39
|
-
|
|
44
|
+
if (this && this.codeFrame) {
|
|
45
|
+
return this.codeFrame.frame;
|
|
46
|
+
}
|
|
47
|
+
return '';
|
|
40
48
|
}
|
|
41
49
|
}
|
|
42
50
|
|
|
@@ -50,7 +58,7 @@ const testomatioReporter = on => {
|
|
|
50
58
|
const state = test.state === 'passed' ? TRConstants.PASSED : TRConstants.FAILED;
|
|
51
59
|
|
|
52
60
|
addSpecTestsPromises.push(client.addTestRun(testId, state, {
|
|
53
|
-
title, time, error, files, suite_title: suiteTitle, suite_id: suiteId
|
|
61
|
+
title, time, example, error, files, suite_title: suiteTitle, suite_id: suiteId
|
|
54
62
|
}));
|
|
55
63
|
}
|
|
56
64
|
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const program = require("commander");
|
|
3
|
+
// const chalk = require("chalk");
|
|
4
|
+
const glob = require('glob');
|
|
5
|
+
|
|
6
|
+
const { APP_PREFIX } = require('../constants');
|
|
7
|
+
const XmlReader = require("../xmlReader");
|
|
8
|
+
|
|
9
|
+
// const apiKey = process.env['INPUT_TESTOMATIO-KEY'] || process.env.TESTOMATIO;
|
|
10
|
+
// const title = process.env.TESTOMATIO_TITLE;
|
|
11
|
+
|
|
12
|
+
// console.log(chalk.cyan.bold(` 🤩 xmlReader by Testomat.io v${version}`));
|
|
13
|
+
|
|
14
|
+
program
|
|
15
|
+
.arguments("<pattern>")
|
|
16
|
+
.option("-d, --dir <dir>", "Project directory")
|
|
17
|
+
.option("--java-tests [java-path]", "Load Java tests from path, by default: src/test/java")
|
|
18
|
+
.option("--lang <lang>", "Language used (python, ruby, java)")
|
|
19
|
+
.action(async (pattern, opts) => {
|
|
20
|
+
if (!pattern.endsWith('.xml')) {
|
|
21
|
+
pattern += '.xml';
|
|
22
|
+
}
|
|
23
|
+
let { javaTests, lang } = opts;
|
|
24
|
+
if (javaTests === true) javaTests = 'src/test/java';
|
|
25
|
+
const runReader = new XmlReader({ javaTests, lang });
|
|
26
|
+
const files = glob.sync(pattern, { cwd: opts.dir || process.cwd() });
|
|
27
|
+
if (!files.length) {
|
|
28
|
+
console.log(APP_PREFIX,`Report can't be created. No XML files found 😥`);
|
|
29
|
+
process.exitCode = 1;
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
for (const file of files) {
|
|
34
|
+
console.log(APP_PREFIX,`Parsed ${file}`);
|
|
35
|
+
runReader.parse(file);
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
await runReader.createRun();
|
|
39
|
+
await runReader.uploadData();
|
|
40
|
+
} catch (err) {
|
|
41
|
+
console.log(APP_PREFIX, 'Error updating status, skipping...', err);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
if (process.argv.length < 1) {
|
|
47
|
+
program.outputHelp();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
program.parse(process.argv);
|
package/lib/client.js
CHANGED
|
@@ -25,6 +25,7 @@ class TestomatClient {
|
|
|
25
25
|
this.apiKey = params.apiKey || process.env.TESTOMATIO;
|
|
26
26
|
this.title = params.title || process.env.TESTOMATIO_TITLE;
|
|
27
27
|
this.createNewTests = !!process.env.TESTOMATIO_CREATE
|
|
28
|
+
this.env = TESTOMATIO_ENV;
|
|
28
29
|
this.parallel = params.parallel;
|
|
29
30
|
this.runId = process.env.runId;
|
|
30
31
|
this.queue = Promise.resolve();
|
|
@@ -45,8 +46,8 @@ class TestomatClient {
|
|
|
45
46
|
api_key: this.apiKey.trim(),
|
|
46
47
|
title: this.title,
|
|
47
48
|
parallel: this.parallel,
|
|
49
|
+
env: this.env,
|
|
48
50
|
group_title: TESTOMATIO_RUNGROUP_TITLE,
|
|
49
|
-
env: TESTOMATIO_ENV,
|
|
50
51
|
};
|
|
51
52
|
|
|
52
53
|
global.testomatioArtifacts = [];
|
|
@@ -233,7 +234,7 @@ class TestomatClient {
|
|
|
233
234
|
|
|
234
235
|
formatError(error, message) {
|
|
235
236
|
if (!message) message = error.message;
|
|
236
|
-
if (error.inspect) message = error.inspect();
|
|
237
|
+
if (error.inspect) message = error.inspect() || '';
|
|
237
238
|
|
|
238
239
|
let stack = `\n${chalk.bold(message)}\n`;
|
|
239
240
|
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
class Adapter {
|
|
2
|
+
|
|
3
|
+
constructor(opts) {
|
|
4
|
+
this.opts = opts;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
getFilePath(t) {
|
|
8
|
+
return t.file;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
formatTest(t) {
|
|
12
|
+
return t;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
formatStack(t) {
|
|
16
|
+
return t.stack || '';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
formatMessage(t) {
|
|
20
|
+
return t.message;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
module.exports = Adapter;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const Adapter = require('./adapter');
|
|
3
|
+
|
|
4
|
+
class JavaAdapter extends Adapter {
|
|
5
|
+
|
|
6
|
+
getFilePath(t) {
|
|
7
|
+
const fileName = namespaceToFileName(t.suite_title)
|
|
8
|
+
return this.opts.javaTests + path.sep + fileName;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
formatTest(t) {
|
|
12
|
+
const fileParts = t.suite_title.split('.')
|
|
13
|
+
const example = t.title.match(/\[(.*)\]/)?.[1];
|
|
14
|
+
if (example) t.example = { "#": example }
|
|
15
|
+
|
|
16
|
+
t.file = namespaceToFileName(t.suite_title);
|
|
17
|
+
t.title = t.title.split('(')[0];
|
|
18
|
+
t.suite_title = fileParts[fileParts.length - 1].replace(/\$/g, ' | ')
|
|
19
|
+
return t;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
formatStack(t) {
|
|
23
|
+
let stack = super.formatStack(t);
|
|
24
|
+
|
|
25
|
+
const file = t.suite_title.split('.');
|
|
26
|
+
|
|
27
|
+
const fileLine = `at .*${file[file.length - 1]}\.java\:(\\d+)`
|
|
28
|
+
const regexp = new RegExp(fileLine,"g")
|
|
29
|
+
return stack.replace(regexp, `${this.opts.javaTests}${path.sep}${namespaceToFileName(t.suite_title)}:$1:`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function namespaceToFileName(fileName) {
|
|
34
|
+
let fileParts = fileName.split('.')
|
|
35
|
+
fileParts[fileParts.length - 1] = fileParts[fileParts.length - 1]?.replace(/\$.*/, '')
|
|
36
|
+
return fileParts.join(path.sep) + '.java';
|
|
37
|
+
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
module.exports = JavaAdapter;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const createCallsiteRecord = require('callsite-record');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const Adapter = require('./adapter');
|
|
4
|
+
|
|
5
|
+
class JavaScriptAdapter extends Adapter {
|
|
6
|
+
|
|
7
|
+
formatStack(t) {
|
|
8
|
+
let stack = super.formatStack(t);
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
const error = new Error(stack.split('\n')[0])
|
|
12
|
+
error.stack = stack;
|
|
13
|
+
const record = createCallsiteRecord({
|
|
14
|
+
forError: error,
|
|
15
|
+
});
|
|
16
|
+
if (record && !record.filename.startsWith('http')) {
|
|
17
|
+
stack += record.renderSync({
|
|
18
|
+
stackFilter: frame =>
|
|
19
|
+
frame.getFileName().indexOf(path.sep) > -1 &&
|
|
20
|
+
frame.getFileName().indexOf('node_modules') < 0 &&
|
|
21
|
+
frame.getFileName().indexOf('internal') < 0,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
return stack;
|
|
25
|
+
} catch (err) {
|
|
26
|
+
return stack;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
module.exports = JavaScriptAdapter;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const Adapter = require('./adapter');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
|
|
5
|
+
class PythonAdapter extends Adapter {
|
|
6
|
+
|
|
7
|
+
getFilePath(t) {
|
|
8
|
+
const fileName = namespaceToFileName(t.suite_title)
|
|
9
|
+
return fileName;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
formatTest(t) {
|
|
13
|
+
const fileParts = t.suite_title.split('.')
|
|
14
|
+
const example = t.title.match(/\[(.*)\]/)?.[1];
|
|
15
|
+
if (example) t.example = { "#": example }
|
|
16
|
+
|
|
17
|
+
t.file = namespaceToFileName(t.suite_title);
|
|
18
|
+
t.title = t.title.split('[')[0];
|
|
19
|
+
t.suite_title = fileParts[fileParts.length - 1].replace(/\$/g, ' | ')
|
|
20
|
+
return t;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
formatMessage(t) {
|
|
24
|
+
return t.message.split(' ')[0];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function namespaceToFileName(fileName) {
|
|
30
|
+
let fileParts = fileName.split('.')
|
|
31
|
+
|
|
32
|
+
while (fileParts.length > 0) {
|
|
33
|
+
const file = fileParts.join(path.sep) + '.py';
|
|
34
|
+
if (fs.existsSync(fileParts.join(path.sep) + '.py')) {
|
|
35
|
+
return file;
|
|
36
|
+
}
|
|
37
|
+
fileParts.pop();
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
module.exports = PythonAdapter;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const Adapter = require('./adapter');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
|
|
5
|
+
class RubyAdapter extends Adapter {
|
|
6
|
+
|
|
7
|
+
formatStack(t) {
|
|
8
|
+
const stack = super.formatStack(t);
|
|
9
|
+
return stack.replace(/\[(.*?\:.\d*)\]/g, '\n$1')
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
module.exports = RubyAdapter;
|
package/lib/util.js
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
+
const { fstat } = require('fs');
|
|
2
|
+
const { includes, split } = require('lodash');
|
|
1
3
|
const { URL } = require('url');
|
|
4
|
+
const { sep } = require('path');
|
|
5
|
+
const chalk = require('chalk');
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const isValid = require('is-valid-path');
|
|
2
8
|
|
|
3
9
|
/**
|
|
4
10
|
* @param {String} testTitle - Test title
|
|
@@ -48,9 +54,98 @@ const isValidUrl = s => {
|
|
|
48
54
|
}
|
|
49
55
|
};
|
|
50
56
|
|
|
57
|
+
const fetchFilesFromStackTrace = (stack = '') => {
|
|
58
|
+
const files = stack.matchAll(/file:?\/(\/.*?\.(png|avi|webm|jpg|html|txt))/g);
|
|
59
|
+
return Array.from(files).map(f => f[1]).filter(f => fs.existsSync(f));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const fetchSourceCodeFromStackTrace = (stack = '') => {
|
|
63
|
+
const stackLines = stack.split('\n')
|
|
64
|
+
.filter(l => l.includes(':'))
|
|
65
|
+
// .map(l => l.match(/\[(.*?)\]/)?.[1] || l) // minitest format
|
|
66
|
+
// .map(l => l.split(':')[0])
|
|
67
|
+
.map(l => l.trim())
|
|
68
|
+
.map(l => l.split(' ').find(p => p.includes(':')))
|
|
69
|
+
.filter(l => isValid(l.split(':')[0]))
|
|
70
|
+
|
|
71
|
+
// // filter out 3rd party libs
|
|
72
|
+
.filter(l => !l.includes('vendor' + sep))
|
|
73
|
+
.filter(l => !l.includes('node_modules' + sep))
|
|
74
|
+
.filter(l => fs.existsSync(l.split(':')[0]))
|
|
75
|
+
.filter(l => fs.lstatSync(l.split(':')[0]).isFile())
|
|
76
|
+
|
|
77
|
+
if (!stackLines.length) return '';
|
|
78
|
+
|
|
79
|
+
const [file, line] = stackLines[0].split(':');
|
|
80
|
+
|
|
81
|
+
const prepend = 3;
|
|
82
|
+
const source = fetchSourceCode(fs.readFileSync(file).toString(), { line, prepend, limit: 7 })
|
|
83
|
+
|
|
84
|
+
return source.split('\n')
|
|
85
|
+
.map((l, i) => {
|
|
86
|
+
if (i === prepend) return `${line} > ${chalk.bold(l)}`;
|
|
87
|
+
return `${line - prepend + i} | ${l}`
|
|
88
|
+
}).join('\n')
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const fetchSourceCode = (contents, opts = {}) => {
|
|
92
|
+
if (!opts.title && !opts.line) return '';
|
|
93
|
+
|
|
94
|
+
// code fragment is 20 lines
|
|
95
|
+
const limit = opts.limit || 50;
|
|
96
|
+
let lineIndex;
|
|
97
|
+
if (opts.line) lineIndex = opts.line - 1;
|
|
98
|
+
const lines = contents.split('\n')
|
|
99
|
+
|
|
100
|
+
// remove special chars from title
|
|
101
|
+
if (!lineIndex && opts.title) {
|
|
102
|
+
const title = opts.title.replace(/[\(\[\@].*/g, '')
|
|
103
|
+
lineIndex = lines.findIndex(l => l.includes(title))
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (opts.prepend) {
|
|
107
|
+
lineIndex -= opts.prepend;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (lineIndex) {
|
|
111
|
+
const result = [];
|
|
112
|
+
for (let i = lineIndex; i < (lineIndex + limit); i++) {
|
|
113
|
+
if (lines[i] === undefined) continue;
|
|
114
|
+
|
|
115
|
+
if (i > lineIndex + 2 && !opts.prepend) {
|
|
116
|
+
// annotation
|
|
117
|
+
if (opts.lang === 'php' && lines[i].trim().startsWith('#[')) break;
|
|
118
|
+
if (opts.lang === 'php' && lines[i].includes(' private function ')) break;
|
|
119
|
+
if (opts.lang === 'php' && lines[i].includes(' protected function ')) break;
|
|
120
|
+
if (opts.lang === 'php' && lines[i].includes(' public function ')) break;
|
|
121
|
+
if (opts.lang === 'python' && lines[i].trim().match(/^\@\w+/)) break;
|
|
122
|
+
if (opts.lang === 'python' && lines[i].includes(' def ')) break;
|
|
123
|
+
if (opts.lang === 'ruby' && lines[i].includes(' def ')) break;
|
|
124
|
+
if (opts.lang === 'ruby' && lines[i].includes(' test ')) break;
|
|
125
|
+
if (opts.lang === 'ruby' && lines[i].includes(' it ')) break;
|
|
126
|
+
if (opts.lang === 'ruby' && lines[i].includes(' specify ')) break;
|
|
127
|
+
if (opts.lang === 'ruby' && lines[i].includes(' context ')) break;
|
|
128
|
+
if (opts.lang === 'ts' && lines[i].includes(' it(')) break;
|
|
129
|
+
if (opts.lang === 'ts' && lines[i].includes(' test(')) break;
|
|
130
|
+
if (opts.lang === 'js' && lines[i].includes(' it(')) break;
|
|
131
|
+
if (opts.lang === 'js' && lines[i].includes(' test(')) break;
|
|
132
|
+
if (opts.lang === 'java' && lines[i].trim().match(/^\@\w+/)) break;
|
|
133
|
+
if (opts.lang === 'java' && lines[i].includes(' public void ')) break;
|
|
134
|
+
if (opts.lang === 'java' && lines[i].includes(' class ')) break;
|
|
135
|
+
}
|
|
136
|
+
result.push(lines[i])
|
|
137
|
+
}
|
|
138
|
+
return result.join('\n');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
}
|
|
142
|
+
|
|
51
143
|
module.exports = {
|
|
52
144
|
parseTest,
|
|
53
145
|
parseSuite,
|
|
54
146
|
ansiRegExp,
|
|
55
147
|
isValidUrl,
|
|
148
|
+
fetchSourceCode,
|
|
149
|
+
fetchSourceCodeFromStackTrace,
|
|
150
|
+
fetchFilesFromStackTrace,
|
|
56
151
|
};
|