@testomatio/reporter 1.2.0-beta → 1.2.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.
@@ -0,0 +1,135 @@
1
+ const { resetConfig } = require('../fileUploader');
2
+ const { APP_PREFIX } = require('../constants');
3
+
4
+ /**
5
+ * Set S3 credentials from the provided artifacts object.
6
+ * @param {Object} artifacts - The artifacts object containing S3 credentials.
7
+ */
8
+ function setS3Credentials(artifacts) {
9
+ if (!Object.keys(artifacts).length) return;
10
+
11
+ console.log(APP_PREFIX, 'S3 were credentials obtained from Testomat.io...');
12
+
13
+ if (artifacts.ACCESS_KEY_ID) process.env.S3_ACCESS_KEY_ID = artifacts.ACCESS_KEY_ID;
14
+ if (artifacts.SECRET_ACCESS_KEY) process.env.S3_SECRET_ACCESS_KEY = artifacts.SECRET_ACCESS_KEY;
15
+ if (artifacts.REGION) process.env.S3_REGION = artifacts.REGION;
16
+ if (artifacts.BUCKET) process.env.S3_BUCKET = artifacts.BUCKET;
17
+ if (artifacts.ENDPOINT) process.env.S3_ENDPOINT = artifacts.ENDPOINT;
18
+ if (artifacts.presign) process.env.TESTOMATIO_PRIVATE_ARTIFACTS = '1';
19
+ resetConfig();
20
+ }
21
+ /**
22
+ * Generates mode request parameters based on the input params.
23
+ * @param {Object} params - The input parameters for the request.
24
+ * @param {string} params.type - The type of the request (e.g., "tag").
25
+ * @param {string} params.id - The ID associated with the request.
26
+ * @param {string} params.apiKey - The API key for authentication.
27
+ * @returns {Object|null} - An object containing the generated request parameters, or null if the type is invalid.
28
+ */
29
+ function generateFilterRequestParams(params) {
30
+ const { type, id, apiKey } = params;
31
+
32
+ if (!type) {
33
+ return;
34
+ }
35
+
36
+ if (!id) {
37
+ console.error(APP_PREFIX, `Please make sure your settings "${type.toUpperCase()}"= "${id}" is correct!`);
38
+ return;
39
+ }
40
+
41
+ return {
42
+ params: {
43
+ type,
44
+ id: encodeURIComponent(id),
45
+ api_key: apiKey
46
+ },
47
+ responseType: "json"
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Parse filter parameters from a string in the format "type=id".
53
+ * @param {string} opts - The input string containing the filter parameters.
54
+ * @returns {Object} An object containing the parsed filter parameters.
55
+ * The object has properties "type" and "id".
56
+ */
57
+ function parseFilterParams(opts) {
58
+ const [type, id] = opts.split("=");
59
+ const validType = updateFilterType(type);
60
+
61
+ return {
62
+ type: validType,
63
+ id
64
+ };
65
+ }
66
+
67
+ /**
68
+ * Update and validate the filter type.
69
+ * @param {string} type - The original filter type.
70
+ * @returns {string|undefined} The updated and validated filter type.
71
+ * Returns undefined if the type is not valid.
72
+ */
73
+ function updateFilterType(type) {
74
+ const typeLowerCase = type.toLowerCase();
75
+
76
+ const filterTypes = [
77
+ "tag-name",
78
+ "plan-id",
79
+ "label",
80
+ "jira-ticket",
81
+ ];
82
+
83
+ const filterApi = [
84
+ "tag",
85
+ "plan",
86
+ "label",
87
+ "jira",
88
+ // "ims-issue", //TODO: WIP
89
+ ];
90
+
91
+ if (!filterTypes.includes(typeLowerCase)) {
92
+ console.log(APP_PREFIX, `❗❗❗ Invalid "filter=${type}" start settings! Available option list: ${filterTypes}`);
93
+ return;
94
+ }
95
+
96
+ const index = filterTypes.indexOf(typeLowerCase);
97
+
98
+ return (index !== -1)
99
+ ? filterApi[index]
100
+ : undefined
101
+ }
102
+
103
+ /**
104
+ * Return an emoji based on the provided status.
105
+ * @param {string} status - The status value ('passed', 'failed', or 'skipped').
106
+ * @returns {string} - An emoji corresponding to the provided status.
107
+ */
108
+ function statusEmoji(status) {
109
+ if (status === 'passed') return '🟢';
110
+ if (status === 'failed') return '🔴';
111
+ if (status === 'skipped') return '🟡';
112
+ return '';
113
+ }
114
+
115
+ /**
116
+ * Generate a full name string based on the provided test object.
117
+ * @param {object} t - The test object.
118
+ * @returns {string} - A formatted full name string for the test object.
119
+ */
120
+ function fullName(t) {
121
+ let line = '';
122
+ if (t.suite_title) line = `${t.suite_title}: `;
123
+ line += `**${t.title}**`;
124
+ if (t.example) line += ` \`[${Object.values(t.example)}]\``;
125
+ return line;
126
+ }
127
+
128
+ module.exports = {
129
+ updateFilterType,
130
+ parseFilterParams,
131
+ generateFilterRequestParams,
132
+ setS3Credentials,
133
+ statusEmoji,
134
+ fullName
135
+ };
@@ -11,7 +11,10 @@ const debug = require('debug')('@testomatio/reporter:util');
11
11
  * @returns {String|null} testId
12
12
  */
13
13
  const parseTest = testTitle => {
14
+ if (!testTitle) return null;
15
+
14
16
  const captures = testTitle.match(/@T([\w\d]+)/);
17
+
15
18
  if (captures) {
16
19
  return captures[1];
17
20
  }
@@ -52,11 +55,21 @@ const isValidUrl = s => {
52
55
  }
53
56
  };
54
57
 
58
+ const fileMatchRegex = /file:(\/\/?[^:\s]+?\.(png|avi|webm|jpg|html|txt))/ig;
59
+
55
60
  const fetchFilesFromStackTrace = (stack = '') => {
56
- const files = stack.matchAll(/file:?\/(\/.*?\.(png|avi|webm|jpg|html|txt))/g);
57
- return Array.from(files)
58
- .map(f => f[1])
59
- .filter(f => fs.existsSync(f));
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
+ });
60
73
  };
61
74
 
62
75
  const fetchSourceCodeFromStackTrace = (stack = '') => {
@@ -84,13 +97,38 @@ const fetchSourceCodeFromStackTrace = (stack = '') => {
84
97
 
85
98
  if (!source) return '';
86
99
 
87
- return source.split('\n')
100
+ return source
101
+ .split('\n')
88
102
  .map((l, i) => {
89
103
  if (i === prepend) return `${line} > ${chalk.bold(l)}`;
90
- return `${line - prepend + i} | ${l}`
91
- }).join('\n');
104
+ return `${line - prepend + i} | ${l}`;
105
+ })
106
+ .join('\n');
92
107
  };
93
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
+
94
132
  const fetchSourceCode = (contents, opts = {}) => {
95
133
  if (!opts.title && !opts.line) return '';
96
134
 
@@ -112,7 +150,7 @@ const fetchSourceCode = (contents, opts = {}) => {
112
150
 
113
151
  if (lineIndex) {
114
152
  const result = [];
115
- for (let i = lineIndex; i < (lineIndex + limit); i++) {
153
+ for (let i = lineIndex; i < lineIndex + limit; i++) {
116
154
  if (lines[i] === undefined) continue;
117
155
 
118
156
  if (i > lineIndex + 2 && !opts.prepend) {
@@ -186,13 +224,78 @@ const fileSystem = {
186
224
  } else {
187
225
  debug(`Trying to delete ${dirPath} but it doesn't exist`);
188
226
  }
189
- }
227
+ },
190
228
  };
191
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
+
192
293
  module.exports = {
193
294
  isSameTest,
194
295
  fetchSourceCode,
195
296
  fetchSourceCodeFromStackTrace,
297
+ fetchIdFromCode,
298
+ fetchIdFromOutput,
196
299
  fetchFilesFromStackTrace,
197
300
  fileSystem,
198
301
  getCurrentDateTime,
@@ -201,4 +304,7 @@ module.exports = {
201
304
  ansiRegExp,
202
305
  parseTest,
203
306
  parseSuite,
204
- };
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