@testomatio/reporter 0.8.0-beta.3 → 0.8.0-beta.30
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/Changelog.md +12 -0
- package/README.md +63 -0
- package/lib/adapter/codecept.js +1 -6
- package/lib/adapter/cucumber/current.js +1 -7
- package/lib/adapter/jasmine.js +1 -8
- package/lib/adapter/jest.js +1 -7
- package/lib/adapter/mocha.js +23 -7
- package/lib/adapter/playwright.js +2 -8
- package/lib/adapter/webdriver.js +1 -4
- package/lib/bin/reportXml.js +6 -2
- package/lib/client.js +29 -15
- package/lib/constants.js +9 -0
- package/lib/fileUploader.js +82 -49
- package/lib/pipe/csv.js +119 -0
- package/lib/pipe/github.js +135 -63
- package/lib/pipe/gitlab.js +229 -0
- package/lib/pipe/index.js +29 -13
- package/lib/pipe/testomatio.js +19 -8
- package/lib/util.js +15 -2
- package/lib/xmlReader.js +30 -17
- package/package.json +9 -4
- package/testcafe/index.js +0 -61
- package/testcafe/package-lock.json +0 -863
- package/testcafe/package.json +0 -14
package/lib/pipe/csv.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const csvWriter = require('csv-writer');
|
|
4
|
+
const chalk = require('chalk');
|
|
5
|
+
const merge = require('lodash.merge');
|
|
6
|
+
const { isSameTest, getCurrentDateTime } = require('../util');
|
|
7
|
+
const { CSV_HEADERS } = require('../constants');
|
|
8
|
+
|
|
9
|
+
class CsvPipe {
|
|
10
|
+
constructor(params, store = {}) {
|
|
11
|
+
this.store = store;
|
|
12
|
+
this.title = params.title || process.env.TESTOMATIO_TITLE;
|
|
13
|
+
this.isEnabled = true;
|
|
14
|
+
this.results = [];
|
|
15
|
+
|
|
16
|
+
this.outputDir = 'export';
|
|
17
|
+
this.csvFilename = process.env.TESTOMATIO_CSV_FILENAME;
|
|
18
|
+
this.isCsvSave = false;
|
|
19
|
+
|
|
20
|
+
if (this.csvFilename !== undefined && this.csvFilename.split('.').length > 0) {
|
|
21
|
+
this.isCsvSave = true;
|
|
22
|
+
|
|
23
|
+
if (this.csvFilename.split('.')[0] === 'report') {
|
|
24
|
+
this.outputFile = path.resolve(process.cwd(), this.outputDir, 'report.csv');
|
|
25
|
+
} else {
|
|
26
|
+
this.outputFile = path.resolve(
|
|
27
|
+
process.cwd(),
|
|
28
|
+
this.outputDir,
|
|
29
|
+
`${getCurrentDateTime()}_${this.csvFilename.split('.')[0]}.csv`,
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async createRun() {
|
|
36
|
+
// empty
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
updateRun() {}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Create a folder that will contain the exported files
|
|
43
|
+
*/
|
|
44
|
+
checkExportDir() {
|
|
45
|
+
if (!fs.existsSync(this.outputDir)) {
|
|
46
|
+
return fs.mkdirSync(this.outputDir);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Save data to the csv file.
|
|
52
|
+
* @param {Object} data - data that will be added to the CSV file.
|
|
53
|
+
* Example: [{suite_title: "Suite #1", test: "Test-case-1", message: "Test msg"}]
|
|
54
|
+
* @param {Object} headers - csv file headers. Example: [{ id: 'suite_title', title: 'Suite_title' }]
|
|
55
|
+
*/
|
|
56
|
+
async saveToCsv(data, headers) {
|
|
57
|
+
// First, we check whether the export directory exists: if yes - OK, no - create it.
|
|
58
|
+
this.checkExportDir();
|
|
59
|
+
|
|
60
|
+
console.log(chalk.yellow(`The test results will be added to the csv. It will take some time...`));
|
|
61
|
+
// Create csv writer object
|
|
62
|
+
const writer = csvWriter.createObjectCsvWriter({
|
|
63
|
+
path: this.outputFile,
|
|
64
|
+
header: headers,
|
|
65
|
+
});
|
|
66
|
+
// Save csv file based on the current data
|
|
67
|
+
try {
|
|
68
|
+
await writer.writeRecords(data);
|
|
69
|
+
console.log(chalk.green(`Recording completed! You can check the result in file = ${this.outputFile}`));
|
|
70
|
+
} catch (e) {
|
|
71
|
+
console.log('Unknown error: ', e);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Add test data to the result array for saving. As a result of this function, we get a result object to save.
|
|
77
|
+
* @param {Object} test - object which includes each test entry.
|
|
78
|
+
*/
|
|
79
|
+
addTest(test) {
|
|
80
|
+
if (!this.isEnabled) return;
|
|
81
|
+
|
|
82
|
+
const index = this.results.findIndex(t => isSameTest(t, test));
|
|
83
|
+
// update if they were already added
|
|
84
|
+
if (index >= 0) {
|
|
85
|
+
this.results[index] = merge(this.results[index], test);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const { suite_title, title, status, message, stack } = test;
|
|
90
|
+
|
|
91
|
+
this.results.push({
|
|
92
|
+
suite_title,
|
|
93
|
+
title,
|
|
94
|
+
status,
|
|
95
|
+
message,
|
|
96
|
+
stack,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Save short tests data to the csv file.
|
|
102
|
+
* @param {Object} runParams - run params.
|
|
103
|
+
*/
|
|
104
|
+
async finishRun(runParams) {
|
|
105
|
+
if (!this.isEnabled) return;
|
|
106
|
+
|
|
107
|
+
if (runParams.tests) runParams.tests.forEach(t => this.addTest(t));
|
|
108
|
+
// Save results based on the default headers
|
|
109
|
+
if (this.isCsvSave) {
|
|
110
|
+
this.saveToCsv(this.results, CSV_HEADERS);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
toString() {
|
|
115
|
+
return 'csv exporter';
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
module.exports = CsvPipe;
|
package/lib/pipe/github.js
CHANGED
|
@@ -1,26 +1,37 @@
|
|
|
1
|
+
const path = require('path');
|
|
1
2
|
const chalk = require('chalk');
|
|
2
|
-
const
|
|
3
|
+
const humanizeDuration = require('humanize-duration');
|
|
4
|
+
const merge = require('lodash.merge');
|
|
5
|
+
const { Octokit } = require('@octokit/rest');
|
|
3
6
|
const { APP_PREFIX } = require('../constants');
|
|
4
7
|
const { ansiRegExp, isSameTest } = require('../util');
|
|
5
|
-
const merge = require('lodash.merge');
|
|
6
|
-
|
|
7
8
|
|
|
8
9
|
class GitHubPipe {
|
|
9
|
-
|
|
10
|
-
isEnabled = false;
|
|
11
|
-
|
|
12
10
|
constructor(params, store = {}) {
|
|
11
|
+
this.isEnabled = false;
|
|
13
12
|
this.store = store;
|
|
14
|
-
this.tests = []
|
|
13
|
+
this.tests = [];
|
|
15
14
|
this.token = params.GH_PAT || process.env.GH_PAT;
|
|
16
|
-
this.ref = process.env.GITHUB_REF
|
|
17
|
-
this.repo = process.env.
|
|
15
|
+
this.ref = process.env.GITHUB_REF;
|
|
16
|
+
this.repo = process.env.GITHUB_REPOSITORY;
|
|
17
|
+
this.hiddenCommentData = `<!--- testomat.io report ${process.env.GITHUB_WORKFLOW || ''} -->`;
|
|
18
|
+
|
|
19
|
+
if (process.env.DEBUG) {
|
|
20
|
+
console.log(APP_PREFIX, 'GitHub Pipe: ', this.token ? 'TOKEN' : '*no token*', this.ref, this.repo);
|
|
21
|
+
}
|
|
22
|
+
|
|
18
23
|
if (!this.token || !this.ref || !this.repo) return;
|
|
19
24
|
this.isEnabled = true;
|
|
20
|
-
|
|
25
|
+
const matchedIssue = this.ref.match(/refs\/pull\/(\d+)\/merge/);
|
|
26
|
+
if (!matchedIssue) return;
|
|
27
|
+
this.issue = matchedIssue[1];
|
|
21
28
|
this.start = new Date();
|
|
29
|
+
|
|
30
|
+
if (process.env.DEBUG) {
|
|
31
|
+
console.log(APP_PREFIX, 'GitHub Pipe: Enabled');
|
|
32
|
+
}
|
|
22
33
|
}
|
|
23
|
-
|
|
34
|
+
|
|
24
35
|
async createRun() {}
|
|
25
36
|
|
|
26
37
|
updateRun() {}
|
|
@@ -28,21 +39,20 @@ class GitHubPipe {
|
|
|
28
39
|
addTest(test) {
|
|
29
40
|
if (!this.isEnabled) return;
|
|
30
41
|
|
|
31
|
-
const index = this.tests.findIndex(t => isSameTest(t, test))
|
|
42
|
+
const index = this.tests.findIndex(t => isSameTest(t, test));
|
|
32
43
|
// update if they were already added
|
|
33
44
|
if (index >= 0) {
|
|
34
45
|
this.tests[index] = merge(this.tests[index], test);
|
|
35
46
|
return;
|
|
36
47
|
}
|
|
37
48
|
|
|
38
|
-
this.tests.push(test)
|
|
49
|
+
this.tests.push(test);
|
|
39
50
|
}
|
|
40
51
|
|
|
41
52
|
async finishRun(runParams) {
|
|
42
53
|
if (!this.isEnabled) return;
|
|
43
54
|
|
|
44
55
|
if (runParams.tests) runParams.tests.forEach(t => this.addTest(t));
|
|
45
|
-
|
|
46
56
|
|
|
47
57
|
this.octokit = new Octokit({
|
|
48
58
|
auth: this.token,
|
|
@@ -56,15 +66,17 @@ class GitHubPipe {
|
|
|
56
66
|
const failedCount = this.tests.filter(t => t.status === 'failed').length;
|
|
57
67
|
const skippedCount = this.tests.filter(t => t.status === 'skipped').length;
|
|
58
68
|
|
|
59
|
-
let summary =
|
|
60
|
-
### Run Report
|
|
69
|
+
let summary = `${this.hiddenCommentData}
|
|
61
70
|
|
|
62
|
-
|
|
|
71
|
+
| [](https://testomat.io) | ${
|
|
72
|
+
statusEmoji(runParams.status)} ${runParams.status.toUpperCase()} ${statusEmoji(runParams.status)} |
|
|
63
73
|
| --- | --- |
|
|
64
74
|
| Tests | ✔️ **${this.tests.length}** tests run |
|
|
65
|
-
| Summary | ${statusEmoji('failed')} **${failedCount}** failed; ${statusEmoji(
|
|
66
|
-
|
|
67
|
-
|
|
75
|
+
| Summary | ${statusEmoji('failed')} **${failedCount}** failed; ${statusEmoji(
|
|
76
|
+
'passed',
|
|
77
|
+
)} **${passedCount}** passed; **${statusEmoji('skipped')}** ${skippedCount} skipped |
|
|
78
|
+
| Duration | 🕐 **${humanizeDuration(parseFloat(this.tests.reduce((a, t) => a + (t.run_time || 0), 0)))}** |
|
|
79
|
+
`;
|
|
68
80
|
if (this.store.runUrl) {
|
|
69
81
|
summary += `| Testomat.io Report | 📊 [Run #${this.store.runId}](${this.store.runUrl}) | `;
|
|
70
82
|
}
|
|
@@ -81,44 +93,67 @@ class GitHubPipe {
|
|
|
81
93
|
summary += `| Run Attempt | 🌒 \`${process.env.GITHUB_RUN_ATTEMPT}\` | `;
|
|
82
94
|
}
|
|
83
95
|
if (process.env.GITHUB_RUN_ID) {
|
|
84
|
-
summary += `| Build Log | ✒️ ${process.env.GITHUB_SERVER_URL || 'https://github.com'}/${
|
|
96
|
+
summary += `| Build Log | ✒️ ${process.env.GITHUB_SERVER_URL || 'https://github.com'}/${
|
|
97
|
+
this.repo
|
|
98
|
+
}/actions/runs/${process.env.GITHUB_RUN_ID} | `;
|
|
85
99
|
}
|
|
86
100
|
|
|
101
|
+
const failures = this.tests
|
|
102
|
+
.filter(t => t.status === 'failed')
|
|
103
|
+
.slice(0, 20)
|
|
104
|
+
.map(t => {
|
|
105
|
+
let text = `#### ${statusEmoji('failed')} ${fullName(t)} `;
|
|
106
|
+
text += '\n\n';
|
|
107
|
+
if (t.message)
|
|
108
|
+
text += `> ${t.message
|
|
109
|
+
.replace(/[^\x20-\x7E]/g, '')
|
|
110
|
+
.replace(ansiRegExp(), '')
|
|
111
|
+
.trim()}\n`;
|
|
112
|
+
if (t.stack) text += `\`\`\`diff\n${t.stack.replace(ansiRegExp(), '').trim()}\n\`\`\`\n`;
|
|
113
|
+
|
|
114
|
+
if (t.artifacts && t.artifacts.length && !process.env.TESTOMATIO_PRIVATE_ARTIFACTS) {
|
|
115
|
+
t.artifacts
|
|
116
|
+
.filter(f => !!f)
|
|
117
|
+
.filter(f => f.endsWith('.png'))
|
|
118
|
+
.forEach(f => {
|
|
119
|
+
if (f.endsWith('.png')) {
|
|
120
|
+
text += `\n`;
|
|
121
|
+
return text;
|
|
122
|
+
}
|
|
123
|
+
text += `[📄 ${path.basename(f)}](${f})\n`;
|
|
124
|
+
return text;
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
text += '\n---\n';
|
|
129
|
+
|
|
130
|
+
return text;
|
|
131
|
+
});
|
|
87
132
|
|
|
88
|
-
const failures = this.tests.filter(t => t.status === 'failed').slice(0, 20).map(t => {
|
|
89
|
-
let text = `#### ${statusEmoji('failed')} ${fullName(t)} `;
|
|
90
|
-
text += "\n\n"
|
|
91
|
-
if (t.message) text += "> " + t.message.replace(/[^\x20-\x7E]/g, '').replace(ansiRegExp(), '').trim() + "\n"
|
|
92
|
-
if (t.stack) text += "```diff\n" + t.stack.replace(ansiRegExp(), '').trim() + "\n```\n";
|
|
93
|
-
|
|
94
|
-
if (t.artifacts && t.artifacts.length && !process.env.TESTOMATIO_PRIVATE_ARTIFACTS) {
|
|
95
|
-
t.artifacts.filter(f => f.endsWith('.png')).forEach(f => {
|
|
96
|
-
if (f.endsWith('.png')) return text+= `\n`
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
text += "\n---\n"
|
|
101
|
-
|
|
102
|
-
return text;
|
|
103
|
-
})
|
|
104
|
-
|
|
105
133
|
let body = summary;
|
|
106
134
|
|
|
107
135
|
if (failures.length) {
|
|
108
|
-
body +=
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
body +=
|
|
136
|
+
body += `\n<details>\n<summary><h3>🟥 Failures (${failures.length})</h4></summary>\n\n${failures.join('\n')}\n`;
|
|
137
|
+
if (failures.length > 20) {
|
|
138
|
+
body += '\n> Notice\n> Only first 20 failures shown*';
|
|
139
|
+
}
|
|
140
|
+
body += '\n\n</details>';
|
|
113
141
|
}
|
|
114
142
|
|
|
115
143
|
if (this.tests.length > 0) {
|
|
116
|
-
body +=
|
|
117
|
-
body += this.tests
|
|
118
|
-
|
|
119
|
-
|
|
144
|
+
body += '\n<details>\n<summary><h3>🐢 Slowest Tests</h3></summary>\n\n';
|
|
145
|
+
body += this.tests
|
|
146
|
+
// eslint-disable-next-line no-unsafe-optional-chaining
|
|
147
|
+
.sort((a, b) => b?.run_time - a?.run_time)
|
|
148
|
+
.slice(0, 5)
|
|
149
|
+
.map(t => `* ${fullName(t)} (${humanizeDuration(parseFloat(t.run_time))})`)
|
|
150
|
+
.join('\n');
|
|
151
|
+
body += '\n</details>';
|
|
120
152
|
}
|
|
121
153
|
|
|
154
|
+
await deletePreviousReport(this.octokit, owner, repo, this.issue, this.hiddenCommentData);
|
|
155
|
+
|
|
156
|
+
// add report as comment
|
|
122
157
|
try {
|
|
123
158
|
const resp = await this.octokit.rest.issues.createComment({
|
|
124
159
|
owner,
|
|
@@ -126,38 +161,75 @@ class GitHubPipe {
|
|
|
126
161
|
issue_number: this.issue,
|
|
127
162
|
body,
|
|
128
163
|
});
|
|
129
|
-
|
|
164
|
+
|
|
130
165
|
const url = resp.data?.html_url;
|
|
131
166
|
this.store.githubUrl = url;
|
|
132
|
-
|
|
133
|
-
console.log(
|
|
134
|
-
APP_PREFIX,
|
|
135
|
-
chalk.yellow('GitHub'),
|
|
136
|
-
`Report created: ${chalk.magenta(url)}`,
|
|
137
|
-
);
|
|
167
|
+
|
|
168
|
+
console.log(APP_PREFIX, chalk.yellow('GitHub'), `Report created: ${chalk.magenta(url)}`);
|
|
138
169
|
} catch (err) {
|
|
139
|
-
console.log(
|
|
140
|
-
APP_PREFIX,
|
|
141
|
-
chalk.yellow('GitHub'),
|
|
142
|
-
`Couldn't create GitHub report ${err}`,
|
|
143
|
-
);
|
|
170
|
+
console.log(APP_PREFIX, chalk.yellow('GitHub'), `Couldn't create GitHub report ${err}`);
|
|
144
171
|
}
|
|
145
172
|
}
|
|
173
|
+
|
|
174
|
+
toString() {
|
|
175
|
+
return 'GitHub Reporter';
|
|
176
|
+
}
|
|
146
177
|
}
|
|
147
178
|
|
|
148
179
|
function statusEmoji(status) {
|
|
149
180
|
if (status === 'passed') return '🟢';
|
|
150
181
|
if (status === 'failed') return '🔴';
|
|
151
182
|
if (status === 'skipped') return '🟡';
|
|
152
|
-
return ''
|
|
183
|
+
return '';
|
|
153
184
|
}
|
|
154
185
|
|
|
155
186
|
function fullName(t) {
|
|
156
187
|
let line = '';
|
|
157
|
-
if (t.suite_title) line = `${t.suite_title}: `;
|
|
158
|
-
line += `**${t.title}
|
|
188
|
+
if (t.suite_title) line = `${t.suite_title}: `;
|
|
189
|
+
line += `**${t.title}**`;
|
|
159
190
|
if (t.example) line += ` \`[${Object.values(t.example)}]\``;
|
|
160
191
|
return line;
|
|
161
192
|
}
|
|
162
193
|
|
|
163
|
-
|
|
194
|
+
async function deletePreviousReport(octokit, owner, repo, issue, hiddenCommentData) {
|
|
195
|
+
if (process.env.GITHUB_KEEP_OUTDATED_REPORTS) return;
|
|
196
|
+
|
|
197
|
+
// get comments
|
|
198
|
+
let comments = [];
|
|
199
|
+
try {
|
|
200
|
+
const response = await octokit.rest.issues.listComments({
|
|
201
|
+
owner,
|
|
202
|
+
repo,
|
|
203
|
+
issue_number: issue,
|
|
204
|
+
});
|
|
205
|
+
comments = response.data;
|
|
206
|
+
} catch (e) {
|
|
207
|
+
console.error('Error while attempt to retrieve comments on GitHub Pull Request:\n', e);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (!comments.length) return;
|
|
211
|
+
|
|
212
|
+
for (const comment of comments) {
|
|
213
|
+
// if comment was left by the same workflow
|
|
214
|
+
if (comment.body.includes(hiddenCommentData)) {
|
|
215
|
+
try {
|
|
216
|
+
// delete previous comment
|
|
217
|
+
await octokit.rest.issues.deleteComment({
|
|
218
|
+
owner,
|
|
219
|
+
repo,
|
|
220
|
+
issue_number: issue,
|
|
221
|
+
comment_id: comment.id,
|
|
222
|
+
});
|
|
223
|
+
} catch (e) {
|
|
224
|
+
console.error(`Can't delete previously added comment with testomat.io report. Ignore.`);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// pass next env var if need to clear all previous reports;
|
|
228
|
+
// only the last one is removed by default
|
|
229
|
+
if (!process.env.GITHUB_REMOVE_ALL_OUTDATED_REPORTS) break;
|
|
230
|
+
// TODO: in case of many reports should implement pagination
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
module.exports = GitHubPipe;
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
const axios = require('axios');
|
|
2
|
+
const chalk = require('chalk');
|
|
3
|
+
const humanizeDuration = require('humanize-duration');
|
|
4
|
+
const merge = require('lodash.merge');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { APP_PREFIX } = require('../constants');
|
|
7
|
+
const { ansiRegExp, isSameTest } = require('../util');
|
|
8
|
+
|
|
9
|
+
//! GITLAB_PAT environment variable is required for this functionality to work
|
|
10
|
+
//! and your pipeline trigger should be merge_request
|
|
11
|
+
|
|
12
|
+
class GitLabPipe {
|
|
13
|
+
constructor(params, store = {}) {
|
|
14
|
+
this.isEnabled = false;
|
|
15
|
+
this.ENV = process.env;
|
|
16
|
+
this.store = store;
|
|
17
|
+
this.tests = [];
|
|
18
|
+
// GitLab PAT looks like glpat-nKGdja3jsG4850sGksh7
|
|
19
|
+
this.token = params.GITLAB_PAT || this.ENV.GITLAB_PAT;
|
|
20
|
+
this.hiddenCommentData = `<!--- testomat.io report ${process.env.CI_JOB_NAME || ''} -->`;
|
|
21
|
+
this.axios = axios.create();
|
|
22
|
+
|
|
23
|
+
if (this.ENV.DEBUG) {
|
|
24
|
+
console.log(
|
|
25
|
+
APP_PREFIX,
|
|
26
|
+
chalk.yellow('GitLab Pipe:'),
|
|
27
|
+
this.token ? 'TOKEN passed' : '*no token*',
|
|
28
|
+
`Project id: ${this.ENV.CI_PROJECT_ID}, MR id: ${this.ENV.CI_MERGE_REQUEST_IID}`,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!this.ENV.CI_PROJECT_ID || !this.ENV.CI_MERGE_REQUEST_IID) {
|
|
33
|
+
// console.warn(`CI pipeline should be run in Merge Request to have ability to add the report comment.`);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!this.token) {
|
|
38
|
+
console.warn(chalk.grey(`Hint: GitLab CI variables are unavailable for unprotected branches by default.`));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
this.isEnabled = true;
|
|
43
|
+
|
|
44
|
+
if (this.ENV.DEBUG) {
|
|
45
|
+
console.log(APP_PREFIX, chalk.yellow('GitLab Pipe: Enabled'));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async createRun() {}
|
|
50
|
+
|
|
51
|
+
addTest(test) {
|
|
52
|
+
if (!this.isEnabled) return;
|
|
53
|
+
|
|
54
|
+
const index = this.tests.findIndex(t => isSameTest(t, test));
|
|
55
|
+
// update if they were already added
|
|
56
|
+
if (index >= 0) {
|
|
57
|
+
this.tests[index] = merge(this.tests[index], test);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
this.tests.push(test);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async finishRun(runParams) {
|
|
65
|
+
if (!this.isEnabled) return;
|
|
66
|
+
|
|
67
|
+
if (runParams.tests) runParams.tests.forEach(t => this.addTest(t));
|
|
68
|
+
|
|
69
|
+
// ... create a comment on GitLab
|
|
70
|
+
const passedCount = this.tests.filter(t => t.status === 'passed').length;
|
|
71
|
+
const failedCount = this.tests.filter(t => t.status === 'failed').length;
|
|
72
|
+
const skippedCount = this.tests.filter(t => t.status === 'skipped').length;
|
|
73
|
+
|
|
74
|
+
// constructing the table
|
|
75
|
+
let summary = `${this.hiddenCommentData}
|
|
76
|
+
|
|
77
|
+
| [](https://testomat.io) | ${
|
|
78
|
+
statusEmoji(runParams.status,
|
|
79
|
+
)} ${runParams.status.toUpperCase()} ${statusEmoji(runParams.status)} |
|
|
80
|
+
| --- | --- |
|
|
81
|
+
| Tests | ✔️ **${this.tests.length}** tests run |
|
|
82
|
+
| Summary | ${statusEmoji('failed')} **${failedCount}** failed; ${statusEmoji(
|
|
83
|
+
'passed',
|
|
84
|
+
)} **${passedCount}** passed; **${statusEmoji('skipped')}** ${skippedCount} skipped |
|
|
85
|
+
| Duration | 🕐 **${humanizeDuration(parseFloat(this.tests.reduce((a, t) => a + (t.run_time || 0), 0)))}** |
|
|
86
|
+
`;
|
|
87
|
+
|
|
88
|
+
if (this.ENV.CI_JOB_NAME && this.ENV.CI_JOB_ID) {
|
|
89
|
+
summary += `| Job | 👷 [${this.ENV.CI_JOB_ID}](${this.ENV.CI_JOB_URL})<br>Name: **${
|
|
90
|
+
this.ENV.CI_JOB_NAME}**<br>Stage: **${this.ENV.CI_JOB_STAGE}** | `;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const failures = this.tests
|
|
94
|
+
.filter(t => t.status === 'failed')
|
|
95
|
+
.slice(0, 20)
|
|
96
|
+
.map(t => {
|
|
97
|
+
let text = `#### ${statusEmoji('failed')} ${fullName(t)} `;
|
|
98
|
+
text += '\n\n';
|
|
99
|
+
if (t.message)
|
|
100
|
+
text += `> ${t.message
|
|
101
|
+
.replace(/[^\x20-\x7E]/g, '')
|
|
102
|
+
.replace(ansiRegExp(), '')
|
|
103
|
+
.trim()}\n`;
|
|
104
|
+
if (t.stack) text += `\`\`\`diff\n${t.stack.replace(ansiRegExp(), '').trim()}\n\`\`\`\n`;
|
|
105
|
+
|
|
106
|
+
if (t.artifacts && t.artifacts.length && !this.ENV.TESTOMATIO_PRIVATE_ARTIFACTS) {
|
|
107
|
+
t.artifacts
|
|
108
|
+
.filter(f => !!f)
|
|
109
|
+
.filter(f => f.endsWith('.png'))
|
|
110
|
+
.forEach(f => {
|
|
111
|
+
if (f.endsWith('.png')) {
|
|
112
|
+
text += `\n`;
|
|
113
|
+
return text;
|
|
114
|
+
}
|
|
115
|
+
text += `[📄 ${path.basename(f)}](${f})\n`;
|
|
116
|
+
return text;
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
text += '\n---\n';
|
|
121
|
+
|
|
122
|
+
return text;
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
let body = summary;
|
|
126
|
+
|
|
127
|
+
if (failures.length) {
|
|
128
|
+
body += `\n<details>\n<summary><h3>🟥 Failures (${failures.length})</h4></summary>\n\n${failures.join('\n')}\n`;
|
|
129
|
+
if (failures.length > 20) {
|
|
130
|
+
body += '\n> Notice\n> Only first 20 failures shown*';
|
|
131
|
+
}
|
|
132
|
+
body += '\n\n</details>';
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (this.tests.length > 0) {
|
|
136
|
+
body += '\n<details>\n<summary><h3>🐢 Slowest Tests</h3></summary>\n\n';
|
|
137
|
+
body += this.tests
|
|
138
|
+
// eslint-disable-next-line no-unsafe-optional-chaining
|
|
139
|
+
.sort((a, b) => b?.run_time - a?.run_time)
|
|
140
|
+
.slice(0, 5)
|
|
141
|
+
.map(t => `* ${fullName(t)} (${humanizeDuration(parseFloat(t.run_time))})`)
|
|
142
|
+
.join('\n');
|
|
143
|
+
body += '\n</details>';
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// eslint-disable-next-line max-len
|
|
147
|
+
const commentsRequestURL = `https://gitlab.com/api/v4/projects/${this.ENV.CI_PROJECT_ID}/merge_requests/${this.ENV.CI_MERGE_REQUEST_IID}/notes`;
|
|
148
|
+
|
|
149
|
+
// delete previous report
|
|
150
|
+
await deletePreviousReport(this.axios, commentsRequestURL, this.hiddenCommentData, this.token);
|
|
151
|
+
|
|
152
|
+
// add current report
|
|
153
|
+
console.info(chalk.grey(`Adding comment via url: ${commentsRequestURL}`));
|
|
154
|
+
try {
|
|
155
|
+
const addCommentResponse = await this.axios.post(`${commentsRequestURL}?access_token=${this.token}`, { body });
|
|
156
|
+
|
|
157
|
+
const commentID = addCommentResponse.data.id;
|
|
158
|
+
// eslint-disable-next-line max-len
|
|
159
|
+
const commentURL = `${this.ENV.CI_PROJECT_URL}/-/merge_requests/${this.ENV.CI_MERGE_REQUEST_IID}#note_${commentID}`;
|
|
160
|
+
|
|
161
|
+
console.log(APP_PREFIX, chalk.yellow('GitLab'), `Report created: ${chalk.magenta(commentURL)}`);
|
|
162
|
+
} catch (err) {
|
|
163
|
+
console.log(
|
|
164
|
+
APP_PREFIX,
|
|
165
|
+
chalk.yellow('GitLab'),
|
|
166
|
+
`Couldn't create GitLab report\n${err}.
|
|
167
|
+
Request url: ${commentsRequestURL}
|
|
168
|
+
Request data: ${body}`,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
toString() {
|
|
174
|
+
return 'GitLab Reporter';
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
updateRun() {}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function statusEmoji(status) {
|
|
181
|
+
if (status === 'passed') return '🟢';
|
|
182
|
+
if (status === 'failed') return '🔴';
|
|
183
|
+
if (status === 'skipped') return '🟡';
|
|
184
|
+
return '';
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function fullName(t) {
|
|
188
|
+
let line = '';
|
|
189
|
+
if (t.suite_title) line = `${t.suite_title}: `;
|
|
190
|
+
line += `**${t.title}**`;
|
|
191
|
+
if (t.example) line += ` \`[${Object.values(t.example)}]\``;
|
|
192
|
+
return line;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function deletePreviousReport(axiosInstance, commentsRequestURL, hiddenCommentData, token) {
|
|
196
|
+
if (process.env.GITLAB_KEEP_OUTDATED_REPORTS) return;
|
|
197
|
+
|
|
198
|
+
// get comments
|
|
199
|
+
let comments = [];
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
const response = await axiosInstance.get(`${commentsRequestURL}?access_token=${token}`);
|
|
203
|
+
comments = response.data;
|
|
204
|
+
} catch (e) {
|
|
205
|
+
console.error('Error while attempt to retrieve comments on GitLab Merge Request:\n', e);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (!comments.length) return;
|
|
209
|
+
|
|
210
|
+
for (const comment of comments) {
|
|
211
|
+
// if comment was left by the same workflow
|
|
212
|
+
if (comment.body.includes(hiddenCommentData)) {
|
|
213
|
+
try {
|
|
214
|
+
// delete previous comment
|
|
215
|
+
const deleteCommentURL = `${commentsRequestURL}/${comment.id}?access_token=${token}`;
|
|
216
|
+
await axiosInstance.delete(deleteCommentURL);
|
|
217
|
+
} catch (e) {
|
|
218
|
+
console.error(`Can't delete previously added comment with testomat.io report. Ignore.`);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// pass next env var if need to clear all previous reports;
|
|
222
|
+
// only the last one is removed by default
|
|
223
|
+
if (!process.env.GITLAB_REMOVE_ALL_OUTDATED_REPORTS) break;
|
|
224
|
+
// TODO: in case of many reports should implement pagination
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
module.exports = GitLabPipe;
|