@testomatio/reporter 1.4.10 → 1.4.11-beta-bitbucket-pipe
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/bin/startTest.js +1 -1
- package/lib/client.js +6 -3
- package/lib/pipe/bitbucket.js +247 -0
- package/lib/pipe/index.js +2 -0
- package/package.json +2 -1
package/lib/bin/startTest.js
CHANGED
package/lib/client.js
CHANGED
|
@@ -279,7 +279,10 @@ class Client {
|
|
|
279
279
|
logs = logs?.trim();
|
|
280
280
|
|
|
281
281
|
if (Array.isArray(steps)) {
|
|
282
|
-
steps = steps
|
|
282
|
+
steps = steps
|
|
283
|
+
.map(step => formatStep(step))
|
|
284
|
+
.flat()
|
|
285
|
+
.join('\n');
|
|
283
286
|
}
|
|
284
287
|
|
|
285
288
|
let testLogs = '';
|
|
@@ -358,8 +361,8 @@ function formatStep(step, shift = 0) {
|
|
|
358
361
|
|
|
359
362
|
for (const child of step.steps || []) {
|
|
360
363
|
lines.push(...formatStep(child, shift + 2));
|
|
361
|
-
}
|
|
362
|
-
|
|
364
|
+
}
|
|
365
|
+
|
|
363
366
|
return lines;
|
|
364
367
|
}
|
|
365
368
|
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
const debug = require('debug')('@testomatio/reporter:pipe:bitbucket');
|
|
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, testomatLogoURL } = require('../constants');
|
|
8
|
+
const { ansiRegExp, isSameTest } = require('../utils/utils');
|
|
9
|
+
const { statusEmoji, fullName } = require('../utils/pipe_utils');
|
|
10
|
+
|
|
11
|
+
//! BITBUCKET_PAT environment variable is required for this functionality to work
|
|
12
|
+
//! and your pipeline trigger should be a pull request
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @class BitbucketPipe
|
|
16
|
+
* @typedef {import('../../types').Pipe} Pipe
|
|
17
|
+
* @typedef {import('../../types').TestData} TestData
|
|
18
|
+
*/
|
|
19
|
+
class BitbucketPipe {
|
|
20
|
+
constructor(params, store = {}) {
|
|
21
|
+
this.isEnabled = false;
|
|
22
|
+
this.ENV = process.env;
|
|
23
|
+
this.store = store;
|
|
24
|
+
this.tests = [];
|
|
25
|
+
// Bitbucket PAT looks like bbpat-*****
|
|
26
|
+
this.token = params.BITBUCKET_PAT || process.env.BITBUCKET_PAT || this.ENV.BITBUCKET_PAT;
|
|
27
|
+
this.hiddenCommentData = `<!--- testomat.io report ${process.env.BITBUCKET_STEP_NAME || ''} -->`;
|
|
28
|
+
|
|
29
|
+
debug(
|
|
30
|
+
chalk.yellow('Bitbucket Pipe:'),
|
|
31
|
+
this.token ? 'TOKEN passed' : '*no token*',
|
|
32
|
+
`Project key: ${this.ENV.BITBUCKET_PROJECT_KEY}, Pull request ID: ${this.ENV.BITBUCKET_PR_ID}`,
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
if (!this.ENV.BITBUCKET_PROJECT_KEY || !this.ENV.BITBUCKET_PR_ID) {
|
|
36
|
+
debug(`CI pipeline should be run in a Pull Request to have the ability to add the report comment.`);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!this.token) {
|
|
41
|
+
debug(`Hint: Bitbucket CI variables are unavailable for unprotected branches by default.`);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
this.isEnabled = true;
|
|
46
|
+
|
|
47
|
+
debug('Bitbucket Pipe: Enabled');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Prepare the run (if needed)
|
|
51
|
+
async prepareRun() {}
|
|
52
|
+
|
|
53
|
+
// Create a new run (if needed)
|
|
54
|
+
async createRun() {}
|
|
55
|
+
|
|
56
|
+
addTest(test) {
|
|
57
|
+
if (!this.isEnabled) return;
|
|
58
|
+
|
|
59
|
+
const index = this.tests.findIndex(t => isSameTest(t, test));
|
|
60
|
+
// Update if they were already added
|
|
61
|
+
if (index >= 0) {
|
|
62
|
+
this.tests[index] = merge(this.tests[index], test);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
this.tests.push(test);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async finishRun(runParams) {
|
|
70
|
+
if (!this.isEnabled) return;
|
|
71
|
+
|
|
72
|
+
if (runParams.tests) runParams.tests.forEach(t => this.addTest(t));
|
|
73
|
+
|
|
74
|
+
// Create a comment on Bitbucket
|
|
75
|
+
const passedCount = this.tests.filter(t => t.status === 'passed').length;
|
|
76
|
+
const failedCount = this.tests.filter(t => t.status === 'failed').length;
|
|
77
|
+
const skippedCount = this.tests.filter(t => t.status === 'skipped').length;
|
|
78
|
+
|
|
79
|
+
// Constructing the table
|
|
80
|
+
let summary = `${this.hiddenCommentData}
|
|
81
|
+
|
|
82
|
+
| [](https://testomat.io) | ${statusEmoji(
|
|
83
|
+
runParams.status,
|
|
84
|
+
)} ${runParams.status.toUpperCase()} ${statusEmoji(runParams.status)} |
|
|
85
|
+
| --- | --- |
|
|
86
|
+
| Tests | ✔️ **${this.tests.length}** tests run |
|
|
87
|
+
| Summary | ${statusEmoji('failed')} **${failedCount}** failed; ${statusEmoji(
|
|
88
|
+
'passed',
|
|
89
|
+
)} **${passedCount}** passed; **${statusEmoji('skipped')}** ${skippedCount} skipped |
|
|
90
|
+
| Duration | 🕐 **${humanizeDuration(
|
|
91
|
+
parseInt(
|
|
92
|
+
this.tests.reduce((a, t) => a + (t.run_time || 0), 0),
|
|
93
|
+
10,
|
|
94
|
+
),
|
|
95
|
+
{
|
|
96
|
+
maxDecimalPoints: 0,
|
|
97
|
+
},
|
|
98
|
+
)}** |
|
|
99
|
+
`;
|
|
100
|
+
|
|
101
|
+
if (this.ENV.BITBUCKET_STEP_NAME && this.ENV.BITBUCKET_STEP_UUID) {
|
|
102
|
+
// eslint-disable-next-line max-len
|
|
103
|
+
summary += `| Job | 👷 [${this.ENV.BITBUCKET_STEP_UUID}](${this.ENV.BITBUCKET_PIPELINE_STEP_URL})<br>Name: **${this.ENV.BITBUCKET_STEP_NAME}** | `;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const failures = this.tests
|
|
107
|
+
.filter(t => t.status === 'failed')
|
|
108
|
+
.slice(0, 20)
|
|
109
|
+
.map(t => {
|
|
110
|
+
let text = `#### ${statusEmoji('failed')} ${fullName(t)} `;
|
|
111
|
+
text += '\n\n';
|
|
112
|
+
if (t.message)
|
|
113
|
+
text += `> ${t.message
|
|
114
|
+
.replace(/[^\x20-\x7E]/g, '')
|
|
115
|
+
.replace(ansiRegExp(), '')
|
|
116
|
+
.trim()}\n`;
|
|
117
|
+
if (t.stack) text += `\`\`\`diff\n${t.stack.replace(ansiRegExp(), '').trim()}\n\`\`\`\n`;
|
|
118
|
+
|
|
119
|
+
if (t.artifacts && t.artifacts.length && !this.ENV.TESTOMATIO_PRIVATE_ARTIFACTS) {
|
|
120
|
+
t.artifacts
|
|
121
|
+
.filter(f => !!f)
|
|
122
|
+
.filter(f => f.endsWith('.png'))
|
|
123
|
+
.forEach(f => {
|
|
124
|
+
if (f.endsWith('.png')) {
|
|
125
|
+
text += `\n`;
|
|
126
|
+
return text;
|
|
127
|
+
}
|
|
128
|
+
text += `[📄 ${path.basename(f)}](${f})\n`;
|
|
129
|
+
return text;
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
text += '\n---\n';
|
|
134
|
+
|
|
135
|
+
return text;
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
let body = summary;
|
|
139
|
+
|
|
140
|
+
if (failures.length) {
|
|
141
|
+
body += `\n<details>\n<summary><h3>🟥 Failures (${failures.length})</h3></summary>\n\n${failures.join('\n')}\n`;
|
|
142
|
+
if (failures.length > 20) {
|
|
143
|
+
body += '\n> Notice\n> Only first 20 failures shown*';
|
|
144
|
+
}
|
|
145
|
+
body += '\n\n</details>';
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (this.tests.length > 0) {
|
|
149
|
+
body += '\n<details>\n<summary><h3>🐢 Slowest Tests</h3></summary>\n\n';
|
|
150
|
+
body += this.tests
|
|
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>';
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Construct Bitbucket API URL for comments
|
|
159
|
+
// eslint-disable-next-line max-len
|
|
160
|
+
const commentsRequestURL = `https://api.bitbucket.org/2.0/repositories/${this.ENV.BITBUCKET_WORKSPACE}/${this.ENV.BITBUCKET_REPO_SLUG}/pullrequests/${this.ENV.BITBUCKET_PR_ID}/comments`;
|
|
161
|
+
|
|
162
|
+
// Delete previous report
|
|
163
|
+
await deletePreviousReport(axios, commentsRequestURL, this.hiddenCommentData, this.token);
|
|
164
|
+
|
|
165
|
+
// Add current report
|
|
166
|
+
debug(`Adding comment via URL: ${commentsRequestURL}`);
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
const addCommentResponse = await axios.post(
|
|
170
|
+
commentsRequestURL,
|
|
171
|
+
{ content: { raw: body } },
|
|
172
|
+
{
|
|
173
|
+
headers: {
|
|
174
|
+
Authorization: `Bearer ${this.token}`,
|
|
175
|
+
'Content-Type': 'application/json',
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
const commentID = addCommentResponse.data.id;
|
|
181
|
+
// eslint-disable-next-line max-len
|
|
182
|
+
const commentURL = `${this.ENV.BITBUCKET_REPO_URL}/pull-requests/${this.ENV.BITBUCKET_PR_ID}#comment-${commentID}`;
|
|
183
|
+
|
|
184
|
+
console.log(APP_PREFIX, chalk.yellow('Bitbucket'), `Report created: ${chalk.magenta(commentURL)}`);
|
|
185
|
+
} catch (err) {
|
|
186
|
+
console.error(
|
|
187
|
+
APP_PREFIX,
|
|
188
|
+
chalk.yellow('Bitbucket'),
|
|
189
|
+
`Couldn't create Bitbucket report\n${err}.
|
|
190
|
+
Request URL: ${commentsRequestURL}
|
|
191
|
+
Request data: ${body}`,
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
toString() {
|
|
197
|
+
return 'Bitbucket Reporter';
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
updateRun() {}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function deletePreviousReport(axiosInstance, commentsRequestURL, hiddenCommentData, token) {
|
|
204
|
+
if (process.env.BITBUCKET_KEEP_OUTDATED_REPORTS) return;
|
|
205
|
+
|
|
206
|
+
// Get comments
|
|
207
|
+
let comments = [];
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
const response = await axiosInstance.get(commentsRequestURL, {
|
|
211
|
+
headers: {
|
|
212
|
+
Authorization: `Bearer ${token}`,
|
|
213
|
+
'Content-Type': 'application/json',
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
comments = response.data.values;
|
|
217
|
+
} catch (e) {
|
|
218
|
+
console.error('Error while attempting to retrieve comments on Bitbucket Pull Request:\n', e);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (!comments.length) return;
|
|
222
|
+
|
|
223
|
+
for (const comment of comments) {
|
|
224
|
+
// If comment was left by the same workflow
|
|
225
|
+
if (comment.content.raw.includes(hiddenCommentData)) {
|
|
226
|
+
try {
|
|
227
|
+
// Delete previous comment
|
|
228
|
+
const deleteCommentURL = `${commentsRequestURL}/${comment.id}`;
|
|
229
|
+
await axiosInstance.delete(deleteCommentURL, {
|
|
230
|
+
headers: {
|
|
231
|
+
Authorization: `Bearer ${token}`,
|
|
232
|
+
'Content-Type': 'application/json',
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
} catch (e) {
|
|
236
|
+
console.warn(`Can't delete previously added comment with testomat.io report. Ignored.`);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Pass next env var if need to clear all previous reports;
|
|
240
|
+
// only the last one is removed by default
|
|
241
|
+
if (!process.env.BITBUCKET_REMOVE_ALL_OUTDATED_REPORTS) break;
|
|
242
|
+
// TODO: in case of many reports should implement pagination
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
module.exports = BitbucketPipe;
|
package/lib/pipe/index.js
CHANGED
|
@@ -7,6 +7,7 @@ const GitHubPipe = require('./github');
|
|
|
7
7
|
const GitLabPipe = require('./gitlab');
|
|
8
8
|
const CsvPipe = require('./csv');
|
|
9
9
|
const HtmlPipe = require('./html');
|
|
10
|
+
const BitbucketPipe = require('./bitbucket');
|
|
10
11
|
|
|
11
12
|
function PipeFactory(params, opts) {
|
|
12
13
|
const extraPipes = [];
|
|
@@ -45,6 +46,7 @@ function PipeFactory(params, opts) {
|
|
|
45
46
|
new GitLabPipe(params, opts),
|
|
46
47
|
new CsvPipe(params, opts),
|
|
47
48
|
new HtmlPipe(params, opts),
|
|
49
|
+
new BitbucketPipe(params, opts),
|
|
48
50
|
...extraPipes,
|
|
49
51
|
];
|
|
50
52
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@testomatio/reporter",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.11-beta-bitbucket-pipe",
|
|
4
4
|
"description": "Testomatio Reporter Client",
|
|
5
5
|
"main": "./lib/reporter.js",
|
|
6
6
|
"typings": "typings/index.d.ts",
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"callsite-record": "^4.1.4",
|
|
18
18
|
"chalk": "^4.1.0",
|
|
19
19
|
"commander": "^4.1.1",
|
|
20
|
+
"cross-spawn": "^7.0.3",
|
|
20
21
|
"csv-writer": "^1.6.0",
|
|
21
22
|
"debug": "^4.3.4",
|
|
22
23
|
"dotenv": "^16.0.1",
|