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