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