@testomatio/reporter 1.2.0-beta → 1.2.0-beta-2
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 +13 -9
- package/lib/_ArtifactStorageOld.js +1 -1
- package/lib/adapter/codecept.js +59 -39
- package/lib/adapter/cucumber/current.js +95 -59
- package/lib/adapter/cucumber/legacy.js +25 -11
- package/lib/adapter/cypress-plugin/index.js +1 -1
- package/lib/adapter/jasmine.js +1 -1
- package/lib/adapter/jest.js +18 -3
- package/lib/adapter/mocha.js +23 -23
- package/lib/adapter/playwright.js +18 -11
- package/lib/adapter/webdriver.js +1 -1
- package/lib/artifactStorage.js +2 -2
- package/lib/bin/reportXml.js +1 -0
- package/lib/bin/startTest.js +25 -4
- package/lib/client.js +102 -34
- package/lib/constants.js +7 -0
- package/lib/dataStorage.js +132 -72
- package/lib/junit-adapter/java.js +27 -9
- package/lib/logger.js +182 -164
- package/lib/pipe/csv.js +4 -1
- package/lib/pipe/github.js +24 -37
- package/lib/pipe/gitlab.js +5 -16
- package/lib/pipe/html.js +262 -0
- package/lib/pipe/index.js +2 -0
- package/lib/pipe/testomatio.js +138 -34
- package/lib/reporter.js +2 -1
- package/lib/template/real-data-example.js +47 -0
- package/lib/template/template.hbs +249 -0
- package/lib/template/testomatio.hbs +695 -0
- package/lib/utils/pipe_utils.js +135 -0
- package/lib/{util.js → utils/utils.js} +116 -10
- package/lib/xmlReader.js +132 -44
- package/package.json +10 -6
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
const { resetConfig } = require('../fileUploader');
|
|
2
|
+
const { APP_PREFIX } = require('../constants');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Set S3 credentials from the provided artifacts object.
|
|
6
|
+
* @param {Object} artifacts - The artifacts object containing S3 credentials.
|
|
7
|
+
*/
|
|
8
|
+
function setS3Credentials(artifacts) {
|
|
9
|
+
if (!Object.keys(artifacts).length) return;
|
|
10
|
+
|
|
11
|
+
console.log(APP_PREFIX, 'S3 were credentials obtained from Testomat.io...');
|
|
12
|
+
|
|
13
|
+
if (artifacts.ACCESS_KEY_ID) process.env.S3_ACCESS_KEY_ID = artifacts.ACCESS_KEY_ID;
|
|
14
|
+
if (artifacts.SECRET_ACCESS_KEY) process.env.S3_SECRET_ACCESS_KEY = artifacts.SECRET_ACCESS_KEY;
|
|
15
|
+
if (artifacts.REGION) process.env.S3_REGION = artifacts.REGION;
|
|
16
|
+
if (artifacts.BUCKET) process.env.S3_BUCKET = artifacts.BUCKET;
|
|
17
|
+
if (artifacts.ENDPOINT) process.env.S3_ENDPOINT = artifacts.ENDPOINT;
|
|
18
|
+
if (artifacts.presign) process.env.TESTOMATIO_PRIVATE_ARTIFACTS = '1';
|
|
19
|
+
resetConfig();
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Generates mode request parameters based on the input params.
|
|
23
|
+
* @param {Object} params - The input parameters for the request.
|
|
24
|
+
* @param {string} params.type - The type of the request (e.g., "tag").
|
|
25
|
+
* @param {string} params.id - The ID associated with the request.
|
|
26
|
+
* @param {string} params.apiKey - The API key for authentication.
|
|
27
|
+
* @returns {Object|null} - An object containing the generated request parameters, or null if the type is invalid.
|
|
28
|
+
*/
|
|
29
|
+
function generateFilterRequestParams(params) {
|
|
30
|
+
const { type, id, apiKey } = params;
|
|
31
|
+
|
|
32
|
+
if (!type) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!id) {
|
|
37
|
+
console.error(APP_PREFIX, `Please make sure your settings "${type.toUpperCase()}"= "${id}" is correct!`);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
params: {
|
|
43
|
+
type,
|
|
44
|
+
id: encodeURIComponent(id),
|
|
45
|
+
api_key: apiKey
|
|
46
|
+
},
|
|
47
|
+
responseType: "json"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Parse filter parameters from a string in the format "type=id".
|
|
53
|
+
* @param {string} opts - The input string containing the filter parameters.
|
|
54
|
+
* @returns {Object} An object containing the parsed filter parameters.
|
|
55
|
+
* The object has properties "type" and "id".
|
|
56
|
+
*/
|
|
57
|
+
function parseFilterParams(opts) {
|
|
58
|
+
const [type, id] = opts.split("=");
|
|
59
|
+
const validType = updateFilterType(type);
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
type: validType,
|
|
63
|
+
id
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Update and validate the filter type.
|
|
69
|
+
* @param {string} type - The original filter type.
|
|
70
|
+
* @returns {string|undefined} The updated and validated filter type.
|
|
71
|
+
* Returns undefined if the type is not valid.
|
|
72
|
+
*/
|
|
73
|
+
function updateFilterType(type) {
|
|
74
|
+
const typeLowerCase = type.toLowerCase();
|
|
75
|
+
|
|
76
|
+
const filterTypes = [
|
|
77
|
+
"tag-name",
|
|
78
|
+
"plan-id",
|
|
79
|
+
"label",
|
|
80
|
+
"jira-ticket",
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
const filterApi = [
|
|
84
|
+
"tag",
|
|
85
|
+
"plan",
|
|
86
|
+
"label",
|
|
87
|
+
"jira",
|
|
88
|
+
// "ims-issue", //TODO: WIP
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
if (!filterTypes.includes(typeLowerCase)) {
|
|
92
|
+
console.log(APP_PREFIX, `❗❗❗ Invalid "filter=${type}" start settings! Available option list: ${filterTypes}`);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const index = filterTypes.indexOf(typeLowerCase);
|
|
97
|
+
|
|
98
|
+
return (index !== -1)
|
|
99
|
+
? filterApi[index]
|
|
100
|
+
: undefined
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Return an emoji based on the provided status.
|
|
105
|
+
* @param {string} status - The status value ('passed', 'failed', or 'skipped').
|
|
106
|
+
* @returns {string} - An emoji corresponding to the provided status.
|
|
107
|
+
*/
|
|
108
|
+
function statusEmoji(status) {
|
|
109
|
+
if (status === 'passed') return '🟢';
|
|
110
|
+
if (status === 'failed') return '🔴';
|
|
111
|
+
if (status === 'skipped') return '🟡';
|
|
112
|
+
return '';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Generate a full name string based on the provided test object.
|
|
117
|
+
* @param {object} t - The test object.
|
|
118
|
+
* @returns {string} - A formatted full name string for the test object.
|
|
119
|
+
*/
|
|
120
|
+
function fullName(t) {
|
|
121
|
+
let line = '';
|
|
122
|
+
if (t.suite_title) line = `${t.suite_title}: `;
|
|
123
|
+
line += `**${t.title}**`;
|
|
124
|
+
if (t.example) line += ` \`[${Object.values(t.example)}]\``;
|
|
125
|
+
return line;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
module.exports = {
|
|
129
|
+
updateFilterType,
|
|
130
|
+
parseFilterParams,
|
|
131
|
+
generateFilterRequestParams,
|
|
132
|
+
setS3Credentials,
|
|
133
|
+
statusEmoji,
|
|
134
|
+
fullName
|
|
135
|
+
};
|
|
@@ -11,7 +11,10 @@ const debug = require('debug')('@testomatio/reporter:util');
|
|
|
11
11
|
* @returns {String|null} testId
|
|
12
12
|
*/
|
|
13
13
|
const parseTest = testTitle => {
|
|
14
|
+
if (!testTitle) return null;
|
|
15
|
+
|
|
14
16
|
const captures = testTitle.match(/@T([\w\d]+)/);
|
|
17
|
+
|
|
15
18
|
if (captures) {
|
|
16
19
|
return captures[1];
|
|
17
20
|
}
|
|
@@ -52,11 +55,21 @@ const isValidUrl = s => {
|
|
|
52
55
|
}
|
|
53
56
|
};
|
|
54
57
|
|
|
58
|
+
const fileMatchRegex = /file:(\/\/?[^:\s]+?\.(png|avi|webm|jpg|html|txt))/ig;
|
|
59
|
+
|
|
55
60
|
const fetchFilesFromStackTrace = (stack = '') => {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
61
|
+
|
|
62
|
+
const files = Array.from(stack.matchAll(fileMatchRegex))
|
|
63
|
+
.map(f => f[1].trim())
|
|
64
|
+
.map(f => f.startsWith('//') ? f.substring(1) : f )
|
|
65
|
+
|
|
66
|
+
debug('Found files in stack trace: ', files);
|
|
67
|
+
|
|
68
|
+
return files.filter(f => {
|
|
69
|
+
const isFile = fs.existsSync(f);
|
|
70
|
+
if (!isFile) debug('File %s could not be found and uploaded as artifact', f);
|
|
71
|
+
return isFile;
|
|
72
|
+
});
|
|
60
73
|
};
|
|
61
74
|
|
|
62
75
|
const fetchSourceCodeFromStackTrace = (stack = '') => {
|
|
@@ -84,13 +97,38 @@ const fetchSourceCodeFromStackTrace = (stack = '') => {
|
|
|
84
97
|
|
|
85
98
|
if (!source) return '';
|
|
86
99
|
|
|
87
|
-
return source
|
|
100
|
+
return source
|
|
101
|
+
.split('\n')
|
|
88
102
|
.map((l, i) => {
|
|
89
103
|
if (i === prepend) return `${line} > ${chalk.bold(l)}`;
|
|
90
|
-
return `${line - prepend + i} | ${l}
|
|
91
|
-
})
|
|
104
|
+
return `${line - prepend + i} | ${l}`;
|
|
105
|
+
})
|
|
106
|
+
.join('\n');
|
|
92
107
|
};
|
|
93
108
|
|
|
109
|
+
const TEST_ID_REGEX = /@T([\w\d]{8})/;
|
|
110
|
+
|
|
111
|
+
const fetchIdFromCode = (code, opts = {}) => {
|
|
112
|
+
const comments = code.split('\n').map(l => l.trim()).filter(l => {
|
|
113
|
+
switch (opts.lang) {
|
|
114
|
+
case 'ruby':
|
|
115
|
+
case 'python':
|
|
116
|
+
return l.startsWith('# ');
|
|
117
|
+
default:
|
|
118
|
+
return l.startsWith('// ');
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
return comments.find(c => c.match(TEST_ID_REGEX))?.match(TEST_ID_REGEX)?.[1];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
const fetchIdFromOutput = (output) => {
|
|
127
|
+
const lines = output.split('\n').map(l => l.trim()).filter(l => l.startsWith('tid://'));
|
|
128
|
+
|
|
129
|
+
return lines.find(c => c.match(TEST_ID_REGEX))?.match(TEST_ID_REGEX)?.[1];
|
|
130
|
+
}
|
|
131
|
+
|
|
94
132
|
const fetchSourceCode = (contents, opts = {}) => {
|
|
95
133
|
if (!opts.title && !opts.line) return '';
|
|
96
134
|
|
|
@@ -112,7 +150,7 @@ const fetchSourceCode = (contents, opts = {}) => {
|
|
|
112
150
|
|
|
113
151
|
if (lineIndex) {
|
|
114
152
|
const result = [];
|
|
115
|
-
for (let i = lineIndex; i <
|
|
153
|
+
for (let i = lineIndex; i < lineIndex + limit; i++) {
|
|
116
154
|
if (lines[i] === undefined) continue;
|
|
117
155
|
|
|
118
156
|
if (i > lineIndex + 2 && !opts.prepend) {
|
|
@@ -186,13 +224,78 @@ const fileSystem = {
|
|
|
186
224
|
} else {
|
|
187
225
|
debug(`Trying to delete ${dirPath} but it doesn't exist`);
|
|
188
226
|
}
|
|
189
|
-
}
|
|
227
|
+
},
|
|
190
228
|
};
|
|
191
229
|
|
|
230
|
+
|
|
231
|
+
const foundedTestLog = (app, tests) => {
|
|
232
|
+
const n = tests.length;
|
|
233
|
+
|
|
234
|
+
return (n === 1)
|
|
235
|
+
? console.log(app, `✅ We found one test!`)
|
|
236
|
+
: console.log(app, `✅ We found ${n} tests!`);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const humanize = text => {
|
|
240
|
+
text = decamelize(text);
|
|
241
|
+
return text
|
|
242
|
+
.replace(/_./g, match => ` ${match.charAt(1).toUpperCase()}`)
|
|
243
|
+
.trim()
|
|
244
|
+
.replace(/^(.)|\s(.)/g, $1 => $1.toUpperCase())
|
|
245
|
+
.trim()
|
|
246
|
+
.replace(/\sA\s/g, ' a ') // replace a|the
|
|
247
|
+
.replace(/\sThe\s/g, ' the ') // replace a|the
|
|
248
|
+
.replace(/^Test\s/, '')
|
|
249
|
+
.replace(/^Should\s/, '');
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* From https://github.com/sindresorhus/decamelize/blob/main/index.js
|
|
254
|
+
* @param {*} text
|
|
255
|
+
* @returns
|
|
256
|
+
*/
|
|
257
|
+
const decamelize = text => {
|
|
258
|
+
const separator = '_';
|
|
259
|
+
const replacement = `$1${separator}$2`;
|
|
260
|
+
|
|
261
|
+
// Split lowercase sequences followed by uppercase character.
|
|
262
|
+
// `dataForUSACounties` → `data_For_USACounties`
|
|
263
|
+
// `myURLstring → `my_URLstring`
|
|
264
|
+
let decamelized = text.replace(/([\p{Lowercase_Letter}\d])(\p{Uppercase_Letter})/gu, replacement);
|
|
265
|
+
|
|
266
|
+
// Lowercase all single uppercase characters. As we
|
|
267
|
+
// want to preserve uppercase sequences, we cannot
|
|
268
|
+
// simply lowercase the separated string at the end.
|
|
269
|
+
// `data_For_USACounties` → `data_for_USACounties`
|
|
270
|
+
decamelized = decamelized.replace(
|
|
271
|
+
/((?<![\p{Uppercase_Letter}\d])[\p{Uppercase_Letter}\d](?![\p{Uppercase_Letter}\d]))/gu,
|
|
272
|
+
$0 => $0.toLowerCase(),
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
// Remaining uppercase sequences will be separated from lowercase sequences.
|
|
276
|
+
// `data_For_USACounties` → `data_for_USA_counties`
|
|
277
|
+
return decamelized.replace(
|
|
278
|
+
/(\p{Uppercase_Letter}+)(\p{Uppercase_Letter}\p{Lowercase_Letter}+)/gu,
|
|
279
|
+
(_, $1, $2) => $1 + separator + $2.toLowerCase(),
|
|
280
|
+
);
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Used to remove color codes
|
|
285
|
+
* @param {*} input
|
|
286
|
+
* @returns
|
|
287
|
+
*/
|
|
288
|
+
function removeColorCodes(input) {
|
|
289
|
+
// eslint-disable-next-line no-control-regex
|
|
290
|
+
return input.replace(/\x1b\[[0-9;]*m/g, '');
|
|
291
|
+
}
|
|
292
|
+
|
|
192
293
|
module.exports = {
|
|
193
294
|
isSameTest,
|
|
194
295
|
fetchSourceCode,
|
|
195
296
|
fetchSourceCodeFromStackTrace,
|
|
297
|
+
fetchIdFromCode,
|
|
298
|
+
fetchIdFromOutput,
|
|
196
299
|
fetchFilesFromStackTrace,
|
|
197
300
|
fileSystem,
|
|
198
301
|
getCurrentDateTime,
|
|
@@ -201,4 +304,7 @@ module.exports = {
|
|
|
201
304
|
ansiRegExp,
|
|
202
305
|
parseTest,
|
|
203
306
|
parseSuite,
|
|
204
|
-
|
|
307
|
+
humanize,
|
|
308
|
+
removeColorCodes,
|
|
309
|
+
foundedTestLog
|
|
310
|
+
};
|
package/lib/xmlReader.js
CHANGED
|
@@ -4,12 +4,17 @@ const chalk = require('chalk');
|
|
|
4
4
|
const fs = require("fs");
|
|
5
5
|
const { XMLParser } = require("fast-xml-parser");
|
|
6
6
|
const { APP_PREFIX, STATUS } = require('./constants');
|
|
7
|
-
const { fetchFilesFromStackTrace,
|
|
7
|
+
const { fetchFilesFromStackTrace,
|
|
8
|
+
fetchIdFromOutput,
|
|
9
|
+
fetchSourceCode,
|
|
10
|
+
fetchSourceCodeFromStackTrace,
|
|
11
|
+
fetchIdFromCode,
|
|
12
|
+
humanize
|
|
13
|
+
} = require('./utils/utils');
|
|
8
14
|
const upload = require('./fileUploader');
|
|
9
15
|
const pipesFactory = require('./pipe');
|
|
10
16
|
const adapterFactory = require('./junit-adapter');
|
|
11
17
|
|
|
12
|
-
|
|
13
18
|
const TESTOMATIO_URL = process.env.TESTOMATIO_URL || "https://app.testomat.io";
|
|
14
19
|
const TESTOMATIO = process.env.TESTOMATIO; // key?
|
|
15
20
|
const { TESTOMATIO_RUNGROUP_TITLE, TESTOMATIO_TITLE, TESTOMATIO_ENV, TESTOMATIO_RUN } = process.env;
|
|
@@ -25,7 +30,7 @@ const options = {
|
|
|
25
30
|
const reduceOptions = {};
|
|
26
31
|
|
|
27
32
|
class XmlReader {
|
|
28
|
-
|
|
33
|
+
|
|
29
34
|
constructor(opts = {}) {
|
|
30
35
|
this.requestParams = {
|
|
31
36
|
apiKey: opts.apiKey || TESTOMATIO,
|
|
@@ -35,12 +40,12 @@ class XmlReader {
|
|
|
35
40
|
group_title: TESTOMATIO_RUNGROUP_TITLE,
|
|
36
41
|
};
|
|
37
42
|
this.runId = opts.runId || TESTOMATIO_RUN;
|
|
38
|
-
this.adapter = adapterFactory(
|
|
43
|
+
this.adapter = adapterFactory(opts.lang?.toLowerCase(), opts)
|
|
39
44
|
if (!this.adapter) throw new Error('XML adapter for this format not found');
|
|
40
45
|
|
|
41
46
|
this.opts = opts || {};
|
|
42
|
-
|
|
43
|
-
this.pipes = pipesFactory(opts, store);
|
|
47
|
+
this.store = {}
|
|
48
|
+
this.pipes = pipesFactory(opts, this.store);
|
|
44
49
|
|
|
45
50
|
this.parser = new XMLParser(options);
|
|
46
51
|
this.tests = []
|
|
@@ -49,27 +54,33 @@ class XmlReader {
|
|
|
49
54
|
this.filesToUpload = {}
|
|
50
55
|
|
|
51
56
|
this.version = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json')).toString()).version;
|
|
52
|
-
console.log(APP_PREFIX, `Testomatio Reporter v${this.version}`);
|
|
57
|
+
console.log(APP_PREFIX, `Testomatio Reporter v${this.version}`);
|
|
53
58
|
}
|
|
54
59
|
|
|
55
60
|
connectAdapter() {
|
|
56
|
-
if (this.opts.javaTests)
|
|
57
|
-
|
|
61
|
+
if (this.opts.javaTests) {
|
|
62
|
+
this.adapter = adapterFactory('java', this.opts);
|
|
63
|
+
return this.adapter;
|
|
64
|
+
}
|
|
65
|
+
this.adapter = adapterFactory(this.stats.language, this.opts);
|
|
66
|
+
return this.adapter;
|
|
58
67
|
}
|
|
59
68
|
|
|
60
|
-
parse(fileName) {
|
|
69
|
+
parse(fileName) {
|
|
61
70
|
const xmlData = fs.readFileSync(path.resolve(fileName));
|
|
62
71
|
const jsonResult = this.parser.parse(xmlData);
|
|
63
72
|
let jsonSuite;
|
|
64
|
-
|
|
73
|
+
|
|
65
74
|
if (jsonResult.testsuites) {
|
|
66
75
|
jsonSuite = jsonResult.testsuites;
|
|
67
76
|
} else if (jsonResult.testsuite) {
|
|
68
77
|
jsonSuite = jsonResult;
|
|
69
78
|
} else if (jsonResult.TestRun) {
|
|
70
|
-
return this.processTRX(jsonResult);
|
|
79
|
+
return this.processTRX(jsonResult);
|
|
71
80
|
} else if (jsonResult['test-run']) {
|
|
72
81
|
return this.processNUnit(jsonResult['test-run']);
|
|
82
|
+
} else if (jsonResult.assemblies) {
|
|
83
|
+
return this.processXUnit(jsonResult.assemblies);
|
|
73
84
|
} else {
|
|
74
85
|
console.log(jsonResult)
|
|
75
86
|
throw new Error("Format can't be parsed")
|
|
@@ -83,9 +94,9 @@ class XmlReader {
|
|
|
83
94
|
|
|
84
95
|
reduceOptions.preferClassname = this.stats.language === 'python';
|
|
85
96
|
const resultTests = processTestSuite(testsuite);
|
|
86
|
-
|
|
97
|
+
|
|
87
98
|
const hasFailures = resultTests.filter(t => t.status === 'failed').length > 0;
|
|
88
|
-
const status = (
|
|
99
|
+
const status = (failures > 0 || errors > 0 || hasFailures) ? 'failed' : 'passed';
|
|
89
100
|
|
|
90
101
|
this.tests = this.tests.concat(resultTests);
|
|
91
102
|
|
|
@@ -118,7 +129,7 @@ class XmlReader {
|
|
|
118
129
|
skipped_count: parseInt(inconclusive + skipped, 10),
|
|
119
130
|
tests: resultTests,
|
|
120
131
|
};
|
|
121
|
-
}
|
|
132
|
+
}
|
|
122
133
|
|
|
123
134
|
processTRX(jsonSuite) {
|
|
124
135
|
let defs = jsonSuite?.TestRun?.TestDefinitions?.UnitTest;
|
|
@@ -127,10 +138,10 @@ class XmlReader {
|
|
|
127
138
|
const tests = defs.map(td => {
|
|
128
139
|
const title = td.name.replace(/\(.*?\)/, '').trim();
|
|
129
140
|
let example = td.name.match(/\((.*?)\)/);
|
|
130
|
-
if (example) example = { ...example[1].split(',')};
|
|
141
|
+
if (example) example = { ...example[1].split(',') };
|
|
131
142
|
const suite = td.TestMethod.className.split(', ')[0].split('.');
|
|
132
143
|
const suite_title = suite.pop();
|
|
133
|
-
return {
|
|
144
|
+
return {
|
|
134
145
|
title,
|
|
135
146
|
example,
|
|
136
147
|
file: suite.join('/'),
|
|
@@ -143,18 +154,18 @@ class XmlReader {
|
|
|
143
154
|
let result = jsonSuite?.TestRun?.Results?.UnitTestResult;
|
|
144
155
|
if (!Array.isArray(result)) result = [result].filter(d => !!d);
|
|
145
156
|
|
|
146
|
-
const results = result.map(td => ({
|
|
157
|
+
const results = result.map(td => ({
|
|
147
158
|
id: td.executionId,
|
|
148
159
|
// seconds are used in junit reports, but ms are used by testomatio
|
|
149
160
|
run_time: parseFloat(td.duration) * 1000,
|
|
150
|
-
status: td.outcome,
|
|
161
|
+
status: td.outcome,
|
|
151
162
|
stack: td.Output.StdOut,
|
|
152
163
|
files: td?.ResultFiles?.ResultFile?.map(rf => rf.path)
|
|
153
164
|
}));
|
|
154
165
|
|
|
155
|
-
|
|
166
|
+
|
|
156
167
|
results.forEach(r => {
|
|
157
|
-
const test = tests.find(t => t.id === r.id) || {
|
|
168
|
+
const test = tests.find(t => t.id === r.id) || {};
|
|
158
169
|
r.suite_title = test.suite_title;
|
|
159
170
|
r.title = test.title?.trim();
|
|
160
171
|
if (test.code) r.code = test.code;
|
|
@@ -169,7 +180,7 @@ class XmlReader {
|
|
|
169
180
|
});
|
|
170
181
|
|
|
171
182
|
debug(results);
|
|
172
|
-
|
|
183
|
+
|
|
173
184
|
const counters = jsonSuite?.TestRun?.ResultSummary?.Counters || {};
|
|
174
185
|
|
|
175
186
|
const failed_count = parseInt(counters.failed, 10) + parseInt(counters.error, 10);
|
|
@@ -187,7 +198,80 @@ class XmlReader {
|
|
|
187
198
|
skipped_count: parseInt(counters.notExecuted, 10),
|
|
188
199
|
failed_count,
|
|
189
200
|
tests: results,
|
|
190
|
-
};
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
processXUnit(assemblies) {
|
|
205
|
+
const tests = [];
|
|
206
|
+
|
|
207
|
+
assemblies = Array.isArray(assemblies.assembly) ? assemblies.assembly : [assemblies.assembly];
|
|
208
|
+
|
|
209
|
+
assemblies.forEach(assembly => {
|
|
210
|
+
const { collection } = assembly;
|
|
211
|
+
|
|
212
|
+
const suites = Array.isArray(collection) ? collection : [collection];
|
|
213
|
+
|
|
214
|
+
suites.forEach(suite => {
|
|
215
|
+
const { test } = suite;
|
|
216
|
+
if (!test) return;
|
|
217
|
+
const cases = Array.isArray(test) ? test : [test];
|
|
218
|
+
cases.forEach(testCase => {
|
|
219
|
+
const { type, time, result } = testCase;
|
|
220
|
+
|
|
221
|
+
let message = '';
|
|
222
|
+
let stack = '';
|
|
223
|
+
|
|
224
|
+
if (testCase.failure) {
|
|
225
|
+
message = testCase.failure.message;
|
|
226
|
+
stack = testCase.failure['stack-trace']
|
|
227
|
+
}
|
|
228
|
+
if (testCase.reason) {
|
|
229
|
+
message = testCase.reason.message;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
let status = STATUS.PASSED;
|
|
233
|
+
if (result === 'Pass') status = STATUS.PASSED;
|
|
234
|
+
if (result === 'Fail') status = STATUS.FAILED;
|
|
235
|
+
if (result === 'Skip') status = STATUS.SKIPPED;
|
|
236
|
+
|
|
237
|
+
const pathParts = type.split('.');
|
|
238
|
+
const suite_title = pathParts[pathParts.length - 1];
|
|
239
|
+
const file = pathParts.slice(0, -1).join('/');
|
|
240
|
+
const title = testCase.method || testCase.name.split('.').pop();
|
|
241
|
+
const run_time = parseFloat(time) * 1000;
|
|
242
|
+
|
|
243
|
+
tests.push({
|
|
244
|
+
create: true,
|
|
245
|
+
stack,
|
|
246
|
+
message,
|
|
247
|
+
file,
|
|
248
|
+
status,
|
|
249
|
+
title,
|
|
250
|
+
suite_title,
|
|
251
|
+
run_time,
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
const hasFailures = tests.filter(t => t.status === STATUS.FAILED).length > 0;
|
|
259
|
+
const status = hasFailures ? STATUS.FAILED : STATUS.PASSED;
|
|
260
|
+
|
|
261
|
+
this.tests = tests;
|
|
262
|
+
|
|
263
|
+
debug(tests);
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
status,
|
|
267
|
+
create_tests: true,
|
|
268
|
+
name: 'xUnit',
|
|
269
|
+
tests_count: tests.length,
|
|
270
|
+
passed_count: tests.filter(t => t.status === STATUS.PASSED).length,
|
|
271
|
+
failed_count: tests.filter(t => t.status === STATUS.FAILED).length,
|
|
272
|
+
skipped_count: tests.filter(t => t.status === STATUS.SKIPPED).length,
|
|
273
|
+
tests,
|
|
274
|
+
};
|
|
191
275
|
}
|
|
192
276
|
|
|
193
277
|
calculateStats() {
|
|
@@ -221,12 +305,19 @@ class XmlReader {
|
|
|
221
305
|
if (file.endsWith('.py')) this.stats.language = 'python';
|
|
222
306
|
if (file.endsWith('.java')) this.stats.language = 'java';
|
|
223
307
|
if (file.endsWith('.rb')) this.stats.language = 'ruby';
|
|
224
|
-
if (file.endsWith('.js')) this.stats.language
|
|
308
|
+
if (file.endsWith('.js')) this.stats.language = 'js';
|
|
225
309
|
if (file.endsWith('.ts')) this.stats.language = 'ts';
|
|
226
310
|
}
|
|
227
311
|
|
|
312
|
+
if (!fs.existsSync(file)) {
|
|
313
|
+
debug('Failed to open file with the source code', file)
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
228
316
|
const contents = fs.readFileSync(file).toString();
|
|
229
317
|
t.code = fetchSourceCode(contents, { ...t, lang: this.stats.language })
|
|
318
|
+
if (t.code) debug('Fetched code for test %s', t.title);
|
|
319
|
+
t.test_id = fetchIdFromCode(t.code, { lang: this.stats.language })
|
|
320
|
+
if (t.test_id) debug('Fetched test id %s for test %s', t.test_id, t.title);
|
|
230
321
|
} catch (err) {
|
|
231
322
|
debug(err)
|
|
232
323
|
}
|
|
@@ -241,18 +332,7 @@ class XmlReader {
|
|
|
241
332
|
|
|
242
333
|
this.adapter.formatTest(t)
|
|
243
334
|
|
|
244
|
-
t.title = t.title
|
|
245
|
-
// insert a space before all caps
|
|
246
|
-
.replace(/([A-Z])/g, ' $1')
|
|
247
|
-
// _ chars to spaces
|
|
248
|
-
.replace(/_/g, ' ')
|
|
249
|
-
// uppercase the first character
|
|
250
|
-
.replace(/^(.)|\s(.)/g, $1 => $1.toLowerCase())
|
|
251
|
-
|
|
252
|
-
// remove standard prefixes
|
|
253
|
-
.replace(/^test\s/, '')
|
|
254
|
-
.replace(/^should\s/, '')
|
|
255
|
-
.trim();
|
|
335
|
+
t.title = humanize(t.title);
|
|
256
336
|
});
|
|
257
337
|
}
|
|
258
338
|
|
|
@@ -281,8 +361,12 @@ class XmlReader {
|
|
|
281
361
|
let files = [];
|
|
282
362
|
if (test.files?.length) files = test.files.map(f => path.join(process.cwd(), f))
|
|
283
363
|
files = [...files, ...fetchFilesFromStackTrace(test.stack)];
|
|
284
|
-
|
|
285
|
-
|
|
364
|
+
|
|
365
|
+
if (!files.length) continue;
|
|
366
|
+
|
|
367
|
+
const runId = this.runId || this.store.runId || Date.now().toString();
|
|
368
|
+
test.artifacts = await Promise.all(files.map(f => upload.uploadFileByPath(f, runId)));
|
|
369
|
+
console.log(APP_PREFIX, `🗄️ Uploaded ${chalk.bold(`${files.length} artifacts`)} for test ${test.title}`);
|
|
286
370
|
}
|
|
287
371
|
}
|
|
288
372
|
|
|
@@ -292,7 +376,7 @@ class XmlReader {
|
|
|
292
376
|
title: this.requestParams.title,
|
|
293
377
|
env: this.requestParams.env,
|
|
294
378
|
group_title: this.requestParams.group_title,
|
|
295
|
-
};
|
|
379
|
+
};
|
|
296
380
|
|
|
297
381
|
debug("Run", runParams);
|
|
298
382
|
|
|
@@ -310,9 +394,9 @@ class XmlReader {
|
|
|
310
394
|
debug(
|
|
311
395
|
'Uploading data',
|
|
312
396
|
{
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
397
|
+
...this.stats,
|
|
398
|
+
tests: this.tests,
|
|
399
|
+
})
|
|
316
400
|
|
|
317
401
|
const dataString = {
|
|
318
402
|
...this.stats,
|
|
@@ -334,6 +418,8 @@ function reduceTestCases(prev, item) {
|
|
|
334
418
|
if (!Array.isArray(testCases)) {
|
|
335
419
|
testCases = [testCases]
|
|
336
420
|
}
|
|
421
|
+
const suiteOutput = item['system-out'] || item.output || item.log || '';
|
|
422
|
+
const suiteErr = item['system-err'] || item.output || item.log || '';
|
|
337
423
|
testCases.filter(t => !!t).forEach(testCaseItem => {
|
|
338
424
|
const file = testCaseItem.file || item.filepath || '';
|
|
339
425
|
|
|
@@ -349,8 +435,9 @@ function reduceTestCases(prev, item) {
|
|
|
349
435
|
if (testCaseItem.error && testCaseItem.error['#text']) stack = testCaseItem.error['#text'];
|
|
350
436
|
if (!message) message = stack.trim().split('\n')[0];
|
|
351
437
|
|
|
352
|
-
//
|
|
353
|
-
stack = `${testCaseItem['system-out'] || testCaseItem.output || testCaseItem.log || ''}\n\n${stack}`.trim()
|
|
438
|
+
// eslint-disable-next-line
|
|
439
|
+
stack = `${testCaseItem['system-out'] || testCaseItem.output || testCaseItem.log || ''}\n\n${stack}\n\n${suiteOutput}\n\n${suiteErr}`.trim()
|
|
440
|
+
const testId = fetchIdFromOutput(stack);
|
|
354
441
|
|
|
355
442
|
let status = STATUS.PASSED.toString();
|
|
356
443
|
if ('failure' in testCaseItem || 'error' in testCaseItem) status = STATUS.FAILED;
|
|
@@ -360,6 +447,7 @@ function reduceTestCases(prev, item) {
|
|
|
360
447
|
create: true,
|
|
361
448
|
file,
|
|
362
449
|
stack,
|
|
450
|
+
test_id: testId,
|
|
363
451
|
message,
|
|
364
452
|
line: testCaseItem.lineno,
|
|
365
453
|
// seconds are used in junit reports, but ms are used by testomatio
|