@testomatio/reporter 2.3.4 β 2.3.5-beta-6-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/junit-adapter/csharp.d.ts +0 -1
- package/lib/junit-adapter/csharp.js +36 -7
- package/lib/pipe/debug.js +1 -1
- package/lib/pipe/testomatio.js +42 -30
- package/lib/utils/utils.js +33 -4
- package/lib/xmlReader.d.ts +9 -0
- package/lib/xmlReader.js +357 -11
- package/package.json +1 -1
- package/src/junit-adapter/csharp.js +40 -6
- package/src/pipe/debug.js +2 -3
- package/src/pipe/testomatio.js +103 -91
- package/src/utils/utils.js +33 -3
- package/src/xmlReader.js +409 -10
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ Testomat.io Reporter (this npm package) supports:
|
|
|
13
13
|
- π [Stack traces](./docs/stacktrace.md) and error messages
|
|
14
14
|
- π [GitHub](./docs/pipes/github.md), [GitLab](./docs/pipes/gitlab.md) & [Bitbucket](./docs/pipes/bitbucket.md) integration
|
|
15
15
|
- π
Realtime reports
|
|
16
|
-
- ποΈ Other test frameworks supported via [
|
|
16
|
+
- ποΈ Other test frameworks supported via [JUnit XML](./docs/junit.md) with [XML import configuration](./docs/xml-imports.md)
|
|
17
17
|
- πΆββοΈ Steps _(work in progress)_
|
|
18
18
|
- π [Logger](./docs/logger.md) _(work in progress, supports Jest for now)_
|
|
19
19
|
- βοΈ Custom properties and metadata _(work in progress)_
|
|
@@ -7,24 +7,53 @@ 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 (example)
|
|
13
|
-
|
|
10
|
+
// Don't override example if it already exists from NUnit XML processing
|
|
11
|
+
// The xmlReader.js already extracts parameters correctly from <arguments>
|
|
12
|
+
if (!t.example) {
|
|
13
|
+
const title = t.title.replace(/\(.*?\)/, '').trim();
|
|
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
|
+
}
|
|
14
21
|
const suite = t.suite_title.split('.');
|
|
15
22
|
t.suite_title = suite.pop();
|
|
16
23
|
t.file = namespaceToFileName(t.file);
|
|
17
|
-
t.title = title.trim();
|
|
18
24
|
return t;
|
|
19
25
|
}
|
|
20
26
|
getFilePath(t) {
|
|
21
|
-
|
|
27
|
+
if (!t.file)
|
|
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);
|
|
22
45
|
return fileName;
|
|
23
46
|
}
|
|
24
47
|
}
|
|
25
48
|
module.exports = CSharpAdapter;
|
|
26
49
|
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
|
+
}
|
|
27
56
|
const fileParts = fileName.split('.');
|
|
28
57
|
fileParts[fileParts.length - 1] = fileParts[fileParts.length - 1]?.replace(/\$.*/, '');
|
|
29
|
-
return `${fileParts.join(
|
|
58
|
+
return `${fileParts.join('/')}.cs`;
|
|
30
59
|
}
|
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 ? false : 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 ? false : 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:
|
|
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)
|
|
@@ -89,6 +89,28 @@ class TestomatioPipe {
|
|
|
89
89
|
console.error(constants_js_1.APP_PREFIX, picocolors_1.default.red(`Error creating report on Testomat.io, report url '${this.url}' is invalid`));
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* Prepares data for sending to Testomat.io.
|
|
94
|
+
* @param {*} data - The data to be formatted.
|
|
95
|
+
* @returns
|
|
96
|
+
*/
|
|
97
|
+
#formatData(data) {
|
|
98
|
+
data.api_key = this.apiKey;
|
|
99
|
+
data.create = this.createNewTests;
|
|
100
|
+
// add test ID + run ID
|
|
101
|
+
if (data.rid)
|
|
102
|
+
data.rid = `${this.runId}-${data.rid}`;
|
|
103
|
+
if (!process.env.TESTOMATIO_STACK_PASSED && data.status === constants_js_1.STATUS.PASSED) {
|
|
104
|
+
data.stack = null;
|
|
105
|
+
}
|
|
106
|
+
if (!process.env.TESTOMATIO_STEPS_PASSED && data.status === constants_js_1.STATUS.PASSED) {
|
|
107
|
+
data.steps = null;
|
|
108
|
+
}
|
|
109
|
+
if (process.env.TESTOMATIO_NO_STEPS) {
|
|
110
|
+
data.steps = null;
|
|
111
|
+
}
|
|
112
|
+
return data;
|
|
113
|
+
}
|
|
92
114
|
/**
|
|
93
115
|
* Asynchronously prepares and retrieves the Testomat.io test grepList based on the provided options.
|
|
94
116
|
* @param {Object} opts - The options for preparing the test grepList.
|
|
@@ -171,7 +193,7 @@ class TestomatioPipe {
|
|
|
171
193
|
method: 'PUT',
|
|
172
194
|
url: `/api/reporter/${this.runId}`,
|
|
173
195
|
data: runParams,
|
|
174
|
-
responseType: 'json'
|
|
196
|
+
responseType: 'json',
|
|
175
197
|
});
|
|
176
198
|
if (resp.data.artifacts)
|
|
177
199
|
(0, pipe_utils_js_1.setS3Credentials)(resp.data.artifacts);
|
|
@@ -184,7 +206,7 @@ class TestomatioPipe {
|
|
|
184
206
|
url: '/api/reporter',
|
|
185
207
|
data: runParams,
|
|
186
208
|
maxContentLength: Infinity,
|
|
187
|
-
responseType: 'json'
|
|
209
|
+
responseType: 'json',
|
|
188
210
|
});
|
|
189
211
|
this.runId = resp.data.uid;
|
|
190
212
|
this.runUrl = `${this.url}/${resp.data.url.split('/').splice(3).join('/')}`;
|
|
@@ -234,28 +256,20 @@ class TestomatioPipe {
|
|
|
234
256
|
return;
|
|
235
257
|
if (this.#cancelTestReportingInCaseOfTooManyReqFailures())
|
|
236
258
|
return;
|
|
237
|
-
data
|
|
238
|
-
data.create = this.createNewTests;
|
|
239
|
-
if (!process.env.TESTOMATIO_STACK_PASSED && data.status === constants_js_1.STATUS.PASSED) {
|
|
240
|
-
data.stack = null;
|
|
241
|
-
}
|
|
242
|
-
if (!process.env.TESTOMATIO_STEPS_PASSED && data.status === constants_js_1.STATUS.PASSED) {
|
|
243
|
-
data.steps = null;
|
|
244
|
-
}
|
|
245
|
-
if (process.env.TESTOMATIO_NO_STEPS) {
|
|
246
|
-
data.steps = null;
|
|
247
|
-
}
|
|
259
|
+
this.#formatData(data);
|
|
248
260
|
const json = json_cycle_1.default.stringify(data);
|
|
249
261
|
debug('Adding test', json);
|
|
250
|
-
return this.client
|
|
262
|
+
return this.client
|
|
263
|
+
.request({
|
|
251
264
|
method: 'POST',
|
|
252
265
|
url: `/api/reporter/${this.runId}/testrun`,
|
|
253
266
|
data: json,
|
|
254
267
|
headers: {
|
|
255
268
|
'Content-Type': 'application/json',
|
|
256
269
|
},
|
|
257
|
-
maxContentLength: Infinity
|
|
258
|
-
})
|
|
270
|
+
maxContentLength: Infinity,
|
|
271
|
+
})
|
|
272
|
+
.catch(err => {
|
|
259
273
|
this.requestFailures++;
|
|
260
274
|
this.notReportedTestsCount++;
|
|
261
275
|
if (err.response) {
|
|
@@ -300,19 +314,21 @@ class TestomatioPipe {
|
|
|
300
314
|
// get tests from batch and clear batch
|
|
301
315
|
const testsToSend = this.batch.tests.splice(0);
|
|
302
316
|
debug('π¨ Batch upload', testsToSend.length, 'tests');
|
|
303
|
-
return this.client
|
|
317
|
+
return this.client
|
|
318
|
+
.request({
|
|
304
319
|
method: 'POST',
|
|
305
320
|
url: `/api/reporter/${this.runId}/testrun`,
|
|
306
321
|
data: {
|
|
307
322
|
api_key: this.apiKey,
|
|
308
323
|
tests: testsToSend,
|
|
309
|
-
batch_index: this.batch.batchIndex
|
|
324
|
+
batch_index: this.batch.batchIndex,
|
|
310
325
|
},
|
|
311
326
|
headers: {
|
|
312
327
|
'Content-Type': 'application/json',
|
|
313
328
|
},
|
|
314
|
-
maxContentLength: Infinity
|
|
315
|
-
})
|
|
329
|
+
maxContentLength: Infinity,
|
|
330
|
+
})
|
|
331
|
+
.catch(err => {
|
|
316
332
|
this.requestFailures++;
|
|
317
333
|
this.notReportedTestsCount += testsToSend.length;
|
|
318
334
|
if (err.response) {
|
|
@@ -344,11 +360,7 @@ class TestomatioPipe {
|
|
|
344
360
|
console.warn(constants_js_1.APP_PREFIX, picocolors_1.default.red('Run ID is not set, skipping test reporting'));
|
|
345
361
|
return;
|
|
346
362
|
}
|
|
347
|
-
|
|
348
|
-
if (data.rid)
|
|
349
|
-
data.rid = `${this.runId}-${data.rid}`;
|
|
350
|
-
data.api_key = this.apiKey;
|
|
351
|
-
data.create = this.createNewTests;
|
|
363
|
+
this.#formatData(data);
|
|
352
364
|
let uploading = null;
|
|
353
365
|
if (!this.batch.isEnabled)
|
|
354
366
|
uploading = this.#uploadSingleTest(data);
|
|
@@ -400,7 +412,7 @@ class TestomatioPipe {
|
|
|
400
412
|
status_event,
|
|
401
413
|
detach: params.detach,
|
|
402
414
|
tests: params.tests,
|
|
403
|
-
}
|
|
415
|
+
},
|
|
404
416
|
});
|
|
405
417
|
console.log(constants_js_1.APP_PREFIX, 'β
Testrun finished');
|
|
406
418
|
if (this.runUrl) {
|
package/lib/utils/utils.js
CHANGED
|
@@ -173,6 +173,8 @@ 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;
|
|
176
178
|
const comments = code
|
|
177
179
|
.split('\n')
|
|
178
180
|
.map(l => l.trim())
|
|
@@ -215,10 +217,29 @@ const fetchSourceCode = (contents, opts = {}) => {
|
|
|
215
217
|
lineIndex = lines.findIndex(l => l.includes(`${title}(`));
|
|
216
218
|
}
|
|
217
219
|
else if (opts.lang === 'csharp') {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
if (lineIndex === -1)
|
|
220
|
+
// Enhanced C# method detection for NUnit tests
|
|
221
|
+
lineIndex = lines.findIndex(l => l.includes(`public void ${title}(`));
|
|
222
|
+
if (lineIndex === -1) {
|
|
223
|
+
lineIndex = lines.findIndex(l => l.includes(`public async Task ${title}(`));
|
|
224
|
+
}
|
|
225
|
+
if (lineIndex === -1) {
|
|
221
226
|
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
|
+
}
|
|
222
243
|
}
|
|
223
244
|
else {
|
|
224
245
|
lineIndex = lines.findIndex(l => l.includes(title));
|
|
@@ -227,7 +248,7 @@ const fetchSourceCode = (contents, opts = {}) => {
|
|
|
227
248
|
if (opts.prepend) {
|
|
228
249
|
lineIndex -= opts.prepend;
|
|
229
250
|
}
|
|
230
|
-
if (lineIndex) {
|
|
251
|
+
if (lineIndex !== -1 && lineIndex !== undefined) {
|
|
231
252
|
const result = [];
|
|
232
253
|
for (let i = lineIndex; i < lineIndex + limit; i++) {
|
|
233
254
|
if (lines[i] === undefined)
|
|
@@ -270,6 +291,14 @@ const fetchSourceCode = (contents, opts = {}) => {
|
|
|
270
291
|
break;
|
|
271
292
|
if (opts.lang === 'java' && lines[i].includes(' class '))
|
|
272
293
|
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;
|
|
273
302
|
}
|
|
274
303
|
result.push(lines[i]);
|
|
275
304
|
}
|
package/lib/xmlReader.d.ts
CHANGED
|
@@ -13,6 +13,8 @@ declare class XmlReader {
|
|
|
13
13
|
runId: any;
|
|
14
14
|
adapter: import("./junit-adapter/adapter.js").default;
|
|
15
15
|
opts: {};
|
|
16
|
+
disableSourceCodeFetching: any;
|
|
17
|
+
suiteOrganization: any;
|
|
16
18
|
store: {};
|
|
17
19
|
pipesPromise: Promise<any[]>;
|
|
18
20
|
parser: XMLParser;
|
|
@@ -77,6 +79,13 @@ declare class XmlReader {
|
|
|
77
79
|
skipped_count: number;
|
|
78
80
|
tests: any[];
|
|
79
81
|
};
|
|
82
|
+
deduplicateTestsByFQN(tests: any): any[];
|
|
83
|
+
generateFQN(test: any): string;
|
|
84
|
+
generateNormalizedFQN(test: any): string;
|
|
85
|
+
extractAssemblyName(test: any): any;
|
|
86
|
+
extractNamespace(test: any): any;
|
|
87
|
+
extractClassName(test: any): any;
|
|
88
|
+
extractCsFileFromPath(test: any): any;
|
|
80
89
|
calculateStats(): {};
|
|
81
90
|
fetchSourceCode(): void;
|
|
82
91
|
formatTests(): void;
|