@testomatio/reporter 1.4.10 → 1.4.12-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.
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- const { spawn } = require('child_process');
2
+ const spawn = require('cross-spawn');
3
3
  const program = require('commander');
4
4
  const chalk = require('chalk');
5
5
  const TestomatClient = require('../client');
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.map(step => formatStep(step)).flat().join('\n');
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,259 @@
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_BRANCH || ''}`;
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
+ async cleanLog(log) {
51
+ const stripAnsi = (await import('strip-ansi')).default;
52
+ return stripAnsi(log);
53
+ }
54
+
55
+ // Prepare the run (if needed)
56
+ async prepareRun() {}
57
+
58
+ // Create a new run (if needed)
59
+ async createRun() {}
60
+
61
+ addTest(test) {
62
+ if (!this.isEnabled) return;
63
+
64
+ const index = this.tests.findIndex(t => isSameTest(t, test));
65
+ // Update if they were already added
66
+ if (index >= 0) {
67
+ this.tests[index] = merge(this.tests[index], test);
68
+ return;
69
+ }
70
+
71
+ this.tests.push(test);
72
+ }
73
+
74
+ async finishRun(runParams) {
75
+ if (!this.isEnabled) return;
76
+
77
+ if (runParams.tests) runParams.tests.forEach(t => this.addTest(t));
78
+
79
+ // Clean up the logs from ANSI codes
80
+ for (let i = 0; i < this.tests.length; i++) {
81
+ this.tests[i].message = await this.cleanLog(this.tests[i].message || '');
82
+ this.tests[i].stack = await this.cleanLog(this.tests[i].stack || '');
83
+ }
84
+
85
+ // Create a comment on Bitbucket
86
+ const passedCount = this.tests.filter(t => t.status === 'passed').length;
87
+ const failedCount = this.tests.filter(t => t.status === 'failed').length;
88
+ const skippedCount = this.tests.filter(t => t.status === 'skipped').length;
89
+
90
+ // Constructing the table
91
+ let summary = `${this.hiddenCommentData}
92
+
93
+ | ![Testomat.io Report](${testomatLogoURL}) | ${statusEmoji(
94
+ runParams.status,
95
+ )} ${runParams.status.toUpperCase()} ${statusEmoji(runParams.status)} |
96
+ | --- | --- |
97
+ | **Tests** | ✔️ **${this.tests.length}** tests run |
98
+ | **Summary** | ${statusEmoji('failed')} **${failedCount}** failed; ${statusEmoji(
99
+ 'passed',
100
+ )} **${passedCount}** passed; **${statusEmoji('skipped')}** ${skippedCount} skipped |
101
+ | **Duration** | 🕐 **${humanizeDuration(
102
+ parseInt(
103
+ this.tests.reduce((a, t) => a + (t.run_time || 0), 0),
104
+ 10,
105
+ ),
106
+ {
107
+ maxDecimalPoints: 0,
108
+ },
109
+ )}** |
110
+ `;
111
+
112
+ if (this.ENV.BITBUCKET_BRANCH && this.ENV.BITBUCKET_COMMIT) {
113
+ // eslint-disable-next-line max-len
114
+ summary += `| **Job** | 👷 [Run: ${this.ENV.BITBUCKET_COMMIT}](https://bitbucket.org/${this.ENV.BITBUCKET_REPO_FULL_NAME}/pipelines/results/${this.ENV.BITBUCKET_BUILD_NUMBER}") Branch: **${this.ENV.BITBUCKET_BRANCH}** |`;
115
+ }
116
+
117
+ const failures = this.tests
118
+ .filter(t => t.status === 'failed')
119
+ .slice(0, 20)
120
+ .map(t => {
121
+ let text = `${statusEmoji('failed')} ${fullName(t)}\n`;
122
+ if (t.message) {
123
+ text += `> ${t.message
124
+ .replace(/[^\x20-\x7E]/g, '')
125
+ .replace(ansiRegExp(), '')
126
+ .trim()}\n`;
127
+ }
128
+ if (t.stack) {
129
+ text += `\n\`\`\`diff\n${t.stack
130
+ .replace(ansiRegExp(), '')
131
+ .replace(
132
+ /^[\s\S]*################\[ Failure \]################/g,
133
+ '################[ Failure ]################',
134
+ )
135
+ .trim()}\n\`\`\`\n`;
136
+ }
137
+ if (t.artifacts && t.artifacts.length && !this.ENV.TESTOMATIO_PRIVATE_ARTIFACTS) {
138
+ t.artifacts
139
+ .filter(f => !!f)
140
+ .forEach(f => {
141
+ if (f.endsWith('.png')) {
142
+ text += `![Image](${f})\n`;
143
+ } else {
144
+ text += `[📄 ${path.basename(f)}](${f})\n`;
145
+ }
146
+ });
147
+ }
148
+ text += `\n---\n`;
149
+ return text;
150
+ });
151
+
152
+ let body = summary;
153
+
154
+ if (failures.length) {
155
+ body += `\n🟥 **Failures (${failures.length})**\n\n* ${failures.join('\n* ')}\n`;
156
+ if (failures.length > 10) {
157
+ body += `\n> Notice: Only the first 10 failures are shown.`;
158
+ }
159
+ }
160
+
161
+ if (this.tests.length > 0) {
162
+ body += `\n\n**🐢 Slowest Tests**\n\n`;
163
+ body += this.tests
164
+ .sort((a, b) => b.run_time - a.run_time)
165
+ .slice(0, 5)
166
+ .map(t => `* **${fullName(t)}** (${humanizeDuration(parseFloat(t.run_time))})`)
167
+ .join('\n');
168
+ }
169
+
170
+ // Construct Bitbucket API URL for comments
171
+ // eslint-disable-next-line max-len
172
+ 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`;
173
+
174
+ // Delete previous report
175
+ await deletePreviousReport(axios, commentsRequestURL, this.hiddenCommentData, this.token);
176
+
177
+ // Add current report
178
+ debug(`Adding comment via URL: ${commentsRequestURL}`);
179
+ debug(`Final Bitbucket API call body: ${body}`);
180
+
181
+ try {
182
+ const addCommentResponse = await axios.post(
183
+ commentsRequestURL,
184
+ { content: { raw: body } },
185
+ {
186
+ headers: {
187
+ Authorization: `Bearer ${this.token}`,
188
+ 'Content-Type': 'application/json',
189
+ },
190
+ },
191
+ );
192
+
193
+ const commentID = addCommentResponse.data.id;
194
+ // eslint-disable-next-line max-len
195
+ const commentURL = `https://bitbucket.org/${this.ENV.BITBUCKET_WORKSPACE}/${this.ENV.BITBUCKET_REPO_SLUG}/pull-requests/${this.ENV.BITBUCKET_PR_ID}#comment-${commentID}`;
196
+
197
+ console.log(APP_PREFIX, chalk.yellow('Bitbucket'), `Report created: ${chalk.magenta(commentURL)}`);
198
+ } catch (err) {
199
+ console.error(
200
+ APP_PREFIX,
201
+ chalk.yellow('Bitbucket'),
202
+ `Couldn't create Bitbucket report\n${err}.
203
+ Request URL: ${commentsRequestURL}
204
+ Request data: ${body}`,
205
+ );
206
+ }
207
+ }
208
+
209
+ toString() {
210
+ return 'Bitbucket Reporter';
211
+ }
212
+
213
+ updateRun() {}
214
+ }
215
+
216
+ async function deletePreviousReport(axiosInstance, commentsRequestURL, hiddenCommentData, token) {
217
+ if (process.env.BITBUCKET_KEEP_OUTDATED_REPORTS) return;
218
+
219
+ // Get comments
220
+ let comments = [];
221
+
222
+ try {
223
+ const response = await axiosInstance.get(commentsRequestURL, {
224
+ headers: {
225
+ Authorization: `Bearer ${token}`,
226
+ 'Content-Type': 'application/json',
227
+ },
228
+ });
229
+ comments = response.data.values;
230
+ } catch (e) {
231
+ console.error('Error while attempting to retrieve comments on Bitbucket Pull Request:\n', e);
232
+ }
233
+
234
+ if (!comments.length) return;
235
+
236
+ for (const comment of comments) {
237
+ // If comment was left by the same workflow
238
+ if (comment.content.raw.includes(hiddenCommentData)) {
239
+ try {
240
+ // Delete previous comment
241
+ const deleteCommentURL = `${commentsRequestURL}/${comment.id}`;
242
+ await axiosInstance.delete(deleteCommentURL, {
243
+ headers: {
244
+ Authorization: `Bearer ${token}`,
245
+ 'Content-Type': 'application/json',
246
+ },
247
+ });
248
+ } catch (e) {
249
+ console.warn(`Can't delete previously added comment with testomat.io report. Ignored.`);
250
+ }
251
+ // Pass next env var if need to clear all previous reports;
252
+ // only the last one is removed by default
253
+ if (!process.env.BITBUCKET_REMOVE_ALL_OUTDATED_REPORTS) break;
254
+ // TODO: in case of many reports should implement pagination
255
+ }
256
+ }
257
+ }
258
+
259
+ 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.10",
3
+ "version": "1.4.12-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",
@@ -32,6 +33,7 @@
32
33
  "lodash.merge": "^4.6.2",
33
34
  "minimatch": "^9.0.3",
34
35
  "promise-retry": "^2.0.1",
36
+ "strip-ansi": "^7.1.0",
35
37
  "uuid": "^9.0.0"
36
38
  },
37
39
  "files": [