@testomatio/reporter 2.3.7-beta.1-xml-import → 2.3.7-beta.11-stack-artifacts

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.
@@ -1,391 +0,0 @@
1
- import createDebugMessages from 'debug';
2
- import { STATUS } from '../constants.js';
3
-
4
- const debug = createDebugMessages('@testomatio/reporter:nunit-parser');
5
-
6
- /**
7
- * Enhanced NUnit XML Parser that properly handles test-suite hierarchy
8
- * and parameterized tests
9
- */
10
- export class NUnitXmlParser {
11
- constructor(options = {}) {
12
- this.options = options;
13
- this.tests = [];
14
- this.stats = {
15
- total: 0,
16
- passed: 0,
17
- failed: 0,
18
- skipped: 0,
19
- inconclusive: 0,
20
- };
21
- }
22
-
23
- /**
24
- * Parse NUnit XML test-run structure
25
- * @param {Object} testRun - Parsed XML test-run object
26
- * @returns {Object} - Parsed test results
27
- */
28
- parseTestRun(testRun) {
29
- debug('Parsing NUnit test-run');
30
-
31
- // Extract run-level statistics
32
- this.stats = {
33
- total: parseInt(testRun.total || 0, 10),
34
- passed: parseInt(testRun.passed || 0, 10),
35
- failed: parseInt(testRun.failed || 0, 10),
36
- skipped: parseInt(testRun.skipped || 0, 10),
37
- inconclusive: parseInt(testRun.inconclusive || 0, 10),
38
- };
39
-
40
- // Process the root test-suite
41
- if (testRun['test-suite']) {
42
- this.parseTestSuite(testRun['test-suite'], []);
43
- }
44
-
45
- debug(`Parsed ${this.tests.length} tests from NUnit XML`);
46
-
47
- return {
48
- status: testRun.result?.toLowerCase() || 'unknown',
49
- create_tests: true,
50
- tests_count: this.tests.length,
51
- passed_count: this.tests.filter(t => t.status === STATUS.PASSED).length,
52
- failed_count: this.tests.filter(t => t.status === STATUS.FAILED).length,
53
- skipped_count: this.tests.filter(t => t.status === STATUS.SKIPPED).length,
54
- tests: this.tests,
55
- };
56
- }
57
-
58
- /**
59
- * Recursively parse test-suite elements based on their type
60
- * @param {Object|Array} testSuite - Test suite object or array
61
- * @param {Array} parentPath - Current path in the hierarchy
62
- */
63
- parseTestSuite(testSuite, parentPath = []) {
64
- if (!testSuite) return;
65
-
66
- // Handle arrays of test suites
67
- if (Array.isArray(testSuite)) {
68
- testSuite.forEach(suite => this.parseTestSuite(suite, parentPath));
69
- return;
70
- }
71
-
72
- const suiteType = testSuite.type;
73
- const suiteName = testSuite.name;
74
- const fullName = testSuite.fullname;
75
-
76
- debug(`Processing test-suite: type=${suiteType}, name=${suiteName}`);
77
-
78
- switch (suiteType) {
79
- case 'Assembly':
80
- // Assembly level - ignore the name, just process children
81
- debug('Processing Assembly level - ignoring name, processing children');
82
- this.processChildren(testSuite, parentPath);
83
- break;
84
-
85
- case 'TestSuite':
86
- // Namespace/grouping level - add to path but don't create test
87
- debug(`Processing TestSuite level - adding '${suiteName}' to path`);
88
- const newPath = [...parentPath, suiteName];
89
- this.processChildren(testSuite, newPath);
90
- break;
91
-
92
- case 'TestFixture':
93
- // Test class level - add to path and process test cases
94
- debug(`Processing TestFixture level - test class '${suiteName}'`);
95
- const testFixturePath = [...parentPath, suiteName];
96
- this.processChildren(testSuite, testFixturePath);
97
- break;
98
-
99
- default:
100
- debug(`Unknown test-suite type: ${suiteType}, treating as TestSuite`);
101
- const unknownPath = [...parentPath, suiteName];
102
- this.processChildren(testSuite, unknownPath);
103
- break;
104
- }
105
- }
106
-
107
- /**
108
- * Process child elements of a test suite
109
- * @param {Object} testSuite - Test suite object
110
- * @param {Array} currentPath - Current path in hierarchy
111
- */
112
- processChildren(testSuite, currentPath) {
113
- // Process nested test-suites
114
- if (testSuite['test-suite']) {
115
- this.parseTestSuite(testSuite['test-suite'], currentPath);
116
- }
117
-
118
- // Process test-cases
119
- if (testSuite['test-case']) {
120
- this.parseTestCases(testSuite['test-case'], currentPath, testSuite);
121
- }
122
- }
123
-
124
- /**
125
- * Parse test-case elements (actual tests)
126
- * @param {Object|Array} testCases - Test case object or array
127
- * @param {Array} suitePath - Path to the test suite
128
- * @param {Object} parentSuite - Parent test suite for context
129
- */
130
- parseTestCases(testCases, suitePath, parentSuite) {
131
- if (!testCases) return;
132
-
133
- // Handle arrays of test cases
134
- if (!Array.isArray(testCases)) {
135
- testCases = [testCases];
136
- }
137
-
138
- testCases.forEach(testCase => {
139
- const parsedTest = this.parseTestCase(testCase, suitePath, parentSuite);
140
- if (parsedTest) {
141
- this.tests.push(parsedTest);
142
- }
143
- });
144
- }
145
-
146
- /**
147
- * Parse individual test case
148
- * @param {Object} testCase - Test case object
149
- * @param {Array} suitePath - Path to the test suite
150
- * @param {Object} parentSuite - Parent test suite for context
151
- * @returns {Object|null} - Parsed test object
152
- */
153
- parseTestCase(testCase, suitePath, parentSuite) {
154
- if (!testCase || !testCase.name) {
155
- debug('Skipping test case without name');
156
- return null;
157
- }
158
-
159
- const testName = testCase.name;
160
- const fullName = testCase.fullname;
161
- const methodName = testCase.methodname || this.extractMethodName(testName);
162
- const className = testCase.classname || parentSuite?.name;
163
-
164
- debug(`Parsing test case: ${testName}`);
165
-
166
- // Extract parameters if this is a parameterized test
167
- const { baseMethodName, parameters, isParameterized } = this.extractParameters(testName);
168
-
169
- // Determine test status
170
- let status = STATUS.PASSED;
171
- if (testCase.result) {
172
- switch (testCase.result.toLowerCase()) {
173
- case 'passed':
174
- status = STATUS.PASSED;
175
- break;
176
- case 'failed':
177
- status = STATUS.FAILED;
178
- break;
179
- case 'skipped':
180
- case 'ignored':
181
- status = STATUS.SKIPPED;
182
- break;
183
- case 'inconclusive':
184
- status = STATUS.SKIPPED; // Treat inconclusive as skipped
185
- break;
186
- default:
187
- status = STATUS.PASSED;
188
- }
189
- }
190
-
191
- // Extract error information
192
- let message = '';
193
- let stack = '';
194
-
195
- if (testCase.failure) {
196
- message = testCase.failure.message || '';
197
- stack = testCase.failure['stack-trace'] || testCase.failure['#text'] || '';
198
- }
199
-
200
- if (testCase.output && testCase.output['#text']) {
201
- stack = `${stack}\n\n${testCase.output['#text']}`.trim();
202
- }
203
-
204
- // Extract test ID from properties
205
- let testId = null;
206
- if (testCase.properties && testCase.properties.property) {
207
- const properties = Array.isArray(testCase.properties.property)
208
- ? testCase.properties.property
209
- : [testCase.properties.property];
210
-
211
- const idProperty = properties.find(p => p.name === 'ID');
212
- if (idProperty) {
213
- testId = idProperty.value;
214
- // Remove @ and T prefixes if present
215
- if (testId.startsWith('@')) testId = testId.slice(1);
216
- if (testId.startsWith('T')) testId = testId.slice(1);
217
- }
218
- }
219
-
220
- // Build file path from suite path and class name
221
- const filePath = this.buildFilePath(suitePath, className, parentSuite);
222
-
223
- return {
224
- title: isParameterized ? testName : methodName || testName,
225
- methodName: baseMethodName || methodName || testName,
226
- fullName: fullName,
227
- suitePath: suitePath,
228
- suite_title: className || suitePath[suitePath.length - 1] || 'Unknown',
229
- file: filePath,
230
- status: status,
231
- message: message,
232
- stack: stack,
233
- run_time: parseFloat(testCase.duration || testCase.time || 0) * 1000,
234
- test_id: testId,
235
- create: true,
236
- retry: false,
237
- // Parameterized test metadata
238
- isParameterized: isParameterized,
239
- parameters: parameters,
240
- baseMethodName: baseMethodName,
241
- };
242
- }
243
-
244
- /**
245
- * Extract method name and parameters from test name
246
- * @param {string} testName - Full test name
247
- * @returns {Object} - Extracted information
248
- */
249
- extractParameters(testName) {
250
- const paramMatch = testName.match(/^(.+?)\((.+)\)$/);
251
-
252
- if (paramMatch) {
253
- const baseMethodName = paramMatch[1].trim();
254
- const paramString = paramMatch[2];
255
-
256
- // Parse parameters - handle quoted strings and nested structures
257
- const parameters = this.parseParameterString(paramString);
258
-
259
- return {
260
- baseMethodName: baseMethodName,
261
- parameters: parameters,
262
- isParameterized: true,
263
- };
264
- }
265
-
266
- return {
267
- baseMethodName: testName,
268
- parameters: [],
269
- isParameterized: false,
270
- };
271
- }
272
-
273
- /**
274
- * Parse parameter string into array of parameters
275
- * @param {string} paramString - Parameter string
276
- * @returns {Array} - Array of parameters
277
- */
278
- parseParameterString(paramString) {
279
- const parameters = [];
280
- let current = '';
281
- let inQuotes = false;
282
- let quoteChar = null;
283
- let depth = 0;
284
-
285
- for (let i = 0; i < paramString.length; i++) {
286
- const char = paramString[i];
287
-
288
- if (!inQuotes && (char === '"' || char === "'")) {
289
- inQuotes = true;
290
- quoteChar = char;
291
- current += char;
292
- } else if (inQuotes && char === quoteChar) {
293
- inQuotes = false;
294
- quoteChar = null;
295
- current += char;
296
- } else if (!inQuotes && char === '(') {
297
- depth++;
298
- current += char;
299
- } else if (!inQuotes && char === ')') {
300
- depth--;
301
- current += char;
302
- } else if (!inQuotes && char === ',' && depth === 0) {
303
- parameters.push(current.trim());
304
- current = '';
305
- } else {
306
- current += char;
307
- }
308
- }
309
-
310
- if (current.trim()) {
311
- parameters.push(current.trim());
312
- }
313
-
314
- // Clean up parameters - remove quotes if they wrap the entire parameter
315
- return parameters.map(param => {
316
- param = param.trim();
317
- if ((param.startsWith('"') && param.endsWith('"')) || (param.startsWith("'") && param.endsWith("'"))) {
318
- return param.slice(1, -1);
319
- }
320
- return param;
321
- });
322
- }
323
-
324
- /**
325
- * Extract method name from test name (fallback)
326
- * @param {string} testName - Test name
327
- * @returns {string} - Method name
328
- */
329
- extractMethodName(testName) {
330
- // Remove parameters if present
331
- const paramMatch = testName.match(/^(.+?)\(/);
332
- return paramMatch ? paramMatch[1].trim() : testName;
333
- }
334
-
335
- /**
336
- * Build file path from suite path and class name
337
- * @param {Array} suitePath - Suite path array
338
- * @param {string} className - Class name
339
- * @param {Object} parentSuite - Parent suite for context
340
- * @returns {string} - File path
341
- */
342
- buildFilePath(suitePath, className, parentSuite) {
343
- // Try to get file path from parent suite
344
- if (parentSuite && parentSuite.filepath) {
345
- return parentSuite.filepath;
346
- }
347
-
348
- // Build path from suite hierarchy
349
- const pathParts = [...suitePath];
350
- if (className && !pathParts.includes(className)) {
351
- pathParts.push(className);
352
- }
353
-
354
- // Convert to file path format
355
- return pathParts.join('/') + '.cs'; // Assume C# for NUnit
356
- }
357
-
358
- /**
359
- * Group parameterized tests by base method name
360
- * @param {Array} tests - Array of parsed tests
361
- * @returns {Object} - Grouped tests
362
- */
363
- groupParameterizedTests(tests) {
364
- const grouped = {};
365
-
366
- tests.forEach(test => {
367
- const key = test.isParameterized
368
- ? `${test.suitePath.join('.')}.${test.baseMethodName}`
369
- : `${test.suitePath.join('.')}.${test.title}`;
370
-
371
- if (!grouped[key]) {
372
- grouped[key] = {
373
- baseTest: {
374
- name: test.baseMethodName || test.title,
375
- suitePath: test.suitePath,
376
- suite_title: test.suite_title,
377
- file: test.file,
378
- isParameterized: test.isParameterized,
379
- },
380
- variations: [],
381
- };
382
- }
383
-
384
- grouped[key].variations.push(test);
385
- });
386
-
387
- return grouped;
388
- }
389
- }
390
-
391
- export default NUnitXmlParser;