@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 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('&#10;')[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
  };
@@ -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
+