@testomatio/reporter 2.3.5-beta-5-xml-import → 2.3.5-beta.8-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/lib/junit-adapter/csharp.d.ts +1 -0
- package/lib/junit-adapter/csharp.js +7 -36
- package/lib/pipe/debug.js +1 -1
- package/lib/pipe/testomatio.js +14 -18
- package/lib/utils/utils.js +4 -33
- package/lib/xmlReader.d.ts +0 -7
- package/lib/xmlReader.js +10 -312
- package/package.json +1 -1
- package/src/junit-adapter/csharp.js +6 -40
- package/src/pipe/debug.js +3 -2
- package/src/pipe/testomatio.js +80 -74
- package/src/utils/utils.js +3 -33
- package/src/xmlReader.js +10 -360
|
@@ -7,53 +7,24 @@ const path_1 = __importDefault(require("path"));
|
|
|
7
7
|
const adapter_js_1 = __importDefault(require("./adapter.js"));
|
|
8
8
|
class CSharpAdapter extends adapter_js_1.default {
|
|
9
9
|
formatTest(t) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
if (
|
|
13
|
-
|
|
14
|
-
const exampleMatch = t.title.match(/\((.*?)\)/);
|
|
15
|
-
if (exampleMatch) {
|
|
16
|
-
// Keep as array for consistency with NUnit XML processing
|
|
17
|
-
t.example = exampleMatch[1].split(',').map(param => param.trim());
|
|
18
|
-
}
|
|
19
|
-
t.title = title.trim();
|
|
20
|
-
}
|
|
10
|
+
const title = t.title.replace(/\(.*?\)/, '').trim();
|
|
11
|
+
const example = t.title.match(/\((.*?)\)/);
|
|
12
|
+
if (example)
|
|
13
|
+
t.example = { ...example[1].split(',') };
|
|
21
14
|
const suite = t.suite_title.split('.');
|
|
22
15
|
t.suite_title = suite.pop();
|
|
23
16
|
t.file = namespaceToFileName(t.file);
|
|
17
|
+
t.title = title.trim();
|
|
24
18
|
return t;
|
|
25
19
|
}
|
|
26
20
|
getFilePath(t) {
|
|
27
|
-
|
|
28
|
-
return null;
|
|
29
|
-
// Normalize path separators for cross-platform compatibility
|
|
30
|
-
let filePath = t.file.replace(/\\/g, '/');
|
|
31
|
-
// If file already has .cs extension, use it directly
|
|
32
|
-
if (filePath.endsWith('.cs')) {
|
|
33
|
-
// Make relative path if it's absolute
|
|
34
|
-
if (path_1.default.isAbsolute(filePath)) {
|
|
35
|
-
// Try to find project-relative path
|
|
36
|
-
const cwd = process.cwd().replace(/\\/g, '/');
|
|
37
|
-
if (filePath.startsWith(cwd)) {
|
|
38
|
-
filePath = path_1.default.relative(cwd, filePath).replace(/\\/g, '/');
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
return filePath;
|
|
42
|
-
}
|
|
43
|
-
// Convert namespace path to file path
|
|
44
|
-
const fileName = namespaceToFileName(filePath);
|
|
21
|
+
const fileName = namespaceToFileName(t.file);
|
|
45
22
|
return fileName;
|
|
46
23
|
}
|
|
47
24
|
}
|
|
48
25
|
module.exports = CSharpAdapter;
|
|
49
26
|
function namespaceToFileName(fileName) {
|
|
50
|
-
if (!fileName)
|
|
51
|
-
return '';
|
|
52
|
-
// If already a .cs file path, clean it up
|
|
53
|
-
if (fileName.endsWith('.cs')) {
|
|
54
|
-
return fileName.replace(/\\/g, '/');
|
|
55
|
-
}
|
|
56
27
|
const fileParts = fileName.split('.');
|
|
57
28
|
fileParts[fileParts.length - 1] = fileParts[fileParts.length - 1]?.replace(/\$.*/, '');
|
|
58
|
-
return `${fileParts.join(
|
|
29
|
+
return `${fileParts.join(path_1.default.sep)}.cs`;
|
|
59
30
|
}
|
package/lib/pipe/debug.js
CHANGED
|
@@ -18,7 +18,7 @@ class DebugPipe {
|
|
|
18
18
|
this.isEnabled = !!process.env.TESTOMATIO_DEBUG || !!process.env.DEBUG;
|
|
19
19
|
if (this.isEnabled) {
|
|
20
20
|
this.batch = {
|
|
21
|
-
isEnabled: this.params.isBatchEnabled ??
|
|
21
|
+
isEnabled: this.params.isBatchEnabled ?? !process.env.TESTOMATIO_DISABLE_BATCH_UPLOAD ?? true,
|
|
22
22
|
intervalFunction: null,
|
|
23
23
|
intervalTime: 5000,
|
|
24
24
|
tests: [],
|
package/lib/pipe/testomatio.js
CHANGED
|
@@ -23,7 +23,7 @@ if (process.env.TESTOMATIO_RUN)
|
|
|
23
23
|
class TestomatioPipe {
|
|
24
24
|
constructor(params, store) {
|
|
25
25
|
this.batch = {
|
|
26
|
-
isEnabled: params?.isBatchEnabled ??
|
|
26
|
+
isEnabled: params?.isBatchEnabled ?? !process.env.TESTOMATIO_DISABLE_BATCH_UPLOAD ?? true,
|
|
27
27
|
intervalFunction: null, // will be created in createRun by setInterval function
|
|
28
28
|
intervalTime: 5000, // how often tests are sent
|
|
29
29
|
tests: [], // array of tests in batch
|
|
@@ -60,7 +60,7 @@ class TestomatioPipe {
|
|
|
60
60
|
retry: constants_js_1.REPORTER_REQUEST_RETRIES.retriesPerRequest,
|
|
61
61
|
retryDelay: constants_js_1.REPORTER_REQUEST_RETRIES.retryTimeout,
|
|
62
62
|
httpMethodsToRetry: ['GET', 'PUT', 'HEAD', 'OPTIONS', 'DELETE', 'POST'],
|
|
63
|
-
shouldRetry: error => {
|
|
63
|
+
shouldRetry: (error) => {
|
|
64
64
|
if (!error.response)
|
|
65
65
|
return false;
|
|
66
66
|
switch (error.response?.status) {
|
|
@@ -73,8 +73,8 @@ class TestomatioPipe {
|
|
|
73
73
|
break;
|
|
74
74
|
}
|
|
75
75
|
return error.response?.status >= 401; // Retry on 401+ and 5xx
|
|
76
|
-
}
|
|
77
|
-
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
78
|
});
|
|
79
79
|
this.isEnabled = true;
|
|
80
80
|
// do not finish this run (for parallel testing)
|
|
@@ -193,7 +193,7 @@ class TestomatioPipe {
|
|
|
193
193
|
method: 'PUT',
|
|
194
194
|
url: `/api/reporter/${this.runId}`,
|
|
195
195
|
data: runParams,
|
|
196
|
-
responseType: 'json'
|
|
196
|
+
responseType: 'json'
|
|
197
197
|
});
|
|
198
198
|
if (resp.data.artifacts)
|
|
199
199
|
(0, pipe_utils_js_1.setS3Credentials)(resp.data.artifacts);
|
|
@@ -206,7 +206,7 @@ class TestomatioPipe {
|
|
|
206
206
|
url: '/api/reporter',
|
|
207
207
|
data: runParams,
|
|
208
208
|
maxContentLength: Infinity,
|
|
209
|
-
responseType: 'json'
|
|
209
|
+
responseType: 'json'
|
|
210
210
|
});
|
|
211
211
|
this.runId = resp.data.uid;
|
|
212
212
|
this.runUrl = `${this.url}/${resp.data.url.split('/').splice(3).join('/')}`;
|
|
@@ -259,17 +259,15 @@ class TestomatioPipe {
|
|
|
259
259
|
this.#formatData(data);
|
|
260
260
|
const json = json_cycle_1.default.stringify(data);
|
|
261
261
|
debug('Adding test', json);
|
|
262
|
-
return this.client
|
|
263
|
-
.request({
|
|
262
|
+
return this.client.request({
|
|
264
263
|
method: 'POST',
|
|
265
264
|
url: `/api/reporter/${this.runId}/testrun`,
|
|
266
265
|
data: json,
|
|
267
266
|
headers: {
|
|
268
267
|
'Content-Type': 'application/json',
|
|
269
268
|
},
|
|
270
|
-
maxContentLength: Infinity
|
|
271
|
-
})
|
|
272
|
-
.catch(err => {
|
|
269
|
+
maxContentLength: Infinity
|
|
270
|
+
}).catch(err => {
|
|
273
271
|
this.requestFailures++;
|
|
274
272
|
this.notReportedTestsCount++;
|
|
275
273
|
if (err.response) {
|
|
@@ -314,21 +312,19 @@ class TestomatioPipe {
|
|
|
314
312
|
// get tests from batch and clear batch
|
|
315
313
|
const testsToSend = this.batch.tests.splice(0);
|
|
316
314
|
debug('📨 Batch upload', testsToSend.length, 'tests');
|
|
317
|
-
return this.client
|
|
318
|
-
.request({
|
|
315
|
+
return this.client.request({
|
|
319
316
|
method: 'POST',
|
|
320
317
|
url: `/api/reporter/${this.runId}/testrun`,
|
|
321
318
|
data: {
|
|
322
319
|
api_key: this.apiKey,
|
|
323
320
|
tests: testsToSend,
|
|
324
|
-
batch_index: this.batch.batchIndex
|
|
321
|
+
batch_index: this.batch.batchIndex
|
|
325
322
|
},
|
|
326
323
|
headers: {
|
|
327
324
|
'Content-Type': 'application/json',
|
|
328
325
|
},
|
|
329
|
-
maxContentLength: Infinity
|
|
330
|
-
})
|
|
331
|
-
.catch(err => {
|
|
326
|
+
maxContentLength: Infinity
|
|
327
|
+
}).catch(err => {
|
|
332
328
|
this.requestFailures++;
|
|
333
329
|
this.notReportedTestsCount += testsToSend.length;
|
|
334
330
|
if (err.response) {
|
|
@@ -412,7 +408,7 @@ class TestomatioPipe {
|
|
|
412
408
|
status_event,
|
|
413
409
|
detach: params.detach,
|
|
414
410
|
tests: params.tests,
|
|
415
|
-
}
|
|
411
|
+
}
|
|
416
412
|
});
|
|
417
413
|
console.log(constants_js_1.APP_PREFIX, '✅ Testrun finished');
|
|
418
414
|
if (this.runUrl) {
|
package/lib/utils/utils.js
CHANGED
|
@@ -173,8 +173,6 @@ exports.fetchSourceCodeFromStackTrace = fetchSourceCodeFromStackTrace;
|
|
|
173
173
|
exports.TEST_ID_REGEX = /@T([\w\d]{8})/;
|
|
174
174
|
exports.SUITE_ID_REGEX = /@S([\w\d]{8})/;
|
|
175
175
|
const fetchIdFromCode = (code, opts = {}) => {
|
|
176
|
-
if (!code)
|
|
177
|
-
return null;
|
|
178
176
|
const comments = code
|
|
179
177
|
.split('\n')
|
|
180
178
|
.map(l => l.trim())
|
|
@@ -217,29 +215,10 @@ const fetchSourceCode = (contents, opts = {}) => {
|
|
|
217
215
|
lineIndex = lines.findIndex(l => l.includes(`${title}(`));
|
|
218
216
|
}
|
|
219
217
|
else if (opts.lang === 'csharp') {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
if (lineIndex === -1)
|
|
223
|
-
lineIndex = lines.findIndex(l => l.includes(`public async Task ${title}(`));
|
|
224
|
-
}
|
|
225
|
-
if (lineIndex === -1) {
|
|
218
|
+
if (lineIndex === -1)
|
|
219
|
+
lineIndex = lines.findIndex(l => l.includes(`public void ${title}`));
|
|
220
|
+
if (lineIndex === -1)
|
|
226
221
|
lineIndex = lines.findIndex(l => l.includes(`${title}(`));
|
|
227
|
-
}
|
|
228
|
-
// Look for TestCase or Test attributes above the method
|
|
229
|
-
if (lineIndex === -1) {
|
|
230
|
-
const testAttributeIndex = lines.findIndex((l, index) => {
|
|
231
|
-
if (l.includes('[TestCase') || l.includes('[Test')) {
|
|
232
|
-
// Check next few lines for the method
|
|
233
|
-
const nextLines = lines.slice(index, Math.min(lines.length, index + 5));
|
|
234
|
-
const hasMethod = nextLines.some(nextLine => nextLine.includes(`${title}(`));
|
|
235
|
-
return hasMethod;
|
|
236
|
-
}
|
|
237
|
-
return false;
|
|
238
|
-
});
|
|
239
|
-
if (testAttributeIndex !== -1) {
|
|
240
|
-
lineIndex = testAttributeIndex;
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
222
|
}
|
|
244
223
|
else {
|
|
245
224
|
lineIndex = lines.findIndex(l => l.includes(title));
|
|
@@ -248,7 +227,7 @@ const fetchSourceCode = (contents, opts = {}) => {
|
|
|
248
227
|
if (opts.prepend) {
|
|
249
228
|
lineIndex -= opts.prepend;
|
|
250
229
|
}
|
|
251
|
-
if (lineIndex
|
|
230
|
+
if (lineIndex) {
|
|
252
231
|
const result = [];
|
|
253
232
|
for (let i = lineIndex; i < lineIndex + limit; i++) {
|
|
254
233
|
if (lines[i] === undefined)
|
|
@@ -291,14 +270,6 @@ const fetchSourceCode = (contents, opts = {}) => {
|
|
|
291
270
|
break;
|
|
292
271
|
if (opts.lang === 'java' && lines[i].includes(' class '))
|
|
293
272
|
break;
|
|
294
|
-
if (opts.lang === 'csharp' && lines[i].trim().match(/^\[Test/))
|
|
295
|
-
break;
|
|
296
|
-
if (opts.lang === 'csharp' && lines[i].includes(' public void '))
|
|
297
|
-
break;
|
|
298
|
-
if (opts.lang === 'csharp' && lines[i].includes(' public async Task '))
|
|
299
|
-
break;
|
|
300
|
-
if (opts.lang === 'csharp' && lines[i].includes(' class ') && lines[i].includes('public'))
|
|
301
|
-
break;
|
|
302
273
|
}
|
|
303
274
|
result.push(lines[i]);
|
|
304
275
|
}
|
package/lib/xmlReader.d.ts
CHANGED
|
@@ -77,13 +77,6 @@ declare class XmlReader {
|
|
|
77
77
|
skipped_count: number;
|
|
78
78
|
tests: any[];
|
|
79
79
|
};
|
|
80
|
-
deduplicateTestsByFQN(tests: any): any[];
|
|
81
|
-
generateFQN(test: any): string;
|
|
82
|
-
generateNormalizedFQN(test: any): string;
|
|
83
|
-
extractAssemblyName(test: any): any;
|
|
84
|
-
extractNamespace(test: any): any;
|
|
85
|
-
extractClassName(test: any): any;
|
|
86
|
-
extractCsFileFromPath(test: any): any;
|
|
87
80
|
calculateStats(): {};
|
|
88
81
|
fetchSourceCode(): void;
|
|
89
82
|
formatTests(): void;
|
package/lib/xmlReader.js
CHANGED
|
@@ -131,21 +131,7 @@ class XmlReader {
|
|
|
131
131
|
const { result, total, passed, failed, inconclusive, skipped } = jsonSuite;
|
|
132
132
|
reduceOptions.preferClassname = this.stats.language === 'python';
|
|
133
133
|
const resultTests = processTestSuite(jsonSuite['test-suite']);
|
|
134
|
-
|
|
135
|
-
debug('Raw tests:', resultTests.map(t => ({ title: t.title, example: t.example, file: t.file })));
|
|
136
|
-
// Optional deduplication for complex NUnit scenarios - can be enabled via options
|
|
137
|
-
let finalTests = resultTests;
|
|
138
|
-
if (this.opts.enableNUnitDeduplication) {
|
|
139
|
-
finalTests = this.deduplicateTestsByFQN(resultTests);
|
|
140
|
-
debug('Tests after deduplication:', finalTests.length);
|
|
141
|
-
debug('Deduplicated tests:', finalTests.map(t => ({
|
|
142
|
-
title: t.title,
|
|
143
|
-
examples: t.examples,
|
|
144
|
-
example: t.example,
|
|
145
|
-
file: t.file,
|
|
146
|
-
})));
|
|
147
|
-
}
|
|
148
|
-
this.tests = this.tests.concat(finalTests);
|
|
134
|
+
this.tests = this.tests.concat(resultTests);
|
|
149
135
|
return {
|
|
150
136
|
status: result?.toLowerCase(),
|
|
151
137
|
create_tests: true,
|
|
@@ -153,7 +139,7 @@ class XmlReader {
|
|
|
153
139
|
passed_count: parseInt(passed, 10),
|
|
154
140
|
failed_count: parseInt(failed, 10),
|
|
155
141
|
skipped_count: parseInt(inconclusive + skipped, 10),
|
|
156
|
-
tests:
|
|
142
|
+
tests: resultTests,
|
|
157
143
|
};
|
|
158
144
|
}
|
|
159
145
|
processTRX(jsonSuite) {
|
|
@@ -289,171 +275,6 @@ class XmlReader {
|
|
|
289
275
|
tests,
|
|
290
276
|
};
|
|
291
277
|
}
|
|
292
|
-
deduplicateTestsByFQN(tests) {
|
|
293
|
-
const fqnMap = new Map();
|
|
294
|
-
tests.forEach(test => {
|
|
295
|
-
const fqn = this.generateNormalizedFQN(test);
|
|
296
|
-
if (fqnMap.has(fqn)) {
|
|
297
|
-
const existingTest = fqnMap.get(fqn);
|
|
298
|
-
// For parameterized tests, merge as Examples
|
|
299
|
-
if (test.example && Array.isArray(test.example) && test.example.length > 0) {
|
|
300
|
-
// Initialize examples array if it doesn't exist
|
|
301
|
-
if (!existingTest.examples) {
|
|
302
|
-
existingTest.examples = [];
|
|
303
|
-
// Add the existing test's example as the first item if it has parameters
|
|
304
|
-
if (existingTest.example && Array.isArray(existingTest.example) && existingTest.example.length > 0) {
|
|
305
|
-
existingTest.examples.push({
|
|
306
|
-
parameters: existingTest.example,
|
|
307
|
-
status: existingTest.status,
|
|
308
|
-
run_time: existingTest.run_time,
|
|
309
|
-
message: existingTest.message,
|
|
310
|
-
stack: existingTest.stack,
|
|
311
|
-
});
|
|
312
|
-
// Clear the main test's example since it's now in examples array
|
|
313
|
-
delete existingTest.example;
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
// Add this test's execution as an example
|
|
317
|
-
existingTest.examples.push({
|
|
318
|
-
parameters: test.example,
|
|
319
|
-
status: test.status,
|
|
320
|
-
run_time: test.run_time,
|
|
321
|
-
message: test.message,
|
|
322
|
-
stack: test.stack,
|
|
323
|
-
});
|
|
324
|
-
// Update the main test status to reflect the worst status
|
|
325
|
-
if (test.status === 'failed' || existingTest.status === 'failed') {
|
|
326
|
-
existingTest.status = 'failed';
|
|
327
|
-
}
|
|
328
|
-
else if (test.status === 'skipped' && existingTest.status !== 'failed') {
|
|
329
|
-
existingTest.status = 'skipped';
|
|
330
|
-
}
|
|
331
|
-
// Update total run time
|
|
332
|
-
existingTest.run_time = (existingTest.run_time || 0) + (test.run_time || 0);
|
|
333
|
-
}
|
|
334
|
-
else {
|
|
335
|
-
// Merge test properties for non-parameterized tests, prioritizing Test Explorer structure
|
|
336
|
-
if (test.test_id && !existingTest.test_id) {
|
|
337
|
-
existingTest.test_id = test.test_id;
|
|
338
|
-
}
|
|
339
|
-
// Keep the most complete test data
|
|
340
|
-
if (test.stack && !existingTest.stack) {
|
|
341
|
-
existingTest.stack = test.stack;
|
|
342
|
-
}
|
|
343
|
-
if (test.message && !existingTest.message) {
|
|
344
|
-
existingTest.message = test.message;
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
// Prefer Test Explorer structure (longer, more complete suite_title)
|
|
348
|
-
if (test.suite_title && test.suite_title.length > existingTest.suite_title.length) {
|
|
349
|
-
existingTest.suite_title = test.suite_title;
|
|
350
|
-
}
|
|
351
|
-
// Always use the source file path if available
|
|
352
|
-
if (test.file && test.file.endsWith('.cs')) {
|
|
353
|
-
existingTest.file = test.file;
|
|
354
|
-
}
|
|
355
|
-
else if (!existingTest.file || !existingTest.file.endsWith('.cs')) {
|
|
356
|
-
existingTest.file = this.extractCsFileFromPath(test);
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
else {
|
|
360
|
-
// Fix file path to use proper .cs file names from source paths
|
|
361
|
-
if (!test.file || !test.file.endsWith('.cs')) {
|
|
362
|
-
test.file = this.extractCsFileFromPath(test);
|
|
363
|
-
}
|
|
364
|
-
fqnMap.set(fqn, test);
|
|
365
|
-
}
|
|
366
|
-
});
|
|
367
|
-
return Array.from(fqnMap.values());
|
|
368
|
-
}
|
|
369
|
-
generateFQN(test) {
|
|
370
|
-
// Generate Fully Qualified Name: Namespace + Class + Method (standard .NET FQN)
|
|
371
|
-
// Don't include assembly as it can vary between different test structures
|
|
372
|
-
const namespace = this.extractNamespace(test);
|
|
373
|
-
const className = this.extractClassName(test);
|
|
374
|
-
const methodName = test.title;
|
|
375
|
-
// Use the most complete namespace.class structure available
|
|
376
|
-
if (test.suite_title && test.suite_title.includes('.')) {
|
|
377
|
-
return `${test.suite_title}.${methodName}`;
|
|
378
|
-
}
|
|
379
|
-
return `${namespace}.${className}.${methodName}`;
|
|
380
|
-
}
|
|
381
|
-
generateNormalizedFQN(test) {
|
|
382
|
-
// Generate normalized FQN for deduplication by extracting the core namespace.class.method
|
|
383
|
-
// For parameterized tests, we want the SAME FQN so they merge into one test with multiple Examples
|
|
384
|
-
const fullClassName = test.suite_title || '';
|
|
385
|
-
const methodName = test.title;
|
|
386
|
-
// Extract the most specific namespace.class pattern
|
|
387
|
-
if (fullClassName.includes('.')) {
|
|
388
|
-
const parts = fullClassName.split('.');
|
|
389
|
-
if (parts.length >= 2) {
|
|
390
|
-
const className = parts[parts.length - 1];
|
|
391
|
-
// Look for common .NET namespace patterns and normalize them:
|
|
392
|
-
// TestProject.Tests.MyClass -> Tests.MyClass
|
|
393
|
-
// Tests.MyClass -> Tests.MyClass
|
|
394
|
-
// MyProject.SubNamespace.Tests.MyClass -> Tests.MyClass
|
|
395
|
-
let normalizedNamespace = '';
|
|
396
|
-
for (let i = parts.length - 2; i >= 0; i--) {
|
|
397
|
-
const part = parts[i];
|
|
398
|
-
// Build namespace from right to left, excluding project names
|
|
399
|
-
if (part === 'Tests' || part.endsWith('Tests') || part.includes('Test')) {
|
|
400
|
-
// Found a test namespace, use it as the normalized namespace
|
|
401
|
-
normalizedNamespace = part;
|
|
402
|
-
break;
|
|
403
|
-
}
|
|
404
|
-
else if (i === parts.length - 2) {
|
|
405
|
-
// If no test namespace found, use the immediate parent as namespace
|
|
406
|
-
normalizedNamespace = part;
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
return `${normalizedNamespace}.${className}.${methodName}`;
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
// Fallback for simple class names
|
|
413
|
-
return `${fullClassName}.${methodName}`;
|
|
414
|
-
}
|
|
415
|
-
extractAssemblyName(test) {
|
|
416
|
-
// Extract assembly name from file path or use default
|
|
417
|
-
if (test.file) {
|
|
418
|
-
const parts = test.file.split(/[/\\]/);
|
|
419
|
-
return parts[0] || 'DefaultAssembly';
|
|
420
|
-
}
|
|
421
|
-
return 'DefaultAssembly';
|
|
422
|
-
}
|
|
423
|
-
extractNamespace(test) {
|
|
424
|
-
// Extract namespace from suite_title or classname
|
|
425
|
-
if (test.suite_title && test.suite_title.includes('.')) {
|
|
426
|
-
const parts = test.suite_title.split('.');
|
|
427
|
-
return parts.slice(0, -1).join('.');
|
|
428
|
-
}
|
|
429
|
-
return test.suite_title || 'DefaultNamespace';
|
|
430
|
-
}
|
|
431
|
-
extractClassName(test) {
|
|
432
|
-
// Extract class name from suite_title
|
|
433
|
-
if (test.suite_title && test.suite_title.includes('.')) {
|
|
434
|
-
const parts = test.suite_title.split('.');
|
|
435
|
-
return parts[parts.length - 1];
|
|
436
|
-
}
|
|
437
|
-
return test.suite_title || 'DefaultClass';
|
|
438
|
-
}
|
|
439
|
-
extractCsFileFromPath(test) {
|
|
440
|
-
// Extract .cs file name from source file path, not namespace
|
|
441
|
-
if (test.file) {
|
|
442
|
-
// Look for actual .cs file path patterns
|
|
443
|
-
const csFileMatch = test.file.match(/([^/\\]+\.cs)$/);
|
|
444
|
-
if (csFileMatch) {
|
|
445
|
-
return test.file;
|
|
446
|
-
}
|
|
447
|
-
// If no .cs extension, assume it's a namespace path and convert to likely file name
|
|
448
|
-
const className = this.extractClassName(test);
|
|
449
|
-
const pathParts = test.file.split(/[/\\]/);
|
|
450
|
-
pathParts[pathParts.length - 1] = `${className}.cs`;
|
|
451
|
-
return pathParts.join('/');
|
|
452
|
-
}
|
|
453
|
-
// Fallback to class name
|
|
454
|
-
const className = this.extractClassName(test);
|
|
455
|
-
return `${className}.cs`;
|
|
456
|
-
}
|
|
457
278
|
calculateStats() {
|
|
458
279
|
this.stats = {
|
|
459
280
|
...this.stats,
|
|
@@ -503,9 +324,7 @@ class XmlReader {
|
|
|
503
324
|
return;
|
|
504
325
|
}
|
|
505
326
|
const contents = fs_1.default.readFileSync(file).toString();
|
|
506
|
-
|
|
507
|
-
const titleForLookup = t.originalTestName ? t.originalTestName.replace(/\(.*?\)/, '').trim() : t.title;
|
|
508
|
-
t.code = (0, utils_js_1.fetchSourceCode)(contents, { ...t, title: titleForLookup, lang: this.stats.language });
|
|
327
|
+
t.code = (0, utils_js_1.fetchSourceCode)(contents, { ...t, lang: this.stats.language });
|
|
509
328
|
if (t.code)
|
|
510
329
|
debug('Fetched code for test %s', t.title);
|
|
511
330
|
t.test_id = (0, utils_js_1.fetchIdFromCode)(t.code, { lang: this.stats.language });
|
|
@@ -611,12 +430,7 @@ function reduceTestCases(prev, item) {
|
|
|
611
430
|
testCases
|
|
612
431
|
.filter(t => !!t)
|
|
613
432
|
.forEach(testCaseItem => {
|
|
614
|
-
|
|
615
|
-
let file = testCaseItem.file || item.filepath || item.fullname || item.package || '';
|
|
616
|
-
// If no file found with simple approach and we have enhanced extraction enabled, use it
|
|
617
|
-
if (!file && item.filepath) {
|
|
618
|
-
file = extractSourceFilePath(testCaseItem, item);
|
|
619
|
-
}
|
|
433
|
+
const file = testCaseItem.file || item.filepath || item.fullname || item.package || '';
|
|
620
434
|
let stack = '';
|
|
621
435
|
let message = '';
|
|
622
436
|
if (testCaseItem.error)
|
|
@@ -640,31 +454,13 @@ function reduceTestCases(prev, item) {
|
|
|
640
454
|
// SpecFlow config
|
|
641
455
|
let { title, tags, testId } = fetchProperties(isParametrized ? item : testCaseItem);
|
|
642
456
|
let example = null;
|
|
643
|
-
|
|
644
|
-
let suiteTitle = preferClassname ? testCaseItem.classname : item.name || testCaseItem.classname;
|
|
645
|
-
if (!suiteTitle && item.fullname) {
|
|
646
|
-
suiteTitle = extractTestExplorerSuiteTitle(testCaseItem, item);
|
|
647
|
-
}
|
|
457
|
+
const suiteTitle = preferClassname ? testCaseItem.classname : item.name || testCaseItem.classname;
|
|
648
458
|
title ||= testCaseItem.name || testCaseItem.methodname || testCaseItem.classname;
|
|
649
459
|
tags ||= [];
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
const args = Array.isArray(testCaseItem.arguments.arg)
|
|
655
|
-
? testCaseItem.arguments.arg
|
|
656
|
-
: [testCaseItem.arguments.arg];
|
|
657
|
-
example = args; // Store as array instead of object
|
|
658
|
-
// Remove parameters from title for NUnit tests
|
|
659
|
-
title = (testCaseItem.methodname || title).replace(/\(.*?\)/, '').trim();
|
|
660
|
-
}
|
|
661
|
-
else {
|
|
662
|
-
// Simple parameter extraction (version 2.1.1 approach)
|
|
663
|
-
const exampleMatches = testCaseItem.name?.match(/\S\((.*?)\)/);
|
|
664
|
-
if (exampleMatches) {
|
|
665
|
-
example = { ...exampleMatches[1].split(',').map(v => v.trim().replace(/[^\w\s-]/g, '')) };
|
|
666
|
-
title = title.replace(/\(.*?\)/, '').trim();
|
|
667
|
-
}
|
|
460
|
+
const exampleMatches = testCaseItem.name?.match(/\S\((.*?)\)/);
|
|
461
|
+
if (exampleMatches) {
|
|
462
|
+
example = { ...exampleMatches[1].split(',').map(v => v.trim().replace(/[^\w\s-]/g, '')) };
|
|
463
|
+
title = title.replace(/\(.*?\)/, '').trim();
|
|
668
464
|
}
|
|
669
465
|
stack = `${testCaseItem['system-out'] || testCaseItem.output || testCaseItem.log || ''}\n\n${stack}\n\n${suiteOutput}\n\n${suiteErr}`.trim();
|
|
670
466
|
if (!testId)
|
|
@@ -712,7 +508,6 @@ function reduceTestCases(prev, item) {
|
|
|
712
508
|
run_time: parseFloat(testCaseItem.time || testCaseItem.duration) * 1000,
|
|
713
509
|
status,
|
|
714
510
|
title,
|
|
715
|
-
originalTestName, // Store original name for enhanced features
|
|
716
511
|
root_suite_id: TESTOMATIO_SUITE,
|
|
717
512
|
suite_title: suiteTitle,
|
|
718
513
|
files,
|
|
@@ -721,102 +516,6 @@ function reduceTestCases(prev, item) {
|
|
|
721
516
|
});
|
|
722
517
|
return prev;
|
|
723
518
|
}
|
|
724
|
-
function extractSourceFilePath(testCaseItem, item) {
|
|
725
|
-
// Priority order for file path extraction to match Test Explorer structure:
|
|
726
|
-
// 1. filepath attribute (direct .cs file path from NUnit)
|
|
727
|
-
// 2. fullname (contains full project path)
|
|
728
|
-
// 3. file attribute from test case
|
|
729
|
-
// 4. package (fallback)
|
|
730
|
-
// NUnit provides filepath attribute with actual .cs file path - use this first
|
|
731
|
-
if (item.filepath) {
|
|
732
|
-
// Clean up Windows/Unix path separators and ensure proper format
|
|
733
|
-
let filePath = item.filepath.replace(/\\/g, '/');
|
|
734
|
-
// Make relative to current working directory if absolute
|
|
735
|
-
if (path_1.default.isAbsolute(item.filepath)) {
|
|
736
|
-
const cwd = process.cwd().replace(/\\/g, '/');
|
|
737
|
-
if (filePath.startsWith(cwd)) {
|
|
738
|
-
filePath = path_1.default.relative(cwd, item.filepath).replace(/\\/g, '/');
|
|
739
|
-
}
|
|
740
|
-
else {
|
|
741
|
-
// Try to extract relative path from common patterns
|
|
742
|
-
const commonPatterns = ['/Tests/', '/test/', '/src/', '/Test/'];
|
|
743
|
-
for (const pattern of commonPatterns) {
|
|
744
|
-
const index = filePath.lastIndexOf(pattern);
|
|
745
|
-
if (index !== -1) {
|
|
746
|
-
filePath = filePath.substring(index + 1);
|
|
747
|
-
break;
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
return filePath;
|
|
753
|
-
}
|
|
754
|
-
if (testCaseItem.file) {
|
|
755
|
-
let filePath = testCaseItem.file.replace(/\\/g, '/');
|
|
756
|
-
// Make relative to current working directory if absolute
|
|
757
|
-
if (path_1.default.isAbsolute(testCaseItem.file)) {
|
|
758
|
-
const cwd = process.cwd().replace(/\\/g, '/');
|
|
759
|
-
if (filePath.startsWith(cwd)) {
|
|
760
|
-
filePath = path_1.default.relative(cwd, testCaseItem.file).replace(/\\/g, '/');
|
|
761
|
-
}
|
|
762
|
-
else {
|
|
763
|
-
// Try to extract relative path from common patterns
|
|
764
|
-
const commonPatterns = ['/Tests/', '/test/', '/src/', '/Test/'];
|
|
765
|
-
for (const pattern of commonPatterns) {
|
|
766
|
-
const index = filePath.lastIndexOf(pattern);
|
|
767
|
-
if (index !== -1) {
|
|
768
|
-
filePath = filePath.substring(index + 1);
|
|
769
|
-
break;
|
|
770
|
-
}
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
}
|
|
774
|
-
return filePath;
|
|
775
|
-
}
|
|
776
|
-
if (item.fullname) {
|
|
777
|
-
// Extract actual file path from fullname if it contains path separators
|
|
778
|
-
const fullnameParts = item.fullname.split('.');
|
|
779
|
-
if (fullnameParts.length > 2) {
|
|
780
|
-
// For ParameterizedMethod, get the class name (not method name)
|
|
781
|
-
// Example: "NUnit_sample_test.Tests.SampleTests.TestBooleanValue" -> "Tests/SampleTests.cs"
|
|
782
|
-
let namespaceParts, className;
|
|
783
|
-
if (item.type === 'ParameterizedMethod') {
|
|
784
|
-
// For parameterized methods, the last part is the method name, second-to-last is class
|
|
785
|
-
namespaceParts = fullnameParts.slice(1, -2); // Skip project name and method name
|
|
786
|
-
className = fullnameParts[fullnameParts.length - 2]; // Get class name
|
|
787
|
-
}
|
|
788
|
-
else {
|
|
789
|
-
// For regular classes/fixtures
|
|
790
|
-
namespaceParts = fullnameParts.slice(1, -1); // Skip project name
|
|
791
|
-
className = fullnameParts[fullnameParts.length - 1];
|
|
792
|
-
}
|
|
793
|
-
return `${namespaceParts.join('/')}/${className}.cs`;
|
|
794
|
-
}
|
|
795
|
-
}
|
|
796
|
-
if (item.package)
|
|
797
|
-
return item.package.replace(/\\/g, '/');
|
|
798
|
-
// Fallback: construct from classname
|
|
799
|
-
if (testCaseItem.classname) {
|
|
800
|
-
const parts = testCaseItem.classname.split('.');
|
|
801
|
-
const className = parts[parts.length - 1];
|
|
802
|
-
const namespacePath = parts.slice(0, -1).join('/');
|
|
803
|
-
return `${namespacePath}/${className}.cs`;
|
|
804
|
-
}
|
|
805
|
-
return '';
|
|
806
|
-
}
|
|
807
|
-
function extractTestExplorerSuiteTitle(testCaseItem, item) {
|
|
808
|
-
// Extract suite title to match Test Explorer structure (Project/Namespace hierarchy)
|
|
809
|
-
// Priority: fullname > classname > name
|
|
810
|
-
if (item.fullname) {
|
|
811
|
-
// Use fullname to maintain Test Explorer structure
|
|
812
|
-
return item.fullname;
|
|
813
|
-
}
|
|
814
|
-
if (testCaseItem.classname) {
|
|
815
|
-
return testCaseItem.classname;
|
|
816
|
-
}
|
|
817
|
-
// Fallback to item name but prefer classname structure
|
|
818
|
-
return item.name || testCaseItem.classname || 'UnknownClass';
|
|
819
|
-
}
|
|
820
519
|
function processTestSuite(testsuite) {
|
|
821
520
|
if (!testsuite)
|
|
822
521
|
return [];
|
|
@@ -828,8 +527,7 @@ function processTestSuite(testsuite) {
|
|
|
828
527
|
if (!Array.isArray(testsuite)) {
|
|
829
528
|
suites = [testsuite];
|
|
830
529
|
}
|
|
831
|
-
|
|
832
|
-
const subSuites = suites.filter(s => s['test-suite'] && !s['test-case']);
|
|
530
|
+
const subSuites = suites.filter(s => s['test-suite'] && !testsuite['test-case']);
|
|
833
531
|
return [...subSuites.map(s => processTestSuite(s['test-suite'])), ...suites.reduce(reduceTestCases, [])].flat();
|
|
834
532
|
}
|
|
835
533
|
function fetchProperties(item) {
|