@testomatio/reporter 2.3.9-beta-bin-fix → 2.4.0
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 +3 -2
- package/lib/adapter/codecept.js +12 -9
- package/lib/bin/cli.js +40 -11
- package/lib/bin/reportXml.js +5 -2
- package/lib/client.d.ts +1 -11
- package/lib/client.js +57 -152
- package/lib/data-storage.d.ts +1 -1
- package/lib/helpers.d.ts +1 -0
- package/lib/helpers.js +4 -0
- package/lib/junit-adapter/csharp.d.ts +0 -1
- package/lib/junit-adapter/csharp.js +43 -7
- package/lib/junit-adapter/nunit-parser.d.ts +82 -0
- package/lib/junit-adapter/nunit-parser.js +433 -0
- package/lib/pipe/bitbucket.js +5 -5
- package/lib/pipe/coverage.d.ts +82 -0
- package/lib/pipe/coverage.js +373 -0
- package/lib/pipe/gitlab.js +4 -4
- package/lib/pipe/index.js +2 -0
- package/lib/pipe/testomatio.d.ts +3 -2
- package/lib/pipe/testomatio.js +44 -18
- package/lib/reporter-functions.js +14 -12
- package/lib/reporter.d.ts +31 -21
- package/lib/reporter.js +40 -5
- package/lib/services/artifacts.d.ts +1 -1
- package/lib/services/key-values.d.ts +1 -1
- package/lib/services/links.d.ts +1 -1
- package/lib/services/logger.d.ts +1 -1
- package/lib/uploader.js +4 -0
- package/lib/utils/log-formatter.d.ts +28 -0
- package/lib/utils/log-formatter.js +127 -0
- package/lib/utils/pipe_utils.d.ts +15 -0
- package/lib/utils/pipe_utils.js +44 -2
- package/lib/utils/utils.d.ts +6 -0
- package/lib/utils/utils.js +260 -25
- package/lib/xmlReader.d.ts +32 -26
- package/lib/xmlReader.js +121 -52
- package/package.json +12 -7
- package/src/adapter/codecept.js +19 -19
- package/src/adapter/mocha.js +1 -1
- package/src/adapter/playwright.js +2 -2
- package/src/bin/cli.js +51 -13
- package/src/bin/reportXml.js +5 -2
- package/src/client.js +69 -130
- package/src/helpers.js +1 -0
- package/src/junit-adapter/csharp.js +48 -6
- package/src/junit-adapter/nunit-parser.js +474 -0
- package/src/pipe/bitbucket.js +5 -5
- package/src/pipe/coverage.js +440 -0
- package/src/pipe/debug.js +1 -2
- package/src/pipe/gitlab.js +4 -4
- package/src/pipe/index.js +2 -0
- package/src/pipe/testomatio.js +109 -85
- package/src/reporter-functions.js +15 -12
- package/src/reporter.js +6 -4
- package/src/services/links.js +1 -1
- package/src/uploader.js +5 -0
- package/src/utils/log-formatter.js +113 -0
- package/src/utils/pipe_utils.js +52 -3
- package/src/utils/utils.js +277 -22
- package/src/xmlReader.js +144 -46
- package/types/types.d.ts +364 -0
- package/types/vitest.types.d.ts +93 -0
|
@@ -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,433 @@
|
|
|
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 utils_js_1 = require("../utils/utils.js");
|
|
10
|
+
const debug = (0, debug_1.default)('@testomatio/reporter:nunit-parser');
|
|
11
|
+
/**
|
|
12
|
+
* Enhanced NUnit XML Parser that properly handles test-suite hierarchy
|
|
13
|
+
* and parameterized tests
|
|
14
|
+
*/
|
|
15
|
+
class NUnitXmlParser {
|
|
16
|
+
constructor(options = {}) {
|
|
17
|
+
this.options = options;
|
|
18
|
+
this.tests = [];
|
|
19
|
+
this.stats = {
|
|
20
|
+
total: 0,
|
|
21
|
+
passed: 0,
|
|
22
|
+
failed: 0,
|
|
23
|
+
skipped: 0,
|
|
24
|
+
inconclusive: 0,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Parse NUnit XML test-run structure
|
|
29
|
+
* @param {Object} testRun - Parsed XML test-run object
|
|
30
|
+
* @returns {Object} - Parsed test results
|
|
31
|
+
*/
|
|
32
|
+
parseTestRun(testRun) {
|
|
33
|
+
debug('Parsing NUnit test-run');
|
|
34
|
+
// Extract run-level statistics
|
|
35
|
+
this.stats = {
|
|
36
|
+
total: parseInt(testRun.total || 0, 10),
|
|
37
|
+
passed: parseInt(testRun.passed || 0, 10),
|
|
38
|
+
failed: parseInt(testRun.failed || 0, 10),
|
|
39
|
+
skipped: parseInt(testRun.skipped || 0, 10),
|
|
40
|
+
inconclusive: parseInt(testRun.inconclusive || 0, 10),
|
|
41
|
+
};
|
|
42
|
+
// Process the root test-suite
|
|
43
|
+
if (testRun['test-suite']) {
|
|
44
|
+
this.parseTestSuite(testRun['test-suite'], []);
|
|
45
|
+
}
|
|
46
|
+
debug(`Parsed ${this.tests.length} tests from NUnit XML`);
|
|
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 === constants_js_1.STATUS.PASSED).length,
|
|
52
|
+
failed_count: this.tests.filter(t => t.status === constants_js_1.STATUS.FAILED).length,
|
|
53
|
+
skipped_count: this.tests.filter(t => t.status === constants_js_1.STATUS.SKIPPED).length,
|
|
54
|
+
tests: this.tests,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Recursively parse test-suite elements based on their type
|
|
59
|
+
* @param {Object|Array} testSuite - Test suite object or array
|
|
60
|
+
* @param {Array} parentPath - Current path in the hierarchy
|
|
61
|
+
*/
|
|
62
|
+
parseTestSuite(testSuite, parentPath = []) {
|
|
63
|
+
if (!testSuite)
|
|
64
|
+
return;
|
|
65
|
+
// Handle arrays of test suites
|
|
66
|
+
if (Array.isArray(testSuite)) {
|
|
67
|
+
testSuite.forEach(suite => this.parseTestSuite(suite, parentPath));
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const suiteType = testSuite.type;
|
|
71
|
+
const suiteName = testSuite.name;
|
|
72
|
+
const fullName = testSuite.fullname;
|
|
73
|
+
debug(`Processing test-suite: type=${suiteType}, name=${suiteName}`);
|
|
74
|
+
switch (suiteType) {
|
|
75
|
+
case 'Assembly':
|
|
76
|
+
// Assembly level - ignore the name, just process children
|
|
77
|
+
debug('Processing Assembly level - ignoring name, processing children');
|
|
78
|
+
this.processChildren(testSuite, parentPath);
|
|
79
|
+
break;
|
|
80
|
+
case 'TestSuite':
|
|
81
|
+
// Namespace/grouping level - add to path but don't create test
|
|
82
|
+
debug(`Processing TestSuite level - adding '${suiteName}' to path`);
|
|
83
|
+
// Avoid adding duplicate suite names to the path
|
|
84
|
+
const newPath = parentPath[parentPath.length - 1] === suiteName ? [...parentPath] : [...parentPath, suiteName];
|
|
85
|
+
this.processChildren(testSuite, newPath);
|
|
86
|
+
break;
|
|
87
|
+
case 'TestFixture':
|
|
88
|
+
// Test class level - add to path and process test cases
|
|
89
|
+
debug(`Processing TestFixture level - test class '${suiteName}'`);
|
|
90
|
+
const testFixturePath = [...parentPath, suiteName];
|
|
91
|
+
this.processChildren(testSuite, testFixturePath);
|
|
92
|
+
break;
|
|
93
|
+
case 'ParameterizedMethod':
|
|
94
|
+
// Parameterized method level - process test cases directly
|
|
95
|
+
debug(`Processing ParameterizedMethod level - method '${suiteName}'`);
|
|
96
|
+
// Don't add to path, just process children directly
|
|
97
|
+
this.processChildren(testSuite, parentPath);
|
|
98
|
+
break;
|
|
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
|
+
* Process child elements of a test suite
|
|
108
|
+
* @param {Object} testSuite - Test suite object
|
|
109
|
+
* @param {Array} currentPath - Current path in hierarchy
|
|
110
|
+
*/
|
|
111
|
+
processChildren(testSuite, currentPath) {
|
|
112
|
+
// Process test-cases first (to maintain order)
|
|
113
|
+
if (testSuite['test-case']) {
|
|
114
|
+
this.parseTestCases(testSuite['test-case'], currentPath, testSuite);
|
|
115
|
+
}
|
|
116
|
+
// Process nested test-suites
|
|
117
|
+
if (testSuite['test-suite']) {
|
|
118
|
+
this.parseTestSuite(testSuite['test-suite'], currentPath);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Parse test-case elements (actual tests)
|
|
123
|
+
* @param {Object|Array} testCases - Test case object or array
|
|
124
|
+
* @param {Array} suitePath - Path to the test suite
|
|
125
|
+
* @param {Object} parentSuite - Parent test suite for context
|
|
126
|
+
*/
|
|
127
|
+
parseTestCases(testCases, suitePath, parentSuite) {
|
|
128
|
+
if (!testCases)
|
|
129
|
+
return;
|
|
130
|
+
// Handle arrays of test cases
|
|
131
|
+
if (!Array.isArray(testCases)) {
|
|
132
|
+
testCases = [testCases];
|
|
133
|
+
}
|
|
134
|
+
testCases.forEach(testCase => {
|
|
135
|
+
const parsedTest = this.parseTestCase(testCase, suitePath, parentSuite);
|
|
136
|
+
if (parsedTest) {
|
|
137
|
+
this.tests.push(parsedTest);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Parse individual test case
|
|
143
|
+
* @param {Object} testCase - Test case object
|
|
144
|
+
* @param {Array} suitePath - Path to the test suite
|
|
145
|
+
* @param {Object} parentSuite - Parent test suite for context
|
|
146
|
+
* @returns {Object|null} - Parsed test object
|
|
147
|
+
*/
|
|
148
|
+
parseTestCase(testCase, suitePath, parentSuite) {
|
|
149
|
+
if (!testCase || !testCase.name) {
|
|
150
|
+
debug('Skipping test case without name');
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
// Use Description from properties if available (for SpecFlow tests), otherwise use name
|
|
154
|
+
let testName = testCase.name;
|
|
155
|
+
if (testCase.properties && testCase.properties.property) {
|
|
156
|
+
const properties = Array.isArray(testCase.properties.property)
|
|
157
|
+
? testCase.properties.property
|
|
158
|
+
: [testCase.properties.property];
|
|
159
|
+
const descriptionProperty = properties.find(p => p.name === 'Description');
|
|
160
|
+
if (descriptionProperty && descriptionProperty.value) {
|
|
161
|
+
// Clean up SpecFlow description format: [C211256] Allow mobile print behavior -> Allow mobile print behavior
|
|
162
|
+
testName = descriptionProperty.value.replace(/^\[[^\]]+\]\s*/, '');
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const fullName = testCase.fullname;
|
|
166
|
+
const methodName = testCase.methodname || this.extractMethodName(testName);
|
|
167
|
+
const className = testCase.classname || parentSuite?.name;
|
|
168
|
+
debug(`Parsing test case: ${testName}`);
|
|
169
|
+
debug(`Test case structure:`, JSON.stringify(testCase, null, 2));
|
|
170
|
+
// Extract parameters if this is a parameterized test
|
|
171
|
+
const { baseMethodName, parameters, isParameterized } = this.extractParameters(testName);
|
|
172
|
+
// Determine test status
|
|
173
|
+
let status = constants_js_1.STATUS.PASSED;
|
|
174
|
+
if (testCase.result) {
|
|
175
|
+
switch (testCase.result.toLowerCase()) {
|
|
176
|
+
case 'passed':
|
|
177
|
+
status = constants_js_1.STATUS.PASSED;
|
|
178
|
+
break;
|
|
179
|
+
case 'failed':
|
|
180
|
+
status = constants_js_1.STATUS.FAILED;
|
|
181
|
+
break;
|
|
182
|
+
case 'skipped':
|
|
183
|
+
case 'ignored':
|
|
184
|
+
status = constants_js_1.STATUS.SKIPPED;
|
|
185
|
+
break;
|
|
186
|
+
case 'inconclusive':
|
|
187
|
+
status = constants_js_1.STATUS.SKIPPED; // Treat inconclusive as skipped
|
|
188
|
+
break;
|
|
189
|
+
default:
|
|
190
|
+
status = constants_js_1.STATUS.PASSED;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// Extract error information
|
|
194
|
+
let message = '';
|
|
195
|
+
let stack = '';
|
|
196
|
+
const files = [];
|
|
197
|
+
// Extract attachments (NUnit format)
|
|
198
|
+
if (testCase.attachments) {
|
|
199
|
+
const attachments = Array.isArray(testCase.attachments.attachment)
|
|
200
|
+
? testCase.attachments.attachment
|
|
201
|
+
: [testCase.attachments.attachment];
|
|
202
|
+
const attachmentFiles = attachments.filter(a => a && a.filePath).map(a => a.filePath);
|
|
203
|
+
files.push(...attachmentFiles);
|
|
204
|
+
}
|
|
205
|
+
if (testCase.failure) {
|
|
206
|
+
message = testCase.failure.message || '';
|
|
207
|
+
stack = testCase.failure['stack-trace'] || testCase.failure['#text'] || '';
|
|
208
|
+
}
|
|
209
|
+
if (testCase.output) {
|
|
210
|
+
const outputText = typeof testCase.output === 'string' ? testCase.output : testCase.output['#text'];
|
|
211
|
+
const stackFiles = (0, utils_js_1.fetchFilesFromStackTrace)(outputText);
|
|
212
|
+
files.push(...stackFiles);
|
|
213
|
+
if (outputText) {
|
|
214
|
+
debug(`Found output in test case: ${outputText.substring(0, 100)}...`);
|
|
215
|
+
stack = `${stack}\n\n${outputText}`.trim();
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
debug('No output text found in test case');
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
debug('No output found in test case');
|
|
223
|
+
}
|
|
224
|
+
// Extract test ID and tags from properties
|
|
225
|
+
let testId = null;
|
|
226
|
+
let tags = [];
|
|
227
|
+
if (testCase.properties && testCase.properties.property) {
|
|
228
|
+
const properties = Array.isArray(testCase.properties.property)
|
|
229
|
+
? testCase.properties.property
|
|
230
|
+
: [testCase.properties.property];
|
|
231
|
+
const idProperty = properties.find(p => p.name === 'ID');
|
|
232
|
+
if (idProperty) {
|
|
233
|
+
testId = idProperty.value;
|
|
234
|
+
// Remove @ and T prefixes if present
|
|
235
|
+
if (testId.startsWith('@'))
|
|
236
|
+
testId = testId.slice(1);
|
|
237
|
+
if (testId.startsWith('T'))
|
|
238
|
+
testId = testId.slice(1);
|
|
239
|
+
}
|
|
240
|
+
// Extract Category properties as tags
|
|
241
|
+
const categoryProperties = properties.filter(p => p.name === 'Category');
|
|
242
|
+
tags = categoryProperties.map(p => p.value);
|
|
243
|
+
}
|
|
244
|
+
// If no test ID found in properties, try to extract from output
|
|
245
|
+
if (!testId && testCase.output) {
|
|
246
|
+
const outputText = typeof testCase.output === 'string' ? testCase.output : testCase.output['#text'];
|
|
247
|
+
if (outputText) {
|
|
248
|
+
debug(`Looking for test ID in output: ${outputText.substring(0, 200)}...`);
|
|
249
|
+
const idMatch = outputText.match(/\[ID\]\s+tid:\/\/@T([a-f0-9]{8})/i);
|
|
250
|
+
if (idMatch) {
|
|
251
|
+
testId = idMatch[1];
|
|
252
|
+
debug(`Found test ID in output: ${testId}`);
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
debug('No test ID found in output');
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
// Build file path from suite path and class name
|
|
260
|
+
const filePath = this.buildFilePath(suitePath, className, parentSuite);
|
|
261
|
+
// For parameterized tests, format example as expected by Testomatio API
|
|
262
|
+
// Convert array of parameters to object with numeric keys
|
|
263
|
+
let example = null;
|
|
264
|
+
if (isParameterized && parameters.length > 0) {
|
|
265
|
+
example = {};
|
|
266
|
+
parameters.forEach((param, index) => {
|
|
267
|
+
example[index] = param;
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
return {
|
|
271
|
+
// For runs: use full test name with parameters (TestBooleanValue(true))
|
|
272
|
+
// For import: API will group by base name using the example field
|
|
273
|
+
title: testName, // Full name with parameters for run display
|
|
274
|
+
methodName: baseMethodName || methodName || testName,
|
|
275
|
+
fullName: fullName,
|
|
276
|
+
suitePath: suitePath,
|
|
277
|
+
suite_title: className || suitePath[suitePath.length - 1] || 'Unknown',
|
|
278
|
+
file: filePath,
|
|
279
|
+
files: files, // Array of files that will be attached
|
|
280
|
+
status: status,
|
|
281
|
+
message: message,
|
|
282
|
+
stack: stack,
|
|
283
|
+
run_time: parseFloat(testCase.duration || testCase.time || 0) * 1000,
|
|
284
|
+
test_id: testId,
|
|
285
|
+
tags: tags, // Array of category tags from properties
|
|
286
|
+
create: true,
|
|
287
|
+
retry: false,
|
|
288
|
+
// Parameterized test metadata
|
|
289
|
+
example: example, // Parameters as object for API grouping
|
|
290
|
+
isParameterized: isParameterized,
|
|
291
|
+
parameters: parameters, // Keep original array for reference
|
|
292
|
+
baseMethodName: baseMethodName,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Extract method name and parameters from test name
|
|
297
|
+
* @param {string} testName - Full test name
|
|
298
|
+
* @returns {Object} - Extracted information
|
|
299
|
+
*/
|
|
300
|
+
extractParameters(testName) {
|
|
301
|
+
const paramMatch = testName.match(/^(.+?)\((.+)\)$/);
|
|
302
|
+
if (paramMatch) {
|
|
303
|
+
const baseMethodName = paramMatch[1].trim();
|
|
304
|
+
const paramString = paramMatch[2];
|
|
305
|
+
// Parse parameters - handle quoted strings and nested structures
|
|
306
|
+
const parameters = this.parseParameterString(paramString);
|
|
307
|
+
return {
|
|
308
|
+
baseMethodName: baseMethodName,
|
|
309
|
+
parameters: parameters,
|
|
310
|
+
isParameterized: true,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
return {
|
|
314
|
+
baseMethodName: testName,
|
|
315
|
+
parameters: [],
|
|
316
|
+
isParameterized: false,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Parse parameter string into array of parameters
|
|
321
|
+
* @param {string} paramString - Parameter string
|
|
322
|
+
* @returns {Array} - Array of parameters
|
|
323
|
+
*/
|
|
324
|
+
parseParameterString(paramString) {
|
|
325
|
+
const parameters = [];
|
|
326
|
+
let current = '';
|
|
327
|
+
let inQuotes = false;
|
|
328
|
+
let quoteChar = null;
|
|
329
|
+
let depth = 0;
|
|
330
|
+
for (let i = 0; i < paramString.length; i++) {
|
|
331
|
+
const char = paramString[i];
|
|
332
|
+
if (!inQuotes && (char === '"' || char === "'")) {
|
|
333
|
+
inQuotes = true;
|
|
334
|
+
quoteChar = char;
|
|
335
|
+
current += char;
|
|
336
|
+
}
|
|
337
|
+
else if (inQuotes && char === quoteChar) {
|
|
338
|
+
inQuotes = false;
|
|
339
|
+
quoteChar = null;
|
|
340
|
+
current += char;
|
|
341
|
+
}
|
|
342
|
+
else if (!inQuotes && char === '(') {
|
|
343
|
+
depth++;
|
|
344
|
+
current += char;
|
|
345
|
+
}
|
|
346
|
+
else if (!inQuotes && char === ')') {
|
|
347
|
+
depth--;
|
|
348
|
+
current += char;
|
|
349
|
+
}
|
|
350
|
+
else if (!inQuotes && char === ',' && depth === 0) {
|
|
351
|
+
parameters.push(current.trim());
|
|
352
|
+
current = '';
|
|
353
|
+
}
|
|
354
|
+
else {
|
|
355
|
+
current += char;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
if (current.trim()) {
|
|
359
|
+
parameters.push(current.trim());
|
|
360
|
+
}
|
|
361
|
+
// Clean up parameters - remove quotes if they wrap the entire parameter and filter empty ones
|
|
362
|
+
return parameters
|
|
363
|
+
.map(param => {
|
|
364
|
+
param = param.trim();
|
|
365
|
+
if ((param.startsWith('"') && param.endsWith('"')) || (param.startsWith("'") && param.endsWith("'"))) {
|
|
366
|
+
return param.slice(1, -1);
|
|
367
|
+
}
|
|
368
|
+
return param;
|
|
369
|
+
})
|
|
370
|
+
.filter(p => !!p);
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Extract method name from test name (fallback)
|
|
374
|
+
* @param {string} testName - Test name
|
|
375
|
+
* @returns {string} - Method name
|
|
376
|
+
*/
|
|
377
|
+
extractMethodName(testName) {
|
|
378
|
+
// Remove parameters if present
|
|
379
|
+
const paramMatch = testName.match(/^(.+?)\(/);
|
|
380
|
+
return paramMatch ? paramMatch[1].trim() : testName;
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Build file path from suite path and class name
|
|
384
|
+
* @param {Array} suitePath - Suite path array
|
|
385
|
+
* @param {string} className - Class name
|
|
386
|
+
* @param {Object} parentSuite - Parent suite for context
|
|
387
|
+
* @returns {string} - File path
|
|
388
|
+
*/
|
|
389
|
+
buildFilePath(suitePath, className, parentSuite) {
|
|
390
|
+
// Try to get file path from parent suite
|
|
391
|
+
if (parentSuite && parentSuite.filepath) {
|
|
392
|
+
return parentSuite.filepath;
|
|
393
|
+
}
|
|
394
|
+
// Build path from suite hierarchy
|
|
395
|
+
const pathParts = [...suitePath];
|
|
396
|
+
if (className && !pathParts.includes(className)) {
|
|
397
|
+
pathParts.push(className);
|
|
398
|
+
}
|
|
399
|
+
// Convert to file path format
|
|
400
|
+
return pathParts.join('/') + '.cs'; // Assume C# for NUnit
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Group parameterized tests by base method name
|
|
404
|
+
* @param {Array} tests - Array of parsed tests
|
|
405
|
+
* @returns {Object} - Grouped tests
|
|
406
|
+
*/
|
|
407
|
+
groupParameterizedTests(tests) {
|
|
408
|
+
const grouped = {};
|
|
409
|
+
tests.forEach(test => {
|
|
410
|
+
const key = test.isParameterized
|
|
411
|
+
? `${test.suitePath.join('.')}.${test.baseMethodName}`
|
|
412
|
+
: `${test.suitePath.join('.')}.${test.title}`;
|
|
413
|
+
if (!grouped[key]) {
|
|
414
|
+
grouped[key] = {
|
|
415
|
+
baseTest: {
|
|
416
|
+
name: test.baseMethodName || test.title,
|
|
417
|
+
suitePath: test.suitePath,
|
|
418
|
+
suite_title: test.suite_title,
|
|
419
|
+
file: test.file,
|
|
420
|
+
isParameterized: test.isParameterized,
|
|
421
|
+
},
|
|
422
|
+
variations: [],
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
grouped[key].variations.push(test);
|
|
426
|
+
});
|
|
427
|
+
return grouped;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
exports.NUnitXmlParser = NUnitXmlParser;
|
|
431
|
+
module.exports = NUnitXmlParser;
|
|
432
|
+
|
|
433
|
+
module.exports.NUnitXmlParser = NUnitXmlParser;
|
package/lib/pipe/bitbucket.js
CHANGED
|
@@ -73,8 +73,8 @@ class BitbucketPipe {
|
|
|
73
73
|
baseURL: 'https://api.bitbucket.org/2.0',
|
|
74
74
|
headers: {
|
|
75
75
|
'Content-Type': 'application/json',
|
|
76
|
-
|
|
77
|
-
}
|
|
76
|
+
Authorization: `Bearer ${this.token}`,
|
|
77
|
+
},
|
|
78
78
|
});
|
|
79
79
|
debug('Bitbucket Pipe: Enabled');
|
|
80
80
|
}
|
|
@@ -185,7 +185,7 @@ class BitbucketPipe {
|
|
|
185
185
|
const addCommentResponse = await this.client.request({
|
|
186
186
|
method: 'POST',
|
|
187
187
|
url: commentsRequestURL,
|
|
188
|
-
data: { content: { raw: body } }
|
|
188
|
+
data: { content: { raw: body } },
|
|
189
189
|
});
|
|
190
190
|
const commentID = addCommentResponse.data.id;
|
|
191
191
|
// eslint-disable-next-line max-len
|
|
@@ -212,7 +212,7 @@ async function deletePreviousReport(client, commentsRequestURL, hiddenCommentDat
|
|
|
212
212
|
try {
|
|
213
213
|
const response = await client.request({
|
|
214
214
|
method: 'GET',
|
|
215
|
-
url: commentsRequestURL
|
|
215
|
+
url: commentsRequestURL,
|
|
216
216
|
});
|
|
217
217
|
comments = response.data.values;
|
|
218
218
|
}
|
|
@@ -229,7 +229,7 @@ async function deletePreviousReport(client, commentsRequestURL, hiddenCommentDat
|
|
|
229
229
|
const deleteCommentURL = `${commentsRequestURL}/${comment.id}`;
|
|
230
230
|
await client.request({
|
|
231
231
|
method: 'DELETE',
|
|
232
|
-
url: deleteCommentURL
|
|
232
|
+
url: deleteCommentURL,
|
|
233
233
|
});
|
|
234
234
|
}
|
|
235
235
|
catch (e) {
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
export default CoveragePipe;
|
|
2
|
+
declare class CoveragePipe {
|
|
3
|
+
constructor(params: any, store: any);
|
|
4
|
+
id: string;
|
|
5
|
+
store: any;
|
|
6
|
+
branch: any;
|
|
7
|
+
isDefaultGitChanges: boolean;
|
|
8
|
+
isEnabled: boolean;
|
|
9
|
+
coverageFilePath: any;
|
|
10
|
+
isBranchDefault: boolean;
|
|
11
|
+
formattedDate: string;
|
|
12
|
+
title: string;
|
|
13
|
+
apiKey: string;
|
|
14
|
+
url: any;
|
|
15
|
+
client: Gaxios;
|
|
16
|
+
parsedCoverage: {};
|
|
17
|
+
changedFiles: any[];
|
|
18
|
+
matchedLines: Set<any>;
|
|
19
|
+
tests: Set<any>;
|
|
20
|
+
suiteIds: Set<any>;
|
|
21
|
+
tagLabels: Set<any>;
|
|
22
|
+
results: any[];
|
|
23
|
+
prepareRun(opts: any): Promise<any[]>;
|
|
24
|
+
addTest(data: any): void;
|
|
25
|
+
createRun(): Promise<void>;
|
|
26
|
+
updateRun(): void;
|
|
27
|
+
finishRun(runParams: any): Promise<void>;
|
|
28
|
+
toString(): string;
|
|
29
|
+
/**
|
|
30
|
+
* Retrieves the list of files changed in the current Git working directory
|
|
31
|
+
* compared to a specified branch.
|
|
32
|
+
*
|
|
33
|
+
* This method builds a Git diff command and attempts to retrieve the changed
|
|
34
|
+
* files using that command. It logs helpful information and errors during the process.
|
|
35
|
+
*
|
|
36
|
+
* If no changed files are found, or an error occurs at any stage, the method logs
|
|
37
|
+
* the issue and returns `undefined`.
|
|
38
|
+
*
|
|
39
|
+
* @returns {this | undefined} Returns the current instance (`this`) if changed files are found;
|
|
40
|
+
* otherwise, returns `undefined`.
|
|
41
|
+
*/
|
|
42
|
+
getGitChangedFiles(): this | undefined;
|
|
43
|
+
/**
|
|
44
|
+
* Validates the coverage file path (stored in `this.coverageFilePath`).
|
|
45
|
+
*
|
|
46
|
+
* This method checks:
|
|
47
|
+
* - That the file exists on disk.
|
|
48
|
+
* - That it is a regular file (not a directory or special file).
|
|
49
|
+
* - That it has a `.yml` extension to ensure it's a YAML file.
|
|
50
|
+
*
|
|
51
|
+
* Logs descriptive error messages for any failures.
|
|
52
|
+
*
|
|
53
|
+
* @returns {this | undefined} "true" in case if coverage file is valid and we can keep going;
|
|
54
|
+
* otherwise, `undefined`.
|
|
55
|
+
*/
|
|
56
|
+
validateCoverageFile(): this | undefined;
|
|
57
|
+
/**
|
|
58
|
+
* Parses the YAML coverage file (located at `this.coverageFilePath`) into a JavaScript object.
|
|
59
|
+
*
|
|
60
|
+
* - Reads the file content using UTF-8 encoding.
|
|
61
|
+
* - Parses it as YAML using the `yaml` library.
|
|
62
|
+
* - Stores the result in `this.parsedCoverage`.
|
|
63
|
+
* - If parsing fails, logs an error and returns `undefined`.
|
|
64
|
+
*
|
|
65
|
+
* @returns {this | undefined} The current parsed coverage yml file change lines, or "undefined" if parsing fails.
|
|
66
|
+
*/
|
|
67
|
+
parseCoverageFile(): this | undefined;
|
|
68
|
+
/**
|
|
69
|
+
* Extracts relevant test identifiers from changed files based on coverage mapping.
|
|
70
|
+
*
|
|
71
|
+
* Iterates over changed files and matches them against patterns in the parsed coverage data.
|
|
72
|
+
* For each match, it extracts test IDs or tags and stores them in corresponding sets:
|
|
73
|
+
* - Test IDs (starting with '@T' or '@S') are stored in `this.tests`
|
|
74
|
+
* - Tag labels (starting with 'tag:') are stored in `this.tagLabels`
|
|
75
|
+
* - Matched file paths are stored in `this.matchedLines`
|
|
76
|
+
*
|
|
77
|
+
* @returns {Promise <Set<string>>} A set of file paths that matched coverage patterns (`this.matchedLines`).
|
|
78
|
+
*/
|
|
79
|
+
extractRelevantTestsFromChanges(): Promise<Set<string>>;
|
|
80
|
+
#private;
|
|
81
|
+
}
|
|
82
|
+
import { Gaxios } from 'gaxios';
|