@testomatio/reporter 2.3.7-beta.3-xml-import → 2.3.7-beta.4-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,404 +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
- // For parameterized tests, format example as expected by Testomatio API
224
- // Convert array of parameters to object with numeric keys
225
- let example = null;
226
- if (isParameterized && parameters.length > 0) {
227
- example = {};
228
- parameters.forEach((param, index) => {
229
- example[index] = param;
230
- });
231
- }
232
-
233
- return {
234
- // For runs: use full test name with parameters (TestBooleanValue(true))
235
- // For import: API will group by base name using the example field
236
- title: testName, // Full name with parameters for run display
237
- methodName: baseMethodName || methodName || testName,
238
- fullName: fullName,
239
- suitePath: suitePath,
240
- suite_title: className || suitePath[suitePath.length - 1] || 'Unknown',
241
- file: filePath,
242
- status: status,
243
- message: message,
244
- stack: stack,
245
- run_time: parseFloat(testCase.duration || testCase.time || 0) * 1000,
246
- test_id: testId,
247
- create: true,
248
- retry: false,
249
- // Parameterized test metadata
250
- example: example, // Parameters as object for API grouping
251
- isParameterized: isParameterized,
252
- parameters: parameters, // Keep original array for reference
253
- baseMethodName: baseMethodName,
254
- };
255
- }
256
-
257
- /**
258
- * Extract method name and parameters from test name
259
- * @param {string} testName - Full test name
260
- * @returns {Object} - Extracted information
261
- */
262
- extractParameters(testName) {
263
- const paramMatch = testName.match(/^(.+?)\((.+)\)$/);
264
-
265
- if (paramMatch) {
266
- const baseMethodName = paramMatch[1].trim();
267
- const paramString = paramMatch[2];
268
-
269
- // Parse parameters - handle quoted strings and nested structures
270
- const parameters = this.parseParameterString(paramString);
271
-
272
- return {
273
- baseMethodName: baseMethodName,
274
- parameters: parameters,
275
- isParameterized: true,
276
- };
277
- }
278
-
279
- return {
280
- baseMethodName: testName,
281
- parameters: [],
282
- isParameterized: false,
283
- };
284
- }
285
-
286
- /**
287
- * Parse parameter string into array of parameters
288
- * @param {string} paramString - Parameter string
289
- * @returns {Array} - Array of parameters
290
- */
291
- parseParameterString(paramString) {
292
- const parameters = [];
293
- let current = '';
294
- let inQuotes = false;
295
- let quoteChar = null;
296
- let depth = 0;
297
-
298
- for (let i = 0; i < paramString.length; i++) {
299
- const char = paramString[i];
300
-
301
- if (!inQuotes && (char === '"' || char === "'")) {
302
- inQuotes = true;
303
- quoteChar = char;
304
- current += char;
305
- } else if (inQuotes && char === quoteChar) {
306
- inQuotes = false;
307
- quoteChar = null;
308
- current += char;
309
- } else if (!inQuotes && char === '(') {
310
- depth++;
311
- current += char;
312
- } else if (!inQuotes && char === ')') {
313
- depth--;
314
- current += char;
315
- } else if (!inQuotes && char === ',' && depth === 0) {
316
- parameters.push(current.trim());
317
- current = '';
318
- } else {
319
- current += char;
320
- }
321
- }
322
-
323
- if (current.trim()) {
324
- parameters.push(current.trim());
325
- }
326
-
327
- // Clean up parameters - remove quotes if they wrap the entire parameter
328
- return parameters.map(param => {
329
- param = param.trim();
330
- if ((param.startsWith('"') && param.endsWith('"')) || (param.startsWith("'") && param.endsWith("'"))) {
331
- return param.slice(1, -1);
332
- }
333
- return param;
334
- });
335
- }
336
-
337
- /**
338
- * Extract method name from test name (fallback)
339
- * @param {string} testName - Test name
340
- * @returns {string} - Method name
341
- */
342
- extractMethodName(testName) {
343
- // Remove parameters if present
344
- const paramMatch = testName.match(/^(.+?)\(/);
345
- return paramMatch ? paramMatch[1].trim() : testName;
346
- }
347
-
348
- /**
349
- * Build file path from suite path and class name
350
- * @param {Array} suitePath - Suite path array
351
- * @param {string} className - Class name
352
- * @param {Object} parentSuite - Parent suite for context
353
- * @returns {string} - File path
354
- */
355
- buildFilePath(suitePath, className, parentSuite) {
356
- // Try to get file path from parent suite
357
- if (parentSuite && parentSuite.filepath) {
358
- return parentSuite.filepath;
359
- }
360
-
361
- // Build path from suite hierarchy
362
- const pathParts = [...suitePath];
363
- if (className && !pathParts.includes(className)) {
364
- pathParts.push(className);
365
- }
366
-
367
- // Convert to file path format
368
- return pathParts.join('/') + '.cs'; // Assume C# for NUnit
369
- }
370
-
371
- /**
372
- * Group parameterized tests by base method name
373
- * @param {Array} tests - Array of parsed tests
374
- * @returns {Object} - Grouped tests
375
- */
376
- groupParameterizedTests(tests) {
377
- const grouped = {};
378
-
379
- tests.forEach(test => {
380
- const key = test.isParameterized
381
- ? `${test.suitePath.join('.')}.${test.baseMethodName}`
382
- : `${test.suitePath.join('.')}.${test.title}`;
383
-
384
- if (!grouped[key]) {
385
- grouped[key] = {
386
- baseTest: {
387
- name: test.baseMethodName || test.title,
388
- suitePath: test.suitePath,
389
- suite_title: test.suite_title,
390
- file: test.file,
391
- isParameterized: test.isParameterized,
392
- },
393
- variations: [],
394
- };
395
- }
396
-
397
- grouped[key].variations.push(test);
398
- });
399
-
400
- return grouped;
401
- }
402
- }
403
-
404
- export default NUnitXmlParser;