@testomatio/reporter 1.0.0-beta.4 → 1.0.1-6.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.
@@ -0,0 +1,310 @@
1
+ const { URL } = require('url');
2
+ const { sep, basename } = require('path');
3
+ const chalk = require('chalk');
4
+ const fs = require('fs');
5
+ const isValid = require('is-valid-path');
6
+ const debug = require('debug')('@testomatio/reporter:util');
7
+
8
+ /**
9
+ * @param {String} testTitle - Test title
10
+ *
11
+ * @returns {String|null} testId
12
+ */
13
+ const parseTest = testTitle => {
14
+ if (!testTitle) return null;
15
+
16
+ const captures = testTitle.match(/@T([\w\d]+)/);
17
+
18
+ if (captures) {
19
+ return captures[1];
20
+ }
21
+
22
+ return null;
23
+ };
24
+
25
+ /**
26
+ * @param {String} suiteTitle - suite title
27
+ *
28
+ * @returns {String|null} suiteId
29
+ */
30
+ const parseSuite = suiteTitle => {
31
+ const captures = suiteTitle.match(/@S([\w\d]+)/);
32
+ if (captures) {
33
+ return captures[1];
34
+ }
35
+
36
+ return null;
37
+ };
38
+
39
+ const ansiRegExp = () => {
40
+ const pattern = [
41
+ '[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)',
42
+ '(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))',
43
+ ].join('|');
44
+
45
+ return new RegExp(pattern, 'g');
46
+ };
47
+
48
+ const isValidUrl = s => {
49
+ try {
50
+ // eslint-disable-next-line no-new
51
+ new URL(s);
52
+ return true;
53
+ } catch (err) {
54
+ return false;
55
+ }
56
+ };
57
+
58
+ const fileMatchRegex = /file:(\/\/?[^:\s]+?\.(png|avi|webm|jpg|html|txt))/ig;
59
+
60
+ const fetchFilesFromStackTrace = (stack = '') => {
61
+
62
+ const files = Array.from(stack.matchAll(fileMatchRegex))
63
+ .map(f => f[1].trim())
64
+ .map(f => f.startsWith('//') ? f.substring(1) : f )
65
+
66
+ debug('Found files in stack trace: ', files);
67
+
68
+ return files.filter(f => {
69
+ const isFile = fs.existsSync(f);
70
+ if (!isFile) debug('File %s could not be found and uploaded as artifact', f);
71
+ return isFile;
72
+ });
73
+ };
74
+
75
+ const fetchSourceCodeFromStackTrace = (stack = '') => {
76
+ const stackLines = stack
77
+ .split('\n')
78
+ .filter(l => l.includes(':'))
79
+ // .map(l => l.match(/\[(.*?)\]/)?.[1] || l) // minitest format
80
+ // .map(l => l.split(':')[0])
81
+ .map(l => l.trim())
82
+ .map(l => l.split(' ').find(p => p.includes(':')) || '')
83
+ .filter(l => isValid(l?.split(':')[0]))
84
+
85
+ // // filter out 3rd party libs
86
+ .filter(l => !l?.includes(`vendor${sep}`))
87
+ .filter(l => !l?.includes(`node_modules${sep}`))
88
+ .filter(l => fs.existsSync(l.split(':')[0]))
89
+ .filter(l => fs.lstatSync(l.split(':')[0]).isFile());
90
+
91
+ if (!stackLines.length) return '';
92
+
93
+ const [file, line] = stackLines[0].split(':');
94
+
95
+ const prepend = 3;
96
+ const source = fetchSourceCode(fs.readFileSync(file).toString(), { line, prepend, limit: 7 });
97
+
98
+ if (!source) return '';
99
+
100
+ return source
101
+ .split('\n')
102
+ .map((l, i) => {
103
+ if (i === prepend) return `${line} > ${chalk.bold(l)}`;
104
+ return `${line - prepend + i} | ${l}`;
105
+ })
106
+ .join('\n');
107
+ };
108
+
109
+ const TEST_ID_REGEX = /@T([\w\d]{8})/;
110
+
111
+ const fetchIdFromCode = (code, opts = {}) => {
112
+ const comments = code.split('\n').map(l => l.trim()).filter(l => {
113
+ switch (opts.lang) {
114
+ case 'ruby':
115
+ case 'python':
116
+ return l.startsWith('# ');
117
+ default:
118
+ return l.startsWith('// ');
119
+ }
120
+ });
121
+
122
+ return comments.find(c => c.match(TEST_ID_REGEX))?.match(TEST_ID_REGEX)?.[1];
123
+ }
124
+
125
+
126
+ const fetchIdFromOutput = (output) => {
127
+ const lines = output.split('\n').map(l => l.trim()).filter(l => l.startsWith('tid://'));
128
+
129
+ return lines.find(c => c.match(TEST_ID_REGEX))?.match(TEST_ID_REGEX)?.[1];
130
+ }
131
+
132
+ const fetchSourceCode = (contents, opts = {}) => {
133
+ if (!opts.title && !opts.line) return '';
134
+
135
+ // code fragment is 20 lines
136
+ const limit = opts.limit || 50;
137
+ let lineIndex;
138
+ if (opts.line) lineIndex = opts.line - 1;
139
+ const lines = contents.split('\n');
140
+
141
+ // remove special chars from title
142
+ if (!lineIndex && opts.title) {
143
+ const title = opts.title.replace(/[([@].*/g, '');
144
+ lineIndex = lines.findIndex(l => l.includes(title));
145
+ }
146
+
147
+ if (opts.prepend) {
148
+ lineIndex -= opts.prepend;
149
+ }
150
+
151
+ if (lineIndex) {
152
+ const result = [];
153
+ for (let i = lineIndex; i < lineIndex + limit; i++) {
154
+ if (lines[i] === undefined) continue;
155
+
156
+ if (i > lineIndex + 2 && !opts.prepend) {
157
+ // annotation
158
+ if (opts.lang === 'php' && lines[i].trim().startsWith('#[')) break;
159
+ if (opts.lang === 'php' && lines[i].includes(' private function ')) break;
160
+ if (opts.lang === 'php' && lines[i].includes(' protected function ')) break;
161
+ if (opts.lang === 'php' && lines[i].includes(' public function ')) break;
162
+ if (opts.lang === 'python' && lines[i].trim().match(/^@\w+/)) break;
163
+ if (opts.lang === 'python' && lines[i].includes(' def ')) break;
164
+ if (opts.lang === 'ruby' && lines[i].includes(' def ')) break;
165
+ if (opts.lang === 'ruby' && lines[i].includes(' test ')) break;
166
+ if (opts.lang === 'ruby' && lines[i].includes(' it ')) break;
167
+ if (opts.lang === 'ruby' && lines[i].includes(' specify ')) break;
168
+ if (opts.lang === 'ruby' && lines[i].includes(' context ')) break;
169
+ if (opts.lang === 'ts' && lines[i].includes(' it(')) break;
170
+ if (opts.lang === 'ts' && lines[i].includes(' test(')) break;
171
+ if (opts.lang === 'js' && lines[i].includes(' it(')) break;
172
+ if (opts.lang === 'js' && lines[i].includes(' test(')) break;
173
+ if (opts.lang === 'java' && lines[i].trim().match(/^@\w+/)) break;
174
+ if (opts.lang === 'java' && lines[i].includes(' public void ')) break;
175
+ if (opts.lang === 'java' && lines[i].includes(' class ')) break;
176
+ }
177
+ result.push(lines[i]);
178
+ }
179
+ return result.join('\n');
180
+ }
181
+ };
182
+
183
+ const isSameTest = (test, t) =>
184
+ typeof t === 'object' &&
185
+ typeof test === 'object' &&
186
+ t.title === test.title &&
187
+ t.suite_title === test.suite_title &&
188
+ Object.values(t.example || {}) === Object.values(test.example || {}) &&
189
+ t.test_id === test.test_id;
190
+
191
+ const getCurrentDateTime = () => {
192
+ const today = new Date();
193
+
194
+ return `${today.getFullYear()}_${
195
+ today.getMonth() + 1
196
+ }_${today.getDate()}_${today.getHours()}_${today.getMinutes()}_${today.getSeconds()}`;
197
+ };
198
+
199
+ /**
200
+ * @param {Object} test - Test adapter object
201
+ *
202
+ * @returns {String|null} testInfo as one string
203
+ */
204
+ const specificTestInfo = test => {
205
+ // TODO: afterEach has another context.... need to add specific handler, maybe...
206
+ if (test?.title && test?.file) {
207
+ return `${basename(test.file).split('.').join('#')}#${test.title.split(' ').join('#')}`;
208
+ }
209
+
210
+ return null;
211
+ };
212
+
213
+ const fileSystem = {
214
+ createDir(dirPath) {
215
+ if (!fs.existsSync(dirPath)) {
216
+ fs.mkdirSync(dirPath, { recursive: true });
217
+ debug('Created dir: ', dirPath);
218
+ }
219
+ },
220
+ clearDir(dirPath) {
221
+ if (fs.existsSync(dirPath)) {
222
+ fs.rmSync(dirPath, { recursive: true });
223
+ debug(`Dir ${dirPath} was deleted`);
224
+ } else {
225
+ debug(`Trying to delete ${dirPath} but it doesn't exist`);
226
+ }
227
+ },
228
+ };
229
+
230
+
231
+ const foundedTestLog = (app, tests) => {
232
+ const n = tests.length;
233
+
234
+ return (n === 1)
235
+ ? console.log(app, `✅ We found one test!`)
236
+ : console.log(app, `✅ We found ${n} tests!`);
237
+ }
238
+
239
+ const humanize = text => {
240
+ text = decamelize(text);
241
+ return text
242
+ .replace(/_./g, match => ` ${match.charAt(1).toUpperCase()}`)
243
+ .trim()
244
+ .replace(/^(.)|\s(.)/g, $1 => $1.toUpperCase())
245
+ .trim()
246
+ .replace(/\sA\s/g, ' a ') // replace a|the
247
+ .replace(/\sThe\s/g, ' the ') // replace a|the
248
+ .replace(/^Test\s/, '')
249
+ .replace(/^Should\s/, '');
250
+ };
251
+
252
+ /**
253
+ * From https://github.com/sindresorhus/decamelize/blob/main/index.js
254
+ * @param {*} text
255
+ * @returns
256
+ */
257
+ const decamelize = text => {
258
+ const separator = '_';
259
+ const replacement = `$1${separator}$2`;
260
+
261
+ // Split lowercase sequences followed by uppercase character.
262
+ // `dataForUSACounties` → `data_For_USACounties`
263
+ // `myURLstring → `my_URLstring`
264
+ let decamelized = text.replace(/([\p{Lowercase_Letter}\d])(\p{Uppercase_Letter})/gu, replacement);
265
+
266
+ // Lowercase all single uppercase characters. As we
267
+ // want to preserve uppercase sequences, we cannot
268
+ // simply lowercase the separated string at the end.
269
+ // `data_For_USACounties` → `data_for_USACounties`
270
+ decamelized = decamelized.replace(
271
+ /((?<![\p{Uppercase_Letter}\d])[\p{Uppercase_Letter}\d](?![\p{Uppercase_Letter}\d]))/gu,
272
+ $0 => $0.toLowerCase(),
273
+ );
274
+
275
+ // Remaining uppercase sequences will be separated from lowercase sequences.
276
+ // `data_For_USACounties` → `data_for_USA_counties`
277
+ return decamelized.replace(
278
+ /(\p{Uppercase_Letter}+)(\p{Uppercase_Letter}\p{Lowercase_Letter}+)/gu,
279
+ (_, $1, $2) => $1 + separator + $2.toLowerCase(),
280
+ );
281
+ };
282
+
283
+ /**
284
+ * Used to remove color codes
285
+ * @param {*} input
286
+ * @returns
287
+ */
288
+ function removeColorCodes(input) {
289
+ // eslint-disable-next-line no-control-regex
290
+ return input.replace(/\x1b\[[0-9;]*m/g, '');
291
+ }
292
+
293
+ module.exports = {
294
+ isSameTest,
295
+ fetchSourceCode,
296
+ fetchSourceCodeFromStackTrace,
297
+ fetchIdFromCode,
298
+ fetchIdFromOutput,
299
+ fetchFilesFromStackTrace,
300
+ fileSystem,
301
+ getCurrentDateTime,
302
+ specificTestInfo,
303
+ isValidUrl,
304
+ ansiRegExp,
305
+ parseTest,
306
+ parseSuite,
307
+ humanize,
308
+ removeColorCodes,
309
+ foundedTestLog
310
+ };
package/lib/xmlReader.js CHANGED
@@ -4,12 +4,17 @@ const chalk = require('chalk');
4
4
  const fs = require("fs");
5
5
  const { XMLParser } = require("fast-xml-parser");
6
6
  const { APP_PREFIX, STATUS } = require('./constants');
7
- const { fetchFilesFromStackTrace, fetchSourceCode, fetchSourceCodeFromStackTrace } = require('./util');
7
+ const { fetchFilesFromStackTrace,
8
+ fetchIdFromOutput,
9
+ fetchSourceCode,
10
+ fetchSourceCodeFromStackTrace,
11
+ fetchIdFromCode,
12
+ humanize
13
+ } = require('./utils/utils');
8
14
  const upload = require('./fileUploader');
9
15
  const pipesFactory = require('./pipe');
10
16
  const adapterFactory = require('./junit-adapter');
11
17
 
12
-
13
18
  const TESTOMATIO_URL = process.env.TESTOMATIO_URL || "https://app.testomat.io";
14
19
  const TESTOMATIO = process.env.TESTOMATIO; // key?
15
20
  const { TESTOMATIO_RUNGROUP_TITLE, TESTOMATIO_TITLE, TESTOMATIO_ENV, TESTOMATIO_RUN } = process.env;
@@ -25,7 +30,7 @@ const options = {
25
30
  const reduceOptions = {};
26
31
 
27
32
  class XmlReader {
28
-
33
+
29
34
  constructor(opts = {}) {
30
35
  this.requestParams = {
31
36
  apiKey: opts.apiKey || TESTOMATIO,
@@ -35,12 +40,12 @@ class XmlReader {
35
40
  group_title: TESTOMATIO_RUNGROUP_TITLE,
36
41
  };
37
42
  this.runId = opts.runId || TESTOMATIO_RUN;
38
- this.adapter = adapterFactory(null, opts)
43
+ this.adapter = adapterFactory(opts.lang?.toLowerCase(), opts)
39
44
  if (!this.adapter) throw new Error('XML adapter for this format not found');
40
45
 
41
46
  this.opts = opts || {};
42
- const store = {}
43
- this.pipes = pipesFactory(opts, store);
47
+ this.store = {}
48
+ this.pipes = pipesFactory(opts, this.store);
44
49
 
45
50
  this.parser = new XMLParser(options);
46
51
  this.tests = []
@@ -49,27 +54,33 @@ class XmlReader {
49
54
  this.filesToUpload = {}
50
55
 
51
56
  this.version = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json')).toString()).version;
52
- console.log(APP_PREFIX, `Testomatio Reporter v${this.version}`);
57
+ console.log(APP_PREFIX, `Testomatio Reporter v${this.version}`);
53
58
  }
54
59
 
55
60
  connectAdapter() {
56
- if (this.opts.javaTests) return adapterFactory('java', this.opts);
57
- return adapterFactory(this.stats.language, this.opts);
61
+ if (this.opts.javaTests) {
62
+ this.adapter = adapterFactory('java', this.opts);
63
+ return this.adapter;
64
+ }
65
+ this.adapter = adapterFactory(this.stats.language, this.opts);
66
+ return this.adapter;
58
67
  }
59
68
 
60
- parse(fileName) {
69
+ parse(fileName) {
61
70
  const xmlData = fs.readFileSync(path.resolve(fileName));
62
71
  const jsonResult = this.parser.parse(xmlData);
63
72
  let jsonSuite;
64
-
73
+
65
74
  if (jsonResult.testsuites) {
66
75
  jsonSuite = jsonResult.testsuites;
67
76
  } else if (jsonResult.testsuite) {
68
77
  jsonSuite = jsonResult;
69
78
  } else if (jsonResult.TestRun) {
70
- return this.processTRX(jsonResult);
79
+ return this.processTRX(jsonResult);
71
80
  } else if (jsonResult['test-run']) {
72
81
  return this.processNUnit(jsonResult['test-run']);
82
+ } else if (jsonResult.assemblies) {
83
+ return this.processXUnit(jsonResult.assemblies);
73
84
  } else {
74
85
  console.log(jsonResult)
75
86
  throw new Error("Format can't be parsed")
@@ -83,9 +94,9 @@ class XmlReader {
83
94
 
84
95
  reduceOptions.preferClassname = this.stats.language === 'python';
85
96
  const resultTests = processTestSuite(testsuite);
86
-
97
+
87
98
  const hasFailures = resultTests.filter(t => t.status === 'failed').length > 0;
88
- const status = ( failures > 0 || errors > 0 || hasFailures) ? 'failed' : 'passed';
99
+ const status = (failures > 0 || errors > 0 || hasFailures) ? 'failed' : 'passed';
89
100
 
90
101
  this.tests = this.tests.concat(resultTests);
91
102
 
@@ -118,7 +129,7 @@ class XmlReader {
118
129
  skipped_count: parseInt(inconclusive + skipped, 10),
119
130
  tests: resultTests,
120
131
  };
121
- }
132
+ }
122
133
 
123
134
  processTRX(jsonSuite) {
124
135
  let defs = jsonSuite?.TestRun?.TestDefinitions?.UnitTest;
@@ -127,10 +138,10 @@ class XmlReader {
127
138
  const tests = defs.map(td => {
128
139
  const title = td.name.replace(/\(.*?\)/, '').trim();
129
140
  let example = td.name.match(/\((.*?)\)/);
130
- if (example) example = { ...example[1].split(',')};
141
+ if (example) example = { ...example[1].split(',') };
131
142
  const suite = td.TestMethod.className.split(', ')[0].split('.');
132
143
  const suite_title = suite.pop();
133
- return {
144
+ return {
134
145
  title,
135
146
  example,
136
147
  file: suite.join('/'),
@@ -143,18 +154,18 @@ class XmlReader {
143
154
  let result = jsonSuite?.TestRun?.Results?.UnitTestResult;
144
155
  if (!Array.isArray(result)) result = [result].filter(d => !!d);
145
156
 
146
- const results = result.map(td => ({
157
+ const results = result.map(td => ({
147
158
  id: td.executionId,
148
159
  // seconds are used in junit reports, but ms are used by testomatio
149
160
  run_time: parseFloat(td.duration) * 1000,
150
- status: td.outcome,
161
+ status: td.outcome,
151
162
  stack: td.Output.StdOut,
152
163
  files: td?.ResultFiles?.ResultFile?.map(rf => rf.path)
153
164
  }));
154
165
 
155
-
166
+
156
167
  results.forEach(r => {
157
- const test = tests.find(t => t.id === r.id) || { };
168
+ const test = tests.find(t => t.id === r.id) || {};
158
169
  r.suite_title = test.suite_title;
159
170
  r.title = test.title?.trim();
160
171
  if (test.code) r.code = test.code;
@@ -169,7 +180,7 @@ class XmlReader {
169
180
  });
170
181
 
171
182
  debug(results);
172
-
183
+
173
184
  const counters = jsonSuite?.TestRun?.ResultSummary?.Counters || {};
174
185
 
175
186
  const failed_count = parseInt(counters.failed, 10) + parseInt(counters.error, 10);
@@ -187,7 +198,80 @@ class XmlReader {
187
198
  skipped_count: parseInt(counters.notExecuted, 10),
188
199
  failed_count,
189
200
  tests: results,
190
- };
201
+ };
202
+ }
203
+
204
+ processXUnit(assemblies) {
205
+ const tests = [];
206
+
207
+ assemblies = Array.isArray(assemblies.assembly) ? assemblies.assembly : [assemblies.assembly];
208
+
209
+ assemblies.forEach(assembly => {
210
+ const { collection } = assembly;
211
+
212
+ const suites = Array.isArray(collection) ? collection : [collection];
213
+
214
+ suites.forEach(suite => {
215
+ const { test } = suite;
216
+ if (!test) return;
217
+ const cases = Array.isArray(test) ? test : [test];
218
+ cases.forEach(testCase => {
219
+ const { type, time, result } = testCase;
220
+
221
+ let message = '';
222
+ let stack = '';
223
+
224
+ if (testCase.failure) {
225
+ message = testCase.failure.message;
226
+ stack = testCase.failure['stack-trace']
227
+ }
228
+ if (testCase.reason) {
229
+ message = testCase.reason.message;
230
+ }
231
+
232
+ let status = STATUS.PASSED;
233
+ if (result === 'Pass') status = STATUS.PASSED;
234
+ if (result === 'Fail') status = STATUS.FAILED;
235
+ if (result === 'Skip') status = STATUS.SKIPPED;
236
+
237
+ const pathParts = type.split('.');
238
+ const suite_title = pathParts[pathParts.length - 1];
239
+ const file = pathParts.slice(0, -1).join('/');
240
+ const title = testCase.method || testCase.name.split('.').pop();
241
+ const run_time = parseFloat(time) * 1000;
242
+
243
+ tests.push({
244
+ create: true,
245
+ stack,
246
+ message,
247
+ file,
248
+ status,
249
+ title,
250
+ suite_title,
251
+ run_time,
252
+ });
253
+
254
+ });
255
+ });
256
+ });
257
+
258
+ const hasFailures = tests.filter(t => t.status === STATUS.FAILED).length > 0;
259
+ const status = hasFailures ? STATUS.FAILED : STATUS.PASSED;
260
+
261
+ this.tests = tests;
262
+
263
+ debug(tests);
264
+
265
+ return {
266
+ status,
267
+ create_tests: true,
268
+ name: 'xUnit',
269
+ tests_count: tests.length,
270
+ passed_count: tests.filter(t => t.status === STATUS.PASSED).length,
271
+ failed_count: tests.filter(t => t.status === STATUS.FAILED).length,
272
+ skipped_count: tests.filter(t => t.status === STATUS.SKIPPED).length,
273
+ tests,
274
+ };
191
275
  }
192
276
 
193
277
  calculateStats() {
@@ -221,12 +305,19 @@ class XmlReader {
221
305
  if (file.endsWith('.py')) this.stats.language = 'python';
222
306
  if (file.endsWith('.java')) this.stats.language = 'java';
223
307
  if (file.endsWith('.rb')) this.stats.language = 'ruby';
224
- if (file.endsWith('.js')) this.stats.language = 'js';
308
+ if (file.endsWith('.js')) this.stats.language = 'js';
225
309
  if (file.endsWith('.ts')) this.stats.language = 'ts';
226
310
  }
227
311
 
312
+ if (!fs.existsSync(file)) {
313
+ debug('Failed to open file with the source code', file)
314
+ return;
315
+ }
228
316
  const contents = fs.readFileSync(file).toString();
229
317
  t.code = fetchSourceCode(contents, { ...t, lang: this.stats.language })
318
+ if (t.code) debug('Fetched code for test %s', t.title);
319
+ t.test_id = fetchIdFromCode(t.code, { lang: this.stats.language })
320
+ if (t.test_id) debug('Fetched test id %s for test %s', t.test_id, t.title);
230
321
  } catch (err) {
231
322
  debug(err)
232
323
  }
@@ -241,18 +332,7 @@ class XmlReader {
241
332
 
242
333
  this.adapter.formatTest(t)
243
334
 
244
- t.title = t.title
245
- // insert a space before all caps
246
- .replace(/([A-Z])/g, ' $1')
247
- // _ chars to spaces
248
- .replace(/_/g, ' ')
249
- // uppercase the first character
250
- .replace(/^(.)|\s(.)/g, $1 => $1.toLowerCase())
251
-
252
- // remove standard prefixes
253
- .replace(/^test\s/, '')
254
- .replace(/^should\s/, '')
255
- .trim();
335
+ t.title = humanize(t.title);
256
336
  });
257
337
  }
258
338
 
@@ -281,8 +361,12 @@ class XmlReader {
281
361
  let files = [];
282
362
  if (test.files?.length) files = test.files.map(f => path.join(process.cwd(), f))
283
363
  files = [...files, ...fetchFilesFromStackTrace(test.stack)];
284
- debug('Uploading files', files)
285
- test.artifacts = await Promise.all(files.map(f => upload.uploadFileByPath(f, this.runId)));
364
+
365
+ if (!files.length) continue;
366
+
367
+ const runId = this.runId || this.store.runId || Date.now().toString();
368
+ test.artifacts = await Promise.all(files.map(f => upload.uploadFileByPath(f, runId)));
369
+ console.log(APP_PREFIX, `🗄️ Uploaded ${chalk.bold(`${files.length} artifacts`)} for test ${test.title}`);
286
370
  }
287
371
  }
288
372
 
@@ -292,7 +376,7 @@ class XmlReader {
292
376
  title: this.requestParams.title,
293
377
  env: this.requestParams.env,
294
378
  group_title: this.requestParams.group_title,
295
- };
379
+ };
296
380
 
297
381
  debug("Run", runParams);
298
382
 
@@ -310,9 +394,9 @@ class XmlReader {
310
394
  debug(
311
395
  'Uploading data',
312
396
  {
313
- ...this.stats,
314
- tests: this.tests,
315
- })
397
+ ...this.stats,
398
+ tests: this.tests,
399
+ })
316
400
 
317
401
  const dataString = {
318
402
  ...this.stats,
@@ -334,6 +418,8 @@ function reduceTestCases(prev, item) {
334
418
  if (!Array.isArray(testCases)) {
335
419
  testCases = [testCases]
336
420
  }
421
+ const suiteOutput = item['system-out'] || item.output || item.log || '';
422
+ const suiteErr = item['system-err'] || item.output || item.log || '';
337
423
  testCases.filter(t => !!t).forEach(testCaseItem => {
338
424
  const file = testCaseItem.file || item.filepath || '';
339
425
 
@@ -349,8 +435,9 @@ function reduceTestCases(prev, item) {
349
435
  if (testCaseItem.error && testCaseItem.error['#text']) stack = testCaseItem.error['#text'];
350
436
  if (!message) message = stack.trim().split('\n')[0];
351
437
 
352
- // prepend system output
353
- stack = `${testCaseItem['system-out'] || testCaseItem.output || testCaseItem.log || ''}\n\n${stack}`.trim()
438
+ // eslint-disable-next-line
439
+ stack = `${testCaseItem['system-out'] || testCaseItem.output || testCaseItem.log || ''}\n\n${stack}\n\n${suiteOutput}\n\n${suiteErr}`.trim()
440
+ const testId = fetchIdFromOutput(stack);
354
441
 
355
442
  let status = STATUS.PASSED.toString();
356
443
  if ('failure' in testCaseItem || 'error' in testCaseItem) status = STATUS.FAILED;
@@ -360,6 +447,7 @@ function reduceTestCases(prev, item) {
360
447
  create: true,
361
448
  file,
362
449
  stack,
450
+ test_id: testId,
363
451
  message,
364
452
  line: testCaseItem.lineno,
365
453
  // seconds are used in junit reports, but ms are used by testomatio