@testomatio/reporter 2.3.6 → 2.3.7-beta.2-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/cli.js +1 -1
- package/lib/bin/reportXml.js +5 -2
- package/lib/bin/startTest.js +3 -3
- package/lib/client.js +7 -3
- package/lib/junit-adapter/csharp.d.ts +0 -1
- package/lib/junit-adapter/csharp.js +40 -7
- package/lib/junit-adapter/nunit-parser.d.ts +82 -0
- package/lib/junit-adapter/nunit-parser.js +369 -0
- package/lib/pipe/debug.js +1 -1
- package/lib/pipe/testomatio.js +19 -15
- package/lib/template/testomatio.hbs +1366 -1026
- package/lib/uploader.js +4 -0
- package/lib/utils/utils.js +90 -11
- package/lib/xmlReader.d.ts +32 -26
- package/lib/xmlReader.js +106 -50
- package/package.json +1 -1
- package/src/bin/cli.js +1 -1
- package/src/bin/reportXml.js +5 -2
- package/src/bin/startTest.js +5 -5
- package/src/client.js +7 -4
- package/src/junit-adapter/csharp.js +45 -6
- package/src/junit-adapter/nunit-parser.js +404 -0
- package/src/pipe/debug.js +2 -3
- package/src/pipe/testomatio.js +75 -81
- package/src/template/testomatio.hbs +1366 -1026
- package/src/uploader.js +5 -0
- package/src/utils/utils.js +96 -9
- package/src/xmlReader.js +128 -45
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
import createDebugMessages from 'debug';
|
|
2
|
+
import { STATUS } from '../constants.js';
|
|
3
|
+
|
|
4
|
+
const debug = createDebugMessages('@testomatio/reporter:nunit-parser');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Enhanced NUnit XML Parser that properly handles test-suite hierarchy
|
|
8
|
+
* and parameterized tests
|
|
9
|
+
*/
|
|
10
|
+
export class NUnitXmlParser {
|
|
11
|
+
constructor(options = {}) {
|
|
12
|
+
this.options = options;
|
|
13
|
+
this.tests = [];
|
|
14
|
+
this.stats = {
|
|
15
|
+
total: 0,
|
|
16
|
+
passed: 0,
|
|
17
|
+
failed: 0,
|
|
18
|
+
skipped: 0,
|
|
19
|
+
inconclusive: 0,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Parse NUnit XML test-run structure
|
|
25
|
+
* @param {Object} testRun - Parsed XML test-run object
|
|
26
|
+
* @returns {Object} - Parsed test results
|
|
27
|
+
*/
|
|
28
|
+
parseTestRun(testRun) {
|
|
29
|
+
debug('Parsing NUnit test-run');
|
|
30
|
+
|
|
31
|
+
// Extract run-level statistics
|
|
32
|
+
this.stats = {
|
|
33
|
+
total: parseInt(testRun.total || 0, 10),
|
|
34
|
+
passed: parseInt(testRun.passed || 0, 10),
|
|
35
|
+
failed: parseInt(testRun.failed || 0, 10),
|
|
36
|
+
skipped: parseInt(testRun.skipped || 0, 10),
|
|
37
|
+
inconclusive: parseInt(testRun.inconclusive || 0, 10),
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Process the root test-suite
|
|
41
|
+
if (testRun['test-suite']) {
|
|
42
|
+
this.parseTestSuite(testRun['test-suite'], []);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
debug(`Parsed ${this.tests.length} tests from NUnit XML`);
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
status: testRun.result?.toLowerCase() || 'unknown',
|
|
49
|
+
create_tests: true,
|
|
50
|
+
tests_count: this.tests.length,
|
|
51
|
+
passed_count: this.tests.filter(t => t.status === STATUS.PASSED).length,
|
|
52
|
+
failed_count: this.tests.filter(t => t.status === STATUS.FAILED).length,
|
|
53
|
+
skipped_count: this.tests.filter(t => t.status === STATUS.SKIPPED).length,
|
|
54
|
+
tests: this.tests,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Recursively parse test-suite elements based on their type
|
|
60
|
+
* @param {Object|Array} testSuite - Test suite object or array
|
|
61
|
+
* @param {Array} parentPath - Current path in the hierarchy
|
|
62
|
+
*/
|
|
63
|
+
parseTestSuite(testSuite, parentPath = []) {
|
|
64
|
+
if (!testSuite) return;
|
|
65
|
+
|
|
66
|
+
// Handle arrays of test suites
|
|
67
|
+
if (Array.isArray(testSuite)) {
|
|
68
|
+
testSuite.forEach(suite => this.parseTestSuite(suite, parentPath));
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const suiteType = testSuite.type;
|
|
73
|
+
const suiteName = testSuite.name;
|
|
74
|
+
const fullName = testSuite.fullname;
|
|
75
|
+
|
|
76
|
+
debug(`Processing test-suite: type=${suiteType}, name=${suiteName}`);
|
|
77
|
+
|
|
78
|
+
switch (suiteType) {
|
|
79
|
+
case 'Assembly':
|
|
80
|
+
// Assembly level - ignore the name, just process children
|
|
81
|
+
debug('Processing Assembly level - ignoring name, processing children');
|
|
82
|
+
this.processChildren(testSuite, parentPath);
|
|
83
|
+
break;
|
|
84
|
+
|
|
85
|
+
case 'TestSuite':
|
|
86
|
+
// Namespace/grouping level - add to path but don't create test
|
|
87
|
+
debug(`Processing TestSuite level - adding '${suiteName}' to path`);
|
|
88
|
+
const newPath = [...parentPath, suiteName];
|
|
89
|
+
this.processChildren(testSuite, newPath);
|
|
90
|
+
break;
|
|
91
|
+
|
|
92
|
+
case 'TestFixture':
|
|
93
|
+
// Test class level - add to path and process test cases
|
|
94
|
+
debug(`Processing TestFixture level - test class '${suiteName}'`);
|
|
95
|
+
const testFixturePath = [...parentPath, suiteName];
|
|
96
|
+
this.processChildren(testSuite, testFixturePath);
|
|
97
|
+
break;
|
|
98
|
+
|
|
99
|
+
default:
|
|
100
|
+
debug(`Unknown test-suite type: ${suiteType}, treating as TestSuite`);
|
|
101
|
+
const unknownPath = [...parentPath, suiteName];
|
|
102
|
+
this.processChildren(testSuite, unknownPath);
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Process child elements of a test suite
|
|
109
|
+
* @param {Object} testSuite - Test suite object
|
|
110
|
+
* @param {Array} currentPath - Current path in hierarchy
|
|
111
|
+
*/
|
|
112
|
+
processChildren(testSuite, currentPath) {
|
|
113
|
+
// Process nested test-suites
|
|
114
|
+
if (testSuite['test-suite']) {
|
|
115
|
+
this.parseTestSuite(testSuite['test-suite'], currentPath);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Process test-cases
|
|
119
|
+
if (testSuite['test-case']) {
|
|
120
|
+
this.parseTestCases(testSuite['test-case'], currentPath, testSuite);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Parse test-case elements (actual tests)
|
|
126
|
+
* @param {Object|Array} testCases - Test case object or array
|
|
127
|
+
* @param {Array} suitePath - Path to the test suite
|
|
128
|
+
* @param {Object} parentSuite - Parent test suite for context
|
|
129
|
+
*/
|
|
130
|
+
parseTestCases(testCases, suitePath, parentSuite) {
|
|
131
|
+
if (!testCases) return;
|
|
132
|
+
|
|
133
|
+
// Handle arrays of test cases
|
|
134
|
+
if (!Array.isArray(testCases)) {
|
|
135
|
+
testCases = [testCases];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
testCases.forEach(testCase => {
|
|
139
|
+
const parsedTest = this.parseTestCase(testCase, suitePath, parentSuite);
|
|
140
|
+
if (parsedTest) {
|
|
141
|
+
this.tests.push(parsedTest);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Parse individual test case
|
|
148
|
+
* @param {Object} testCase - Test case object
|
|
149
|
+
* @param {Array} suitePath - Path to the test suite
|
|
150
|
+
* @param {Object} parentSuite - Parent test suite for context
|
|
151
|
+
* @returns {Object|null} - Parsed test object
|
|
152
|
+
*/
|
|
153
|
+
parseTestCase(testCase, suitePath, parentSuite) {
|
|
154
|
+
if (!testCase || !testCase.name) {
|
|
155
|
+
debug('Skipping test case without name');
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const testName = testCase.name;
|
|
160
|
+
const fullName = testCase.fullname;
|
|
161
|
+
const methodName = testCase.methodname || this.extractMethodName(testName);
|
|
162
|
+
const className = testCase.classname || parentSuite?.name;
|
|
163
|
+
|
|
164
|
+
debug(`Parsing test case: ${testName}`);
|
|
165
|
+
|
|
166
|
+
// Extract parameters if this is a parameterized test
|
|
167
|
+
const { baseMethodName, parameters, isParameterized } = this.extractParameters(testName);
|
|
168
|
+
|
|
169
|
+
// Determine test status
|
|
170
|
+
let status = STATUS.PASSED;
|
|
171
|
+
if (testCase.result) {
|
|
172
|
+
switch (testCase.result.toLowerCase()) {
|
|
173
|
+
case 'passed':
|
|
174
|
+
status = STATUS.PASSED;
|
|
175
|
+
break;
|
|
176
|
+
case 'failed':
|
|
177
|
+
status = STATUS.FAILED;
|
|
178
|
+
break;
|
|
179
|
+
case 'skipped':
|
|
180
|
+
case 'ignored':
|
|
181
|
+
status = STATUS.SKIPPED;
|
|
182
|
+
break;
|
|
183
|
+
case 'inconclusive':
|
|
184
|
+
status = STATUS.SKIPPED; // Treat inconclusive as skipped
|
|
185
|
+
break;
|
|
186
|
+
default:
|
|
187
|
+
status = STATUS.PASSED;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Extract error information
|
|
192
|
+
let message = '';
|
|
193
|
+
let stack = '';
|
|
194
|
+
|
|
195
|
+
if (testCase.failure) {
|
|
196
|
+
message = testCase.failure.message || '';
|
|
197
|
+
stack = testCase.failure['stack-trace'] || testCase.failure['#text'] || '';
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (testCase.output && testCase.output['#text']) {
|
|
201
|
+
stack = `${stack}\n\n${testCase.output['#text']}`.trim();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Extract test ID from properties
|
|
205
|
+
let testId = null;
|
|
206
|
+
if (testCase.properties && testCase.properties.property) {
|
|
207
|
+
const properties = Array.isArray(testCase.properties.property)
|
|
208
|
+
? testCase.properties.property
|
|
209
|
+
: [testCase.properties.property];
|
|
210
|
+
|
|
211
|
+
const idProperty = properties.find(p => p.name === 'ID');
|
|
212
|
+
if (idProperty) {
|
|
213
|
+
testId = idProperty.value;
|
|
214
|
+
// Remove @ and T prefixes if present
|
|
215
|
+
if (testId.startsWith('@')) testId = testId.slice(1);
|
|
216
|
+
if (testId.startsWith('T')) testId = testId.slice(1);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Build file path from suite path and class name
|
|
221
|
+
const filePath = this.buildFilePath(suitePath, className, parentSuite);
|
|
222
|
+
|
|
223
|
+
// For parameterized tests, format example as expected by Testomatio API
|
|
224
|
+
// Convert array of parameters to object with numeric keys
|
|
225
|
+
let example = null;
|
|
226
|
+
if (isParameterized && parameters.length > 0) {
|
|
227
|
+
example = {};
|
|
228
|
+
parameters.forEach((param, index) => {
|
|
229
|
+
example[index] = param;
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
// For runs: use full test name with parameters (TestBooleanValue(true))
|
|
235
|
+
// For import: API will group by base name using the example field
|
|
236
|
+
title: testName, // Full name with parameters for run display
|
|
237
|
+
methodName: baseMethodName || methodName || testName,
|
|
238
|
+
fullName: fullName,
|
|
239
|
+
suitePath: suitePath,
|
|
240
|
+
suite_title: className || suitePath[suitePath.length - 1] || 'Unknown',
|
|
241
|
+
file: filePath,
|
|
242
|
+
status: status,
|
|
243
|
+
message: message,
|
|
244
|
+
stack: stack,
|
|
245
|
+
run_time: parseFloat(testCase.duration || testCase.time || 0) * 1000,
|
|
246
|
+
test_id: testId,
|
|
247
|
+
create: true,
|
|
248
|
+
retry: false,
|
|
249
|
+
// Parameterized test metadata
|
|
250
|
+
example: example, // Parameters as object for API grouping
|
|
251
|
+
isParameterized: isParameterized,
|
|
252
|
+
parameters: parameters, // Keep original array for reference
|
|
253
|
+
baseMethodName: baseMethodName,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Extract method name and parameters from test name
|
|
259
|
+
* @param {string} testName - Full test name
|
|
260
|
+
* @returns {Object} - Extracted information
|
|
261
|
+
*/
|
|
262
|
+
extractParameters(testName) {
|
|
263
|
+
const paramMatch = testName.match(/^(.+?)\((.+)\)$/);
|
|
264
|
+
|
|
265
|
+
if (paramMatch) {
|
|
266
|
+
const baseMethodName = paramMatch[1].trim();
|
|
267
|
+
const paramString = paramMatch[2];
|
|
268
|
+
|
|
269
|
+
// Parse parameters - handle quoted strings and nested structures
|
|
270
|
+
const parameters = this.parseParameterString(paramString);
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
baseMethodName: baseMethodName,
|
|
274
|
+
parameters: parameters,
|
|
275
|
+
isParameterized: true,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return {
|
|
280
|
+
baseMethodName: testName,
|
|
281
|
+
parameters: [],
|
|
282
|
+
isParameterized: false,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Parse parameter string into array of parameters
|
|
288
|
+
* @param {string} paramString - Parameter string
|
|
289
|
+
* @returns {Array} - Array of parameters
|
|
290
|
+
*/
|
|
291
|
+
parseParameterString(paramString) {
|
|
292
|
+
const parameters = [];
|
|
293
|
+
let current = '';
|
|
294
|
+
let inQuotes = false;
|
|
295
|
+
let quoteChar = null;
|
|
296
|
+
let depth = 0;
|
|
297
|
+
|
|
298
|
+
for (let i = 0; i < paramString.length; i++) {
|
|
299
|
+
const char = paramString[i];
|
|
300
|
+
|
|
301
|
+
if (!inQuotes && (char === '"' || char === "'")) {
|
|
302
|
+
inQuotes = true;
|
|
303
|
+
quoteChar = char;
|
|
304
|
+
current += char;
|
|
305
|
+
} else if (inQuotes && char === quoteChar) {
|
|
306
|
+
inQuotes = false;
|
|
307
|
+
quoteChar = null;
|
|
308
|
+
current += char;
|
|
309
|
+
} else if (!inQuotes && char === '(') {
|
|
310
|
+
depth++;
|
|
311
|
+
current += char;
|
|
312
|
+
} else if (!inQuotes && char === ')') {
|
|
313
|
+
depth--;
|
|
314
|
+
current += char;
|
|
315
|
+
} else if (!inQuotes && char === ',' && depth === 0) {
|
|
316
|
+
parameters.push(current.trim());
|
|
317
|
+
current = '';
|
|
318
|
+
} else {
|
|
319
|
+
current += char;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (current.trim()) {
|
|
324
|
+
parameters.push(current.trim());
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Clean up parameters - remove quotes if they wrap the entire parameter
|
|
328
|
+
return parameters.map(param => {
|
|
329
|
+
param = param.trim();
|
|
330
|
+
if ((param.startsWith('"') && param.endsWith('"')) || (param.startsWith("'") && param.endsWith("'"))) {
|
|
331
|
+
return param.slice(1, -1);
|
|
332
|
+
}
|
|
333
|
+
return param;
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Extract method name from test name (fallback)
|
|
339
|
+
* @param {string} testName - Test name
|
|
340
|
+
* @returns {string} - Method name
|
|
341
|
+
*/
|
|
342
|
+
extractMethodName(testName) {
|
|
343
|
+
// Remove parameters if present
|
|
344
|
+
const paramMatch = testName.match(/^(.+?)\(/);
|
|
345
|
+
return paramMatch ? paramMatch[1].trim() : testName;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Build file path from suite path and class name
|
|
350
|
+
* @param {Array} suitePath - Suite path array
|
|
351
|
+
* @param {string} className - Class name
|
|
352
|
+
* @param {Object} parentSuite - Parent suite for context
|
|
353
|
+
* @returns {string} - File path
|
|
354
|
+
*/
|
|
355
|
+
buildFilePath(suitePath, className, parentSuite) {
|
|
356
|
+
// Try to get file path from parent suite
|
|
357
|
+
if (parentSuite && parentSuite.filepath) {
|
|
358
|
+
return parentSuite.filepath;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Build path from suite hierarchy
|
|
362
|
+
const pathParts = [...suitePath];
|
|
363
|
+
if (className && !pathParts.includes(className)) {
|
|
364
|
+
pathParts.push(className);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Convert to file path format
|
|
368
|
+
return pathParts.join('/') + '.cs'; // Assume C# for NUnit
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Group parameterized tests by base method name
|
|
373
|
+
* @param {Array} tests - Array of parsed tests
|
|
374
|
+
* @returns {Object} - Grouped tests
|
|
375
|
+
*/
|
|
376
|
+
groupParameterizedTests(tests) {
|
|
377
|
+
const grouped = {};
|
|
378
|
+
|
|
379
|
+
tests.forEach(test => {
|
|
380
|
+
const key = test.isParameterized
|
|
381
|
+
? `${test.suitePath.join('.')}.${test.baseMethodName}`
|
|
382
|
+
: `${test.suitePath.join('.')}.${test.title}`;
|
|
383
|
+
|
|
384
|
+
if (!grouped[key]) {
|
|
385
|
+
grouped[key] = {
|
|
386
|
+
baseTest: {
|
|
387
|
+
name: test.baseMethodName || test.title,
|
|
388
|
+
suitePath: test.suitePath,
|
|
389
|
+
suite_title: test.suite_title,
|
|
390
|
+
file: test.file,
|
|
391
|
+
isParameterized: test.isParameterized,
|
|
392
|
+
},
|
|
393
|
+
variations: [],
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
grouped[key].variations.push(test);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
return grouped;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
export default NUnitXmlParser;
|
package/src/pipe/debug.js
CHANGED
|
@@ -15,7 +15,7 @@ export class DebugPipe {
|
|
|
15
15
|
this.isEnabled = !!process.env.TESTOMATIO_DEBUG || !!process.env.DEBUG;
|
|
16
16
|
if (this.isEnabled) {
|
|
17
17
|
this.batch = {
|
|
18
|
-
isEnabled: this.params.isBatchEnabled ??
|
|
18
|
+
isEnabled: this.params.isBatchEnabled ?? (process.env.TESTOMATIO_DISABLE_BATCH_UPLOAD ? false : true),
|
|
19
19
|
intervalFunction: null,
|
|
20
20
|
intervalTime: 5000,
|
|
21
21
|
tests: [],
|
|
@@ -93,8 +93,7 @@ export class DebugPipe {
|
|
|
93
93
|
const logData = { action: 'addTest', testId: data };
|
|
94
94
|
if (this.store.runId) logData.runId = this.store.runId;
|
|
95
95
|
this.logToFile(logData);
|
|
96
|
-
}
|
|
97
|
-
else this.batch.tests.push(data);
|
|
96
|
+
} else this.batch.tests.push(data);
|
|
98
97
|
|
|
99
98
|
if (!this.batch.intervalFunction) await this.batchUpload();
|
|
100
99
|
}
|
package/src/pipe/testomatio.js
CHANGED
|
@@ -20,7 +20,7 @@ if (process.env.TESTOMATIO_RUN) process.env.runId = process.env.TESTOMATIO_RUN;
|
|
|
20
20
|
class TestomatioPipe {
|
|
21
21
|
constructor(params, store) {
|
|
22
22
|
this.batch = {
|
|
23
|
-
isEnabled: params?.isBatchEnabled ??
|
|
23
|
+
isEnabled: params?.isBatchEnabled ?? (process.env.TESTOMATIO_DISABLE_BATCH_UPLOAD ? false : true),
|
|
24
24
|
intervalFunction: null, // will be created in createRun by setInterval function
|
|
25
25
|
intervalTime: 5000, // how often tests are sent
|
|
26
26
|
tests: [], // array of tests in batch
|
|
@@ -60,8 +60,8 @@ class TestomatioPipe {
|
|
|
60
60
|
retryConfig: {
|
|
61
61
|
retry: REPORTER_REQUEST_RETRIES.retriesPerRequest,
|
|
62
62
|
retryDelay: REPORTER_REQUEST_RETRIES.retryTimeout,
|
|
63
|
-
httpMethodsToRetry: ['GET','PUT','HEAD','OPTIONS','DELETE','POST'],
|
|
64
|
-
shouldRetry:
|
|
63
|
+
httpMethodsToRetry: ['GET', 'PUT', 'HEAD', 'OPTIONS', 'DELETE', 'POST'],
|
|
64
|
+
shouldRetry: error => {
|
|
65
65
|
if (!error.response) return false;
|
|
66
66
|
switch (error.response?.status) {
|
|
67
67
|
case 400: // Bad request (probably wrong API key)
|
|
@@ -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
|
|
|
80
80
|
this.isEnabled = true;
|
|
@@ -104,7 +104,6 @@ class TestomatioPipe {
|
|
|
104
104
|
// add test ID + run ID
|
|
105
105
|
if (data.rid) data.rid = `${this.runId}-${data.rid}`;
|
|
106
106
|
|
|
107
|
-
|
|
108
107
|
if (!process.env.TESTOMATIO_STACK_PASSED && data.status === STATUS.PASSED) {
|
|
109
108
|
data.stack = null;
|
|
110
109
|
}
|
|
@@ -120,7 +119,6 @@ class TestomatioPipe {
|
|
|
120
119
|
return data;
|
|
121
120
|
}
|
|
122
121
|
|
|
123
|
-
|
|
124
122
|
/**
|
|
125
123
|
* Asynchronously prepares and retrieves the Testomat.io test grepList based on the provided options.
|
|
126
124
|
* @param {Object} opts - The options for preparing the test grepList.
|
|
@@ -215,7 +213,7 @@ class TestomatioPipe {
|
|
|
215
213
|
method: 'PUT',
|
|
216
214
|
url: `/api/reporter/${this.runId}`,
|
|
217
215
|
data: runParams,
|
|
218
|
-
responseType: 'json'
|
|
216
|
+
responseType: 'json',
|
|
219
217
|
});
|
|
220
218
|
if (resp.data.artifacts) setS3Credentials(resp.data.artifacts);
|
|
221
219
|
return;
|
|
@@ -228,7 +226,7 @@ class TestomatioPipe {
|
|
|
228
226
|
url: '/api/reporter',
|
|
229
227
|
data: runParams,
|
|
230
228
|
maxContentLength: Infinity,
|
|
231
|
-
responseType: 'json'
|
|
229
|
+
responseType: 'json',
|
|
232
230
|
});
|
|
233
231
|
|
|
234
232
|
this.runId = resp.data.uid;
|
|
@@ -287,44 +285,44 @@ class TestomatioPipe {
|
|
|
287
285
|
|
|
288
286
|
debug('Adding test', json);
|
|
289
287
|
|
|
290
|
-
return this.client
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
288
|
+
return this.client
|
|
289
|
+
.request({
|
|
290
|
+
method: 'POST',
|
|
291
|
+
url: `/api/reporter/${this.runId}/testrun`,
|
|
292
|
+
data: json,
|
|
293
|
+
headers: {
|
|
294
|
+
'Content-Type': 'application/json',
|
|
295
|
+
},
|
|
296
|
+
maxContentLength: Infinity,
|
|
297
|
+
})
|
|
298
|
+
.catch(err => {
|
|
299
|
+
this.requestFailures++;
|
|
300
|
+
this.notReportedTestsCount++;
|
|
301
|
+
if (err.response) {
|
|
302
|
+
if (err.response.status >= 400) {
|
|
303
|
+
const responseData = err.response.data || { message: '' };
|
|
304
|
+
console.log(
|
|
305
|
+
APP_PREFIX,
|
|
306
|
+
pc.yellow(`Warning: ${responseData.message} (${err.response.status})`),
|
|
307
|
+
pc.gray(data?.title || ''),
|
|
308
|
+
);
|
|
309
|
+
if (err.response?.data?.message?.includes('could not be matched')) {
|
|
310
|
+
this.hasUnmatchedTests = true;
|
|
311
|
+
}
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
304
314
|
console.log(
|
|
305
315
|
APP_PREFIX,
|
|
306
|
-
pc.yellow(`Warning: ${
|
|
307
|
-
|
|
316
|
+
pc.yellow(`Warning: ${data?.title || ''} (${err.response?.status})`),
|
|
317
|
+
`Report couldn't be processed: ${err?.response?.data?.message}`,
|
|
308
318
|
);
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
return;
|
|
319
|
+
printCreateIssue(err);
|
|
320
|
+
} else {
|
|
321
|
+
console.log(APP_PREFIX, pc.blue(data?.title || ''), "Report couldn't be processed", err);
|
|
313
322
|
}
|
|
314
|
-
|
|
315
|
-
APP_PREFIX,
|
|
316
|
-
pc.yellow(`Warning: ${data?.title || ''} (${err.response?.status})`),
|
|
317
|
-
`Report couldn't be processed: ${err?.response?.data?.message}`,
|
|
318
|
-
);
|
|
319
|
-
printCreateIssue(err);
|
|
320
|
-
} else {
|
|
321
|
-
console.log(APP_PREFIX, pc.blue(data?.title || ''), "Report couldn't be processed", err);
|
|
322
|
-
}
|
|
323
|
-
});
|
|
323
|
+
});
|
|
324
324
|
};
|
|
325
325
|
|
|
326
|
-
|
|
327
|
-
|
|
328
326
|
/**
|
|
329
327
|
* Uploads tests as a batch (multiple tests at once). Intended to be used with a setInterval
|
|
330
328
|
*/
|
|
@@ -349,43 +347,42 @@ class TestomatioPipe {
|
|
|
349
347
|
const testsToSend = this.batch.tests.splice(0);
|
|
350
348
|
debug('📨 Batch upload', testsToSend.length, 'tests');
|
|
351
349
|
|
|
352
|
-
return this.client
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
350
|
+
return this.client
|
|
351
|
+
.request({
|
|
352
|
+
method: 'POST',
|
|
353
|
+
url: `/api/reporter/${this.runId}/testrun`,
|
|
354
|
+
data: {
|
|
355
|
+
api_key: this.apiKey,
|
|
356
|
+
tests: testsToSend,
|
|
357
|
+
batch_index: this.batch.batchIndex,
|
|
358
|
+
},
|
|
359
|
+
headers: {
|
|
360
|
+
'Content-Type': 'application/json',
|
|
361
|
+
},
|
|
362
|
+
maxContentLength: Infinity,
|
|
363
|
+
})
|
|
364
|
+
.catch(err => {
|
|
365
|
+
this.requestFailures++;
|
|
366
|
+
this.notReportedTestsCount += testsToSend.length;
|
|
367
|
+
if (err.response) {
|
|
368
|
+
if (err.response.status >= 400) {
|
|
369
|
+
const responseData = err.response.data || { message: '' };
|
|
370
|
+
console.log(APP_PREFIX, pc.yellow(`Warning: ${responseData.message} (${err.response.status})`));
|
|
371
|
+
if (err.response?.data?.message?.includes('could not be matched')) {
|
|
372
|
+
this.hasUnmatchedTests = true;
|
|
373
|
+
}
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
370
376
|
console.log(
|
|
371
377
|
APP_PREFIX,
|
|
372
|
-
pc.yellow(`Warning:
|
|
378
|
+
pc.yellow(`Warning: (${err.response?.status})`),
|
|
379
|
+
`Report couldn't be processed: ${err?.response?.data?.message}`,
|
|
373
380
|
);
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
return;
|
|
381
|
+
printCreateIssue(err);
|
|
382
|
+
} else {
|
|
383
|
+
console.log(APP_PREFIX, "Report couldn't be processed", err);
|
|
378
384
|
}
|
|
379
|
-
|
|
380
|
-
APP_PREFIX,
|
|
381
|
-
pc.yellow(`Warning: (${err.response?.status})`),
|
|
382
|
-
`Report couldn't be processed: ${err?.response?.data?.message}`,
|
|
383
|
-
);
|
|
384
|
-
printCreateIssue(err);
|
|
385
|
-
} else {
|
|
386
|
-
console.log(APP_PREFIX, "Report couldn't be processed", err);
|
|
387
|
-
}
|
|
388
|
-
});
|
|
385
|
+
});
|
|
389
386
|
};
|
|
390
387
|
|
|
391
388
|
/**
|
|
@@ -408,9 +405,9 @@ class TestomatioPipe {
|
|
|
408
405
|
else this.batch.tests.push(data);
|
|
409
406
|
|
|
410
407
|
// if test is added after run which is already finished
|
|
411
|
-
|
|
408
|
+
if (!this.batch.intervalFunction) uploading = this.#batchUpload();
|
|
412
409
|
|
|
413
|
-
|
|
410
|
+
// return promise to be able to wait for it
|
|
414
411
|
return uploading;
|
|
415
412
|
}
|
|
416
413
|
|
|
@@ -459,7 +456,7 @@ class TestomatioPipe {
|
|
|
459
456
|
status_event,
|
|
460
457
|
detach: params.detach,
|
|
461
458
|
tests: params.tests,
|
|
462
|
-
}
|
|
459
|
+
},
|
|
463
460
|
});
|
|
464
461
|
|
|
465
462
|
if (this.runUrl) {
|
|
@@ -472,7 +469,7 @@ class TestomatioPipe {
|
|
|
472
469
|
if (this.runUrl && this.proceed) {
|
|
473
470
|
const notFinishedMessage = pc.yellow(pc.bold('Run was not finished because of $TESTOMATIO_PROCEED'));
|
|
474
471
|
console.log(APP_PREFIX, `📊 ${notFinishedMessage}. Report URL: ${pc.magenta(this.runUrl)}`);
|
|
475
|
-
console.log(APP_PREFIX, `🛬 Run to finish it: TESTOMATIO_RUN=${this.runId} npx
|
|
472
|
+
console.log(APP_PREFIX, `🛬 Run to finish it: TESTOMATIO_RUN=${this.runId} npx @testomatio/reporter finish`);
|
|
476
473
|
}
|
|
477
474
|
|
|
478
475
|
if (this.hasUnmatchedTests) {
|
|
@@ -525,9 +522,6 @@ function printCreateIssue(err) {
|
|
|
525
522
|
console.log({ body: body?.replace(/"(tstmt_[^"]+)"/g, 'tstmt_*'), url, baseURL, method, time });
|
|
526
523
|
console.log('```');
|
|
527
524
|
});
|
|
528
|
-
|
|
529
525
|
}
|
|
530
526
|
|
|
531
|
-
|
|
532
|
-
|
|
533
527
|
export default TestomatioPipe;
|