@testomatio/reporter 2.3.7-beta.1-xml-import → 2.3.7-beta.100
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/adapter/codecept.js +22 -2
- package/lib/bin/cli.js +13 -3
- package/lib/bin/reportXml.js +1 -4
- package/lib/bin/startTest.js +3 -3
- package/lib/bin/uploadArtifacts.js +0 -0
- package/lib/client.d.ts +1 -1
- package/lib/client.js +32 -23
- package/lib/junit-adapter/csharp.d.ts +1 -0
- package/lib/junit-adapter/csharp.js +7 -36
- package/lib/pipe/debug.js +1 -1
- package/lib/pipe/testomatio.d.ts +2 -1
- package/lib/pipe/testomatio.js +17 -21
- package/lib/reporter.d.ts +19 -9
- package/lib/reporter.js +40 -5
- package/lib/template/testomatio.hbs +1366 -1026
- package/lib/uploader.js +0 -4
- package/lib/utils/utils.d.ts +1 -0
- package/lib/utils/utils.js +23 -35
- package/lib/xmlReader.d.ts +26 -11
- package/lib/xmlReader.js +1 -50
- package/package.json +8 -4
- package/src/adapter/codecept.js +27 -3
- package/src/bin/cli.js +1 -1
- package/src/bin/reportXml.js +1 -1
- package/src/bin/startTest.js +5 -5
- package/src/client.js +55 -27
- package/src/junit-adapter/csharp.js +11 -6
- package/src/junit-adapter/nunit-parser.js +83 -12
- package/src/pipe/debug.js +3 -2
- package/src/pipe/testomatio.js +81 -77
- package/src/reporter.js +7 -4
- package/src/template/testomatio.hbs +1366 -1026
- package/src/utils/utils.js +205 -32
- package/src/xmlReader.js +70 -45
- package/types/types.d.ts +364 -0
- package/types/vitest.types.d.ts +93 -0
- package/lib/junit-adapter/nunit-parser.d.ts +0 -82
- package/lib/junit-adapter/nunit-parser.js +0 -357
package/lib/uploader.js
CHANGED
|
@@ -170,10 +170,6 @@ 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
|
-
}
|
|
177
173
|
const data = { rid, file: filePath, uploaded };
|
|
178
174
|
const jsonLine = `${JSON.stringify(data)}\n`;
|
|
179
175
|
fs_1.default.appendFileSync(tempFilePath, jsonLine);
|
package/lib/utils/utils.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ export function getPackageVersion(): any;
|
|
|
2
2
|
export const TEST_ID_REGEX: RegExp;
|
|
3
3
|
export const SUITE_ID_REGEX: RegExp;
|
|
4
4
|
export function ansiRegExp(): RegExp;
|
|
5
|
+
export function truncate(s: any, size?: number): any;
|
|
5
6
|
export function cleanLatestRunId(): any;
|
|
6
7
|
export function isSameTest(test: any, t: any): boolean;
|
|
7
8
|
export function fetchSourceCode(contents: any, opts?: {}): string;
|
package/lib/utils/utils.js
CHANGED
|
@@ -38,6 +38,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
39
|
exports.validateSuiteId = exports.testRunnerHelper = exports.specificTestInfo = exports.parseSuite = exports.isValidUrl = exports.humanize = exports.getTestomatIdFromTestTitle = exports.getCurrentDateTime = exports.foundedTestLog = exports.fileSystem = exports.fetchFilesFromStackTrace = exports.fetchIdFromOutput = exports.fetchIdFromCode = exports.fetchSourceCodeFromStackTrace = exports.fetchSourceCode = exports.isSameTest = exports.ansiRegExp = exports.SUITE_ID_REGEX = exports.TEST_ID_REGEX = void 0;
|
|
40
40
|
exports.getPackageVersion = getPackageVersion;
|
|
41
|
+
exports.truncate = truncate;
|
|
41
42
|
exports.cleanLatestRunId = cleanLatestRunId;
|
|
42
43
|
exports.formatStep = formatStep;
|
|
43
44
|
exports.readLatestRunId = readLatestRunId;
|
|
@@ -121,8 +122,12 @@ const fetchFilesFromStackTrace = (stack = '', checkExists = true) => {
|
|
|
121
122
|
.map(f => f[1].trim())
|
|
122
123
|
.map(f => f.replace(/^\/+/, '/').replace(/^\/([A-Za-z]:)/, '$1')) // Remove extra slashes, handle Windows paths
|
|
123
124
|
.map(f => {
|
|
124
|
-
//
|
|
125
|
-
|
|
125
|
+
// Convert Windows paths to Linux paths for testing purposes
|
|
126
|
+
if (f.match(/^[A-Za-z]:[\\\/]/)) {
|
|
127
|
+
// Convert Windows path to Linux equivalent for test scenarios
|
|
128
|
+
return f.replace(/^[A-Za-z]:[\\\/]/, '/').replace(/\\/g, '/');
|
|
129
|
+
}
|
|
130
|
+
return f;
|
|
126
131
|
});
|
|
127
132
|
debug('Found files in stack trace: ', files);
|
|
128
133
|
return files.filter(f => {
|
|
@@ -169,8 +174,6 @@ exports.fetchSourceCodeFromStackTrace = fetchSourceCodeFromStackTrace;
|
|
|
169
174
|
exports.TEST_ID_REGEX = /@T([\w\d]{8})/;
|
|
170
175
|
exports.SUITE_ID_REGEX = /@S([\w\d]{8})/;
|
|
171
176
|
const fetchIdFromCode = (code, opts = {}) => {
|
|
172
|
-
if (!code)
|
|
173
|
-
return null;
|
|
174
177
|
const comments = code
|
|
175
178
|
.split('\n')
|
|
176
179
|
.map(l => l.trim())
|
|
@@ -213,29 +216,10 @@ const fetchSourceCode = (contents, opts = {}) => {
|
|
|
213
216
|
lineIndex = lines.findIndex(l => l.includes(`${title}(`));
|
|
214
217
|
}
|
|
215
218
|
else if (opts.lang === 'csharp') {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
if (lineIndex === -1)
|
|
219
|
-
lineIndex = lines.findIndex(l => l.includes(`public async Task ${title}(`));
|
|
220
|
-
}
|
|
221
|
-
if (lineIndex === -1) {
|
|
219
|
+
if (lineIndex === -1)
|
|
220
|
+
lineIndex = lines.findIndex(l => l.includes(`public void ${title}`));
|
|
221
|
+
if (lineIndex === -1)
|
|
222
222
|
lineIndex = lines.findIndex(l => l.includes(`${title}(`));
|
|
223
|
-
}
|
|
224
|
-
// Look for TestCase or Test attributes above the method
|
|
225
|
-
if (lineIndex === -1) {
|
|
226
|
-
const testAttributeIndex = lines.findIndex((l, index) => {
|
|
227
|
-
if (l.includes('[TestCase') || l.includes('[Test')) {
|
|
228
|
-
// Check next few lines for the method
|
|
229
|
-
const nextLines = lines.slice(index, Math.min(lines.length, index + 5));
|
|
230
|
-
const hasMethod = nextLines.some(nextLine => nextLine.includes(`${title}(`));
|
|
231
|
-
return hasMethod;
|
|
232
|
-
}
|
|
233
|
-
return false;
|
|
234
|
-
});
|
|
235
|
-
if (testAttributeIndex !== -1) {
|
|
236
|
-
lineIndex = testAttributeIndex;
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
223
|
}
|
|
240
224
|
else {
|
|
241
225
|
lineIndex = lines.findIndex(l => l.includes(title));
|
|
@@ -244,7 +228,7 @@ const fetchSourceCode = (contents, opts = {}) => {
|
|
|
244
228
|
if (opts.prepend) {
|
|
245
229
|
lineIndex -= opts.prepend;
|
|
246
230
|
}
|
|
247
|
-
if (lineIndex
|
|
231
|
+
if (lineIndex) {
|
|
248
232
|
const result = [];
|
|
249
233
|
for (let i = lineIndex; i < lineIndex + limit; i++) {
|
|
250
234
|
if (lines[i] === undefined)
|
|
@@ -287,14 +271,6 @@ const fetchSourceCode = (contents, opts = {}) => {
|
|
|
287
271
|
break;
|
|
288
272
|
if (opts.lang === 'java' && lines[i].includes(' class '))
|
|
289
273
|
break;
|
|
290
|
-
if (opts.lang === 'csharp' && lines[i].trim().match(/^\[Test/))
|
|
291
|
-
break;
|
|
292
|
-
if (opts.lang === 'csharp' && lines[i].includes(' public void '))
|
|
293
|
-
break;
|
|
294
|
-
if (opts.lang === 'csharp' && lines[i].includes(' public async Task '))
|
|
295
|
-
break;
|
|
296
|
-
if (opts.lang === 'csharp' && lines[i].includes(' class ') && lines[i].includes('public'))
|
|
297
|
-
break;
|
|
298
274
|
}
|
|
299
275
|
result.push(lines[i]);
|
|
300
276
|
}
|
|
@@ -494,9 +470,21 @@ function transformEnvVarToBoolean(value) {
|
|
|
494
470
|
// if not recognized, return truthy if any value is set
|
|
495
471
|
return Boolean(value);
|
|
496
472
|
}
|
|
473
|
+
function truncate(s, size = 255) {
|
|
474
|
+
if (s === undefined || s === null) {
|
|
475
|
+
return '';
|
|
476
|
+
}
|
|
477
|
+
const str = s.toString();
|
|
478
|
+
if (str.trim().length < size) {
|
|
479
|
+
return str;
|
|
480
|
+
}
|
|
481
|
+
return `${str.substring(0, size)}...`;
|
|
482
|
+
}
|
|
497
483
|
|
|
498
484
|
module.exports.getPackageVersion = getPackageVersion;
|
|
499
485
|
|
|
486
|
+
module.exports.truncate = truncate;
|
|
487
|
+
|
|
500
488
|
module.exports.cleanLatestRunId = cleanLatestRunId;
|
|
501
489
|
|
|
502
490
|
module.exports.formatStep = formatStep;
|
package/lib/xmlReader.d.ts
CHANGED
|
@@ -19,11 +19,25 @@ declare class XmlReader {
|
|
|
19
19
|
tests: any[];
|
|
20
20
|
stats: {};
|
|
21
21
|
uploader: S3Uploader;
|
|
22
|
-
enhancedNunit: boolean;
|
|
23
|
-
groupParameterized: boolean;
|
|
24
22
|
version: any;
|
|
25
23
|
connectAdapter(): import("./junit-adapter/adapter.js").default;
|
|
26
|
-
parse(fileName: any):
|
|
24
|
+
parse(fileName: any): {
|
|
25
|
+
status: string;
|
|
26
|
+
create_tests: boolean;
|
|
27
|
+
tests_count: number;
|
|
28
|
+
passed_count: number;
|
|
29
|
+
skipped_count: number;
|
|
30
|
+
failed_count: number;
|
|
31
|
+
tests: any;
|
|
32
|
+
} | {
|
|
33
|
+
status: any;
|
|
34
|
+
create_tests: boolean;
|
|
35
|
+
tests_count: number;
|
|
36
|
+
passed_count: number;
|
|
37
|
+
failed_count: number;
|
|
38
|
+
skipped_count: number;
|
|
39
|
+
tests: any[];
|
|
40
|
+
};
|
|
27
41
|
processJUnit(jsonSuite: any): {
|
|
28
42
|
create_tests: boolean;
|
|
29
43
|
duration: number;
|
|
@@ -35,14 +49,15 @@ declare class XmlReader {
|
|
|
35
49
|
tests: any[];
|
|
36
50
|
tests_count: number;
|
|
37
51
|
};
|
|
38
|
-
processNUnit(jsonSuite: any):
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
52
|
+
processNUnit(jsonSuite: any): {
|
|
53
|
+
status: any;
|
|
54
|
+
create_tests: boolean;
|
|
55
|
+
tests_count: number;
|
|
56
|
+
passed_count: number;
|
|
57
|
+
failed_count: number;
|
|
58
|
+
skipped_count: number;
|
|
59
|
+
tests: any[];
|
|
60
|
+
};
|
|
46
61
|
processTRX(jsonSuite: any): {
|
|
47
62
|
status: string;
|
|
48
63
|
create_tests: boolean;
|
package/lib/xmlReader.js
CHANGED
|
@@ -11,7 +11,6 @@ const fast_xml_parser_1 = require("fast-xml-parser");
|
|
|
11
11
|
const constants_js_1 = require("./constants.js");
|
|
12
12
|
const crypto_1 = require("crypto");
|
|
13
13
|
const url_1 = require("url");
|
|
14
|
-
const nunit_parser_js_1 = require("./junit-adapter/nunit-parser.js");
|
|
15
14
|
const utils_js_1 = require("./utils/utils.js");
|
|
16
15
|
const index_js_1 = require("./pipe/index.js");
|
|
17
16
|
const index_js_2 = __importDefault(require("./junit-adapter/index.js"));
|
|
@@ -55,9 +54,6 @@ class XmlReader {
|
|
|
55
54
|
this.stats = {};
|
|
56
55
|
this.stats.language = opts.lang?.toLowerCase();
|
|
57
56
|
this.uploader = new uploader_js_1.S3Uploader();
|
|
58
|
-
// Enhanced NUnit parsing - enabled by default for NUnit XML
|
|
59
|
-
this.enhancedNunit = opts.enhancedNunit !== false; // Default true, can be disabled
|
|
60
|
-
this.groupParameterized = opts.groupParameterized !== false; // Default true, can be disabled
|
|
61
57
|
// @ts-ignore
|
|
62
58
|
const packageJsonPath = path_1.default.resolve(__dirname, '..', 'package.json');
|
|
63
59
|
this.version = JSON.parse(fs_1.default.readFileSync(packageJsonPath).toString()).version;
|
|
@@ -106,8 +102,7 @@ class XmlReader {
|
|
|
106
102
|
return this.processJUnit(jsonSuite);
|
|
107
103
|
}
|
|
108
104
|
processJUnit(jsonSuite) {
|
|
109
|
-
const { testsuite, name, failures, errors } = jsonSuite;
|
|
110
|
-
const tests = testsuite?.tests || jsonSuite.tests;
|
|
105
|
+
const { testsuite, name, tests, failures, errors } = jsonSuite;
|
|
111
106
|
reduceOptions.preferClassname = this.stats.language === 'python';
|
|
112
107
|
const resultTests = processTestSuite(testsuite);
|
|
113
108
|
const hasFailures = resultTests.filter(t => t.status === 'failed').length > 0;
|
|
@@ -133,13 +128,6 @@ class XmlReader {
|
|
|
133
128
|
};
|
|
134
129
|
}
|
|
135
130
|
processNUnit(jsonSuite) {
|
|
136
|
-
// Use enhanced NUnit parser if enabled and this is actually NUnit XML
|
|
137
|
-
if (this.enhancedNunit && this.isNUnitXml(jsonSuite)) {
|
|
138
|
-
debug('Using enhanced NUnit parser');
|
|
139
|
-
return this.processNUnitEnhanced(jsonSuite);
|
|
140
|
-
}
|
|
141
|
-
// Fallback to legacy parser for backward compatibility
|
|
142
|
-
debug('Using legacy NUnit parser');
|
|
143
131
|
const { result, total, passed, failed, inconclusive, skipped } = jsonSuite;
|
|
144
132
|
reduceOptions.preferClassname = this.stats.language === 'python';
|
|
145
133
|
const resultTests = processTestSuite(jsonSuite['test-suite']);
|
|
@@ -154,43 +142,6 @@ class XmlReader {
|
|
|
154
142
|
tests: resultTests,
|
|
155
143
|
};
|
|
156
144
|
}
|
|
157
|
-
/**
|
|
158
|
-
* Check if the XML is actually NUnit format (has test-suite hierarchy)
|
|
159
|
-
* @param {Object} jsonSuite - Parsed XML suite object
|
|
160
|
-
* @returns {boolean} - True if this is NUnit XML format
|
|
161
|
-
*/
|
|
162
|
-
isNUnitXml(jsonSuite) {
|
|
163
|
-
// NUnit XML has test-suite elements with type attributes
|
|
164
|
-
if (jsonSuite['test-suite']) {
|
|
165
|
-
const testSuite = Array.isArray(jsonSuite['test-suite']) ? jsonSuite['test-suite'][0] : jsonSuite['test-suite'];
|
|
166
|
-
// Check for NUnit-specific test-suite types
|
|
167
|
-
return (testSuite &&
|
|
168
|
-
testSuite.type &&
|
|
169
|
-
['Assembly', 'TestSuite', 'TestFixture', 'ParameterizedMethod'].includes(testSuite.type));
|
|
170
|
-
}
|
|
171
|
-
return false;
|
|
172
|
-
}
|
|
173
|
-
processNUnitEnhanced(jsonSuite) {
|
|
174
|
-
debug('Processing NUnit XML with enhanced parser');
|
|
175
|
-
try {
|
|
176
|
-
const nunitParser = new nunit_parser_js_1.NUnitXmlParser({
|
|
177
|
-
groupParameterized: this.groupParameterized,
|
|
178
|
-
...this.opts,
|
|
179
|
-
});
|
|
180
|
-
const result = nunitParser.parseTestRun(jsonSuite);
|
|
181
|
-
// Add parsed tests to our collection
|
|
182
|
-
this.tests = this.tests.concat(result.tests);
|
|
183
|
-
debug(`Enhanced NUnit parser processed ${result.tests.length} tests`);
|
|
184
|
-
return result;
|
|
185
|
-
}
|
|
186
|
-
catch (error) {
|
|
187
|
-
debug('Enhanced NUnit parser failed, falling back to legacy parser:', error.message);
|
|
188
|
-
console.warn(`${constants_js_1.APP_PREFIX} Enhanced NUnit parsing failed, using legacy parser: ${error.message}`);
|
|
189
|
-
// Fallback to legacy parser
|
|
190
|
-
this.enhancedNunit = false;
|
|
191
|
-
return this.processNUnit(jsonSuite);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
145
|
processTRX(jsonSuite) {
|
|
195
146
|
let defs = jsonSuite?.TestRun?.TestDefinitions?.UnitTest;
|
|
196
147
|
if (!Array.isArray(defs))
|
package/package.json
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@testomatio/reporter",
|
|
3
|
-
"version": "2.3.7-beta.
|
|
3
|
+
"version": "2.3.7-beta.100",
|
|
4
4
|
"description": "Testomatio Reporter Client",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=18"
|
|
7
7
|
},
|
|
8
|
-
"
|
|
8
|
+
"main": "lib/reporter.js",
|
|
9
|
+
"module": "src/reporter.js",
|
|
10
|
+
"types": "types/types.d.ts",
|
|
9
11
|
"repository": "git@github.com:testomatio/reporter.git",
|
|
10
12
|
"author": "Michael Bodnarchuk <davert@testomat.io>,Koushik Mohan <koushikmohan1996@gmail.com>",
|
|
11
13
|
"license": "MIT",
|
|
@@ -45,7 +47,8 @@
|
|
|
45
47
|
"bin",
|
|
46
48
|
"lib",
|
|
47
49
|
"src",
|
|
48
|
-
"testcafe"
|
|
50
|
+
"testcafe",
|
|
51
|
+
"types"
|
|
49
52
|
],
|
|
50
53
|
"scripts": {
|
|
51
54
|
"clear-exportdir": "rm -rf export/",
|
|
@@ -57,7 +60,8 @@
|
|
|
57
60
|
"test": "mocha 'tests/unit/**/*_test.js'",
|
|
58
61
|
"test:playwright": "mocha tests/adapter/playwright.test.js",
|
|
59
62
|
"test:codecept": "mocha tests/adapter/codecept.test.js tests/adapter/codecept_comprehensive.test.js tests/adapter/codecept_steps_sections.test.js",
|
|
60
|
-
"test:
|
|
63
|
+
"test:vitest": "mocha tests/adapter/vitest.test.js",
|
|
64
|
+
"test:frameworks": "npm run test:playwright && npm run test:codecept && npm run test:vitest",
|
|
61
65
|
"test:all": "npm run test && npm run test:frameworks",
|
|
62
66
|
"test:adapters": "mocha tests/adapter/*.test.js",
|
|
63
67
|
"test:codecept:bug948": "mocha tests/adapter/codecept_aftersuite_failure.test.js",
|
package/src/adapter/codecept.js
CHANGED
|
@@ -2,7 +2,7 @@ import createDebugMessages from 'debug';
|
|
|
2
2
|
import pc from 'picocolors';
|
|
3
3
|
import TestomatClient from '../client.js';
|
|
4
4
|
import { STATUS, APP_PREFIX, TESTOMAT_TMP_STORAGE_DIR } from '../constants.js';
|
|
5
|
-
import { getTestomatIdFromTestTitle, fileSystem } from '../utils/utils.js';
|
|
5
|
+
import { getTestomatIdFromTestTitle, truncate, fileSystem } from '../utils/utils.js';
|
|
6
6
|
import { services } from '../services/index.js';
|
|
7
7
|
import { dataStorage } from '../data-storage.js';
|
|
8
8
|
import codeceptjs from 'codeceptjs';
|
|
@@ -127,6 +127,28 @@ function CodeceptReporter(config) {
|
|
|
127
127
|
});
|
|
128
128
|
|
|
129
129
|
|
|
130
|
+
// mark as failed all tests inside the failed hook
|
|
131
|
+
event.dispatcher.on(event.hook.failed, hook => {
|
|
132
|
+
if (hook.name !== 'BeforeSuiteHook') return;
|
|
133
|
+
const suite = hook.runnable.parent;
|
|
134
|
+
|
|
135
|
+
if (!suite) return;
|
|
136
|
+
|
|
137
|
+
const error = hook?.ctx?.currentTest?.err;
|
|
138
|
+
|
|
139
|
+
for (const test of suite.tests) {
|
|
140
|
+
client.addTestRun('failed', {
|
|
141
|
+
...stripExampleFromTitle(test.title),
|
|
142
|
+
rid: test.uid,
|
|
143
|
+
test_id: getTestomatIdFromTestTitle(test.title),
|
|
144
|
+
suite_title: stripTagsFromTitle(suite.title),
|
|
145
|
+
error,
|
|
146
|
+
time: hook?.runnable?.duration,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
|
|
130
152
|
event.dispatcher.on(event.suite.before, suite => {
|
|
131
153
|
dataStorage.setContext(suite.fullTitle());
|
|
132
154
|
});
|
|
@@ -441,7 +463,7 @@ function formatCodeceptStep(step) {
|
|
|
441
463
|
if (!step) return null;
|
|
442
464
|
|
|
443
465
|
const category = step.constructor.name === 'HelperStep' ? 'framework' : 'user';
|
|
444
|
-
const title = step
|
|
466
|
+
const title = truncate(step); // Use built-in toString
|
|
445
467
|
const duration = step.duration || 0; // Use built-in duration
|
|
446
468
|
|
|
447
469
|
const formattedStep = {
|
|
@@ -469,10 +491,11 @@ function formatHookStep(step) {
|
|
|
469
491
|
if (step.actor && step.name) {
|
|
470
492
|
title = `${step.actor} ${step.name}`;
|
|
471
493
|
if (step.args && step.args.length > 0) {
|
|
472
|
-
const argsStr = step.args.map(arg => JSON.stringify(arg)).join(', ');
|
|
494
|
+
const argsStr = step.args.map(arg => truncate(JSON.stringify(arg))).join(', ');
|
|
473
495
|
title += ` ${argsStr}`;
|
|
474
496
|
}
|
|
475
497
|
}
|
|
498
|
+
title = truncate(title);
|
|
476
499
|
|
|
477
500
|
return {
|
|
478
501
|
category: 'hook',
|
|
@@ -481,5 +504,6 @@ function formatHookStep(step) {
|
|
|
481
504
|
};
|
|
482
505
|
}
|
|
483
506
|
|
|
507
|
+
|
|
484
508
|
export { CodeceptReporter };
|
|
485
509
|
export default CodeceptReporter;
|
package/src/bin/cli.js
CHANGED
|
@@ -158,7 +158,7 @@ program
|
|
|
158
158
|
.option('--lang <lang>', 'Language used (python, ruby, java)')
|
|
159
159
|
.option('--timelimit <time>', 'default time limit in seconds to kill a stuck process')
|
|
160
160
|
.action(async (pattern, opts) => {
|
|
161
|
-
if (!pattern.endsWith('.xml')) {
|
|
161
|
+
if (!pattern.endsWith('.xml') && !pattern.includes('*')) {
|
|
162
162
|
pattern += '.xml';
|
|
163
163
|
}
|
|
164
164
|
let { javaTests, lang } = opts;
|
package/src/bin/reportXml.js
CHANGED
|
@@ -23,7 +23,7 @@ program
|
|
|
23
23
|
.option('--timelimit <time>', 'default time limit in seconds to kill a stuck process')
|
|
24
24
|
.option('--env-file <envfile>', 'Load environment variables from env file')
|
|
25
25
|
.action(async (pattern, opts) => {
|
|
26
|
-
if (!pattern.endsWith('.xml')) {
|
|
26
|
+
if (!pattern.endsWith('.xml') && !pattern.includes('*')) {
|
|
27
27
|
pattern += '.xml';
|
|
28
28
|
}
|
|
29
29
|
let { javaTests, lang } = opts;
|
package/src/bin/startTest.js
CHANGED
|
@@ -18,7 +18,7 @@ const newArgs = ['run'];
|
|
|
18
18
|
let i = 0;
|
|
19
19
|
while (i < args.length) {
|
|
20
20
|
const arg = args[i];
|
|
21
|
-
|
|
21
|
+
|
|
22
22
|
if (arg === '-c' || arg === '--command') {
|
|
23
23
|
// Map -c/--command to positional argument for run command
|
|
24
24
|
i++;
|
|
@@ -33,7 +33,7 @@ while (i < args.length) {
|
|
|
33
33
|
// Map --launch to start command
|
|
34
34
|
newArgs[0] = 'start';
|
|
35
35
|
} else if (arg === '--finish') {
|
|
36
|
-
// Map --finish to finish command
|
|
36
|
+
// Map --finish to finish command
|
|
37
37
|
newArgs[0] = 'finish';
|
|
38
38
|
} else {
|
|
39
39
|
// Pass through other arguments
|
|
@@ -45,9 +45,9 @@ while (i < args.length) {
|
|
|
45
45
|
// Execute the main CLI with mapped arguments
|
|
46
46
|
|
|
47
47
|
const child = spawn(process.execPath, [cliPath, ...newArgs], {
|
|
48
|
-
stdio: 'inherit'
|
|
48
|
+
stdio: 'inherit',
|
|
49
49
|
});
|
|
50
50
|
|
|
51
|
-
child.on('exit',
|
|
51
|
+
child.on('exit', code => {
|
|
52
52
|
process.exit(code);
|
|
53
|
-
});
|
|
53
|
+
});
|
package/src/client.js
CHANGED
|
@@ -10,11 +10,21 @@ import { glob } from 'glob';
|
|
|
10
10
|
import path, { sep } from 'path';
|
|
11
11
|
import { fileURLToPath } from 'node:url';
|
|
12
12
|
import { S3Uploader } from './uploader.js';
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
formatStep,
|
|
15
|
+
truncate,
|
|
16
|
+
readLatestRunId,
|
|
17
|
+
storeRunId,
|
|
18
|
+
validateSuiteId,
|
|
19
|
+
transformEnvVarToBoolean
|
|
20
|
+
} from './utils/utils.js';
|
|
14
21
|
import { filesize as prettyBytes } from 'filesize';
|
|
22
|
+
import { stripVTControlCharacters } from 'util';
|
|
15
23
|
|
|
16
24
|
const debug = createDebugMessages('@testomatio/reporter:client');
|
|
17
25
|
|
|
26
|
+
const stripColors = stripVTControlCharacters || ((str) => str?.replace(/\x1b\[[0-9;]*m/g, '') || '');
|
|
27
|
+
|
|
18
28
|
// removed __dirname usage, because:
|
|
19
29
|
// 1. replaced with ESM syntax (import.meta.url), but it throws an error on tsc compilation;
|
|
20
30
|
// 2. got error "__dirname already defined" in compiles js code (cjs dir)
|
|
@@ -139,19 +149,6 @@ class Client {
|
|
|
139
149
|
* @returns {Promise<PipeResult[]>}
|
|
140
150
|
*/
|
|
141
151
|
async addTestRun(status, testData) {
|
|
142
|
-
if (!this.pipes || !this.pipes.length)
|
|
143
|
-
this.pipes = await pipesFactory(this.paramsForPipesFactory || {}, this.pipeStore);
|
|
144
|
-
|
|
145
|
-
// all pipes disabled, skipping
|
|
146
|
-
if (!this.pipes?.filter(p => p.isEnabled).length) return [];
|
|
147
|
-
|
|
148
|
-
if (isTestShouldBeExculedFromReport(testData)) return [];
|
|
149
|
-
|
|
150
|
-
if (status === STATUS.SKIPPED && process.env.TESTOMATIO_EXCLUDE_SKIPPED) {
|
|
151
|
-
debug('Skipping test from report', testData?.title);
|
|
152
|
-
return []; // do not log skipped tests
|
|
153
|
-
}
|
|
154
|
-
|
|
155
152
|
if (!testData)
|
|
156
153
|
testData = {
|
|
157
154
|
title: 'Unknown test',
|
|
@@ -169,15 +166,23 @@ class Client {
|
|
|
169
166
|
const {
|
|
170
167
|
rid,
|
|
171
168
|
error = null,
|
|
169
|
+
steps: originalSteps,
|
|
170
|
+
title,
|
|
171
|
+
suite_title,
|
|
172
|
+
} = testData;
|
|
173
|
+
let steps = originalSteps;
|
|
174
|
+
|
|
175
|
+
const uploadedFiles = [];
|
|
176
|
+
const stackArtifactsEnabled = transformEnvVarToBoolean(process.env.TESTOMATIO_STACK_ARTIFACTS);
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
const {
|
|
172
180
|
time = 0,
|
|
173
181
|
example = null,
|
|
174
182
|
files = [],
|
|
175
183
|
filesBuffers = [],
|
|
176
|
-
steps,
|
|
177
184
|
code = null,
|
|
178
|
-
title,
|
|
179
185
|
file,
|
|
180
|
-
suite_title,
|
|
181
186
|
suite_id,
|
|
182
187
|
test_id,
|
|
183
188
|
timestamp,
|
|
@@ -188,7 +193,6 @@ class Client {
|
|
|
188
193
|
} = testData;
|
|
189
194
|
let { message = '', meta = {} } = testData;
|
|
190
195
|
|
|
191
|
-
// stringify meta values and limit keys and values length to 255
|
|
192
196
|
meta = Object.entries(meta)
|
|
193
197
|
.filter(([, value]) => value !== null && value !== undefined)
|
|
194
198
|
.reduce((acc, [key, value]) => {
|
|
@@ -196,7 +200,6 @@ class Client {
|
|
|
196
200
|
return acc;
|
|
197
201
|
}, {});
|
|
198
202
|
|
|
199
|
-
// Get links from storage using the test context
|
|
200
203
|
const testContext = suite_title ? `${suite_title} ${title}` : title;
|
|
201
204
|
|
|
202
205
|
let errorFormatted = '';
|
|
@@ -205,13 +208,38 @@ class Client {
|
|
|
205
208
|
message = error?.message;
|
|
206
209
|
}
|
|
207
210
|
|
|
208
|
-
|
|
209
|
-
const fullLogs = this.formatLogs({ error: errorFormatted, steps, logs: testData.logs });
|
|
211
|
+
let fullLogs = this.formatLogs({ error: errorFormatted, steps, logs: testData.logs });
|
|
210
212
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
+
if (stackArtifactsEnabled && fullLogs?.trim()?.length > 0) {
|
|
214
|
+
uploadedFiles.push(
|
|
215
|
+
this.uploader.uploadFileAsBuffer(
|
|
216
|
+
Buffer.from(stripColors(fullLogs), 'utf8'),
|
|
217
|
+
[this.runId, rid, `logs_${+new Date}.log`]
|
|
218
|
+
)
|
|
219
|
+
);
|
|
220
|
+
fullLogs = '';
|
|
221
|
+
steps = null;
|
|
222
|
+
}
|
|
213
223
|
|
|
214
|
-
|
|
224
|
+
|
|
225
|
+
if (!this.pipes || !this.pipes.length)
|
|
226
|
+
this.pipes = await pipesFactory(this.paramsForPipesFactory || {}, this.pipeStore);
|
|
227
|
+
|
|
228
|
+
if (!this.pipes?.filter(p => p.isEnabled).length) {
|
|
229
|
+
if (uploadedFiles.length > 0) {
|
|
230
|
+
await Promise.all(uploadedFiles);
|
|
231
|
+
}
|
|
232
|
+
return [];
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (isTestShouldBeExculedFromReport(testData)) return [];
|
|
236
|
+
|
|
237
|
+
if (status === STATUS.SKIPPED && process.env.TESTOMATIO_EXCLUDE_SKIPPED) {
|
|
238
|
+
debug('Skipping test from report', testData?.title);
|
|
239
|
+
return [];
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (manuallyAttachedArtifacts?.length) files.push(...manuallyAttachedArtifacts);
|
|
215
243
|
|
|
216
244
|
for (let f of files) {
|
|
217
245
|
if (!f) continue; // f === null
|
|
@@ -308,7 +336,7 @@ class Client {
|
|
|
308
336
|
const uploadedArtifacts = this.uploader.successfulUploads.map(file => ({
|
|
309
337
|
relativePath: file.path.replace(process.cwd(), ''),
|
|
310
338
|
link: file.link,
|
|
311
|
-
sizePretty: prettyBytes(file.size, { round: 0 }).toString(),
|
|
339
|
+
sizePretty: file.size == null ? 'unknown' : prettyBytes(file.size, { round: 0 }).toString(),
|
|
312
340
|
}));
|
|
313
341
|
|
|
314
342
|
uploadedArtifacts.forEach(upload => {
|
|
@@ -330,7 +358,7 @@ class Client {
|
|
|
330
358
|
);
|
|
331
359
|
const failedUploads = this.uploader.failedUploads.map(file => ({
|
|
332
360
|
relativePath: file.path.replace(process.cwd(), ''),
|
|
333
|
-
sizePretty: prettyBytes(file.size, { round: 0 }).toString(),
|
|
361
|
+
sizePretty: file.size == null ? 'unknown' : prettyBytes(file.size, { round: 0 }).toString(),
|
|
334
362
|
}));
|
|
335
363
|
|
|
336
364
|
const pathPadding = Math.max(...failedUploads.map(upload => upload.relativePath.length)) + 1;
|
|
@@ -387,7 +415,7 @@ class Client {
|
|
|
387
415
|
*/
|
|
388
416
|
formatLogs({ error, steps, logs }) {
|
|
389
417
|
error = error?.trim();
|
|
390
|
-
logs = logs?.trim();
|
|
418
|
+
logs = logs?.trim().split('\n').map(l => truncate(l)).join('\n');
|
|
391
419
|
|
|
392
420
|
if (Array.isArray(steps)) {
|
|
393
421
|
steps = steps
|
|
@@ -3,18 +3,23 @@ import Adapter from './adapter.js';
|
|
|
3
3
|
|
|
4
4
|
class CSharpAdapter extends Adapter {
|
|
5
5
|
formatTest(t) {
|
|
6
|
-
//
|
|
7
|
-
// The xmlReader.js already extracts parameters correctly from <arguments>
|
|
6
|
+
// Extract example from title if not already present
|
|
8
7
|
if (!t.example) {
|
|
9
|
-
const title = t.title.replace(/\(.*?\)/, '').trim();
|
|
10
8
|
const exampleMatch = t.title.match(/\((.*?)\)/);
|
|
11
9
|
if (exampleMatch) {
|
|
12
|
-
//
|
|
13
|
-
|
|
10
|
+
// Extract parameters as object with numeric keys for API
|
|
11
|
+
const params = exampleMatch[1].split(',').map(param => param.trim());
|
|
12
|
+
t.example = {};
|
|
13
|
+
params.forEach((param, index) => {
|
|
14
|
+
t.example[index] = param;
|
|
15
|
+
});
|
|
14
16
|
}
|
|
15
|
-
t.title = title.trim();
|
|
16
17
|
}
|
|
17
18
|
|
|
19
|
+
// Remove parameters from title to avoid duplicates in Test Suite
|
|
20
|
+
// The example field will be used for grouping on import
|
|
21
|
+
t.title = t.title.replace(/\(.*?\)/, '').trim();
|
|
22
|
+
|
|
18
23
|
const suite = t.suite_title.split('.');
|
|
19
24
|
t.suite_title = suite.pop();
|
|
20
25
|
t.file = namespaceToFileName(t.file);
|