@testomatio/reporter 2.3.8-rc.1 → 2.3.9-beta-bin-fix
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 +3 -13
- package/lib/bin/reportXml.js +0 -0
- package/lib/bin/startTest.js +0 -0
- package/lib/bin/uploadArtifacts.js +0 -0
- package/lib/client.d.ts +1 -1
- package/lib/client.js +22 -31
- package/lib/pipe/testomatio.d.ts +1 -2
- package/lib/pipe/testomatio.js +1 -2
- package/lib/reporter.d.ts +9 -19
- package/lib/reporter.js +5 -40
- package/lib/utils/utils.js +3 -7
- package/package.json +5 -9
- package/src/bin/cli.js +1 -1
- package/src/bin/reportXml.js +2 -5
- package/src/client.js +26 -54
- package/src/junit-adapter/csharp.js +6 -45
- package/src/reporter.js +4 -7
- package/src/uploader.js +0 -5
- package/src/utils/utils.js +22 -213
- package/src/xmlReader.js +45 -131
- package/src/junit-adapter/nunit-parser.js +0 -462
- package/types/types.d.ts +0 -364
- package/types/vitest.types.d.ts +0 -93
package/src/reporter.js
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
import
|
|
2
|
-
import * as
|
|
1
|
+
// import TestomatClient from './client.js';
|
|
2
|
+
// import * as TRConstants from './constants.js';
|
|
3
3
|
import { services } from './services/index.js';
|
|
4
4
|
import reporterFunctions from './reporter-functions.js';
|
|
5
5
|
|
|
6
|
-
export { Client };
|
|
7
|
-
export const STATUS = TestomatioConstants.STATUS;
|
|
8
6
|
export const artifact = reporterFunctions.artifact;
|
|
9
7
|
export const log = reporterFunctions.log;
|
|
10
8
|
export const logger = services.logger;
|
|
@@ -37,7 +35,6 @@ export default {
|
|
|
37
35
|
linkTest: reporterFunctions.linkTest,
|
|
38
36
|
linkJira: reporterFunctions.linkJira,
|
|
39
37
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
38
|
+
// TestomatClient,
|
|
39
|
+
// TRConstants,
|
|
43
40
|
};
|
package/src/uploader.js
CHANGED
|
@@ -194,11 +194,6 @@ export class S3Uploader {
|
|
|
194
194
|
filePath = path.join(process.cwd(), filePath);
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
-
// Normalize path separators for cross-platform compatibility
|
|
198
|
-
if (typeof filePath === 'string') {
|
|
199
|
-
filePath = filePath.replace(/\\/g, '/');
|
|
200
|
-
}
|
|
201
|
-
|
|
202
197
|
const data = { rid, file: filePath, uploaded };
|
|
203
198
|
const jsonLine = `${JSON.stringify(data)}\n`;
|
|
204
199
|
fs.appendFileSync(tempFilePath, jsonLine);
|
package/src/utils/utils.js
CHANGED
|
@@ -76,29 +76,21 @@ const isValidUrl = s => {
|
|
|
76
76
|
}
|
|
77
77
|
};
|
|
78
78
|
|
|
79
|
-
const fileMatchRegex = /file:(
|
|
79
|
+
const fileMatchRegex = /file:(\/+(?:[A-Za-z]:[\\/]|\/)?[^\s]*?\.(png|avi|webm|jpg|html|txt))/gi;
|
|
80
80
|
|
|
81
81
|
const fetchFilesFromStackTrace = (stack = '', checkExists = true) => {
|
|
82
|
-
|
|
83
|
-
.map(
|
|
84
|
-
// match[0] is full match, match[1] is slashes, match[2] is path, match[3] is extension
|
|
85
|
-
const slashes = match[1] || '';
|
|
86
|
-
const path = match[2];
|
|
87
|
-
const extension = match[3];
|
|
88
|
-
return `${slashes}${path}.${extension}`;
|
|
89
|
-
})
|
|
90
|
-
.map(f => f.trim())
|
|
82
|
+
const files = Array.from(stack.matchAll(fileMatchRegex))
|
|
83
|
+
.map(f => f[1].trim())
|
|
91
84
|
.map(f => f.replace(/^\/+/, '/').replace(/^\/([A-Za-z]:)/, '$1')) // Remove extra slashes, handle Windows paths
|
|
92
85
|
.map(f => {
|
|
93
|
-
//
|
|
94
|
-
|
|
86
|
+
// Convert Windows paths to Linux paths for testing purposes
|
|
87
|
+
if (f.match(/^[A-Za-z]:[\\\/]/)) {
|
|
88
|
+
// Convert Windows path to Linux equivalent for test scenarios
|
|
89
|
+
return f.replace(/^[A-Za-z]:[\\\/]/, '/').replace(/\\/g, '/');
|
|
90
|
+
}
|
|
91
|
+
return f;
|
|
95
92
|
});
|
|
96
93
|
|
|
97
|
-
// If we're not checking file existence, remove Windows drive letters for consistency
|
|
98
|
-
if (!checkExists) {
|
|
99
|
-
files = files.map(f => f.replace(/^([A-Za-z]):/, ''));
|
|
100
|
-
}
|
|
101
|
-
|
|
102
94
|
debug('Found files in stack trace: ', files);
|
|
103
95
|
|
|
104
96
|
return files.filter(f => {
|
|
@@ -113,92 +105,21 @@ const fetchSourceCodeFromStackTrace = (stack = '') => {
|
|
|
113
105
|
const stackLines = stack
|
|
114
106
|
.split('\n')
|
|
115
107
|
.filter(l => l.includes(':'))
|
|
108
|
+
// .map(l => l.match(/\[(.*?)\]/)?.[1] || l) // minitest format
|
|
109
|
+
// .map(l => l.split(':')[0])
|
|
116
110
|
.map(l => l.trim())
|
|
117
|
-
.map(l =>
|
|
118
|
-
|
|
119
|
-
if (l.startsWith('at ')) {
|
|
120
|
-
return l.substring(3).trim();
|
|
121
|
-
}
|
|
122
|
-
// Find the part that looks like a file path with line number
|
|
123
|
-
const parts = l.split(' ');
|
|
124
|
-
for (const part of parts) {
|
|
125
|
-
// Check if this part has a colon
|
|
126
|
-
if (part.includes(':')) {
|
|
127
|
-
// For Windows paths, we need to handle drive letters (C:, D:, etc.)
|
|
128
|
-
// Split by colon but keep drive letter with the path
|
|
129
|
-
const colonParts = part.split(':');
|
|
130
|
-
let filePath;
|
|
131
|
-
|
|
132
|
-
// Check if first part is a Windows drive letter (single letter)
|
|
133
|
-
if (colonParts.length >= 2 && colonParts[0].length === 1 && /[A-Za-z]/.test(colonParts[0])) {
|
|
134
|
-
// Windows path like D:\path\file.php:24
|
|
135
|
-
// Reconstruct as D:\path\file.php
|
|
136
|
-
filePath = colonParts[0] + ':' + colonParts[1];
|
|
137
|
-
} else {
|
|
138
|
-
// Unix path like /path/file.php:24
|
|
139
|
-
filePath = colonParts[0];
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Only consider it valid if the file exists
|
|
143
|
-
if (fs.existsSync(filePath)) {
|
|
144
|
-
return part;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
// If no valid file path found in parts, return the whole line
|
|
149
|
-
// It will be filtered out later if it's not a valid file path
|
|
150
|
-
return parts.find(p => p.includes(':')) || l;
|
|
151
|
-
})
|
|
152
|
-
.filter(l => {
|
|
153
|
-
// Extract file path from line (accounting for Windows drive letters)
|
|
154
|
-
if (!l) return false;
|
|
155
|
-
const colonParts = l.split(':');
|
|
156
|
-
let filePath;
|
|
157
|
-
|
|
158
|
-
if (colonParts.length >= 2 && colonParts[0].length === 1 && /[A-Za-z]/.test(colonParts[0])) {
|
|
159
|
-
// Windows path
|
|
160
|
-
filePath = colonParts[0] + ':' + colonParts[1];
|
|
161
|
-
} else {
|
|
162
|
-
// Unix path
|
|
163
|
-
filePath = colonParts[0];
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
return filePath && fs.existsSync(filePath);
|
|
167
|
-
})
|
|
111
|
+
.map(l => l.split(' ').find(p => p.includes(':')) || '')
|
|
112
|
+
.filter(l => isValid(l?.split(':')[0]))
|
|
168
113
|
|
|
169
114
|
// // filter out 3rd party libs
|
|
170
115
|
.filter(l => !l?.includes(`vendor${sep}`))
|
|
171
116
|
.filter(l => !l?.includes(`node_modules${sep}`))
|
|
172
|
-
.filter(l =>
|
|
173
|
-
|
|
174
|
-
const colonParts = l.split(':');
|
|
175
|
-
let filePath;
|
|
176
|
-
|
|
177
|
-
if (colonParts.length >= 2 && colonParts[0].length === 1 && /[A-Za-z]/.test(colonParts[0])) {
|
|
178
|
-
filePath = colonParts[0] + ':' + colonParts[1];
|
|
179
|
-
} else {
|
|
180
|
-
filePath = colonParts[0];
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
return fs.lstatSync(filePath).isFile();
|
|
184
|
-
});
|
|
117
|
+
.filter(l => fs.existsSync(l.split(':')[0]))
|
|
118
|
+
.filter(l => fs.lstatSync(l.split(':')[0]).isFile());
|
|
185
119
|
|
|
186
120
|
if (!stackLines.length) return '';
|
|
187
121
|
|
|
188
|
-
|
|
189
|
-
const firstLine = stackLines[0];
|
|
190
|
-
const colonParts = firstLine.split(':');
|
|
191
|
-
let file, line;
|
|
192
|
-
|
|
193
|
-
if (colonParts.length >= 3 && colonParts[0].length === 1 && /[A-Za-z]/.test(colonParts[0])) {
|
|
194
|
-
// Windows path like D:\path\file.php:24
|
|
195
|
-
file = colonParts[0] + ':' + colonParts[1];
|
|
196
|
-
line = colonParts[2];
|
|
197
|
-
} else {
|
|
198
|
-
// Unix path like /path/file.php:24
|
|
199
|
-
file = colonParts[0];
|
|
200
|
-
line = colonParts[1];
|
|
201
|
-
}
|
|
122
|
+
const [file, line] = stackLines[0].split(':');
|
|
202
123
|
|
|
203
124
|
const prepend = 3;
|
|
204
125
|
const source = fetchSourceCode(fs.readFileSync(file).toString(), { line, prepend, limit: 7 });
|
|
@@ -218,8 +139,6 @@ export const TEST_ID_REGEX = /@T([\w\d]{8})/;
|
|
|
218
139
|
export const SUITE_ID_REGEX = /@S([\w\d]{8})/;
|
|
219
140
|
|
|
220
141
|
const fetchIdFromCode = (code, opts = {}) => {
|
|
221
|
-
if (!code) return null;
|
|
222
|
-
|
|
223
142
|
const comments = code
|
|
224
143
|
.split('\n')
|
|
225
144
|
.map(l => l.trim())
|
|
@@ -261,65 +180,8 @@ const fetchSourceCode = (contents, opts = {}) => {
|
|
|
261
180
|
if (lineIndex === -1) lineIndex = lines.findIndex(l => l.includes(`public void ${title}`));
|
|
262
181
|
if (lineIndex === -1) lineIndex = lines.findIndex(l => l.includes(`${title}(`));
|
|
263
182
|
} else if (opts.lang === 'csharp') {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
if (methodLineIndex === -1) {
|
|
268
|
-
methodLineIndex = lines.findIndex(l => l.includes(`public async Task ${title}(`));
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
if (methodLineIndex === -1) {
|
|
272
|
-
methodLineIndex = lines.findIndex(l => l.includes(`${title}(`));
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// If found, scan upwards to find [TestCase], [Test] attributes and XML comments
|
|
276
|
-
if (methodLineIndex !== -1) {
|
|
277
|
-
lineIndex = methodLineIndex;
|
|
278
|
-
|
|
279
|
-
// Scan upwards to find the start of attributes and comments
|
|
280
|
-
for (let i = methodLineIndex - 1; i >= 0; i--) {
|
|
281
|
-
const trimmedLine = lines[i].trim();
|
|
282
|
-
|
|
283
|
-
// Include [TestCase], [Test], and other attributes
|
|
284
|
-
if (trimmedLine.startsWith('[')) {
|
|
285
|
-
lineIndex = i;
|
|
286
|
-
continue;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
// Include XML documentation comments
|
|
290
|
-
if (trimmedLine.startsWith('///')) {
|
|
291
|
-
lineIndex = i;
|
|
292
|
-
continue;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// Stop at empty lines (with some tolerance)
|
|
296
|
-
if (trimmedLine === '') {
|
|
297
|
-
// Check if next non-empty line is an attribute or comment
|
|
298
|
-
let hasMoreAttributes = false;
|
|
299
|
-
for (let j = i - 1; j >= 0; j--) {
|
|
300
|
-
const nextTrimmed = lines[j].trim();
|
|
301
|
-
if (nextTrimmed === '') continue;
|
|
302
|
-
if (nextTrimmed.startsWith('[') || nextTrimmed.startsWith('///')) {
|
|
303
|
-
hasMoreAttributes = true;
|
|
304
|
-
lineIndex = j;
|
|
305
|
-
}
|
|
306
|
-
break;
|
|
307
|
-
}
|
|
308
|
-
if (!hasMoreAttributes) break;
|
|
309
|
-
continue;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// Stop at other method declarations or class-level elements
|
|
313
|
-
if (
|
|
314
|
-
trimmedLine.includes('public ') ||
|
|
315
|
-
trimmedLine.includes('private ') ||
|
|
316
|
-
trimmedLine.includes('protected ') ||
|
|
317
|
-
trimmedLine.includes('internal ')
|
|
318
|
-
) {
|
|
319
|
-
if (!trimmedLine.startsWith('[')) break;
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
}
|
|
183
|
+
if (lineIndex === -1) lineIndex = lines.findIndex(l => l.includes(`public void ${title}`));
|
|
184
|
+
if (lineIndex === -1) lineIndex = lines.findIndex(l => l.includes(`${title}(`));
|
|
323
185
|
} else {
|
|
324
186
|
lineIndex = lines.findIndex(l => l.includes(title));
|
|
325
187
|
}
|
|
@@ -329,31 +191,11 @@ const fetchSourceCode = (contents, opts = {}) => {
|
|
|
329
191
|
lineIndex -= opts.prepend;
|
|
330
192
|
}
|
|
331
193
|
|
|
332
|
-
if (lineIndex
|
|
194
|
+
if (lineIndex) {
|
|
333
195
|
const result = [];
|
|
334
|
-
let braceDepth = 0; // Track brace depth for C# methods
|
|
335
|
-
let methodStartFound = false; // Flag to indicate we've found the method opening brace
|
|
336
|
-
|
|
337
196
|
for (let i = lineIndex; i < lineIndex + limit; i++) {
|
|
338
197
|
if (lines[i] === undefined) continue;
|
|
339
198
|
|
|
340
|
-
// Track brace depth for C# to stop after method closes
|
|
341
|
-
if (opts.lang === 'csharp') {
|
|
342
|
-
const line = lines[i];
|
|
343
|
-
// Count opening and closing braces
|
|
344
|
-
const openBraces = (line.match(/\{/g) || []).length;
|
|
345
|
-
const closeBraces = (line.match(/\}/g) || []).length;
|
|
346
|
-
|
|
347
|
-
if (openBraces > 0) methodStartFound = true;
|
|
348
|
-
braceDepth += openBraces - closeBraces;
|
|
349
|
-
|
|
350
|
-
// If we've started the method and depth returns to 0, method is complete
|
|
351
|
-
if (methodStartFound && braceDepth === 0 && closeBraces > 0) {
|
|
352
|
-
// Don't include the closing brace - just break
|
|
353
|
-
break;
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
199
|
if (i > lineIndex + 2 && !opts.prepend) {
|
|
358
200
|
// annotation
|
|
359
201
|
if (opts.lang === 'php' && lines[i].trim().startsWith('#[')) break;
|
|
@@ -374,36 +216,7 @@ const fetchSourceCode = (contents, opts = {}) => {
|
|
|
374
216
|
if (opts.lang === 'java' && lines[i].trim().match(/^@\w+/)) break;
|
|
375
217
|
if (opts.lang === 'java' && lines[i].includes(' public void ')) break;
|
|
376
218
|
if (opts.lang === 'java' && lines[i].includes(' class ')) break;
|
|
377
|
-
// For C#, additional checks if brace tracking didn't stop us
|
|
378
|
-
if (opts.lang === 'csharp') {
|
|
379
|
-
const trimmed = lines[i].trim();
|
|
380
|
-
// Stop at attribute that marks beginning of next test (but not if we're still in the current method)
|
|
381
|
-
if (trimmed.match(/^\[(Test|TestCase|Theory|Fact)/) && methodStartFound && braceDepth === 0) break;
|
|
382
|
-
// Stop at XML documentation comments that belong to next method
|
|
383
|
-
if (trimmed.startsWith('///') && methodStartFound && braceDepth === 0) break;
|
|
384
|
-
// Stop at another method declaration (but not if we're still in the current method)
|
|
385
|
-
if (
|
|
386
|
-
trimmed.match(/^\s*(public|private|protected|internal)\s+(\w+|async\s+\w+)\s+\w+\s*\(/) &&
|
|
387
|
-
methodStartFound &&
|
|
388
|
-
braceDepth === 0
|
|
389
|
-
)
|
|
390
|
-
break;
|
|
391
|
-
// Stop at class declaration
|
|
392
|
-
if (trimmed.includes(' class ') && trimmed.includes('public')) break;
|
|
393
|
-
// Stop at helper method calls (like ProcessBooleanValue, AddNumbers) - these are private methods
|
|
394
|
-
if (methodStartFound && trimmed.match(/^\s*\/\/\s*Helper methods for testing/)) break;
|
|
395
|
-
}
|
|
396
219
|
}
|
|
397
|
-
|
|
398
|
-
// For C# tests, stop if we encounter helper method calls in the method body
|
|
399
|
-
if (opts.lang === 'csharp' && methodStartFound && braceDepth > 0) {
|
|
400
|
-
const trimmed = lines[i].trim();
|
|
401
|
-
// Stop at comment indicating helper methods section
|
|
402
|
-
if (trimmed.match(/^\s*\/\/\s*Helper methods for testing/)) {
|
|
403
|
-
break;
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
|
|
407
220
|
result.push(lines[i]);
|
|
408
221
|
}
|
|
409
222
|
return result.join('\n');
|
|
@@ -616,14 +429,10 @@ function transformEnvVarToBoolean(value) {
|
|
|
616
429
|
}
|
|
617
430
|
|
|
618
431
|
function truncate(s, size = 255) {
|
|
619
|
-
if (s
|
|
620
|
-
return
|
|
621
|
-
}
|
|
622
|
-
const str = s.toString();
|
|
623
|
-
if (str.trim().length < size) {
|
|
624
|
-
return str;
|
|
432
|
+
if (s.toString().trim().length < size) {
|
|
433
|
+
return s.toString();
|
|
625
434
|
}
|
|
626
|
-
return `${
|
|
435
|
+
return `${s.toString().substring(0, size)}...`;
|
|
627
436
|
}
|
|
628
437
|
|
|
629
438
|
export {
|
package/src/xmlReader.js
CHANGED
|
@@ -6,7 +6,6 @@ import { XMLParser } from 'fast-xml-parser';
|
|
|
6
6
|
import { APP_PREFIX, STATUS } from './constants.js';
|
|
7
7
|
import { randomUUID } from 'crypto';
|
|
8
8
|
import { fileURLToPath } from 'url';
|
|
9
|
-
import { NUnitXmlParser } from './junit-adapter/nunit-parser.js';
|
|
10
9
|
import {
|
|
11
10
|
fetchFilesFromStackTrace,
|
|
12
11
|
fetchIdFromOutput,
|
|
@@ -15,7 +14,6 @@ import {
|
|
|
15
14
|
fetchIdFromCode,
|
|
16
15
|
humanize,
|
|
17
16
|
TEST_ID_REGEX,
|
|
18
|
-
transformEnvVarToBoolean,
|
|
19
17
|
} from './utils/utils.js';
|
|
20
18
|
import { pipesFactory } from './pipe/index.js';
|
|
21
19
|
import adapterFactory from './junit-adapter/index.js';
|
|
@@ -37,7 +35,6 @@ const {
|
|
|
37
35
|
TESTOMATIO_ENV,
|
|
38
36
|
TESTOMATIO_RUN,
|
|
39
37
|
TESTOMATIO_MARK_DETACHED,
|
|
40
|
-
TESTOMATIO_LEGACY_NUNIT,
|
|
41
38
|
} = process.env;
|
|
42
39
|
|
|
43
40
|
const options = {
|
|
@@ -78,11 +75,6 @@ class XmlReader {
|
|
|
78
75
|
this.stats.language = opts.lang?.toLowerCase();
|
|
79
76
|
this.uploader = new S3Uploader();
|
|
80
77
|
|
|
81
|
-
// Enhanced NUnit parsing - enabled by default for NUnit XML
|
|
82
|
-
// Can be disabled via opts.enhancedNunit = false or TESTOMATIO_LEGACY_NUNIT=1
|
|
83
|
-
this.enhancedNunit = opts.enhancedNunit !== false && !transformEnvVarToBoolean(TESTOMATIO_LEGACY_NUNIT); // Default true, can be disabled
|
|
84
|
-
this.groupParameterized = opts.groupParameterized !== false; // Default true, can be disabled
|
|
85
|
-
|
|
86
78
|
// @ts-ignore
|
|
87
79
|
const packageJsonPath = path.resolve(__dirname, '..', 'package.json');
|
|
88
80
|
this.version = JSON.parse(fs.readFileSync(packageJsonPath).toString()).version;
|
|
@@ -134,8 +126,7 @@ class XmlReader {
|
|
|
134
126
|
}
|
|
135
127
|
|
|
136
128
|
processJUnit(jsonSuite) {
|
|
137
|
-
const { testsuite, name, failures, errors } = jsonSuite;
|
|
138
|
-
const tests = testsuite?.tests || jsonSuite.tests;
|
|
129
|
+
const { testsuite, name, tests, failures, errors } = jsonSuite;
|
|
139
130
|
|
|
140
131
|
reduceOptions.preferClassname = this.stats.language === 'python';
|
|
141
132
|
const resultTests = processTestSuite(testsuite);
|
|
@@ -166,14 +157,6 @@ class XmlReader {
|
|
|
166
157
|
}
|
|
167
158
|
|
|
168
159
|
processNUnit(jsonSuite) {
|
|
169
|
-
// Use enhanced NUnit parser if enabled and this is actually NUnit XML
|
|
170
|
-
if (this.enhancedNunit && this.isNUnitXml(jsonSuite)) {
|
|
171
|
-
debug('Using enhanced NUnit parser');
|
|
172
|
-
return this.processNUnitEnhanced(jsonSuite);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// Fallback to legacy parser for backward compatibility
|
|
176
|
-
debug('Using legacy NUnit parser');
|
|
177
160
|
const { result, total, passed, failed, inconclusive, skipped } = jsonSuite;
|
|
178
161
|
|
|
179
162
|
reduceOptions.preferClassname = this.stats.language === 'python';
|
|
@@ -192,71 +175,63 @@ class XmlReader {
|
|
|
192
175
|
};
|
|
193
176
|
}
|
|
194
177
|
|
|
195
|
-
/**
|
|
196
|
-
* Check if the XML is actually NUnit format (has test-suite hierarchy)
|
|
197
|
-
* @param {Object} jsonSuite - Parsed XML suite object
|
|
198
|
-
* @returns {boolean} - True if this is NUnit XML format
|
|
199
|
-
*/
|
|
200
|
-
isNUnitXml(jsonSuite) {
|
|
201
|
-
// NUnit XML has test-suite elements with type attributes
|
|
202
|
-
if (jsonSuite['test-suite']) {
|
|
203
|
-
const testSuite = Array.isArray(jsonSuite['test-suite']) ? jsonSuite['test-suite'][0] : jsonSuite['test-suite'];
|
|
204
|
-
|
|
205
|
-
// Check for NUnit-specific test-suite types
|
|
206
|
-
return (
|
|
207
|
-
testSuite &&
|
|
208
|
-
testSuite.type &&
|
|
209
|
-
['Assembly', 'TestSuite', 'TestFixture', 'ParameterizedMethod'].includes(testSuite.type)
|
|
210
|
-
);
|
|
211
|
-
}
|
|
212
|
-
return false;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
processNUnitEnhanced(jsonSuite) {
|
|
216
|
-
debug('Processing NUnit XML with enhanced parser');
|
|
217
|
-
|
|
218
|
-
try {
|
|
219
|
-
const nunitParser = new NUnitXmlParser({
|
|
220
|
-
groupParameterized: this.groupParameterized,
|
|
221
|
-
...this.opts,
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
const result = nunitParser.parseTestRun(jsonSuite);
|
|
225
|
-
|
|
226
|
-
// Add parsed tests to our collection
|
|
227
|
-
this.tests = this.tests.concat(result.tests);
|
|
228
|
-
|
|
229
|
-
debug(`Enhanced NUnit parser processed ${result.tests.length} tests`);
|
|
230
|
-
|
|
231
|
-
return result;
|
|
232
|
-
} catch (error) {
|
|
233
|
-
debug('Enhanced NUnit parser failed, falling back to legacy parser:', error.message);
|
|
234
|
-
console.warn(`${APP_PREFIX} Enhanced NUnit parsing failed, using legacy parser: ${error.message}`);
|
|
235
|
-
|
|
236
|
-
// Fallback to legacy parser
|
|
237
|
-
this.enhancedNunit = false;
|
|
238
|
-
return this.processNUnit(jsonSuite);
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
178
|
processTRX(jsonSuite) {
|
|
243
179
|
let defs = jsonSuite?.TestRun?.TestDefinitions?.UnitTest;
|
|
244
180
|
if (!Array.isArray(defs)) defs = [defs].filter(d => !!d);
|
|
245
181
|
|
|
246
|
-
|
|
247
|
-
|
|
182
|
+
const tests =
|
|
183
|
+
defs.map(td => {
|
|
184
|
+
const title = td.name.replace(/\(.*?\)/, '').trim();
|
|
185
|
+
let example = td.name.match(/\((.*?)\)/);
|
|
186
|
+
if (example) example = { ...example[1].split(',') };
|
|
187
|
+
const suite = td.TestMethod.className.split(', ')[0].split('.');
|
|
188
|
+
const suite_title = suite.pop();
|
|
189
|
+
return {
|
|
190
|
+
title,
|
|
191
|
+
example,
|
|
192
|
+
file: suite.join('/'),
|
|
193
|
+
description: td.Description,
|
|
194
|
+
suite_title,
|
|
195
|
+
id: td.Execution.id,
|
|
196
|
+
};
|
|
197
|
+
}) || [];
|
|
248
198
|
|
|
249
|
-
// Parse test results
|
|
250
199
|
let result = jsonSuite?.TestRun?.Results?.UnitTestResult;
|
|
251
200
|
if (!Array.isArray(result)) result = [result].filter(d => !!d);
|
|
252
201
|
|
|
253
|
-
const results = result.map(td =>
|
|
202
|
+
const results = result.map(td => ({
|
|
203
|
+
id: td.executionId,
|
|
204
|
+
// seconds are used in junit reports, but ms are used by testomatio
|
|
205
|
+
run_time: parseFloat(td.duration) * 1000,
|
|
206
|
+
status: td.outcome,
|
|
207
|
+
stack: td.Output.StdOut,
|
|
208
|
+
files: td?.ResultFiles?.ResultFile?.map(rf => rf.path),
|
|
209
|
+
}));
|
|
210
|
+
|
|
211
|
+
results.forEach(r => {
|
|
212
|
+
const test = tests.find(t => t.id === r.id) || {};
|
|
213
|
+
r.suite_title = test.suite_title;
|
|
214
|
+
r.title = test.title?.trim();
|
|
215
|
+
if (test.code) r.code = test.code;
|
|
216
|
+
if (test.description) r.description = test.description;
|
|
217
|
+
if (test.example) r.example = test.example;
|
|
218
|
+
if (test.file) r.file = test.file;
|
|
219
|
+
r.create = true;
|
|
220
|
+
r.overwrite = true;
|
|
221
|
+
if (r.status === 'Passed') r.status = STATUS.PASSED;
|
|
222
|
+
if (r.status === 'Failed') r.status = STATUS.FAILED;
|
|
223
|
+
if (r.status === 'Skipped') r.status = STATUS.SKIPPED;
|
|
224
|
+
delete r.id;
|
|
225
|
+
});
|
|
254
226
|
|
|
255
227
|
debug(results);
|
|
256
228
|
|
|
257
229
|
const counters = jsonSuite?.TestRun?.ResultSummary?.Counters || {};
|
|
230
|
+
|
|
258
231
|
const failed_count = parseInt(counters.failed, 10) + parseInt(counters.error, 10);
|
|
259
|
-
|
|
232
|
+
|
|
233
|
+
let status = STATUS.PASSED.toString();
|
|
234
|
+
if (failed_count > 0) status = STATUS.FAILED;
|
|
260
235
|
|
|
261
236
|
this.tests = results.filter(t => !!t.title);
|
|
262
237
|
|
|
@@ -271,67 +246,6 @@ class XmlReader {
|
|
|
271
246
|
};
|
|
272
247
|
}
|
|
273
248
|
|
|
274
|
-
_parseTRXTestDefinition(td) {
|
|
275
|
-
const title = td.name.replace(/\(.*?\)/, '').trim();
|
|
276
|
-
const exampleMatch = td.name.match(/\((.*?)\)/);
|
|
277
|
-
const example = exampleMatch ? { ...exampleMatch[1].split(',') } : null;
|
|
278
|
-
|
|
279
|
-
const suite = td.TestMethod.className.split(', ')[0].split('.');
|
|
280
|
-
const suite_title = suite.pop();
|
|
281
|
-
|
|
282
|
-
// Convert namespace to file path for C#
|
|
283
|
-
const file = `${suite.join('/')}.cs`;
|
|
284
|
-
|
|
285
|
-
return {
|
|
286
|
-
title, // Base name without parameters for test import
|
|
287
|
-
example, // Parameters object for parameterized tests
|
|
288
|
-
file, // File path with .cs extension
|
|
289
|
-
description: td.Description,
|
|
290
|
-
suite_title,
|
|
291
|
-
id: td.Execution.id,
|
|
292
|
-
};
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
_parseTRXTestResult(td, tests) {
|
|
296
|
-
const test = tests.find(t => t.id === td.executionId) || {};
|
|
297
|
-
|
|
298
|
-
const result = {
|
|
299
|
-
suite_title: test.suite_title,
|
|
300
|
-
title: test.title?.trim(),
|
|
301
|
-
file: test.file,
|
|
302
|
-
description: test.description,
|
|
303
|
-
code: test.code,
|
|
304
|
-
run_time: parseFloat(td.duration) * 1000,
|
|
305
|
-
stack: td.Output?.StdOut || '',
|
|
306
|
-
files: td?.ResultFiles?.ResultFile?.map(rf => rf.path),
|
|
307
|
-
create: true,
|
|
308
|
-
overwrite: true,
|
|
309
|
-
};
|
|
310
|
-
|
|
311
|
-
// Add example for parameterized tests
|
|
312
|
-
if (test.example) {
|
|
313
|
-
result.example = test.example;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
// Map TRX status to Testomat.io status
|
|
317
|
-
result.status = this._mapTRXStatus(td.outcome);
|
|
318
|
-
|
|
319
|
-
return result;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
_mapTRXStatus(outcome) {
|
|
323
|
-
switch (outcome) {
|
|
324
|
-
case 'Passed':
|
|
325
|
-
return STATUS.PASSED;
|
|
326
|
-
case 'Failed':
|
|
327
|
-
return STATUS.FAILED;
|
|
328
|
-
case 'Skipped':
|
|
329
|
-
return STATUS.SKIPPED;
|
|
330
|
-
default:
|
|
331
|
-
return STATUS.PASSED;
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
|
|
335
249
|
processXUnit(assemblies) {
|
|
336
250
|
const tests = [];
|
|
337
251
|
|