@testomatio/reporter 0.5.10 → 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/README.md +18 -0
- package/lib/bin/reportXml.js +50 -0
- 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/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
|
|
@@ -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);
|
|
@@ -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
|
};
|
package/lib/xmlReader.js
ADDED
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
const axios = require('axios');
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
|
|
6
|
+
// const util = require("util"); // you can see a result
|
|
7
|
+
const { XMLParser } = require("fast-xml-parser");
|
|
8
|
+
const { APP_PREFIX } = require('./constants');
|
|
9
|
+
const { isValidUrl, fetchFilesFromStackTrace, fetchSourceCode, fetchSourceCodeFromStackTrace } = require('./util');
|
|
10
|
+
const upload = require('./fileUploader');
|
|
11
|
+
|
|
12
|
+
const Adapter = require('./junit-adapter/adapter');
|
|
13
|
+
const JavaScriptAdapter = require('./junit-adapter/javascript');
|
|
14
|
+
const JavaAdapter = require('./junit-adapter/java');
|
|
15
|
+
const PythonAdapter = require('./junit-adapter/python');
|
|
16
|
+
const RubyAdapter = require('./junit-adapter/ruby');
|
|
17
|
+
|
|
18
|
+
const TESTOMATIO_URL = process.env.TESTOMATIO_URL || "https://app.testomat.io";
|
|
19
|
+
const TESTOMATIO = process.env.TESTOMATIO; // key?
|
|
20
|
+
const { TESTOMATIO_RUNGROUP_TITLE, TESTOMATIO_TITLE, TESTOMATIO_ENV, TESTOMATIO_RUN } = process.env;
|
|
21
|
+
|
|
22
|
+
const options = {
|
|
23
|
+
ignoreDeclaration: true,
|
|
24
|
+
ignoreAttributes: false,
|
|
25
|
+
alwaysCreateTextNode: false,
|
|
26
|
+
attributeNamePrefix: "",
|
|
27
|
+
parseTagValue: true,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
class XmlReader {
|
|
31
|
+
|
|
32
|
+
constructor(opts = {}) {
|
|
33
|
+
this.requestParams = {
|
|
34
|
+
apiKey: opts.apiKey || TESTOMATIO,
|
|
35
|
+
url: opts.url || TESTOMATIO_URL,
|
|
36
|
+
title: TESTOMATIO_TITLE,
|
|
37
|
+
env: TESTOMATIO_ENV,
|
|
38
|
+
group_title: TESTOMATIO_RUNGROUP_TITLE,
|
|
39
|
+
};
|
|
40
|
+
this.runId = opts.runId || TESTOMATIO_RUN;
|
|
41
|
+
this.adapter = new Adapter(opts)
|
|
42
|
+
this.opts = opts;
|
|
43
|
+
this.axios = axios.create();
|
|
44
|
+
this.parser = new XMLParser(options);
|
|
45
|
+
this.tests = []
|
|
46
|
+
this.stats = {}
|
|
47
|
+
this.stats.language = opts.lang?.toLowerCase();
|
|
48
|
+
this.filesToUpload = {}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
connectAdapter() {
|
|
52
|
+
if (this.opts.javaTests || this.stats.language === 'java') {
|
|
53
|
+
this.adapter = new JavaAdapter(this.opts);
|
|
54
|
+
}
|
|
55
|
+
if (this.stats.language == 'js') {
|
|
56
|
+
this.adapter = new JavaScriptAdapter(this.opts);
|
|
57
|
+
}
|
|
58
|
+
if (this.stats.language === 'python') {
|
|
59
|
+
this.adapter = new PythonAdapter(this.opts);
|
|
60
|
+
}
|
|
61
|
+
if (this.stats.language === 'ruby') {
|
|
62
|
+
this.adapter = new RubyAdapter(this.opts);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
parse(fileName) {
|
|
67
|
+
const xmlData = fs.readFileSync(path.resolve(fileName));
|
|
68
|
+
const jsonResult = this.parser.parse(xmlData);
|
|
69
|
+
let jsonSuite;
|
|
70
|
+
|
|
71
|
+
if (jsonResult.testsuites) {
|
|
72
|
+
jsonSuite = jsonResult.testsuites;
|
|
73
|
+
} else if (jsonResult.testsuite) {
|
|
74
|
+
jsonSuite = jsonResult;
|
|
75
|
+
} else {
|
|
76
|
+
console.log(jsonResult)
|
|
77
|
+
throw new Error("Format can't be parsed")
|
|
78
|
+
}
|
|
79
|
+
const { testsuite, name, tests, failures, errors, filepath } = jsonSuite;
|
|
80
|
+
|
|
81
|
+
const resultTests = processTestSuite(testsuite);
|
|
82
|
+
|
|
83
|
+
const hasFailures = resultTests.filter(t => t.status === 'failed').length > 0;
|
|
84
|
+
const status = ( failures > 0 || errors > 0 || hasFailures) ? 'failed' : 'passed';
|
|
85
|
+
|
|
86
|
+
this.tests = this.tests.concat(resultTests);
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
status,
|
|
90
|
+
create_tests: true,
|
|
91
|
+
name,
|
|
92
|
+
tests_count: parseInt(tests, 10),
|
|
93
|
+
passed_count: parseInt(tests - failures, 10),
|
|
94
|
+
failed_count: parseInt(failures, 10),
|
|
95
|
+
skipped_count: 0,
|
|
96
|
+
tests: resultTests,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
calculateStats() {
|
|
101
|
+
this.stats = {
|
|
102
|
+
...this.stats,
|
|
103
|
+
status: 'passed',
|
|
104
|
+
create_tests: true,
|
|
105
|
+
tests_count: 0,
|
|
106
|
+
passed_count: 0,
|
|
107
|
+
failed_count: 0,
|
|
108
|
+
skipped_count: 0,
|
|
109
|
+
}
|
|
110
|
+
this.tests.forEach(t => {
|
|
111
|
+
this.stats.tests_count++;
|
|
112
|
+
if (t.status === 'passed') this.stats.passed_count++;
|
|
113
|
+
if (t.status === 'failed') this.stats.failed_count++;
|
|
114
|
+
})
|
|
115
|
+
if (this.stats.failed_count) this.stats.status = 'failed';
|
|
116
|
+
|
|
117
|
+
return this.stats;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
fetchSourceCode() {
|
|
121
|
+
this.tests.forEach(t => {
|
|
122
|
+
try {
|
|
123
|
+
let file = this.adapter.getFilePath(t)
|
|
124
|
+
if (!file) return;
|
|
125
|
+
|
|
126
|
+
if (!this.stats.language) {
|
|
127
|
+
if (file.endsWith('.php')) this.stats.language = 'php';
|
|
128
|
+
if (file.endsWith('.py')) this.stats.language = 'python';
|
|
129
|
+
if (file.endsWith('.java')) this.stats.language = 'java';
|
|
130
|
+
if (file.endsWith('.rb')) this.stats.language = 'ruby';
|
|
131
|
+
if (file.endsWith('.js')) this.stats.language = 'js';
|
|
132
|
+
if (file.endsWith('.ts')) this.stats.language = 'ts';
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const contents = fs.readFileSync(file).toString();
|
|
136
|
+
t.code = fetchSourceCode(contents, { ...t, lang: this.stats.language })
|
|
137
|
+
} catch (err) {
|
|
138
|
+
if (process.env['DEBUG']) {
|
|
139
|
+
console.log(err)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
formatTests() {
|
|
146
|
+
this.tests.forEach(t => {
|
|
147
|
+
if (t.file) {
|
|
148
|
+
t.file = t.file.replace(process.cwd() + path.sep, '')
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
this.adapter.formatTest(t)
|
|
152
|
+
|
|
153
|
+
t.title = t.title
|
|
154
|
+
// insert a space before all caps
|
|
155
|
+
.replace(/([A-Z])/g, ' $1')
|
|
156
|
+
// _ chars to spaces
|
|
157
|
+
.replace(/_/g, ' ')
|
|
158
|
+
// uppercase the first character
|
|
159
|
+
.replace(/^(.)|\s(.)/g, $1 => $1.toLowerCase())
|
|
160
|
+
|
|
161
|
+
// remove standard prefixes
|
|
162
|
+
.replace(/^test\s/, '')
|
|
163
|
+
.replace(/^should\s/, '')
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
formatErrors() {
|
|
168
|
+
this.tests.filter(t => !!t.stack).forEach(t => {
|
|
169
|
+
t.stack = this.formatStack(t)
|
|
170
|
+
t.message = this.adapter.formatMessage(t);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
formatStack(t) {
|
|
176
|
+
let stack = this.adapter.formatStack(t);
|
|
177
|
+
|
|
178
|
+
const sourcePart = fetchSourceCodeFromStackTrace(stack);
|
|
179
|
+
|
|
180
|
+
if (!sourcePart) return stack;
|
|
181
|
+
|
|
182
|
+
return `${stack}\n\n${chalk.bold.red('################[ Failure ]################')}\n${fetchSourceCodeFromStackTrace(stack)}`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async uploadArtifacts() {
|
|
186
|
+
if (!this.runId) return;
|
|
187
|
+
for (const t of this.tests.filter(t => !!t.stack)) {
|
|
188
|
+
const files = fetchFilesFromStackTrace(t.stack);
|
|
189
|
+
t.artifacts = await Promise.all(files.map(f => upload.uploadFileByPath(f, this.runId)));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async createRun() {
|
|
194
|
+
if (this.runId) return;
|
|
195
|
+
|
|
196
|
+
const runParams = {
|
|
197
|
+
api_key: this.requestParams.apiKey,
|
|
198
|
+
title: this.requestParams.title,
|
|
199
|
+
env: this.requestParams.env,
|
|
200
|
+
group_title: this.requestParams.runGroup,
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
const resp = await this.axios.post(this.url, runParams, {
|
|
205
|
+
maxContentLength: Infinity,
|
|
206
|
+
maxBodyLength: Infinity,
|
|
207
|
+
headers: {
|
|
208
|
+
// Overwrite Axios's automatically set Content-Type
|
|
209
|
+
'Content-Type': 'application/json',
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
if (resp.status >= 400) {
|
|
213
|
+
const data = resp.data || { message: '' };
|
|
214
|
+
console.log(
|
|
215
|
+
APP_PREFIX,
|
|
216
|
+
`Report couldn't be processed: (${resp.status}) ${data.message}`,
|
|
217
|
+
);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
this.runId = resp.data.uid;
|
|
221
|
+
this.runUrl = `${TESTOMATIO_URL}/${resp.data.url.split('/').splice(3).join('/')}`;
|
|
222
|
+
} catch(err) {
|
|
223
|
+
if (process.env.DEBUG) console.log(err)
|
|
224
|
+
const data = err?.response?.data || { message: '' };
|
|
225
|
+
console.log(APP_PREFIX, 'Error creating run, skipping...', err?.response?.statusText, data);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async uploadData() {
|
|
230
|
+
await this.uploadArtifacts();
|
|
231
|
+
this.calculateStats();
|
|
232
|
+
this.connectAdapter();
|
|
233
|
+
this.fetchSourceCode();
|
|
234
|
+
this.formatErrors();
|
|
235
|
+
this.formatTests();
|
|
236
|
+
|
|
237
|
+
if (process.env['DEBUG']) {
|
|
238
|
+
console.log({
|
|
239
|
+
...this.stats,
|
|
240
|
+
tests: this.tests,
|
|
241
|
+
})
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const dataString = JSON.stringify({
|
|
245
|
+
...this.stats,
|
|
246
|
+
api_key: this.requestParams.apiKey,
|
|
247
|
+
tests: this.tests,
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
const resp = await this.axios.put(`${this.url}/${this.runId}`, dataString, {
|
|
252
|
+
maxContentLength: Infinity,
|
|
253
|
+
maxBodyLength: Infinity,
|
|
254
|
+
headers: {
|
|
255
|
+
// Overwrite Axios's automatically set Content-Type
|
|
256
|
+
'Content-Type': 'application/json',
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
if (resp.status >= 400) {
|
|
261
|
+
const data = resp.data || { message: '' };
|
|
262
|
+
console.log(
|
|
263
|
+
APP_PREFIX,
|
|
264
|
+
`Report couldn't be processed: (${resp.status}) ${data.message}`,
|
|
265
|
+
);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (this.runUrl) {
|
|
270
|
+
console.log(APP_PREFIX, '📊 Report Saved. Report URL:', chalk.magenta(this.runUrl));
|
|
271
|
+
}
|
|
272
|
+
process.env.runId = this.runId;
|
|
273
|
+
return resp;
|
|
274
|
+
} catch (err) {
|
|
275
|
+
// if (process.env.DEBUG) console.log(err.response)
|
|
276
|
+
const data = err.response.data || { message: '' };
|
|
277
|
+
console.log(APP_PREFIX, 'Error uploading, skipping...', err.response.statusText, data);
|
|
278
|
+
return err.response;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
get url() {
|
|
284
|
+
if (!isValidUrl(this.requestParams.url)) {
|
|
285
|
+
console.log(
|
|
286
|
+
APP_PREFIX,
|
|
287
|
+
chalk.red(`Error creating report on Testomat.io, report url '${TESTOMAT_URL}' is invalid`),
|
|
288
|
+
);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return `${this.requestParams.url}/api/reporter`;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
module.exports = XmlReader;
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
function reduceTestCases(prev, item) {
|
|
300
|
+
let testCases = item.testcase;
|
|
301
|
+
if (!Array.isArray(testCases)) {
|
|
302
|
+
testCases = [testCases]
|
|
303
|
+
}
|
|
304
|
+
testCases.filter(t => !!t).forEach(testCaseItem => {
|
|
305
|
+
let file = testCaseItem.file || item.filepath || '';
|
|
306
|
+
|
|
307
|
+
let stack = '';
|
|
308
|
+
let message = '';
|
|
309
|
+
if (testCaseItem.error) stack = testCaseItem.error;
|
|
310
|
+
if (testCaseItem.failure) stack = testCaseItem.failure;
|
|
311
|
+
if (testCaseItem?.failure?.message) message = testCaseItem.failure.message;
|
|
312
|
+
if (testCaseItem?.error?.message) message = testCaseItem.error.message;
|
|
313
|
+
|
|
314
|
+
if (testCaseItem.failure && testCaseItem.failure['#text']) stack = testCaseItem.failure['#text'];
|
|
315
|
+
if (testCaseItem.error && testCaseItem.error['#text']) stack = testCaseItem.error['#text'];
|
|
316
|
+
if (!message) message = stack.trim().split('\n')[0];
|
|
317
|
+
|
|
318
|
+
// prepend system output
|
|
319
|
+
stack = `${testCaseItem['system-out'] || testCaseItem['log'] || ''}\n\n${stack}`.trim()
|
|
320
|
+
|
|
321
|
+
prev.push({
|
|
322
|
+
create: true,
|
|
323
|
+
file,
|
|
324
|
+
stack,
|
|
325
|
+
message,
|
|
326
|
+
line: testCaseItem.lineno,
|
|
327
|
+
run_time: testCaseItem.time,
|
|
328
|
+
status: (testCaseItem.failure || testCaseItem.error) ? 'failed' : 'passed',
|
|
329
|
+
title: testCaseItem.name,
|
|
330
|
+
suite_title: testCaseItem.classname || item.name,
|
|
331
|
+
})
|
|
332
|
+
});
|
|
333
|
+
return prev;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function processTestSuite(testsuite) {
|
|
337
|
+
if (testsuite.testsuite) return processTestSuite(testsuite.testsuite)
|
|
338
|
+
|
|
339
|
+
let suites = testsuite;
|
|
340
|
+
if (!Array.isArray(testsuite)) {
|
|
341
|
+
suites = [testsuite];
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const res = suites.reduce(reduceTestCases, []);
|
|
345
|
+
|
|
346
|
+
return res;
|
|
347
|
+
}
|
|
348
|
+
|