@testomatio/reporter 2.3.6 → 2.3.7-beta.2-xml-import
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 +1 -1
- package/lib/bin/cli.js +1 -1
- package/lib/bin/reportXml.js +5 -2
- package/lib/bin/startTest.js +3 -3
- package/lib/client.js +7 -3
- package/lib/junit-adapter/csharp.d.ts +0 -1
- package/lib/junit-adapter/csharp.js +40 -7
- package/lib/junit-adapter/nunit-parser.d.ts +82 -0
- package/lib/junit-adapter/nunit-parser.js +369 -0
- package/lib/pipe/debug.js +1 -1
- package/lib/pipe/testomatio.js +19 -15
- package/lib/template/testomatio.hbs +1366 -1026
- package/lib/uploader.js +4 -0
- package/lib/utils/utils.js +90 -11
- package/lib/xmlReader.d.ts +32 -26
- package/lib/xmlReader.js +106 -50
- package/package.json +1 -1
- package/src/bin/cli.js +1 -1
- package/src/bin/reportXml.js +5 -2
- package/src/bin/startTest.js +5 -5
- package/src/client.js +7 -4
- package/src/junit-adapter/csharp.js +45 -6
- package/src/junit-adapter/nunit-parser.js +404 -0
- package/src/pipe/debug.js +2 -3
- package/src/pipe/testomatio.js +75 -81
- package/src/template/testomatio.hbs +1366 -1026
- package/src/uploader.js +5 -0
- package/src/utils/utils.js +96 -9
- package/src/xmlReader.js +128 -45
package/lib/uploader.js
CHANGED
|
@@ -170,6 +170,10 @@ class S3Uploader {
|
|
|
170
170
|
if (typeof filePath === 'string' && !path_1.default.isAbsolute(filePath)) {
|
|
171
171
|
filePath = path_1.default.join(process.cwd(), filePath);
|
|
172
172
|
}
|
|
173
|
+
// Normalize path separators for cross-platform compatibility
|
|
174
|
+
if (typeof filePath === 'string') {
|
|
175
|
+
filePath = filePath.replace(/\\/g, '/');
|
|
176
|
+
}
|
|
173
177
|
const data = { rid, file: filePath, uploaded };
|
|
174
178
|
const jsonLine = `${JSON.stringify(data)}\n`;
|
|
175
179
|
fs_1.default.appendFileSync(tempFilePath, jsonLine);
|
package/lib/utils/utils.js
CHANGED
|
@@ -122,12 +122,8 @@ const fetchFilesFromStackTrace = (stack = '', checkExists = true) => {
|
|
|
122
122
|
.map(f => f[1].trim())
|
|
123
123
|
.map(f => f.replace(/^\/+/, '/').replace(/^\/([A-Za-z]:)/, '$1')) // Remove extra slashes, handle Windows paths
|
|
124
124
|
.map(f => {
|
|
125
|
-
//
|
|
126
|
-
|
|
127
|
-
// Convert Windows path to Linux equivalent for test scenarios
|
|
128
|
-
return f.replace(/^[A-Za-z]:[\\\/]/, '/').replace(/\\/g, '/');
|
|
129
|
-
}
|
|
130
|
-
return f;
|
|
125
|
+
// Normalize path separators for cross-platform compatibility
|
|
126
|
+
return f.replace(/\\/g, '/');
|
|
131
127
|
});
|
|
132
128
|
debug('Found files in stack trace: ', files);
|
|
133
129
|
return files.filter(f => {
|
|
@@ -174,6 +170,8 @@ exports.fetchSourceCodeFromStackTrace = fetchSourceCodeFromStackTrace;
|
|
|
174
170
|
exports.TEST_ID_REGEX = /@T([\w\d]{8})/;
|
|
175
171
|
exports.SUITE_ID_REGEX = /@S([\w\d]{8})/;
|
|
176
172
|
const fetchIdFromCode = (code, opts = {}) => {
|
|
173
|
+
if (!code)
|
|
174
|
+
return null;
|
|
177
175
|
const comments = code
|
|
178
176
|
.split('\n')
|
|
179
177
|
.map(l => l.trim())
|
|
@@ -216,10 +214,58 @@ const fetchSourceCode = (contents, opts = {}) => {
|
|
|
216
214
|
lineIndex = lines.findIndex(l => l.includes(`${title}(`));
|
|
217
215
|
}
|
|
218
216
|
else if (opts.lang === 'csharp') {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
if (
|
|
222
|
-
|
|
217
|
+
// Find the method declaration line
|
|
218
|
+
let methodLineIndex = lines.findIndex(l => l.includes(`public void ${title}(`));
|
|
219
|
+
if (methodLineIndex === -1) {
|
|
220
|
+
methodLineIndex = lines.findIndex(l => l.includes(`public async Task ${title}(`));
|
|
221
|
+
}
|
|
222
|
+
if (methodLineIndex === -1) {
|
|
223
|
+
methodLineIndex = lines.findIndex(l => l.includes(`${title}(`));
|
|
224
|
+
}
|
|
225
|
+
// If found, scan upwards to find [TestCase], [Test] attributes and XML comments
|
|
226
|
+
if (methodLineIndex !== -1) {
|
|
227
|
+
lineIndex = methodLineIndex;
|
|
228
|
+
// Scan upwards to find the start of attributes and comments
|
|
229
|
+
for (let i = methodLineIndex - 1; i >= 0; i--) {
|
|
230
|
+
const trimmedLine = lines[i].trim();
|
|
231
|
+
// Include [TestCase], [Test], and other attributes
|
|
232
|
+
if (trimmedLine.startsWith('[')) {
|
|
233
|
+
lineIndex = i;
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
// Include XML documentation comments
|
|
237
|
+
if (trimmedLine.startsWith('///')) {
|
|
238
|
+
lineIndex = i;
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
// Stop at empty lines (with some tolerance)
|
|
242
|
+
if (trimmedLine === '') {
|
|
243
|
+
// Check if next non-empty line is an attribute or comment
|
|
244
|
+
let hasMoreAttributes = false;
|
|
245
|
+
for (let j = i - 1; j >= 0; j--) {
|
|
246
|
+
const nextTrimmed = lines[j].trim();
|
|
247
|
+
if (nextTrimmed === '')
|
|
248
|
+
continue;
|
|
249
|
+
if (nextTrimmed.startsWith('[') || nextTrimmed.startsWith('///')) {
|
|
250
|
+
hasMoreAttributes = true;
|
|
251
|
+
lineIndex = j;
|
|
252
|
+
}
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
if (!hasMoreAttributes)
|
|
256
|
+
break;
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
// Stop at other method declarations or class-level elements
|
|
260
|
+
if (trimmedLine.includes('public ') ||
|
|
261
|
+
trimmedLine.includes('private ') ||
|
|
262
|
+
trimmedLine.includes('protected ') ||
|
|
263
|
+
trimmedLine.includes('internal ')) {
|
|
264
|
+
if (!trimmedLine.startsWith('['))
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
223
269
|
}
|
|
224
270
|
else {
|
|
225
271
|
lineIndex = lines.findIndex(l => l.includes(title));
|
|
@@ -228,11 +274,28 @@ const fetchSourceCode = (contents, opts = {}) => {
|
|
|
228
274
|
if (opts.prepend) {
|
|
229
275
|
lineIndex -= opts.prepend;
|
|
230
276
|
}
|
|
231
|
-
if (lineIndex) {
|
|
277
|
+
if (lineIndex !== -1 && lineIndex !== undefined) {
|
|
232
278
|
const result = [];
|
|
279
|
+
let braceDepth = 0; // Track brace depth for C# methods
|
|
280
|
+
let methodStartFound = false; // Flag to indicate we've found the method opening brace
|
|
233
281
|
for (let i = lineIndex; i < lineIndex + limit; i++) {
|
|
234
282
|
if (lines[i] === undefined)
|
|
235
283
|
continue;
|
|
284
|
+
// Track brace depth for C# to stop after method closes
|
|
285
|
+
if (opts.lang === 'csharp') {
|
|
286
|
+
const line = lines[i];
|
|
287
|
+
// Count opening and closing braces
|
|
288
|
+
const openBraces = (line.match(/\{/g) || []).length;
|
|
289
|
+
const closeBraces = (line.match(/\}/g) || []).length;
|
|
290
|
+
if (openBraces > 0)
|
|
291
|
+
methodStartFound = true;
|
|
292
|
+
braceDepth += openBraces - closeBraces;
|
|
293
|
+
// If we've started the method and depth returns to 0, method is complete
|
|
294
|
+
if (methodStartFound && braceDepth === 0 && closeBraces > 0) {
|
|
295
|
+
result.push(lines[i]);
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
236
299
|
if (i > lineIndex + 2 && !opts.prepend) {
|
|
237
300
|
// annotation
|
|
238
301
|
if (opts.lang === 'php' && lines[i].trim().startsWith('#['))
|
|
@@ -271,6 +334,22 @@ const fetchSourceCode = (contents, opts = {}) => {
|
|
|
271
334
|
break;
|
|
272
335
|
if (opts.lang === 'java' && lines[i].includes(' class '))
|
|
273
336
|
break;
|
|
337
|
+
// For C#, additional checks if brace tracking didn't stop us
|
|
338
|
+
if (opts.lang === 'csharp') {
|
|
339
|
+
const trimmed = lines[i].trim();
|
|
340
|
+
// Stop at attribute that marks beginning of next test
|
|
341
|
+
if (trimmed.match(/^\[(Test|TestCase|Theory|Fact)/))
|
|
342
|
+
break;
|
|
343
|
+
// Stop at XML documentation comments that belong to next method
|
|
344
|
+
if (trimmed.startsWith('///'))
|
|
345
|
+
break;
|
|
346
|
+
// Stop at another method declaration
|
|
347
|
+
if (trimmed.match(/^\s*(public|private|protected|internal)\s+(\w+|async\s+\w+)\s+\w+\s*\(/))
|
|
348
|
+
break;
|
|
349
|
+
// Stop at class declaration
|
|
350
|
+
if (trimmed.includes(' class ') && trimmed.includes('public'))
|
|
351
|
+
break;
|
|
352
|
+
}
|
|
274
353
|
}
|
|
275
354
|
result.push(lines[i]);
|
|
276
355
|
}
|
package/lib/xmlReader.d.ts
CHANGED
|
@@ -19,25 +19,11 @@ declare class XmlReader {
|
|
|
19
19
|
tests: any[];
|
|
20
20
|
stats: {};
|
|
21
21
|
uploader: S3Uploader;
|
|
22
|
+
enhancedNunit: boolean;
|
|
23
|
+
groupParameterized: boolean;
|
|
22
24
|
version: any;
|
|
23
25
|
connectAdapter(): import("./junit-adapter/adapter.js").default;
|
|
24
|
-
parse(fileName: any):
|
|
25
|
-
status: string;
|
|
26
|
-
create_tests: boolean;
|
|
27
|
-
tests_count: number;
|
|
28
|
-
passed_count: number;
|
|
29
|
-
skipped_count: number;
|
|
30
|
-
failed_count: number;
|
|
31
|
-
tests: any;
|
|
32
|
-
} | {
|
|
33
|
-
status: any;
|
|
34
|
-
create_tests: boolean;
|
|
35
|
-
tests_count: number;
|
|
36
|
-
passed_count: number;
|
|
37
|
-
failed_count: number;
|
|
38
|
-
skipped_count: number;
|
|
39
|
-
tests: any[];
|
|
40
|
-
};
|
|
26
|
+
parse(fileName: any): any;
|
|
41
27
|
processJUnit(jsonSuite: any): {
|
|
42
28
|
create_tests: boolean;
|
|
43
29
|
duration: number;
|
|
@@ -49,15 +35,14 @@ declare class XmlReader {
|
|
|
49
35
|
tests: any[];
|
|
50
36
|
tests_count: number;
|
|
51
37
|
};
|
|
52
|
-
processNUnit(jsonSuite: any):
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
};
|
|
38
|
+
processNUnit(jsonSuite: any): any;
|
|
39
|
+
/**
|
|
40
|
+
* Check if the XML is actually NUnit format (has test-suite hierarchy)
|
|
41
|
+
* @param {Object} jsonSuite - Parsed XML suite object
|
|
42
|
+
* @returns {boolean} - True if this is NUnit XML format
|
|
43
|
+
*/
|
|
44
|
+
isNUnitXml(jsonSuite: any): boolean;
|
|
45
|
+
processNUnitEnhanced(jsonSuite: any): any;
|
|
61
46
|
processTRX(jsonSuite: any): {
|
|
62
47
|
status: string;
|
|
63
48
|
create_tests: boolean;
|
|
@@ -67,6 +52,27 @@ declare class XmlReader {
|
|
|
67
52
|
failed_count: number;
|
|
68
53
|
tests: any;
|
|
69
54
|
};
|
|
55
|
+
_parseTRXTestDefinition(td: any): {
|
|
56
|
+
title: any;
|
|
57
|
+
example: any;
|
|
58
|
+
file: string;
|
|
59
|
+
description: any;
|
|
60
|
+
suite_title: any;
|
|
61
|
+
id: any;
|
|
62
|
+
};
|
|
63
|
+
_parseTRXTestResult(td: any, tests: any): {
|
|
64
|
+
suite_title: any;
|
|
65
|
+
title: any;
|
|
66
|
+
file: any;
|
|
67
|
+
description: any;
|
|
68
|
+
code: any;
|
|
69
|
+
run_time: number;
|
|
70
|
+
stack: any;
|
|
71
|
+
files: any;
|
|
72
|
+
create: boolean;
|
|
73
|
+
overwrite: boolean;
|
|
74
|
+
};
|
|
75
|
+
_mapTRXStatus(outcome: any): string;
|
|
70
76
|
processXUnit(assemblies: any): {
|
|
71
77
|
status: string;
|
|
72
78
|
create_tests: boolean;
|
package/lib/xmlReader.js
CHANGED
|
@@ -11,6 +11,7 @@ const fast_xml_parser_1 = require("fast-xml-parser");
|
|
|
11
11
|
const constants_js_1 = require("./constants.js");
|
|
12
12
|
const crypto_1 = require("crypto");
|
|
13
13
|
const url_1 = require("url");
|
|
14
|
+
const nunit_parser_js_1 = require("./junit-adapter/nunit-parser.js");
|
|
14
15
|
const utils_js_1 = require("./utils/utils.js");
|
|
15
16
|
const index_js_1 = require("./pipe/index.js");
|
|
16
17
|
const index_js_2 = __importDefault(require("./junit-adapter/index.js"));
|
|
@@ -54,6 +55,9 @@ class XmlReader {
|
|
|
54
55
|
this.stats = {};
|
|
55
56
|
this.stats.language = opts.lang?.toLowerCase();
|
|
56
57
|
this.uploader = new uploader_js_1.S3Uploader();
|
|
58
|
+
// Enhanced NUnit parsing - enabled by default for NUnit XML
|
|
59
|
+
this.enhancedNunit = opts.enhancedNunit !== false; // Default true, can be disabled
|
|
60
|
+
this.groupParameterized = opts.groupParameterized !== false; // Default true, can be disabled
|
|
57
61
|
// @ts-ignore
|
|
58
62
|
const packageJsonPath = path_1.default.resolve(__dirname, '..', 'package.json');
|
|
59
63
|
this.version = JSON.parse(fs_1.default.readFileSync(packageJsonPath).toString()).version;
|
|
@@ -102,7 +106,8 @@ class XmlReader {
|
|
|
102
106
|
return this.processJUnit(jsonSuite);
|
|
103
107
|
}
|
|
104
108
|
processJUnit(jsonSuite) {
|
|
105
|
-
const { testsuite, name,
|
|
109
|
+
const { testsuite, name, failures, errors } = jsonSuite;
|
|
110
|
+
const tests = testsuite?.tests || jsonSuite.tests;
|
|
106
111
|
reduceOptions.preferClassname = this.stats.language === 'python';
|
|
107
112
|
const resultTests = processTestSuite(testsuite);
|
|
108
113
|
const hasFailures = resultTests.filter(t => t.status === 'failed').length > 0;
|
|
@@ -128,6 +133,13 @@ class XmlReader {
|
|
|
128
133
|
};
|
|
129
134
|
}
|
|
130
135
|
processNUnit(jsonSuite) {
|
|
136
|
+
// Use enhanced NUnit parser if enabled and this is actually NUnit XML
|
|
137
|
+
if (this.enhancedNunit && this.isNUnitXml(jsonSuite)) {
|
|
138
|
+
debug('Using enhanced NUnit parser');
|
|
139
|
+
return this.processNUnitEnhanced(jsonSuite);
|
|
140
|
+
}
|
|
141
|
+
// Fallback to legacy parser for backward compatibility
|
|
142
|
+
debug('Using legacy NUnit parser');
|
|
131
143
|
const { result, total, passed, failed, inconclusive, skipped } = jsonSuite;
|
|
132
144
|
reduceOptions.preferClassname = this.stats.language === 'python';
|
|
133
145
|
const resultTests = processTestSuite(jsonSuite['test-suite']);
|
|
@@ -142,65 +154,58 @@ class XmlReader {
|
|
|
142
154
|
tests: resultTests,
|
|
143
155
|
};
|
|
144
156
|
}
|
|
157
|
+
/**
|
|
158
|
+
* Check if the XML is actually NUnit format (has test-suite hierarchy)
|
|
159
|
+
* @param {Object} jsonSuite - Parsed XML suite object
|
|
160
|
+
* @returns {boolean} - True if this is NUnit XML format
|
|
161
|
+
*/
|
|
162
|
+
isNUnitXml(jsonSuite) {
|
|
163
|
+
// NUnit XML has test-suite elements with type attributes
|
|
164
|
+
if (jsonSuite['test-suite']) {
|
|
165
|
+
const testSuite = Array.isArray(jsonSuite['test-suite']) ? jsonSuite['test-suite'][0] : jsonSuite['test-suite'];
|
|
166
|
+
// Check for NUnit-specific test-suite types
|
|
167
|
+
return (testSuite &&
|
|
168
|
+
testSuite.type &&
|
|
169
|
+
['Assembly', 'TestSuite', 'TestFixture', 'ParameterizedMethod'].includes(testSuite.type));
|
|
170
|
+
}
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
processNUnitEnhanced(jsonSuite) {
|
|
174
|
+
debug('Processing NUnit XML with enhanced parser');
|
|
175
|
+
try {
|
|
176
|
+
const nunitParser = new nunit_parser_js_1.NUnitXmlParser({
|
|
177
|
+
groupParameterized: this.groupParameterized,
|
|
178
|
+
...this.opts,
|
|
179
|
+
});
|
|
180
|
+
const result = nunitParser.parseTestRun(jsonSuite);
|
|
181
|
+
// Add parsed tests to our collection
|
|
182
|
+
this.tests = this.tests.concat(result.tests);
|
|
183
|
+
debug(`Enhanced NUnit parser processed ${result.tests.length} tests`);
|
|
184
|
+
return result;
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
debug('Enhanced NUnit parser failed, falling back to legacy parser:', error.message);
|
|
188
|
+
console.warn(`${constants_js_1.APP_PREFIX} Enhanced NUnit parsing failed, using legacy parser: ${error.message}`);
|
|
189
|
+
// Fallback to legacy parser
|
|
190
|
+
this.enhancedNunit = false;
|
|
191
|
+
return this.processNUnit(jsonSuite);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
145
194
|
processTRX(jsonSuite) {
|
|
146
195
|
let defs = jsonSuite?.TestRun?.TestDefinitions?.UnitTest;
|
|
147
196
|
if (!Array.isArray(defs))
|
|
148
197
|
defs = [defs].filter(d => !!d);
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
}) || [];
|
|
198
|
+
// Parse test definitions
|
|
199
|
+
const tests = defs.map(td => this._parseTRXTestDefinition(td));
|
|
200
|
+
// Parse test results
|
|
165
201
|
let result = jsonSuite?.TestRun?.Results?.UnitTestResult;
|
|
166
202
|
if (!Array.isArray(result))
|
|
167
203
|
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
|
-
r.overwrite = true;
|
|
190
|
-
if (r.status === 'Passed')
|
|
191
|
-
r.status = constants_js_1.STATUS.PASSED;
|
|
192
|
-
if (r.status === 'Failed')
|
|
193
|
-
r.status = constants_js_1.STATUS.FAILED;
|
|
194
|
-
if (r.status === 'Skipped')
|
|
195
|
-
r.status = constants_js_1.STATUS.SKIPPED;
|
|
196
|
-
delete r.id;
|
|
197
|
-
});
|
|
204
|
+
const results = result.map(td => this._parseTRXTestResult(td, tests));
|
|
198
205
|
debug(results);
|
|
199
206
|
const counters = jsonSuite?.TestRun?.ResultSummary?.Counters || {};
|
|
200
207
|
const failed_count = parseInt(counters.failed, 10) + parseInt(counters.error, 10);
|
|
201
|
-
|
|
202
|
-
if (failed_count > 0)
|
|
203
|
-
status = constants_js_1.STATUS.FAILED;
|
|
208
|
+
const status = failed_count > 0 ? constants_js_1.STATUS.FAILED : constants_js_1.STATUS.PASSED.toString();
|
|
204
209
|
this.tests = results.filter(t => !!t.title);
|
|
205
210
|
return {
|
|
206
211
|
status,
|
|
@@ -212,6 +217,57 @@ class XmlReader {
|
|
|
212
217
|
tests: results,
|
|
213
218
|
};
|
|
214
219
|
}
|
|
220
|
+
_parseTRXTestDefinition(td) {
|
|
221
|
+
const title = td.name.replace(/\(.*?\)/, '').trim();
|
|
222
|
+
const exampleMatch = td.name.match(/\((.*?)\)/);
|
|
223
|
+
const example = exampleMatch ? { ...exampleMatch[1].split(',') } : null;
|
|
224
|
+
const suite = td.TestMethod.className.split(', ')[0].split('.');
|
|
225
|
+
const suite_title = suite.pop();
|
|
226
|
+
// Convert namespace to file path for C#
|
|
227
|
+
const file = `${suite.join('/')}.cs`;
|
|
228
|
+
return {
|
|
229
|
+
title, // Base name without parameters for test import
|
|
230
|
+
example, // Parameters object for parameterized tests
|
|
231
|
+
file, // File path with .cs extension
|
|
232
|
+
description: td.Description,
|
|
233
|
+
suite_title,
|
|
234
|
+
id: td.Execution.id,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
_parseTRXTestResult(td, tests) {
|
|
238
|
+
const test = tests.find(t => t.id === td.executionId) || {};
|
|
239
|
+
const result = {
|
|
240
|
+
suite_title: test.suite_title,
|
|
241
|
+
title: test.title?.trim(),
|
|
242
|
+
file: test.file,
|
|
243
|
+
description: test.description,
|
|
244
|
+
code: test.code,
|
|
245
|
+
run_time: parseFloat(td.duration) * 1000,
|
|
246
|
+
stack: td.Output?.StdOut || '',
|
|
247
|
+
files: td?.ResultFiles?.ResultFile?.map(rf => rf.path),
|
|
248
|
+
create: true,
|
|
249
|
+
overwrite: true,
|
|
250
|
+
};
|
|
251
|
+
// Add example for parameterized tests
|
|
252
|
+
if (test.example) {
|
|
253
|
+
result.example = test.example;
|
|
254
|
+
}
|
|
255
|
+
// Map TRX status to Testomat.io status
|
|
256
|
+
result.status = this._mapTRXStatus(td.outcome);
|
|
257
|
+
return result;
|
|
258
|
+
}
|
|
259
|
+
_mapTRXStatus(outcome) {
|
|
260
|
+
switch (outcome) {
|
|
261
|
+
case 'Passed':
|
|
262
|
+
return constants_js_1.STATUS.PASSED;
|
|
263
|
+
case 'Failed':
|
|
264
|
+
return constants_js_1.STATUS.FAILED;
|
|
265
|
+
case 'Skipped':
|
|
266
|
+
return constants_js_1.STATUS.SKIPPED;
|
|
267
|
+
default:
|
|
268
|
+
return constants_js_1.STATUS.PASSED;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
215
271
|
processXUnit(assemblies) {
|
|
216
272
|
const tests = [];
|
|
217
273
|
assemblies = Array.isArray(assemblies.assembly) ? assemblies.assembly : [assemblies.assembly];
|
package/package.json
CHANGED
package/src/bin/cli.js
CHANGED
|
@@ -158,7 +158,7 @@ program
|
|
|
158
158
|
.option('--lang <lang>', 'Language used (python, ruby, java)')
|
|
159
159
|
.option('--timelimit <time>', 'default time limit in seconds to kill a stuck process')
|
|
160
160
|
.action(async (pattern, opts) => {
|
|
161
|
-
if (!pattern.endsWith('.xml')) {
|
|
161
|
+
if (!pattern.endsWith('.xml') && !pattern.includes('*')) {
|
|
162
162
|
pattern += '.xml';
|
|
163
163
|
}
|
|
164
164
|
let { javaTests, lang } = opts;
|
package/src/bin/reportXml.js
CHANGED
|
@@ -23,7 +23,7 @@ program
|
|
|
23
23
|
.option('--timelimit <time>', 'default time limit in seconds to kill a stuck process')
|
|
24
24
|
.option('--env-file <envfile>', 'Load environment variables from env file')
|
|
25
25
|
.action(async (pattern, opts) => {
|
|
26
|
-
if (!pattern.endsWith('.xml')) {
|
|
26
|
+
if (!pattern.endsWith('.xml') && !pattern.includes('*')) {
|
|
27
27
|
pattern += '.xml';
|
|
28
28
|
}
|
|
29
29
|
let { javaTests, lang } = opts;
|
|
@@ -34,7 +34,10 @@ program
|
|
|
34
34
|
}
|
|
35
35
|
lang = lang?.toLowerCase();
|
|
36
36
|
if (javaTests === true || (lang === 'java' && !javaTests)) javaTests = 'src/test/java';
|
|
37
|
-
const runReader = new XmlReader({
|
|
37
|
+
const runReader = new XmlReader({
|
|
38
|
+
javaTests,
|
|
39
|
+
lang,
|
|
40
|
+
});
|
|
38
41
|
const files = glob.sync(pattern, { cwd: opts.dir || process.cwd() });
|
|
39
42
|
if (!files.length) {
|
|
40
43
|
console.log(APP_PREFIX, `Report can't be created. No XML files found 😥`);
|
package/src/bin/startTest.js
CHANGED
|
@@ -18,7 +18,7 @@ const newArgs = ['run'];
|
|
|
18
18
|
let i = 0;
|
|
19
19
|
while (i < args.length) {
|
|
20
20
|
const arg = args[i];
|
|
21
|
-
|
|
21
|
+
|
|
22
22
|
if (arg === '-c' || arg === '--command') {
|
|
23
23
|
// Map -c/--command to positional argument for run command
|
|
24
24
|
i++;
|
|
@@ -33,7 +33,7 @@ while (i < args.length) {
|
|
|
33
33
|
// Map --launch to start command
|
|
34
34
|
newArgs[0] = 'start';
|
|
35
35
|
} else if (arg === '--finish') {
|
|
36
|
-
// Map --finish to finish command
|
|
36
|
+
// Map --finish to finish command
|
|
37
37
|
newArgs[0] = 'finish';
|
|
38
38
|
} else {
|
|
39
39
|
// Pass through other arguments
|
|
@@ -45,9 +45,9 @@ while (i < args.length) {
|
|
|
45
45
|
// Execute the main CLI with mapped arguments
|
|
46
46
|
|
|
47
47
|
const child = spawn(process.execPath, [cliPath, ...newArgs], {
|
|
48
|
-
stdio: 'inherit'
|
|
48
|
+
stdio: 'inherit',
|
|
49
49
|
});
|
|
50
50
|
|
|
51
|
-
child.on('exit',
|
|
51
|
+
child.on('exit', code => {
|
|
52
52
|
process.exit(code);
|
|
53
|
-
});
|
|
53
|
+
});
|
package/src/client.js
CHANGED
|
@@ -37,9 +37,8 @@ class Client {
|
|
|
37
37
|
this.runId = '';
|
|
38
38
|
this.queue = Promise.resolve();
|
|
39
39
|
|
|
40
|
-
//
|
|
41
|
-
const
|
|
42
|
-
const pathToPackageJSON = path.join(__dirname, '../package.json');
|
|
40
|
+
// Get package.json path - use a simple approach that works in both environments
|
|
41
|
+
const pathToPackageJSON = path.join(process.cwd(), 'package.json');
|
|
43
42
|
try {
|
|
44
43
|
this.version = JSON.parse(fs.readFileSync(pathToPackageJSON).toString()).version;
|
|
45
44
|
console.log(APP_PREFIX, `Testomatio Reporter v${this.version}`);
|
|
@@ -387,7 +386,11 @@ class Client {
|
|
|
387
386
|
*/
|
|
388
387
|
formatLogs({ error, steps, logs }) {
|
|
389
388
|
error = error?.trim();
|
|
390
|
-
logs = logs
|
|
389
|
+
logs = logs
|
|
390
|
+
?.trim()
|
|
391
|
+
.split('\n')
|
|
392
|
+
.map(l => truncate(l))
|
|
393
|
+
.join('\n');
|
|
391
394
|
|
|
392
395
|
if (Array.isArray(steps)) {
|
|
393
396
|
steps = steps
|
|
@@ -3,18 +3,50 @@ import Adapter from './adapter.js';
|
|
|
3
3
|
|
|
4
4
|
class CSharpAdapter extends Adapter {
|
|
5
5
|
formatTest(t) {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
// Extract example from title if not already present
|
|
7
|
+
if (!t.example) {
|
|
8
|
+
const exampleMatch = t.title.match(/\((.*?)\)/);
|
|
9
|
+
if (exampleMatch) {
|
|
10
|
+
// Extract parameters as object with numeric keys for API
|
|
11
|
+
const params = exampleMatch[1].split(',').map(param => param.trim());
|
|
12
|
+
t.example = {};
|
|
13
|
+
params.forEach((param, index) => {
|
|
14
|
+
t.example[index] = param;
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// For runs: keep full title with parameters for display
|
|
20
|
+
// The example field will be used for grouping on import
|
|
21
|
+
// Do NOT remove parameters from title
|
|
22
|
+
|
|
9
23
|
const suite = t.suite_title.split('.');
|
|
10
24
|
t.suite_title = suite.pop();
|
|
11
25
|
t.file = namespaceToFileName(t.file);
|
|
12
|
-
t.title = title.trim();
|
|
13
26
|
return t;
|
|
14
27
|
}
|
|
15
28
|
|
|
16
29
|
getFilePath(t) {
|
|
17
|
-
|
|
30
|
+
if (!t.file) return null;
|
|
31
|
+
|
|
32
|
+
// Normalize path separators for cross-platform compatibility
|
|
33
|
+
let filePath = t.file.replace(/\\/g, '/');
|
|
34
|
+
|
|
35
|
+
// If file already has .cs extension, use it directly
|
|
36
|
+
if (filePath.endsWith('.cs')) {
|
|
37
|
+
// Make relative path if it's absolute
|
|
38
|
+
if (path.isAbsolute(filePath)) {
|
|
39
|
+
// Try to find project-relative path
|
|
40
|
+
const cwd = process.cwd().replace(/\\/g, '/');
|
|
41
|
+
if (filePath.startsWith(cwd)) {
|
|
42
|
+
filePath = path.relative(cwd, filePath).replace(/\\/g, '/');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return filePath;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Convert namespace path to file path
|
|
49
|
+
const fileName = namespaceToFileName(filePath);
|
|
18
50
|
return fileName;
|
|
19
51
|
}
|
|
20
52
|
}
|
|
@@ -22,7 +54,14 @@ class CSharpAdapter extends Adapter {
|
|
|
22
54
|
export default CSharpAdapter;
|
|
23
55
|
|
|
24
56
|
function namespaceToFileName(fileName) {
|
|
57
|
+
if (!fileName) return '';
|
|
58
|
+
|
|
59
|
+
// If already a .cs file path, clean it up
|
|
60
|
+
if (fileName.endsWith('.cs')) {
|
|
61
|
+
return fileName.replace(/\\/g, '/');
|
|
62
|
+
}
|
|
63
|
+
|
|
25
64
|
const fileParts = fileName.split('.');
|
|
26
65
|
fileParts[fileParts.length - 1] = fileParts[fileParts.length - 1]?.replace(/\$.*/, '');
|
|
27
|
-
return `${fileParts.join(
|
|
66
|
+
return `${fileParts.join('/')}.cs`;
|
|
28
67
|
}
|