@testomatio/reporter 2.0.1-beta.4 → 2.0.1-beta.5-timestamp

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.
Files changed (148) hide show
  1. package/lib/adapter/codecept.js +335 -293
  2. package/lib/adapter/cucumber/current.js +203 -195
  3. package/lib/adapter/cucumber/legacy.js +155 -130
  4. package/lib/adapter/cucumber.js +16 -5
  5. package/lib/adapter/cypress-plugin/index.js +105 -91
  6. package/lib/adapter/jasmine.js +53 -54
  7. package/lib/adapter/jest.js +99 -97
  8. package/lib/adapter/mocha.js +141 -112
  9. package/lib/adapter/playwright.js +231 -199
  10. package/lib/adapter/vitest.js +149 -150
  11. package/lib/adapter/webdriver.js +121 -144
  12. package/lib/bin/cli.js +211 -229
  13. package/lib/bin/reportXml.js +52 -51
  14. package/lib/bin/startTest.js +95 -83
  15. package/lib/bin/uploadArtifacts.js +61 -56
  16. package/lib/client.js +465 -424
  17. package/lib/config.js +23 -18
  18. package/lib/constants.js +44 -50
  19. package/lib/data-storage.js +188 -216
  20. package/lib/junit-adapter/adapter.js +20 -17
  21. package/lib/junit-adapter/csharp.js +14 -28
  22. package/lib/junit-adapter/index.js +25 -27
  23. package/lib/junit-adapter/java.js +53 -41
  24. package/lib/junit-adapter/javascript.js +27 -30
  25. package/lib/junit-adapter/python.js +37 -38
  26. package/lib/junit-adapter/ruby.js +8 -11
  27. package/lib/output.js +52 -44
  28. package/lib/pipe/bitbucket.js +230 -223
  29. package/lib/pipe/csv.js +126 -113
  30. package/lib/pipe/debug.js +99 -118
  31. package/lib/pipe/github.js +213 -218
  32. package/lib/pipe/gitlab.js +206 -183
  33. package/lib/pipe/html.js +321 -258
  34. package/lib/pipe/index.js +66 -94
  35. package/lib/pipe/testomatio.js +474 -429
  36. package/lib/reporter-functions.js +26 -28
  37. package/lib/reporter.js +29 -34
  38. package/lib/services/artifacts.js +51 -55
  39. package/lib/services/index.js +12 -14
  40. package/lib/services/key-values.js +53 -56
  41. package/lib/services/logger.js +245 -226
  42. package/lib/template/testomatio.hbs +1366 -1026
  43. package/lib/uploader.js +364 -295
  44. package/lib/utils/pipe_utils.js +85 -89
  45. package/lib/utils/utils.js +307 -398
  46. package/lib/xmlReader.js +532 -525
  47. package/package.json +21 -64
  48. package/lib/adapter/codecept.d.ts +0 -2
  49. package/lib/adapter/cucumber/current.d.ts +0 -14
  50. package/lib/adapter/cucumber/legacy.d.ts +0 -0
  51. package/lib/adapter/cucumber.d.ts +0 -2
  52. package/lib/adapter/cypress-plugin/index.d.ts +0 -2
  53. package/lib/adapter/jasmine.d.ts +0 -11
  54. package/lib/adapter/jest.d.ts +0 -13
  55. package/lib/adapter/mocha.d.ts +0 -2
  56. package/lib/adapter/nightwatch.d.ts +0 -4
  57. package/lib/adapter/nightwatch.js +0 -80
  58. package/lib/adapter/playwright.d.ts +0 -14
  59. package/lib/adapter/vitest.d.ts +0 -35
  60. package/lib/adapter/webdriver.d.ts +0 -24
  61. package/lib/bin/cli.d.ts +0 -2
  62. package/lib/bin/reportXml.d.ts +0 -2
  63. package/lib/bin/startTest.d.ts +0 -2
  64. package/lib/bin/uploadArtifacts.d.ts +0 -2
  65. package/lib/client.d.ts +0 -76
  66. package/lib/config.d.ts +0 -1
  67. package/lib/constants.d.ts +0 -25
  68. package/lib/data-storage.d.ts +0 -34
  69. package/lib/junit-adapter/adapter.d.ts +0 -9
  70. package/lib/junit-adapter/csharp.d.ts +0 -5
  71. package/lib/junit-adapter/index.d.ts +0 -3
  72. package/lib/junit-adapter/java.d.ts +0 -5
  73. package/lib/junit-adapter/javascript.d.ts +0 -4
  74. package/lib/junit-adapter/python.d.ts +0 -5
  75. package/lib/junit-adapter/ruby.d.ts +0 -4
  76. package/lib/output.d.ts +0 -11
  77. package/lib/package.json +0 -3
  78. package/lib/pipe/bitbucket.d.ts +0 -25
  79. package/lib/pipe/csv.d.ts +0 -47
  80. package/lib/pipe/debug.d.ts +0 -29
  81. package/lib/pipe/github.d.ts +0 -30
  82. package/lib/pipe/gitlab.d.ts +0 -25
  83. package/lib/pipe/html.d.ts +0 -35
  84. package/lib/pipe/index.d.ts +0 -1
  85. package/lib/pipe/testomatio.d.ts +0 -71
  86. package/lib/replay.d.ts +0 -31
  87. package/lib/replay.js +0 -237
  88. package/lib/reporter-functions.d.ts +0 -34
  89. package/lib/reporter.d.ts +0 -232
  90. package/lib/services/artifacts.d.ts +0 -33
  91. package/lib/services/index.d.ts +0 -9
  92. package/lib/services/key-values.d.ts +0 -27
  93. package/lib/services/logger.d.ts +0 -64
  94. package/lib/uploader.d.ts +0 -60
  95. package/lib/utils/pipe_utils.d.ts +0 -41
  96. package/lib/utils/utils.d.ts +0 -54
  97. package/lib/xmlReader.d.ts +0 -92
  98. package/src/adapter/codecept.js +0 -373
  99. package/src/adapter/cucumber/current.js +0 -228
  100. package/src/adapter/cucumber/legacy.js +0 -158
  101. package/src/adapter/cucumber.js +0 -4
  102. package/src/adapter/cypress-plugin/index.js +0 -110
  103. package/src/adapter/jasmine.js +0 -60
  104. package/src/adapter/jest.js +0 -107
  105. package/src/adapter/mocha.cjs +0 -2
  106. package/src/adapter/mocha.js +0 -156
  107. package/src/adapter/nightwatch.js +0 -88
  108. package/src/adapter/playwright.js +0 -254
  109. package/src/adapter/vitest.js +0 -183
  110. package/src/adapter/webdriver.js +0 -142
  111. package/src/bin/cli.js +0 -348
  112. package/src/bin/reportXml.js +0 -77
  113. package/src/bin/startTest.js +0 -124
  114. package/src/bin/uploadArtifacts.js +0 -91
  115. package/src/client.js +0 -508
  116. package/src/config.js +0 -30
  117. package/src/constants.js +0 -53
  118. package/src/data-storage.js +0 -204
  119. package/src/junit-adapter/adapter.js +0 -23
  120. package/src/junit-adapter/csharp.js +0 -28
  121. package/src/junit-adapter/index.js +0 -28
  122. package/src/junit-adapter/java.js +0 -58
  123. package/src/junit-adapter/javascript.js +0 -31
  124. package/src/junit-adapter/python.js +0 -42
  125. package/src/junit-adapter/ruby.js +0 -10
  126. package/src/output.js +0 -57
  127. package/src/pipe/bitbucket.js +0 -252
  128. package/src/pipe/csv.js +0 -140
  129. package/src/pipe/debug.js +0 -119
  130. package/src/pipe/github.js +0 -232
  131. package/src/pipe/gitlab.js +0 -247
  132. package/src/pipe/html.js +0 -373
  133. package/src/pipe/index.js +0 -71
  134. package/src/pipe/testomatio.js +0 -504
  135. package/src/replay.js +0 -245
  136. package/src/reporter-functions.js +0 -55
  137. package/src/reporter.cjs_decprecated +0 -21
  138. package/src/reporter.js +0 -33
  139. package/src/services/artifacts.js +0 -59
  140. package/src/services/index.js +0 -13
  141. package/src/services/key-values.js +0 -59
  142. package/src/services/logger.js +0 -315
  143. package/src/template/emptyData.svg +0 -23
  144. package/src/template/testomatio.hbs +0 -1081
  145. package/src/uploader.js +0 -376
  146. package/src/utils/pipe_utils.js +0 -119
  147. package/src/utils/utils.js +0 -416
  148. package/src/xmlReader.js +0 -614
package/lib/xmlReader.js CHANGED
@@ -1,549 +1,556 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- const debug_1 = __importDefault(require("debug"));
7
- const path_1 = __importDefault(require("path"));
8
- const picocolors_1 = __importDefault(require("picocolors"));
9
- const fs_1 = __importDefault(require("fs"));
10
- const fast_xml_parser_1 = require("fast-xml-parser");
11
- const constants_js_1 = require("./constants.js");
12
- const crypto_1 = require("crypto");
13
- const url_1 = require("url");
14
- const utils_js_1 = require("./utils/utils.js");
15
- const index_js_1 = require("./pipe/index.js");
16
- const index_js_2 = __importDefault(require("./junit-adapter/index.js"));
17
- const config_js_1 = require("./config.js");
18
- const uploader_js_1 = require("./uploader.js");
19
- // @ts-ignore this line will be removed in compiled code, because __dirname is defined in commonjs
20
- const debug = (0, debug_1.default)('@testomatio/reporter:xml');
21
- const ridRunId = (0, crypto_1.randomUUID)();
1
+ const debug = require('debug')('@testomatio/reporter:xml');
2
+ const path = require('path');
3
+ const chalk = require('chalk');
4
+ const fs = require('fs');
5
+ const { randomUUID } = require('crypto');
6
+ const { XMLParser } = require('fast-xml-parser');
7
+ const { APP_PREFIX, STATUS } = require('./constants');
8
+ const {
9
+ fetchFilesFromStackTrace,
10
+ fetchIdFromOutput,
11
+ fetchSourceCode,
12
+ fetchSourceCodeFromStackTrace,
13
+ fetchIdFromCode,
14
+ humanize,
15
+ } = require('./utils/utils');
16
+ const S3Uploader = require('./uploader');
17
+ const pipesFactory = require('./pipe');
18
+ const adapterFactory = require('./junit-adapter');
19
+ const config = require('./config');
20
+
21
+ const ridRunId = randomUUID();
22
+
22
23
  const TESTOMATIO_URL = process.env.TESTOMATIO_URL || 'https://app.testomat.io';
23
- const { TESTOMATIO_RUNGROUP_TITLE, TESTOMATIO_SUITE, TESTOMATIO_MAX_STACK_TRACE, TESTOMATIO_TITLE, TESTOMATIO_ENV, TESTOMATIO_RUN, TESTOMATIO_MARK_DETACHED, } = process.env;
24
+ const { TESTOMATIO_RUNGROUP_TITLE, TESTOMATIO_TITLE, TESTOMATIO_ENV, TESTOMATIO_RUN, TESTOMATIO_MARK_DETACHED } =
25
+ process.env;
26
+
24
27
  const options = {
25
- ignoreDeclaration: true,
26
- ignoreAttributes: false,
27
- alwaysCreateTextNode: false,
28
- attributeNamePrefix: '',
29
- parseTagValue: true,
28
+ ignoreDeclaration: true,
29
+ ignoreAttributes: false,
30
+ alwaysCreateTextNode: false,
31
+ attributeNamePrefix: '',
32
+ parseTagValue: true,
30
33
  };
31
- const MAX_OUTPUT_LENGTH = parseInt(TESTOMATIO_MAX_STACK_TRACE, 10) || 10000;
34
+
32
35
  const reduceOptions = {};
36
+
33
37
  class XmlReader {
34
- constructor(opts = {}) {
35
- this.requestParams = {
36
- apiKey: opts.apiKey || config_js_1.config.TESTOMATIO,
37
- url: opts.url || TESTOMATIO_URL,
38
- title: TESTOMATIO_TITLE,
39
- env: TESTOMATIO_ENV,
40
- group_title: TESTOMATIO_RUNGROUP_TITLE,
41
- detach: TESTOMATIO_MARK_DETACHED,
42
- // batch uploading is implemented for xml already
43
- isBatchEnabled: false,
44
- };
45
- this.runId = opts.runId || TESTOMATIO_RUN;
46
- this.adapter = (0, index_js_2.default)(opts.lang?.toLowerCase(), opts);
47
- if (!this.adapter)
48
- throw new Error('XML adapter for this format not found');
49
- this.opts = opts || {};
50
- this.store = {};
51
- this.pipesPromise = (0, index_js_1.pipesFactory)(opts, this.store);
52
- this.parser = new fast_xml_parser_1.XMLParser(options);
53
- this.tests = [];
54
- this.stats = {};
55
- this.stats.language = opts.lang?.toLowerCase();
56
- this.uploader = new uploader_js_1.S3Uploader();
57
- // @ts-ignore
58
- const packageJsonPath = path_1.default.resolve(__dirname, '..', 'package.json');
59
- this.version = JSON.parse(fs_1.default.readFileSync(packageJsonPath).toString()).version;
60
- console.log(constants_js_1.APP_PREFIX, `Testomatio Reporter v${this.version}`);
61
- }
62
- connectAdapter() {
63
- if (this.opts.javaTests) {
64
- this.adapter = (0, index_js_2.default)('java', this.opts);
65
- return this.adapter;
66
- }
67
- this.adapter = (0, index_js_2.default)(this.stats.language, this.opts);
68
- return this.adapter;
38
+ constructor(opts = {}) {
39
+ this.requestParams = {
40
+ apiKey: opts.apiKey || config.TESTOMATIO,
41
+ url: opts.url || TESTOMATIO_URL,
42
+ title: TESTOMATIO_TITLE,
43
+ env: TESTOMATIO_ENV,
44
+ group_title: TESTOMATIO_RUNGROUP_TITLE,
45
+ detach: TESTOMATIO_MARK_DETACHED,
46
+ // batch uploading is implemented for xml already
47
+ isBatchEnabled: false,
48
+ };
49
+ this.runId = opts.runId || TESTOMATIO_RUN;
50
+ this.adapter = adapterFactory(opts.lang?.toLowerCase(), opts);
51
+ if (!this.adapter) throw new Error('XML adapter for this format not found');
52
+
53
+ this.opts = opts || {};
54
+ this.store = {};
55
+ this.pipes = pipesFactory(opts, this.store);
56
+
57
+ this.parser = new XMLParser(options);
58
+ this.tests = [];
59
+ this.stats = {};
60
+ this.stats.language = opts.lang?.toLowerCase();
61
+ this.uploader = new S3Uploader();
62
+
63
+ this.version = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json')).toString()).version;
64
+ console.log(APP_PREFIX, `Testomatio Reporter v${this.version}`);
65
+ }
66
+
67
+ connectAdapter() {
68
+ if (this.opts.javaTests) {
69
+ this.adapter = adapterFactory('java', this.opts);
70
+ return this.adapter;
69
71
  }
70
- parse(fileName) {
71
- let xmlData = fs_1.default.readFileSync(path_1.default.resolve(fileName)).toString();
72
- // we remove too long stack traces
73
- const cutRegexes = [
74
- /(<output><!\[CDATA\[)([\s\S]*?)(\]\]><\/output>)/g,
75
- /(<system-err><!\[CDATA\[)([\s\S]*?)(\]\]><\/system-err>)/g,
76
- /(<system-out><!\[CDATA\[)([\s\S]*?)(\]\]><\/system-out>)/g,
77
- ];
78
- for (const regex of cutRegexes) {
79
- xmlData = xmlData.replace(regex, (_, p1, p2, p3) => `${p1}${p2.substring(0, MAX_OUTPUT_LENGTH)}${p3}`);
80
- }
81
- const jsonResult = this.parser.parse(xmlData);
82
- let jsonSuite;
83
- if (jsonResult.testsuites) {
84
- jsonSuite = jsonResult.testsuites;
85
- }
86
- else if (jsonResult.testsuite) {
87
- jsonSuite = jsonResult;
88
- }
89
- else if (jsonResult.TestRun) {
90
- return this.processTRX(jsonResult);
91
- }
92
- else if (jsonResult['test-run']) {
93
- return this.processNUnit(jsonResult['test-run']);
94
- }
95
- else if (jsonResult.assemblies) {
96
- return this.processXUnit(jsonResult.assemblies);
97
- }
98
- else {
99
- console.log(jsonResult);
100
- throw new Error("Format can't be parsed");
101
- }
102
- return this.processJUnit(jsonSuite);
72
+ this.adapter = adapterFactory(this.stats.language, this.opts);
73
+ return this.adapter;
74
+ }
75
+
76
+ parse(fileName) {
77
+ let xmlData = fs.readFileSync(path.resolve(fileName)).toString();
78
+
79
+ // we remove too long stack traces
80
+ const cutRegexes = [
81
+ /(<output><!\[CDATA\[)([\s\S]*?)(\]\]><\/output>)/g,
82
+ /(<system-err><!\[CDATA\[)([\s\S]*?)(\]\]><\/system-err>)/g,
83
+ /(<system-out><!\[CDATA\[)([\s\S]*?)(\]\]><\/system-out>)/g,
84
+ ];
85
+
86
+ for (const regex of cutRegexes) {
87
+ xmlData = xmlData.replace(regex, (_, p1, p2, p3) => `${p1}${p2.substring(0, 5000)}${p3}`);
103
88
  }
104
- processJUnit(jsonSuite) {
105
- const { testsuite, name, tests, failures, errors } = jsonSuite;
106
- reduceOptions.preferClassname = this.stats.language === 'python';
107
- const resultTests = processTestSuite(testsuite);
108
- const hasFailures = resultTests.filter(t => t.status === 'failed').length > 0;
109
- const status = failures > 0 || errors > 0 || hasFailures ? 'failed' : 'passed';
110
- const time = testsuite.time || 0;
111
- // debug('time', jsonSuite, time)
112
- if (time) {
113
- if (!this.stats.duration)
114
- this.stats.duration = 0;
115
- this.stats.duration += parseFloat(time);
116
- }
117
- this.tests = this.tests.concat(resultTests);
118
- return {
119
- create_tests: true,
120
- duration: parseFloat(time),
121
- failed_count: parseInt(failures, 10),
122
- name,
123
- passed_count: parseInt(tests, 10) - parseInt(failures, 10),
124
- skipped_count: 0,
125
- status,
126
- tests: resultTests,
127
- tests_count: parseInt(tests, 10),
128
- };
89
+
90
+ const jsonResult = this.parser.parse(xmlData);
91
+ let jsonSuite;
92
+
93
+ if (jsonResult.testsuites) {
94
+ jsonSuite = jsonResult.testsuites;
95
+ } else if (jsonResult.testsuite) {
96
+ jsonSuite = jsonResult;
97
+ } else if (jsonResult.TestRun) {
98
+ return this.processTRX(jsonResult);
99
+ } else if (jsonResult['test-run']) {
100
+ return this.processNUnit(jsonResult['test-run']);
101
+ } else if (jsonResult.assemblies) {
102
+ return this.processXUnit(jsonResult.assemblies);
103
+ } else {
104
+ console.log(jsonResult);
105
+ throw new Error("Format can't be parsed");
129
106
  }
130
- processNUnit(jsonSuite) {
131
- const { result, total, passed, failed, inconclusive, skipped } = jsonSuite;
132
- reduceOptions.preferClassname = this.stats.language === 'python';
133
- const resultTests = processTestSuite(jsonSuite['test-suite']);
134
- this.tests = this.tests.concat(resultTests);
135
- return {
136
- status: result?.toLowerCase(),
137
- create_tests: true,
138
- tests_count: parseInt(total, 10),
139
- passed_count: parseInt(passed, 10),
140
- failed_count: parseInt(failed, 10),
141
- skipped_count: parseInt(inconclusive + skipped, 10),
142
- tests: resultTests,
143
- };
107
+
108
+ return this.processJUnit(jsonSuite);
109
+ }
110
+
111
+ processJUnit(jsonSuite) {
112
+ const { testsuite, name, tests, failures, errors } = jsonSuite;
113
+
114
+ reduceOptions.preferClassname = this.stats.language === 'python';
115
+ const resultTests = processTestSuite(testsuite);
116
+
117
+ const hasFailures = resultTests.filter(t => t.status === 'failed').length > 0;
118
+ const status = failures > 0 || errors > 0 || hasFailures ? 'failed' : 'passed';
119
+
120
+ const time = testsuite.time || 0;
121
+ // debug('time', jsonSuite, time)
122
+ if (time) {
123
+ if (!this.stats.duration) this.stats.duration = 0;
124
+ this.stats.duration += parseFloat(time);
144
125
  }
145
- processTRX(jsonSuite) {
146
- let defs = jsonSuite?.TestRun?.TestDefinitions?.UnitTest;
147
- if (!Array.isArray(defs))
148
- defs = [defs].filter(d => !!d);
149
- const tests = defs.map(td => {
150
- const title = td.name.replace(/\(.*?\)/, '').trim();
151
- let example = td.name.match(/\((.*?)\)/);
152
- if (example)
153
- example = { ...example[1].split(',') };
154
- const suite = td.TestMethod.className.split(', ')[0].split('.');
155
- const suite_title = suite.pop();
156
- return {
157
- title,
158
- example,
159
- file: suite.join('/'),
160
- description: td.Description,
161
- suite_title,
162
- id: td.Execution.id,
163
- };
164
- }) || [];
165
- let result = jsonSuite?.TestRun?.Results?.UnitTestResult;
166
- if (!Array.isArray(result))
167
- result = [result].filter(d => !!d);
168
- const results = result.map(td => ({
169
- id: td.executionId,
170
- // seconds are used in junit reports, but ms are used by testomatio
171
- run_time: parseFloat(td.duration) * 1000,
172
- status: td.outcome,
173
- stack: td.Output.StdOut,
174
- files: td?.ResultFiles?.ResultFile?.map(rf => rf.path),
175
- }));
176
- results.forEach(r => {
177
- const test = tests.find(t => t.id === r.id) || {};
178
- r.suite_title = test.suite_title;
179
- r.title = test.title?.trim();
180
- if (test.code)
181
- r.code = test.code;
182
- if (test.description)
183
- r.description = test.description;
184
- if (test.example)
185
- r.example = test.example;
186
- if (test.file)
187
- r.file = test.file;
188
- r.create = true;
189
- if (r.status === 'Passed')
190
- r.status = constants_js_1.STATUS.PASSED;
191
- if (r.status === 'Failed')
192
- r.status = constants_js_1.STATUS.FAILED;
193
- if (r.status === 'Skipped')
194
- r.status = constants_js_1.STATUS.SKIPPED;
195
- delete r.id;
196
- });
197
- debug(results);
198
- const counters = jsonSuite?.TestRun?.ResultSummary?.Counters || {};
199
- const failed_count = parseInt(counters.failed, 10) + parseInt(counters.error, 10);
200
- let status = constants_js_1.STATUS.PASSED.toString();
201
- if (failed_count > 0)
202
- status = constants_js_1.STATUS.FAILED;
203
- this.tests = results.filter(t => !!t.title);
126
+
127
+ this.tests = this.tests.concat(resultTests);
128
+
129
+ return {
130
+ status,
131
+ create_tests: true,
132
+ name,
133
+ duration: parseFloat(time),
134
+ tests_count: parseInt(tests, 10),
135
+ passed_count: parseInt(tests, 10) - parseInt(failures, 10),
136
+ failed_count: parseInt(failures, 10),
137
+ skipped_count: 0,
138
+ tests: resultTests,
139
+ };
140
+ }
141
+
142
+ processNUnit(jsonSuite) {
143
+ const { result, total, passed, failed, inconclusive, skipped } = jsonSuite;
144
+
145
+ reduceOptions.preferClassname = this.stats.language === 'python';
146
+ const resultTests = processTestSuite(jsonSuite['test-suite']);
147
+
148
+ this.tests = this.tests.concat(resultTests);
149
+
150
+ return {
151
+ status: result?.toLowerCase(),
152
+ create_tests: true,
153
+ tests_count: parseInt(total, 10),
154
+ passed_count: parseInt(passed, 10),
155
+ failed_count: parseInt(failed, 10),
156
+ skipped_count: parseInt(inconclusive + skipped, 10),
157
+ tests: resultTests,
158
+ };
159
+ }
160
+
161
+ processTRX(jsonSuite) {
162
+ let defs = jsonSuite?.TestRun?.TestDefinitions?.UnitTest;
163
+ if (!Array.isArray(defs)) defs = [defs].filter(d => !!d);
164
+
165
+ const tests =
166
+ defs.map(td => {
167
+ const title = td.name.replace(/\(.*?\)/, '').trim();
168
+ let example = td.name.match(/\((.*?)\)/);
169
+ if (example) example = { ...example[1].split(',') };
170
+ const suite = td.TestMethod.className.split(', ')[0].split('.');
171
+ const suite_title = suite.pop();
204
172
  return {
205
- status,
206
- create_tests: !process.env.IGNORE_NEW_TESTS,
207
- tests_count: parseInt(counters.total, 10),
208
- passed_count: parseInt(counters.passed, 10),
209
- skipped_count: parseInt(counters.notExecuted, 10),
210
- failed_count,
211
- tests: results,
173
+ title,
174
+ example,
175
+ file: suite.join('/'),
176
+ description: td.Description,
177
+ suite_title,
178
+ id: td.Execution.id,
212
179
  };
213
- }
214
- processXUnit(assemblies) {
215
- const tests = [];
216
- assemblies = Array.isArray(assemblies.assembly) ? assemblies.assembly : [assemblies.assembly];
217
- assemblies.forEach(assembly => {
218
- const { collection } = assembly;
219
- const suites = Array.isArray(collection) ? collection : [collection];
220
- suites.forEach(suite => {
221
- const { test } = suite;
222
- if (!test)
223
- return;
224
- const cases = Array.isArray(test) ? test : [test];
225
- cases.forEach(testCase => {
226
- const { type, time, result } = testCase;
227
- let message = '';
228
- let stack = '';
229
- if (testCase.failure) {
230
- message = testCase.failure.message;
231
- stack = testCase.failure['stack-trace'];
232
- }
233
- if (testCase.reason) {
234
- message = testCase.reason.message;
235
- }
236
- let status = constants_js_1.STATUS.PASSED;
237
- if (result === 'Pass')
238
- status = constants_js_1.STATUS.PASSED;
239
- if (result === 'Fail')
240
- status = constants_js_1.STATUS.FAILED;
241
- if (result === 'Skip')
242
- status = constants_js_1.STATUS.SKIPPED;
243
- const pathParts = type.split('.');
244
- const suite_title = pathParts[pathParts.length - 1];
245
- const file = pathParts.slice(0, -1).join('/');
246
- const title = testCase.method || testCase.name.split('.').pop();
247
- const run_time = parseFloat(time) * 1000;
248
- tests.push({
249
- create: true,
250
- stack,
251
- message,
252
- file,
253
- status,
254
- title,
255
- suite_title,
256
- run_time,
257
- });
258
- });
259
- });
260
- });
261
- const hasFailures = tests.filter(t => t.status === constants_js_1.STATUS.FAILED).length > 0;
262
- const status = hasFailures ? constants_js_1.STATUS.FAILED : constants_js_1.STATUS.PASSED;
263
- this.tests = tests;
264
- debug(tests);
265
- return {
180
+ }) || [];
181
+
182
+ let result = jsonSuite?.TestRun?.Results?.UnitTestResult;
183
+ if (!Array.isArray(result)) result = [result].filter(d => !!d);
184
+
185
+ const results = result.map(td => ({
186
+ id: td.executionId,
187
+ // seconds are used in junit reports, but ms are used by testomatio
188
+ run_time: parseFloat(td.duration) * 1000,
189
+ status: td.outcome,
190
+ stack: td.Output.StdOut,
191
+ files: td?.ResultFiles?.ResultFile?.map(rf => rf.path),
192
+ }));
193
+
194
+ results.forEach(r => {
195
+ const test = tests.find(t => t.id === r.id) || {};
196
+ r.suite_title = test.suite_title;
197
+ r.title = test.title?.trim();
198
+ if (test.code) r.code = test.code;
199
+ if (test.description) r.description = test.description;
200
+ if (test.example) r.example = test.example;
201
+ if (test.file) r.file = test.file;
202
+ r.create = true;
203
+ if (r.status === 'Passed') r.status = STATUS.PASSED;
204
+ if (r.status === 'Failed') r.status = STATUS.FAILED;
205
+ if (r.status === 'Skipped') r.status = STATUS.SKIPPED;
206
+ delete r.id;
207
+ });
208
+
209
+ debug(results);
210
+
211
+ const counters = jsonSuite?.TestRun?.ResultSummary?.Counters || {};
212
+
213
+ const failed_count = parseInt(counters.failed, 10) + parseInt(counters.error, 10);
214
+
215
+ let status = STATUS.PASSED.toString();
216
+ if (failed_count > 0) status = STATUS.FAILED;
217
+
218
+ this.tests = results.filter(t => !!t.title);
219
+
220
+ return {
221
+ status,
222
+ create_tests: true,
223
+ tests_count: parseInt(counters.total, 10),
224
+ passed_count: parseInt(counters.passed, 10),
225
+ skipped_count: parseInt(counters.notExecuted, 10),
226
+ failed_count,
227
+ tests: results,
228
+ };
229
+ }
230
+
231
+ processXUnit(assemblies) {
232
+ const tests = [];
233
+
234
+ assemblies = Array.isArray(assemblies.assembly) ? assemblies.assembly : [assemblies.assembly];
235
+
236
+ assemblies.forEach(assembly => {
237
+ const { collection } = assembly;
238
+
239
+ const suites = Array.isArray(collection) ? collection : [collection];
240
+
241
+ suites.forEach(suite => {
242
+ const { test } = suite;
243
+ if (!test) return;
244
+ const cases = Array.isArray(test) ? test : [test];
245
+ cases.forEach(testCase => {
246
+ const { type, time, result } = testCase;
247
+
248
+ let message = '';
249
+ let stack = '';
250
+
251
+ if (testCase.failure) {
252
+ message = testCase.failure.message;
253
+ stack = testCase.failure['stack-trace'];
254
+ }
255
+ if (testCase.reason) {
256
+ message = testCase.reason.message;
257
+ }
258
+
259
+ let status = STATUS.PASSED;
260
+ if (result === 'Pass') status = STATUS.PASSED;
261
+ if (result === 'Fail') status = STATUS.FAILED;
262
+ if (result === 'Skip') status = STATUS.SKIPPED;
263
+
264
+ const pathParts = type.split('.');
265
+ const suite_title = pathParts[pathParts.length - 1];
266
+ const file = pathParts.slice(0, -1).join('/');
267
+ const title = testCase.method || testCase.name.split('.').pop();
268
+ const run_time = parseFloat(time) * 1000;
269
+
270
+ tests.push({
271
+ create: true,
272
+ stack,
273
+ message,
274
+ file,
266
275
  status,
267
- create_tests: true,
268
- name: 'xUnit',
269
- tests_count: tests.length,
270
- passed_count: tests.filter(t => t.status === constants_js_1.STATUS.PASSED).length,
271
- failed_count: tests.filter(t => t.status === constants_js_1.STATUS.FAILED).length,
272
- skipped_count: tests.filter(t => t.status === constants_js_1.STATUS.SKIPPED).length,
273
- tests,
274
- };
275
- }
276
- calculateStats() {
277
- this.stats = {
278
- ...this.stats,
279
- detach: this.requestParams.detach,
280
- status: 'passed',
281
- create_tests: true,
282
- tests_count: 0,
283
- passed_count: 0,
284
- failed_count: 0,
285
- skipped_count: 0,
286
- };
287
- this.tests.forEach(t => {
288
- this.stats.tests_count++;
289
- if (t.status === 'passed')
290
- this.stats.passed_count++;
291
- if (t.status === 'failed')
292
- this.stats.failed_count++;
293
- });
294
- if (this.stats.failed_count)
295
- this.stats.status = 'failed';
296
- return this.stats;
297
- }
298
- fetchSourceCode() {
299
- this.tests.forEach(t => {
300
- try {
301
- const file = this.adapter.getFilePath(t);
302
- if (!file)
303
- return;
304
- if (!this.stats.language) {
305
- if (file.endsWith('.php'))
306
- this.stats.language = 'php';
307
- if (file.endsWith('.py'))
308
- this.stats.language = 'python';
309
- if (file.endsWith('.java'))
310
- this.stats.language = 'java';
311
- if (file.endsWith('.rb'))
312
- this.stats.language = 'ruby';
313
- if (file.endsWith('.js'))
314
- this.stats.language = 'js';
315
- if (file.endsWith('.ts'))
316
- this.stats.language = 'ts';
317
- if (file.endsWith('.cs'))
318
- this.stats.language = 'csharp';
319
- }
320
- if (!fs_1.default.existsSync(file)) {
321
- debug('Failed to open file with the source code', file);
322
- return;
323
- }
324
- const contents = fs_1.default.readFileSync(file).toString();
325
- t.code = (0, utils_js_1.fetchSourceCode)(contents, { ...t, lang: this.stats.language });
326
- if (t.code)
327
- debug('Fetched code for test %s', t.title);
328
- t.test_id = (0, utils_js_1.fetchIdFromCode)(t.code, { lang: this.stats.language });
329
- if (t.test_id)
330
- debug('Fetched test id %s for test %s', t.test_id, t.title);
331
- }
332
- catch (err) {
333
- debug(err);
334
- }
335
- });
336
- }
337
- formatTests() {
338
- this.tests.forEach(t => {
339
- if (t.file) {
340
- t.file = t.file.replace(process.cwd() + path_1.default.sep, '');
341
- }
342
- this.adapter.formatTest(t);
343
- t.title = (0, utils_js_1.humanize)(t.title);
344
- });
345
- }
346
- formatErrors() {
347
- this.tests
348
- .filter(t => !!t.stack)
349
- .forEach(t => {
350
- t.stack = this.formatStack(t);
351
- t.message = this.adapter.formatMessage(t);
276
+ title,
277
+ suite_title,
278
+ run_time,
279
+ });
352
280
  });
353
- }
354
- formatStack(t) {
355
- const stack = this.adapter.formatStack(t);
356
- const sourcePart = (0, utils_js_1.fetchSourceCodeFromStackTrace)(stack);
357
- if (!sourcePart)
358
- return stack;
359
- const separator = picocolors_1.default.bold(picocolors_1.default.red('################[ Failure ]################'));
360
- return `${stack}\n\n${separator}\n${(0, utils_js_1.fetchSourceCodeFromStackTrace)(stack)}`;
361
- }
362
- async uploadArtifacts() {
363
- for (const test of this.tests.filter(t => !!t.stack)) {
364
- let files = [];
365
- if (!test.files?.length)
366
- continue;
367
- files = test.files.map(f => (path_1.default.isAbsolute(f) ? f : path_1.default.join(process.cwd(), f)));
368
- if (!files.length)
369
- continue;
370
- const runId = this.runId || this.store.runId || Date.now().toString();
371
- test.artifacts = await Promise.all(files.map(f => this.uploader.uploadFileByPath(f, [runId, path_1.default.basename(f)])));
372
- console.log(constants_js_1.APP_PREFIX, `🗄️ Uploaded ${picocolors_1.default.bold(`${files.length} artifacts`)} for test ${test.title}`);
281
+ });
282
+ });
283
+
284
+ const hasFailures = tests.filter(t => t.status === STATUS.FAILED).length > 0;
285
+ const status = hasFailures ? STATUS.FAILED : STATUS.PASSED;
286
+
287
+ this.tests = tests;
288
+
289
+ debug(tests);
290
+
291
+ return {
292
+ status,
293
+ create_tests: true,
294
+ name: 'xUnit',
295
+ tests_count: tests.length,
296
+ passed_count: tests.filter(t => t.status === STATUS.PASSED).length,
297
+ failed_count: tests.filter(t => t.status === STATUS.FAILED).length,
298
+ skipped_count: tests.filter(t => t.status === STATUS.SKIPPED).length,
299
+ tests,
300
+ };
301
+ }
302
+
303
+ calculateStats() {
304
+ this.stats = {
305
+ ...this.stats,
306
+ status: 'passed',
307
+ create_tests: true,
308
+ tests_count: 0,
309
+ passed_count: 0,
310
+ failed_count: 0,
311
+ skipped_count: 0,
312
+ };
313
+ this.tests.forEach(t => {
314
+ this.stats.tests_count++;
315
+ if (t.status === 'passed') this.stats.passed_count++;
316
+ if (t.status === 'failed') this.stats.failed_count++;
317
+ });
318
+ if (this.stats.failed_count) this.stats.status = 'failed';
319
+
320
+ return this.stats;
321
+ }
322
+
323
+ fetchSourceCode() {
324
+ this.tests.forEach(t => {
325
+ try {
326
+ const file = this.adapter.getFilePath(t);
327
+ if (!file) return;
328
+
329
+ if (!this.stats.language) {
330
+ if (file.endsWith('.php')) this.stats.language = 'php';
331
+ if (file.endsWith('.py')) this.stats.language = 'python';
332
+ if (file.endsWith('.java')) this.stats.language = 'java';
333
+ if (file.endsWith('.rb')) this.stats.language = 'ruby';
334
+ if (file.endsWith('.js')) this.stats.language = 'js';
335
+ if (file.endsWith('.ts')) this.stats.language = 'ts';
373
336
  }
337
+
338
+ if (!fs.existsSync(file)) {
339
+ debug('Failed to open file with the source code', file);
340
+ return;
341
+ }
342
+ const contents = fs.readFileSync(file).toString();
343
+ t.code = fetchSourceCode(contents, { ...t, lang: this.stats.language });
344
+ if (t.code) debug('Fetched code for test %s', t.title);
345
+ t.test_id = fetchIdFromCode(t.code, { lang: this.stats.language });
346
+ if (t.test_id) debug('Fetched test id %s for test %s', t.test_id, t.title);
347
+ } catch (err) {
348
+ debug(err);
349
+ }
350
+ });
351
+ }
352
+
353
+ formatTests() {
354
+ this.tests.forEach(t => {
355
+ if (t.file) {
356
+ t.file = t.file.replace(process.cwd() + path.sep, '');
357
+ }
358
+
359
+ this.adapter.formatTest(t);
360
+
361
+ t.title = humanize(t.title);
362
+ });
363
+ }
364
+
365
+ formatErrors() {
366
+ this.tests
367
+ .filter(t => !!t.stack)
368
+ .forEach(t => {
369
+ t.stack = this.formatStack(t);
370
+ t.message = this.adapter.formatMessage(t);
371
+ });
372
+ }
373
+
374
+ formatStack(t) {
375
+ const stack = this.adapter.formatStack(t);
376
+
377
+ const sourcePart = fetchSourceCodeFromStackTrace(stack);
378
+
379
+ if (!sourcePart) return stack;
380
+
381
+ const separator = chalk.bold.red('################[ Failure ]################');
382
+
383
+ return `${stack}\n\n${separator}\n${fetchSourceCodeFromStackTrace(stack)}`;
384
+ }
385
+
386
+ async uploadArtifacts() {
387
+ for (const test of this.tests.filter(t => !!t.stack)) {
388
+ let files = [];
389
+ if (test.files?.length) files = test.files.map(f => path.join(process.cwd(), f));
390
+ files = [...files, ...fetchFilesFromStackTrace(test.stack)];
391
+
392
+ if (!files.length) continue;
393
+
394
+ const runId = this.runId || this.store.runId || Date.now().toString();
395
+ test.artifacts = await Promise.all(files.map(f => this.uploader.uploadFileByPath(f, [runId])));
396
+ console.log(APP_PREFIX, `🗄️ Uploaded ${chalk.bold(`${files.length} artifacts`)} for test ${test.title}`);
374
397
  }
375
- async createRun() {
376
- const runParams = {
377
- api_key: this.requestParams.apiKey,
378
- title: this.requestParams.title,
379
- env: this.requestParams.env,
380
- group_title: this.requestParams.group_title,
381
- isBatchEnabled: this.requestParams.isBatchEnabled,
382
- };
383
- debug('Run', runParams);
384
- this.pipes = this.pipes || (await this.pipesPromise);
385
- const run = await Promise.all(this.pipes.map(p => p.createRun(runParams)));
386
- this.uploader.checkEnabled();
387
- return run;
388
- }
389
- async uploadData() {
390
- await this.uploadArtifacts();
391
- this.calculateStats();
392
- this.connectAdapter();
393
- this.fetchSourceCode();
394
- this.formatErrors();
395
- this.formatTests();
396
- const dataString = {
397
- ...this.stats,
398
- api_key: this.requestParams.apiKey,
399
- status: 'finished',
400
- duration: this.stats.duration,
401
- tests: this.tests,
402
- };
403
- debug('Uploading data', dataString);
404
- this.pipes = this.pipes || (await this.pipesPromise);
405
- return Promise.all(this.pipes.map(p => p.finishRun(dataString)));
406
- }
407
- async _finishRun() {
408
- this.pipes = this.pipes || (await this.pipesPromise);
409
- return Promise.all(this.pipes.map(p => p.finishRun({ status: 'finished' })));
410
- }
398
+ }
399
+
400
+ async createRun() {
401
+ const runParams = {
402
+ api_key: this.requestParams.apiKey,
403
+ title: this.requestParams.title,
404
+ env: this.requestParams.env,
405
+ group_title: this.requestParams.group_title,
406
+ isBatchEnabled: this.requestParams.isBatchEnabled,
407
+ };
408
+
409
+ debug('Run', runParams);
410
+
411
+ await Promise.all(this.pipes.map(p => p.createRun(runParams)));
412
+
413
+ this.uploader.checkEnabled();
414
+ }
415
+
416
+ async uploadData() {
417
+ await this.uploadArtifacts();
418
+ this.calculateStats();
419
+ this.connectAdapter();
420
+ this.fetchSourceCode();
421
+ this.formatErrors();
422
+ this.formatTests();
423
+
424
+ const dataString = {
425
+ ...this.stats,
426
+ detach: this.requestParams.detach,
427
+ api_key: this.requestParams.apiKey,
428
+ status: 'finished',
429
+ duration: this.stats.duration,
430
+ tests: this.tests,
431
+ };
432
+
433
+ debug('Uploading data', dataString);
434
+
435
+ return Promise.all(this.pipes.map(p => p.finishRun(dataString)));
436
+ }
437
+
438
+ async _finishRun() {
439
+ return Promise.all(this.pipes.map(p => p.finishRun({ status: 'finished' })));
440
+ }
411
441
  }
442
+
412
443
  module.exports = XmlReader;
444
+
413
445
  function reduceTestCases(prev, item) {
414
- let testCases = item.testcase;
415
- if (!testCases)
416
- testCases = item['test-case'];
417
- if (!Array.isArray(testCases)) {
418
- testCases = [testCases];
419
- }
420
- // suite inside test case
421
- const testCase = item['test-suite']?.['test-case'];
422
- if (testCase) {
423
- const nestedCases = Array.isArray(testCase) ? testCase : [testCase];
424
- testCases.push(...nestedCases);
425
- }
426
- const suiteOutput = item['system-out'] || item.output || item.log || '';
427
- const suiteErr = item['system-err'] || item.output || item.log || '';
428
- testCases
429
- .filter(t => !!t)
430
- .forEach(testCaseItem => {
431
- const file = testCaseItem.file || item.filepath || item.fullname || item.package || '';
432
- let stack = '';
433
- let message = '';
434
- if (testCaseItem.error)
435
- stack = testCaseItem.error;
436
- if (testCaseItem.failure)
437
- stack = testCaseItem.failure;
438
- if (testCaseItem?.failure?.['stack-trace'])
439
- stack = testCaseItem.failure['stack-trace'];
440
- if (testCaseItem?.failure?.message)
441
- message = testCaseItem.failure.message;
442
- if (testCaseItem?.error?.message)
443
- message = testCaseItem.error.message;
444
- if (testCaseItem.failure && testCaseItem.failure['#text'])
445
- stack = testCaseItem.failure['#text'];
446
- if (testCaseItem.error && testCaseItem.error['#text'])
447
- stack = testCaseItem.error['#text'];
448
- if (!message)
449
- message = stack.trim().split('\n')[0];
450
- const isParametrized = item.type === 'ParameterizedMethod';
451
- const preferClassname = reduceOptions.preferClassname || isParametrized;
452
- // SpecFlow config
453
- let { title, tags, testId } = fetchProperties(isParametrized ? item : testCaseItem);
454
- let example = null;
455
- const suiteTitle = preferClassname ? testCaseItem.classname : item.name || testCaseItem.classname;
456
- title ||= testCaseItem.name || testCaseItem.methodname || testCaseItem.classname;
457
- tags ||= [];
458
- const exampleMatches = testCaseItem.name?.match(/\S\((.*?)\)/);
459
- if (exampleMatches) {
460
- example = { ...exampleMatches[1].split(',').map(v => v.trim().replace(/[^\w\s-]/g, '')) };
461
- title = title.replace(/\(.*?\)/, '').trim();
462
- }
463
- stack = `${testCaseItem['system-out'] || testCaseItem.output || testCaseItem.log || ''}\n\n${stack}\n\n${suiteOutput}\n\n${suiteErr}`.trim();
464
- if (!testId)
465
- testId = (0, utils_js_1.fetchIdFromOutput)(stack);
466
- if (tags?.length && !testId) {
467
- testId = tags
468
- .filter(t => t.startsWith('T'))
469
- .map(t => `@${t}`)
470
- .find(t => t.match(utils_js_1.TEST_ID_REGEX))
471
- ?.slice(2);
472
- }
473
- let status = constants_js_1.STATUS.PASSED.toString();
474
- if ('failure' in testCaseItem || 'error' in testCaseItem)
475
- status = constants_js_1.STATUS.FAILED;
476
- if ('skipped' in testCaseItem)
477
- status = constants_js_1.STATUS.SKIPPED;
478
- if (testCaseItem.result && Object.values(constants_js_1.STATUS).includes(testCaseItem.result.toLowerCase())) {
479
- status = testCaseItem.result.toLowerCase();
480
- }
481
- let rid = null;
482
- if (testCaseItem.id)
483
- rid = `${ridRunId}-${testCaseItem.id}`;
484
- // Extract attachments
485
- let files = [];
486
- if (testCaseItem.attachments) {
487
- const attachments = Array.isArray(testCaseItem.attachments.attachment)
488
- ? testCaseItem.attachments.attachment
489
- : [testCaseItem.attachments.attachment];
490
- files = attachments.filter(a => a && a.filePath).map(a => a.filePath);
491
- }
492
- // Extract files from stack trace using existing utility
493
- const stackFiles = (0, utils_js_1.fetchFilesFromStackTrace)(stack);
494
- files = [...new Set([...files, ...stackFiles])]; // Remove duplicates
495
- prev.push({
496
- rid,
497
- file,
498
- stack,
499
- example,
500
- tags,
501
- create: true,
502
- test_id: testId,
503
- message,
504
- line: testCaseItem.lineno,
505
- // seconds are used in junit reports, but ms are used by testomatio
506
- run_time: parseFloat(testCaseItem.time || testCaseItem.duration) * 1000,
507
- status,
508
- title,
509
- root_suite_id: TESTOMATIO_SUITE,
510
- suite_title: suiteTitle,
511
- files,
512
- });
446
+ let testCases = item.testcase;
447
+ if (!testCases) testCases = item['test-case'];
448
+ if (!Array.isArray(testCases)) {
449
+ testCases = [testCases];
450
+ }
451
+
452
+ // suite inside test case
453
+ const testCase = item['test-suite']?.['test-case'];
454
+ if (testCase) {
455
+ const nestedCases = Array.isArray(testCase) ? testCase : [testCase];
456
+ testCases.push(...nestedCases);
457
+ }
458
+
459
+ const suiteOutput = item['system-out'] || item.output || item.log || '';
460
+ const suiteErr = item['system-err'] || item.output || item.log || '';
461
+ testCases
462
+ .filter(t => !!t)
463
+ .forEach(testCaseItem => {
464
+ const file = testCaseItem.file || item.filepath || '';
465
+
466
+ let stack = '';
467
+ let message = '';
468
+ if (testCaseItem.error) stack = testCaseItem.error;
469
+ if (testCaseItem.failure) stack = testCaseItem.failure;
470
+ if (testCaseItem?.failure?.['stack-trace']) stack = testCaseItem.failure['stack-trace'];
471
+ if (testCaseItem?.failure?.message) message = testCaseItem.failure.message;
472
+ if (testCaseItem?.error?.message) message = testCaseItem.error.message;
473
+
474
+ if (testCaseItem.failure && testCaseItem.failure['#text']) stack = testCaseItem.failure['#text'];
475
+ if (testCaseItem.error && testCaseItem.error['#text']) stack = testCaseItem.error['#text'];
476
+ if (!message) message = stack.trim().split('\n')[0];
477
+
478
+ const isParametrized = item.type === 'ParameterizedMethod';
479
+ const preferClassname = reduceOptions.preferClassname || isParametrized;
480
+
481
+ // SpecFlow config
482
+ let { title, tags } = fetchProperties(isParametrized ? item : testCaseItem);
483
+ let example = null;
484
+ const suiteTitle = preferClassname ? testCaseItem.classname : item.name || testCaseItem.classname;
485
+
486
+ title ||= testCaseItem.name || testCaseItem.methodname || testCaseItem.classname;
487
+ tags ||= [];
488
+
489
+ const exampleMatches = testCaseItem.name?.match(/\S\((.*?)\)/);
490
+ if (exampleMatches) {
491
+ example = { ...exampleMatches[1].split(',').map(v => v.trim().replace(/[^\w\s-]/g, '')) };
492
+ title = title.replace(/\(.*?\)/, '').trim();
493
+ }
494
+
495
+ // eslint-disable-next-line
496
+ stack = `${
497
+ testCaseItem['system-out'] || testCaseItem.output || testCaseItem.log || ''
498
+ }\n\n${stack}\n\n${suiteOutput}\n\n${suiteErr}`.trim();
499
+ const testId = fetchIdFromOutput(stack);
500
+
501
+ let status = STATUS.PASSED.toString();
502
+ if ('failure' in testCaseItem || 'error' in testCaseItem) status = STATUS.FAILED;
503
+ if ('skipped' in testCaseItem) status = STATUS.SKIPPED;
504
+
505
+ let rid = null;
506
+ if (testCaseItem.id) rid = `${ridRunId}-${testCaseItem.id}`;
507
+
508
+ prev.push({
509
+ rid,
510
+ file,
511
+ stack,
512
+ example,
513
+ tags,
514
+ create: true,
515
+ test_id: testId,
516
+ message,
517
+ line: testCaseItem.lineno,
518
+ // seconds are used in junit reports, but ms are used by testomatio
519
+ run_time: parseFloat(testCaseItem.time || testCaseItem.duration) * 1000,
520
+ status,
521
+ title,
522
+ suite_title: suiteTitle,
523
+ });
513
524
  });
514
- return prev;
525
+ return prev;
515
526
  }
527
+
516
528
  function processTestSuite(testsuite) {
517
- if (!testsuite)
518
- return [];
519
- if (testsuite.testsuite)
520
- return processTestSuite(testsuite.testsuite);
521
- if (testsuite['test-suite'] && !testsuite['test-case'])
522
- return processTestSuite(testsuite['test-suite']);
523
- let suites = testsuite;
524
- if (!Array.isArray(testsuite)) {
525
- suites = [testsuite];
526
- }
527
- const subSuites = suites.filter(s => s['test-suite'] && !testsuite['test-case']);
528
- return [...subSuites.map(s => processTestSuite(s['test-suite'])), ...suites.reduce(reduceTestCases, [])].flat();
529
+ if (!testsuite) return [];
530
+ if (testsuite.testsuite) return processTestSuite(testsuite.testsuite);
531
+ if (testsuite['test-suite'] && !testsuite['test-case']) return processTestSuite(testsuite['test-suite']);
532
+
533
+ let suites = testsuite;
534
+ if (!Array.isArray(testsuite)) {
535
+ suites = [testsuite];
536
+ }
537
+
538
+ const subSuites = suites.filter(s => s['test-suite'] && !testsuite['test-case']);
539
+
540
+ return [...subSuites.map(s => processTestSuite(s['test-suite'])), ...suites.reduce(reduceTestCases, [])].flat();
529
541
  }
542
+
530
543
  function fetchProperties(item) {
531
- const tags = [];
532
- let title = '';
533
- if (!item.properties)
534
- return {};
535
- // Handle both single property and array of properties
536
- const properties = Array.isArray(item.properties.property)
537
- ? item.properties.property
538
- : [item.properties.property].filter(Boolean);
539
- const prop = properties.find(p => p.name === 'Description');
540
- if (prop)
541
- title = prop.value;
542
- let testId = properties.find(p => p.name === 'ID')?.value;
543
- if (testId?.startsWith('@'))
544
- testId = testId.slice(1);
545
- if (testId?.startsWith('T'))
546
- testId = testId.slice(1);
547
- properties.filter(p => p.name === 'Category').forEach(p => tags.push(p.value));
548
- return { title, tags, testId };
544
+ const tags = [];
545
+ let title = '';
546
+
547
+ if (!item.properties) return {};
548
+
549
+ const prop = [item.properties?.property].flat().find(p => p.name === 'Description');
550
+ if (prop) title = prop.value;
551
+ [item.properties?.property]
552
+ .flat()
553
+ .filter(p => p.name === 'Category')
554
+ .forEach(p => tags.push(p.value));
555
+ return { title, tags };
549
556
  }