@testomatio/reporter 2.3.6-beta.2-fix-beforesuite β 2.3.7-beta.1-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/bin/reportXml.js +4 -1
- package/lib/junit-adapter/csharp.d.ts +0 -1
- package/lib/junit-adapter/csharp.js +36 -7
- package/lib/junit-adapter/nunit-parser.d.ts +82 -0
- package/lib/junit-adapter/nunit-parser.js +357 -0
- package/lib/pipe/debug.js +1 -1
- package/lib/pipe/testomatio.js +19 -15
- package/lib/uploader.js +4 -0
- package/lib/utils/utils.js +35 -10
- package/lib/xmlReader.d.ts +11 -26
- package/lib/xmlReader.js +50 -1
- package/package.json +1 -1
- package/src/bin/reportXml.js +4 -1
- package/src/junit-adapter/csharp.js +40 -6
- package/src/junit-adapter/nunit-parser.js +391 -0
- package/src/pipe/debug.js +2 -3
- package/src/pipe/testomatio.js +75 -81
- package/src/uploader.js +5 -0
- package/src/utils/utils.js +35 -9
- package/src/xmlReader.js +62 -1
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)_
|
package/lib/bin/reportXml.js
CHANGED
|
@@ -37,7 +37,10 @@ program
|
|
|
37
37
|
lang = lang?.toLowerCase();
|
|
38
38
|
if (javaTests === true || (lang === 'java' && !javaTests))
|
|
39
39
|
javaTests = 'src/test/java';
|
|
40
|
-
const runReader = new xmlReader_js_1.default({
|
|
40
|
+
const runReader = new xmlReader_js_1.default({
|
|
41
|
+
javaTests,
|
|
42
|
+
lang,
|
|
43
|
+
});
|
|
41
44
|
const files = glob_1.glob.sync(pattern, { cwd: opts.dir || process.cwd() });
|
|
42
45
|
if (!files.length) {
|
|
43
46
|
console.log(constants_js_1.APP_PREFIX, `Report can't be created. No XML files found π₯`);
|
|
@@ -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
|
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enhanced NUnit XML Parser that properly handles test-suite hierarchy
|
|
3
|
+
* and parameterized tests
|
|
4
|
+
*/
|
|
5
|
+
export class NUnitXmlParser {
|
|
6
|
+
constructor(options?: {});
|
|
7
|
+
options: {};
|
|
8
|
+
tests: any[];
|
|
9
|
+
stats: {
|
|
10
|
+
total: number;
|
|
11
|
+
passed: number;
|
|
12
|
+
failed: number;
|
|
13
|
+
skipped: number;
|
|
14
|
+
inconclusive: number;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Parse NUnit XML test-run structure
|
|
18
|
+
* @param {Object} testRun - Parsed XML test-run object
|
|
19
|
+
* @returns {Object} - Parsed test results
|
|
20
|
+
*/
|
|
21
|
+
parseTestRun(testRun: any): any;
|
|
22
|
+
/**
|
|
23
|
+
* Recursively parse test-suite elements based on their type
|
|
24
|
+
* @param {Object|Array} testSuite - Test suite object or array
|
|
25
|
+
* @param {Array} parentPath - Current path in the hierarchy
|
|
26
|
+
*/
|
|
27
|
+
parseTestSuite(testSuite: any | any[], parentPath?: any[]): void;
|
|
28
|
+
/**
|
|
29
|
+
* Process child elements of a test suite
|
|
30
|
+
* @param {Object} testSuite - Test suite object
|
|
31
|
+
* @param {Array} currentPath - Current path in hierarchy
|
|
32
|
+
*/
|
|
33
|
+
processChildren(testSuite: any, currentPath: any[]): void;
|
|
34
|
+
/**
|
|
35
|
+
* Parse test-case elements (actual tests)
|
|
36
|
+
* @param {Object|Array} testCases - Test case object or array
|
|
37
|
+
* @param {Array} suitePath - Path to the test suite
|
|
38
|
+
* @param {Object} parentSuite - Parent test suite for context
|
|
39
|
+
*/
|
|
40
|
+
parseTestCases(testCases: any | any[], suitePath: any[], parentSuite: any): void;
|
|
41
|
+
/**
|
|
42
|
+
* Parse individual test case
|
|
43
|
+
* @param {Object} testCase - Test case object
|
|
44
|
+
* @param {Array} suitePath - Path to the test suite
|
|
45
|
+
* @param {Object} parentSuite - Parent test suite for context
|
|
46
|
+
* @returns {Object|null} - Parsed test object
|
|
47
|
+
*/
|
|
48
|
+
parseTestCase(testCase: any, suitePath: any[], parentSuite: any): any | null;
|
|
49
|
+
/**
|
|
50
|
+
* Extract method name and parameters from test name
|
|
51
|
+
* @param {string} testName - Full test name
|
|
52
|
+
* @returns {Object} - Extracted information
|
|
53
|
+
*/
|
|
54
|
+
extractParameters(testName: string): any;
|
|
55
|
+
/**
|
|
56
|
+
* Parse parameter string into array of parameters
|
|
57
|
+
* @param {string} paramString - Parameter string
|
|
58
|
+
* @returns {Array} - Array of parameters
|
|
59
|
+
*/
|
|
60
|
+
parseParameterString(paramString: string): any[];
|
|
61
|
+
/**
|
|
62
|
+
* Extract method name from test name (fallback)
|
|
63
|
+
* @param {string} testName - Test name
|
|
64
|
+
* @returns {string} - Method name
|
|
65
|
+
*/
|
|
66
|
+
extractMethodName(testName: string): string;
|
|
67
|
+
/**
|
|
68
|
+
* Build file path from suite path and class name
|
|
69
|
+
* @param {Array} suitePath - Suite path array
|
|
70
|
+
* @param {string} className - Class name
|
|
71
|
+
* @param {Object} parentSuite - Parent suite for context
|
|
72
|
+
* @returns {string} - File path
|
|
73
|
+
*/
|
|
74
|
+
buildFilePath(suitePath: any[], className: string, parentSuite: any): string;
|
|
75
|
+
/**
|
|
76
|
+
* Group parameterized tests by base method name
|
|
77
|
+
* @param {Array} tests - Array of parsed tests
|
|
78
|
+
* @returns {Object} - Grouped tests
|
|
79
|
+
*/
|
|
80
|
+
groupParameterizedTests(tests: any[]): any;
|
|
81
|
+
}
|
|
82
|
+
export default NUnitXmlParser;
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.NUnitXmlParser = void 0;
|
|
7
|
+
const debug_1 = __importDefault(require("debug"));
|
|
8
|
+
const constants_js_1 = require("../constants.js");
|
|
9
|
+
const debug = (0, debug_1.default)('@testomatio/reporter:nunit-parser');
|
|
10
|
+
/**
|
|
11
|
+
* Enhanced NUnit XML Parser that properly handles test-suite hierarchy
|
|
12
|
+
* and parameterized tests
|
|
13
|
+
*/
|
|
14
|
+
class NUnitXmlParser {
|
|
15
|
+
constructor(options = {}) {
|
|
16
|
+
this.options = options;
|
|
17
|
+
this.tests = [];
|
|
18
|
+
this.stats = {
|
|
19
|
+
total: 0,
|
|
20
|
+
passed: 0,
|
|
21
|
+
failed: 0,
|
|
22
|
+
skipped: 0,
|
|
23
|
+
inconclusive: 0,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Parse NUnit XML test-run structure
|
|
28
|
+
* @param {Object} testRun - Parsed XML test-run object
|
|
29
|
+
* @returns {Object} - Parsed test results
|
|
30
|
+
*/
|
|
31
|
+
parseTestRun(testRun) {
|
|
32
|
+
debug('Parsing NUnit test-run');
|
|
33
|
+
// Extract run-level statistics
|
|
34
|
+
this.stats = {
|
|
35
|
+
total: parseInt(testRun.total || 0, 10),
|
|
36
|
+
passed: parseInt(testRun.passed || 0, 10),
|
|
37
|
+
failed: parseInt(testRun.failed || 0, 10),
|
|
38
|
+
skipped: parseInt(testRun.skipped || 0, 10),
|
|
39
|
+
inconclusive: parseInt(testRun.inconclusive || 0, 10),
|
|
40
|
+
};
|
|
41
|
+
// Process the root test-suite
|
|
42
|
+
if (testRun['test-suite']) {
|
|
43
|
+
this.parseTestSuite(testRun['test-suite'], []);
|
|
44
|
+
}
|
|
45
|
+
debug(`Parsed ${this.tests.length} tests from NUnit XML`);
|
|
46
|
+
return {
|
|
47
|
+
status: testRun.result?.toLowerCase() || 'unknown',
|
|
48
|
+
create_tests: true,
|
|
49
|
+
tests_count: this.tests.length,
|
|
50
|
+
passed_count: this.tests.filter(t => t.status === constants_js_1.STATUS.PASSED).length,
|
|
51
|
+
failed_count: this.tests.filter(t => t.status === constants_js_1.STATUS.FAILED).length,
|
|
52
|
+
skipped_count: this.tests.filter(t => t.status === constants_js_1.STATUS.SKIPPED).length,
|
|
53
|
+
tests: this.tests,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Recursively parse test-suite elements based on their type
|
|
58
|
+
* @param {Object|Array} testSuite - Test suite object or array
|
|
59
|
+
* @param {Array} parentPath - Current path in the hierarchy
|
|
60
|
+
*/
|
|
61
|
+
parseTestSuite(testSuite, parentPath = []) {
|
|
62
|
+
if (!testSuite)
|
|
63
|
+
return;
|
|
64
|
+
// Handle arrays of test suites
|
|
65
|
+
if (Array.isArray(testSuite)) {
|
|
66
|
+
testSuite.forEach(suite => this.parseTestSuite(suite, parentPath));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const suiteType = testSuite.type;
|
|
70
|
+
const suiteName = testSuite.name;
|
|
71
|
+
const fullName = testSuite.fullname;
|
|
72
|
+
debug(`Processing test-suite: type=${suiteType}, name=${suiteName}`);
|
|
73
|
+
switch (suiteType) {
|
|
74
|
+
case 'Assembly':
|
|
75
|
+
// Assembly level - ignore the name, just process children
|
|
76
|
+
debug('Processing Assembly level - ignoring name, processing children');
|
|
77
|
+
this.processChildren(testSuite, parentPath);
|
|
78
|
+
break;
|
|
79
|
+
case 'TestSuite':
|
|
80
|
+
// Namespace/grouping level - add to path but don't create test
|
|
81
|
+
debug(`Processing TestSuite level - adding '${suiteName}' to path`);
|
|
82
|
+
const newPath = [...parentPath, suiteName];
|
|
83
|
+
this.processChildren(testSuite, newPath);
|
|
84
|
+
break;
|
|
85
|
+
case 'TestFixture':
|
|
86
|
+
// Test class level - add to path and process test cases
|
|
87
|
+
debug(`Processing TestFixture level - test class '${suiteName}'`);
|
|
88
|
+
const testFixturePath = [...parentPath, suiteName];
|
|
89
|
+
this.processChildren(testSuite, testFixturePath);
|
|
90
|
+
break;
|
|
91
|
+
default:
|
|
92
|
+
debug(`Unknown test-suite type: ${suiteType}, treating as TestSuite`);
|
|
93
|
+
const unknownPath = [...parentPath, suiteName];
|
|
94
|
+
this.processChildren(testSuite, unknownPath);
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Process child elements of a test suite
|
|
100
|
+
* @param {Object} testSuite - Test suite object
|
|
101
|
+
* @param {Array} currentPath - Current path in hierarchy
|
|
102
|
+
*/
|
|
103
|
+
processChildren(testSuite, currentPath) {
|
|
104
|
+
// Process nested test-suites
|
|
105
|
+
if (testSuite['test-suite']) {
|
|
106
|
+
this.parseTestSuite(testSuite['test-suite'], currentPath);
|
|
107
|
+
}
|
|
108
|
+
// Process test-cases
|
|
109
|
+
if (testSuite['test-case']) {
|
|
110
|
+
this.parseTestCases(testSuite['test-case'], currentPath, testSuite);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Parse test-case elements (actual tests)
|
|
115
|
+
* @param {Object|Array} testCases - Test case object or array
|
|
116
|
+
* @param {Array} suitePath - Path to the test suite
|
|
117
|
+
* @param {Object} parentSuite - Parent test suite for context
|
|
118
|
+
*/
|
|
119
|
+
parseTestCases(testCases, suitePath, parentSuite) {
|
|
120
|
+
if (!testCases)
|
|
121
|
+
return;
|
|
122
|
+
// Handle arrays of test cases
|
|
123
|
+
if (!Array.isArray(testCases)) {
|
|
124
|
+
testCases = [testCases];
|
|
125
|
+
}
|
|
126
|
+
testCases.forEach(testCase => {
|
|
127
|
+
const parsedTest = this.parseTestCase(testCase, suitePath, parentSuite);
|
|
128
|
+
if (parsedTest) {
|
|
129
|
+
this.tests.push(parsedTest);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Parse individual test case
|
|
135
|
+
* @param {Object} testCase - Test case object
|
|
136
|
+
* @param {Array} suitePath - Path to the test suite
|
|
137
|
+
* @param {Object} parentSuite - Parent test suite for context
|
|
138
|
+
* @returns {Object|null} - Parsed test object
|
|
139
|
+
*/
|
|
140
|
+
parseTestCase(testCase, suitePath, parentSuite) {
|
|
141
|
+
if (!testCase || !testCase.name) {
|
|
142
|
+
debug('Skipping test case without name');
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
const testName = testCase.name;
|
|
146
|
+
const fullName = testCase.fullname;
|
|
147
|
+
const methodName = testCase.methodname || this.extractMethodName(testName);
|
|
148
|
+
const className = testCase.classname || parentSuite?.name;
|
|
149
|
+
debug(`Parsing test case: ${testName}`);
|
|
150
|
+
// Extract parameters if this is a parameterized test
|
|
151
|
+
const { baseMethodName, parameters, isParameterized } = this.extractParameters(testName);
|
|
152
|
+
// Determine test status
|
|
153
|
+
let status = constants_js_1.STATUS.PASSED;
|
|
154
|
+
if (testCase.result) {
|
|
155
|
+
switch (testCase.result.toLowerCase()) {
|
|
156
|
+
case 'passed':
|
|
157
|
+
status = constants_js_1.STATUS.PASSED;
|
|
158
|
+
break;
|
|
159
|
+
case 'failed':
|
|
160
|
+
status = constants_js_1.STATUS.FAILED;
|
|
161
|
+
break;
|
|
162
|
+
case 'skipped':
|
|
163
|
+
case 'ignored':
|
|
164
|
+
status = constants_js_1.STATUS.SKIPPED;
|
|
165
|
+
break;
|
|
166
|
+
case 'inconclusive':
|
|
167
|
+
status = constants_js_1.STATUS.SKIPPED; // Treat inconclusive as skipped
|
|
168
|
+
break;
|
|
169
|
+
default:
|
|
170
|
+
status = constants_js_1.STATUS.PASSED;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// Extract error information
|
|
174
|
+
let message = '';
|
|
175
|
+
let stack = '';
|
|
176
|
+
if (testCase.failure) {
|
|
177
|
+
message = testCase.failure.message || '';
|
|
178
|
+
stack = testCase.failure['stack-trace'] || testCase.failure['#text'] || '';
|
|
179
|
+
}
|
|
180
|
+
if (testCase.output && testCase.output['#text']) {
|
|
181
|
+
stack = `${stack}\n\n${testCase.output['#text']}`.trim();
|
|
182
|
+
}
|
|
183
|
+
// Extract test ID from properties
|
|
184
|
+
let testId = null;
|
|
185
|
+
if (testCase.properties && testCase.properties.property) {
|
|
186
|
+
const properties = Array.isArray(testCase.properties.property)
|
|
187
|
+
? testCase.properties.property
|
|
188
|
+
: [testCase.properties.property];
|
|
189
|
+
const idProperty = properties.find(p => p.name === 'ID');
|
|
190
|
+
if (idProperty) {
|
|
191
|
+
testId = idProperty.value;
|
|
192
|
+
// Remove @ and T prefixes if present
|
|
193
|
+
if (testId.startsWith('@'))
|
|
194
|
+
testId = testId.slice(1);
|
|
195
|
+
if (testId.startsWith('T'))
|
|
196
|
+
testId = testId.slice(1);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
// Build file path from suite path and class name
|
|
200
|
+
const filePath = this.buildFilePath(suitePath, className, parentSuite);
|
|
201
|
+
return {
|
|
202
|
+
title: isParameterized ? testName : methodName || testName,
|
|
203
|
+
methodName: baseMethodName || methodName || testName,
|
|
204
|
+
fullName: fullName,
|
|
205
|
+
suitePath: suitePath,
|
|
206
|
+
suite_title: className || suitePath[suitePath.length - 1] || 'Unknown',
|
|
207
|
+
file: filePath,
|
|
208
|
+
status: status,
|
|
209
|
+
message: message,
|
|
210
|
+
stack: stack,
|
|
211
|
+
run_time: parseFloat(testCase.duration || testCase.time || 0) * 1000,
|
|
212
|
+
test_id: testId,
|
|
213
|
+
create: true,
|
|
214
|
+
retry: false,
|
|
215
|
+
// Parameterized test metadata
|
|
216
|
+
isParameterized: isParameterized,
|
|
217
|
+
parameters: parameters,
|
|
218
|
+
baseMethodName: baseMethodName,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Extract method name and parameters from test name
|
|
223
|
+
* @param {string} testName - Full test name
|
|
224
|
+
* @returns {Object} - Extracted information
|
|
225
|
+
*/
|
|
226
|
+
extractParameters(testName) {
|
|
227
|
+
const paramMatch = testName.match(/^(.+?)\((.+)\)$/);
|
|
228
|
+
if (paramMatch) {
|
|
229
|
+
const baseMethodName = paramMatch[1].trim();
|
|
230
|
+
const paramString = paramMatch[2];
|
|
231
|
+
// Parse parameters - handle quoted strings and nested structures
|
|
232
|
+
const parameters = this.parseParameterString(paramString);
|
|
233
|
+
return {
|
|
234
|
+
baseMethodName: baseMethodName,
|
|
235
|
+
parameters: parameters,
|
|
236
|
+
isParameterized: true,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
baseMethodName: testName,
|
|
241
|
+
parameters: [],
|
|
242
|
+
isParameterized: false,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Parse parameter string into array of parameters
|
|
247
|
+
* @param {string} paramString - Parameter string
|
|
248
|
+
* @returns {Array} - Array of parameters
|
|
249
|
+
*/
|
|
250
|
+
parseParameterString(paramString) {
|
|
251
|
+
const parameters = [];
|
|
252
|
+
let current = '';
|
|
253
|
+
let inQuotes = false;
|
|
254
|
+
let quoteChar = null;
|
|
255
|
+
let depth = 0;
|
|
256
|
+
for (let i = 0; i < paramString.length; i++) {
|
|
257
|
+
const char = paramString[i];
|
|
258
|
+
if (!inQuotes && (char === '"' || char === "'")) {
|
|
259
|
+
inQuotes = true;
|
|
260
|
+
quoteChar = char;
|
|
261
|
+
current += char;
|
|
262
|
+
}
|
|
263
|
+
else if (inQuotes && char === quoteChar) {
|
|
264
|
+
inQuotes = false;
|
|
265
|
+
quoteChar = null;
|
|
266
|
+
current += char;
|
|
267
|
+
}
|
|
268
|
+
else if (!inQuotes && char === '(') {
|
|
269
|
+
depth++;
|
|
270
|
+
current += char;
|
|
271
|
+
}
|
|
272
|
+
else if (!inQuotes && char === ')') {
|
|
273
|
+
depth--;
|
|
274
|
+
current += char;
|
|
275
|
+
}
|
|
276
|
+
else if (!inQuotes && char === ',' && depth === 0) {
|
|
277
|
+
parameters.push(current.trim());
|
|
278
|
+
current = '';
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
current += char;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (current.trim()) {
|
|
285
|
+
parameters.push(current.trim());
|
|
286
|
+
}
|
|
287
|
+
// Clean up parameters - remove quotes if they wrap the entire parameter
|
|
288
|
+
return parameters.map(param => {
|
|
289
|
+
param = param.trim();
|
|
290
|
+
if ((param.startsWith('"') && param.endsWith('"')) || (param.startsWith("'") && param.endsWith("'"))) {
|
|
291
|
+
return param.slice(1, -1);
|
|
292
|
+
}
|
|
293
|
+
return param;
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Extract method name from test name (fallback)
|
|
298
|
+
* @param {string} testName - Test name
|
|
299
|
+
* @returns {string} - Method name
|
|
300
|
+
*/
|
|
301
|
+
extractMethodName(testName) {
|
|
302
|
+
// Remove parameters if present
|
|
303
|
+
const paramMatch = testName.match(/^(.+?)\(/);
|
|
304
|
+
return paramMatch ? paramMatch[1].trim() : testName;
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Build file path from suite path and class name
|
|
308
|
+
* @param {Array} suitePath - Suite path array
|
|
309
|
+
* @param {string} className - Class name
|
|
310
|
+
* @param {Object} parentSuite - Parent suite for context
|
|
311
|
+
* @returns {string} - File path
|
|
312
|
+
*/
|
|
313
|
+
buildFilePath(suitePath, className, parentSuite) {
|
|
314
|
+
// Try to get file path from parent suite
|
|
315
|
+
if (parentSuite && parentSuite.filepath) {
|
|
316
|
+
return parentSuite.filepath;
|
|
317
|
+
}
|
|
318
|
+
// Build path from suite hierarchy
|
|
319
|
+
const pathParts = [...suitePath];
|
|
320
|
+
if (className && !pathParts.includes(className)) {
|
|
321
|
+
pathParts.push(className);
|
|
322
|
+
}
|
|
323
|
+
// Convert to file path format
|
|
324
|
+
return pathParts.join('/') + '.cs'; // Assume C# for NUnit
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Group parameterized tests by base method name
|
|
328
|
+
* @param {Array} tests - Array of parsed tests
|
|
329
|
+
* @returns {Object} - Grouped tests
|
|
330
|
+
*/
|
|
331
|
+
groupParameterizedTests(tests) {
|
|
332
|
+
const grouped = {};
|
|
333
|
+
tests.forEach(test => {
|
|
334
|
+
const key = test.isParameterized
|
|
335
|
+
? `${test.suitePath.join('.')}.${test.baseMethodName}`
|
|
336
|
+
: `${test.suitePath.join('.')}.${test.title}`;
|
|
337
|
+
if (!grouped[key]) {
|
|
338
|
+
grouped[key] = {
|
|
339
|
+
baseTest: {
|
|
340
|
+
name: test.baseMethodName || test.title,
|
|
341
|
+
suitePath: test.suitePath,
|
|
342
|
+
suite_title: test.suite_title,
|
|
343
|
+
file: test.file,
|
|
344
|
+
isParameterized: test.isParameterized,
|
|
345
|
+
},
|
|
346
|
+
variations: [],
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
grouped[key].variations.push(test);
|
|
350
|
+
});
|
|
351
|
+
return grouped;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
exports.NUnitXmlParser = NUnitXmlParser;
|
|
355
|
+
module.exports = NUnitXmlParser;
|
|
356
|
+
|
|
357
|
+
module.exports.NUnitXmlParser = NUnitXmlParser;
|
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)
|
|
@@ -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,15 +259,17 @@ 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
|
|
262
|
+
return this.client
|
|
263
|
+
.request({
|
|
263
264
|
method: 'POST',
|
|
264
265
|
url: `/api/reporter/${this.runId}/testrun`,
|
|
265
266
|
data: json,
|
|
266
267
|
headers: {
|
|
267
268
|
'Content-Type': 'application/json',
|
|
268
269
|
},
|
|
269
|
-
maxContentLength: Infinity
|
|
270
|
-
})
|
|
270
|
+
maxContentLength: Infinity,
|
|
271
|
+
})
|
|
272
|
+
.catch(err => {
|
|
271
273
|
this.requestFailures++;
|
|
272
274
|
this.notReportedTestsCount++;
|
|
273
275
|
if (err.response) {
|
|
@@ -312,19 +314,21 @@ class TestomatioPipe {
|
|
|
312
314
|
// get tests from batch and clear batch
|
|
313
315
|
const testsToSend = this.batch.tests.splice(0);
|
|
314
316
|
debug('π¨ Batch upload', testsToSend.length, 'tests');
|
|
315
|
-
return this.client
|
|
317
|
+
return this.client
|
|
318
|
+
.request({
|
|
316
319
|
method: 'POST',
|
|
317
320
|
url: `/api/reporter/${this.runId}/testrun`,
|
|
318
321
|
data: {
|
|
319
322
|
api_key: this.apiKey,
|
|
320
323
|
tests: testsToSend,
|
|
321
|
-
batch_index: this.batch.batchIndex
|
|
324
|
+
batch_index: this.batch.batchIndex,
|
|
322
325
|
},
|
|
323
326
|
headers: {
|
|
324
327
|
'Content-Type': 'application/json',
|
|
325
328
|
},
|
|
326
|
-
maxContentLength: Infinity
|
|
327
|
-
})
|
|
329
|
+
maxContentLength: Infinity,
|
|
330
|
+
})
|
|
331
|
+
.catch(err => {
|
|
328
332
|
this.requestFailures++;
|
|
329
333
|
this.notReportedTestsCount += testsToSend.length;
|
|
330
334
|
if (err.response) {
|
|
@@ -408,9 +412,9 @@ class TestomatioPipe {
|
|
|
408
412
|
status_event,
|
|
409
413
|
detach: params.detach,
|
|
410
414
|
tests: params.tests,
|
|
411
|
-
}
|
|
415
|
+
},
|
|
412
416
|
});
|
|
413
|
-
|
|
417
|
+
debug(constants_js_1.APP_PREFIX, 'β
Testrun finished');
|
|
414
418
|
if (this.runUrl) {
|
|
415
419
|
console.log(constants_js_1.APP_PREFIX, 'π Report Saved. Report URL:', picocolors_1.default.magenta(this.runUrl));
|
|
416
420
|
}
|
package/lib/uploader.js
CHANGED
|
@@ -170,6 +170,10 @@ class S3Uploader {
|
|
|
170
170
|
if (typeof filePath === 'string' && !path_1.default.isAbsolute(filePath)) {
|
|
171
171
|
filePath = path_1.default.join(process.cwd(), filePath);
|
|
172
172
|
}
|
|
173
|
+
// Normalize path separators for cross-platform compatibility
|
|
174
|
+
if (typeof filePath === 'string') {
|
|
175
|
+
filePath = filePath.replace(/\\/g, '/');
|
|
176
|
+
}
|
|
173
177
|
const data = { rid, file: filePath, uploaded };
|
|
174
178
|
const jsonLine = `${JSON.stringify(data)}\n`;
|
|
175
179
|
fs_1.default.appendFileSync(tempFilePath, jsonLine);
|