@testomatio/reporter 1.5.1-beta → 1.6.0-beta-1-artifacts

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.
@@ -0,0 +1,254 @@
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_ACCESS_TOKEN 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_ACCESS_TOKEN || process.env.BITBUCKET_ACCESS_TOKEN || this.ENV.BITBUCKET_ACCESS_TOKEN;
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.token) {
36
+ debug(`Hint: Bitbucket CI variables are unavailable for unprotected branches by default.`);
37
+ return;
38
+ }
39
+
40
+ this.isEnabled = true;
41
+
42
+ debug('Bitbucket Pipe: Enabled');
43
+ }
44
+
45
+ async cleanLog(log) {
46
+ const stripAnsi = (await import('strip-ansi')).default;
47
+ return stripAnsi(log);
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
+ // Clean up the logs from ANSI codes
75
+ for (let i = 0; i < this.tests.length; i++) {
76
+ this.tests[i].message = await this.cleanLog(this.tests[i].message || '');
77
+ this.tests[i].stack = await this.cleanLog(this.tests[i].stack || '');
78
+ }
79
+
80
+ // Create a comment on Bitbucket
81
+ const passedCount = this.tests.filter(t => t.status === 'passed').length;
82
+ const failedCount = this.tests.filter(t => t.status === 'failed').length;
83
+ const skippedCount = this.tests.filter(t => t.status === 'skipped').length;
84
+
85
+ // Constructing the table
86
+ let summary = `${this.hiddenCommentData}
87
+
88
+ | ![Testomat.io Report](${testomatLogoURL}) | ${statusEmoji(
89
+ runParams.status,
90
+ )} ${runParams.status.toUpperCase()} ${statusEmoji(runParams.status)} |
91
+ | --- | --- |
92
+ | **Tests** | ✔️ **${this.tests.length}** tests run |
93
+ | **Summary** | ${statusEmoji('failed')} **${failedCount}** failed; ${statusEmoji(
94
+ 'passed',
95
+ )} **${passedCount}** passed; **${statusEmoji('skipped')}** ${skippedCount} skipped |
96
+ | **Duration** | 🕐 **${humanizeDuration(
97
+ parseInt(
98
+ this.tests.reduce((a, t) => a + (t.run_time || 0), 0),
99
+ 10,
100
+ ),
101
+ {
102
+ maxDecimalPoints: 0,
103
+ },
104
+ )}** |
105
+ `;
106
+
107
+ if (this.ENV.BITBUCKET_BRANCH && this.ENV.BITBUCKET_COMMIT) {
108
+ // eslint-disable-next-line max-len
109
+ summary += `| **Job** | 👷 [#${this.ENV.BITBUCKET_BUILD_NUMBER}](https://bitbucket.org/${this.ENV.BITBUCKET_REPO_FULL_NAME}/pipelines/results/${this.ENV.BITBUCKET_BUILD_NUMBER}") by commit: **${this.ENV.BITBUCKET_COMMIT}** |`;
110
+ }
111
+
112
+ const failures = this.tests
113
+ .filter(t => t.status === 'failed')
114
+ .slice(0, 20)
115
+ .map(t => {
116
+ let text = `${statusEmoji('failed')} ${fullName(t)}\n`;
117
+ if (t.message) {
118
+ text += `> ${t.message
119
+ .replace(/[^\x20-\x7E]/g, '')
120
+ .replace(ansiRegExp(), '')
121
+ .trim()}\n`;
122
+ }
123
+ if (t.stack) {
124
+ text += `\n\`\`\`diff\n${t.stack
125
+ .replace(ansiRegExp(), '')
126
+ .replace(
127
+ /^[\s\S]*################\[ Failure \]################/g,
128
+ '################[ Failure ]################',
129
+ )
130
+ .trim()}\n\`\`\`\n`;
131
+ }
132
+ if (t.artifacts && t.artifacts.length && !this.ENV.TESTOMATIO_PRIVATE_ARTIFACTS) {
133
+ t.artifacts
134
+ .filter(f => !!f)
135
+ .forEach(f => {
136
+ if (f.endsWith('.png')) {
137
+ text += `![Image](${f})\n`;
138
+ } else {
139
+ text += `[📄 ${path.basename(f)}](${f})\n`;
140
+ }
141
+ });
142
+ }
143
+ text += `\n---\n`;
144
+ return text;
145
+ });
146
+
147
+ let body = summary;
148
+
149
+ if (failures.length) {
150
+ body += `\n🟥 **Failures (${failures.length})**\n\n* ${failures.join('\n* ')}\n`;
151
+ if (failures.length > 10) {
152
+ body += `\n> Notice: Only the first 10 failures are shown.`;
153
+ }
154
+ }
155
+
156
+ if (this.tests.length > 0) {
157
+ body += `\n\n**🐢 Slowest Tests**\n\n`;
158
+ body += this.tests
159
+ .sort((a, b) => b.run_time - a.run_time)
160
+ .slice(0, 5)
161
+ .map(t => `* **${fullName(t)}** (${humanizeDuration(parseFloat(t.run_time))})`)
162
+ .join('\n');
163
+ }
164
+
165
+ // Construct Bitbucket API URL for comments
166
+ // eslint-disable-next-line max-len
167
+ 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`;
168
+
169
+ // Delete previous report
170
+ await deletePreviousReport(axios, commentsRequestURL, this.hiddenCommentData, this.token);
171
+
172
+ // Add current report
173
+ debug(`Adding comment via URL: ${commentsRequestURL}`);
174
+ debug(`Final Bitbucket API call body: ${body}`);
175
+
176
+ try {
177
+ const addCommentResponse = await axios.post(
178
+ commentsRequestURL,
179
+ { content: { raw: body } },
180
+ {
181
+ headers: {
182
+ Authorization: `Bearer ${this.token}`,
183
+ 'Content-Type': 'application/json',
184
+ },
185
+ },
186
+ );
187
+
188
+ const commentID = addCommentResponse.data.id;
189
+ // eslint-disable-next-line max-len
190
+ const commentURL = `https://bitbucket.org/${this.ENV.BITBUCKET_WORKSPACE}/${this.ENV.BITBUCKET_REPO_SLUG}/pull-requests/${this.ENV.BITBUCKET_PR_ID}#comment-${commentID}`;
191
+
192
+ console.log(APP_PREFIX, chalk.yellow('Bitbucket'), `Report created: ${chalk.magenta(commentURL)}`);
193
+ } catch (err) {
194
+ console.error(
195
+ APP_PREFIX,
196
+ chalk.yellow('Bitbucket'),
197
+ `Couldn't create Bitbucket report\n${err}.
198
+ Request URL: ${commentsRequestURL}
199
+ Request data: ${body}`,
200
+ );
201
+ }
202
+ }
203
+
204
+ toString() {
205
+ return 'Bitbucket Reporter';
206
+ }
207
+
208
+ updateRun() {}
209
+ }
210
+
211
+ async function deletePreviousReport(axiosInstance, commentsRequestURL, hiddenCommentData, token) {
212
+ if (process.env.BITBUCKET_KEEP_OUTDATED_REPORTS) return;
213
+
214
+ // Get comments
215
+ let comments = [];
216
+
217
+ try {
218
+ const response = await axiosInstance.get(commentsRequestURL, {
219
+ headers: {
220
+ Authorization: `Bearer ${token}`,
221
+ 'Content-Type': 'application/json',
222
+ },
223
+ });
224
+ comments = response.data.values;
225
+ } catch (e) {
226
+ console.error('Error while attempting to retrieve comments on Bitbucket Pull Request:\n', e);
227
+ }
228
+
229
+ if (!comments.length) return;
230
+
231
+ for (const comment of comments) {
232
+ // If comment was left by the same workflow
233
+ if (comment.content.raw.includes(hiddenCommentData)) {
234
+ try {
235
+ // Delete previous comment
236
+ const deleteCommentURL = `${commentsRequestURL}/${comment.id}`;
237
+ await axiosInstance.delete(deleteCommentURL, {
238
+ headers: {
239
+ Authorization: `Bearer ${token}`,
240
+ 'Content-Type': 'application/json',
241
+ },
242
+ });
243
+ } catch (e) {
244
+ console.warn(`Can't delete previously added comment with testomat.io report. Ignored.`);
245
+ }
246
+ // Pass next env var if need to clear all previous reports;
247
+ // only the last one is removed by default
248
+ if (!process.env.BITBUCKET_REMOVE_ALL_OUTDATED_REPORTS) break;
249
+ // TODO: in case of many reports should implement pagination
250
+ }
251
+ }
252
+ }
253
+
254
+ 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
 
@@ -37,7 +37,7 @@ class TestomatioPipe {
37
37
  this.isEnabled = false;
38
38
  this.url = params.testomatioUrl || process.env.TESTOMATIO_URL || 'https://app.testomat.io';
39
39
  this.apiKey = params.apiKey || config.TESTOMATIO;
40
- debug('Testomatio Pipe: ', this.apiKey ? 'API KEY' : '*no api key*');
40
+
41
41
  if (!this.apiKey) {
42
42
  return;
43
43
  }
@@ -50,6 +50,7 @@ class TestomatioPipe {
50
50
  this.store = store || {};
51
51
  this.title = params.title || process.env.TESTOMATIO_TITLE;
52
52
  this.sharedRun = !!process.env.TESTOMATIO_SHARED_RUN;
53
+ this.sharedRunTimeout = !!process.env.TESTOMATIO_SHARED_RUN_TIMEOUT;
53
54
  this.groupTitle = params.groupTitle || process.env.TESTOMATIO_RUNGROUP_TITLE;
54
55
  this.env = process.env.TESTOMATIO_ENV;
55
56
  this.label = process.env.TESTOMATIO_LABEL;
@@ -148,7 +149,6 @@ class TestomatioPipe {
148
149
  */
149
150
  async createRun(params = {}) {
150
151
  this.batch.isEnabled = params.isBatchEnabled ?? this.batch.isEnabled;
151
- debug('Creating run...');
152
152
  if (!this.isEnabled) return;
153
153
  if (this.batch.isEnabled) this.batch.intervalFunction = setInterval(this.#batchUpload, this.batch.intervalTime);
154
154
 
@@ -184,17 +184,20 @@ class TestomatioPipe {
184
184
  title: this.title,
185
185
  label: this.label,
186
186
  shared_run: this.sharedRun,
187
+ shared_run_timeout: this.sharedRunTimeout,
187
188
  }).filter(([, value]) => !!value),
188
189
  );
189
190
  debug('Run params', JSON.stringify(runParams, null, 2));
190
191
 
191
192
  if (this.runId) {
193
+ this.store.runId = this.runId;
192
194
  debug(`Run with id ${this.runId} already created, updating...`);
193
195
  const resp = await this.axios.put(`/api/reporter/${this.runId}`, runParams);
194
196
  if (resp.data.artifacts) setS3Credentials(resp.data.artifacts);
195
197
  return;
196
198
  }
197
199
 
200
+ debug('Creating run...');
198
201
  try {
199
202
  const resp = await this.axios.post(`/api/reporter`, runParams, {
200
203
  maxContentLength: Infinity,
@@ -296,7 +299,7 @@ class TestomatioPipe {
296
299
  }
297
300
  });
298
301
  };
299
-
302
+
300
303
 
301
304
  /**
302
305
  * Uploads tests as a batch (multiple tests at once). Intended to be used with a setInterval
@@ -310,6 +313,8 @@ class TestomatioPipe {
310
313
  const testsToSend = this.batch.tests.splice(0);
311
314
  debug('📨 Batch upload', testsToSend.length, 'tests');
312
315
 
316
+ testsToSend.forEach(debug);
317
+
313
318
  return this.axios
314
319
  .post(
315
320
  `/api/reporter/${this.runId}/testrun`,
@@ -367,7 +372,7 @@ class TestomatioPipe {
367
372
  */
368
373
  async finishRun(params) {
369
374
  if (!this.isEnabled) return;
370
-
375
+
371
376
  await this.#batchUpload();
372
377
  if (this.batch.intervalFunction) clearInterval(this.batch.intervalFunction);
373
378
 
@@ -408,6 +413,7 @@ class TestomatioPipe {
408
413
  console.log(APP_PREFIX, `📊 ${notFinishedMessage}. Report URL: ${chalk.magenta(this.runUrl)}`);
409
414
  console.log(APP_PREFIX, `🛬 Run to finish it: TESTOMATIO_RUN=${this.runId} npx start-test-run --finish`);
410
415
  }
416
+
411
417
  if (this.hasUnmatchedTests) {
412
418
  console.log('');
413
419
  // eslint-disable-next-line max-len
@@ -0,0 +1,308 @@
1
+ const debug = require('debug')('@testomatio/reporter:uploader');
2
+ const { S3 } = require('@aws-sdk/client-s3');
3
+ const { Upload } = require('@aws-sdk/lib-storage');
4
+ const fs = require('fs');
5
+ const os = require('os');
6
+ const path = require('path');
7
+ const promiseRetry = require('promise-retry');
8
+ const chalk = require('chalk');
9
+ const { APP_PREFIX } = require('./constants');
10
+
11
+ class S3Uploader {
12
+ constructor() {
13
+ this.isEnabled = undefined;
14
+ this.storeEnabled = true;
15
+ this.config = undefined;
16
+
17
+ // counters
18
+ this.skippedUpload = 0;
19
+ this.failedUpload = 0;
20
+ this.totalUploaded = 0;
21
+
22
+ this.succesfulUploads = {};
23
+
24
+ this.configKeys = [
25
+ 'S3_ENDPOINT',
26
+ 'S3_REGION',
27
+ 'S3_BUCKET',
28
+ 'S3_ACCESS_KEY_ID',
29
+ 'S3_SECRET_ACCESS_KEY',
30
+ 'S3_SESSION_TOKEN',
31
+ 'S3_FORCE_PATH_STYLE',
32
+ 'TESTOMATIO_DISABLE_ARTIFACTS',
33
+ 'TESTOMATIO_PRIVATE_ARTIFACTS',
34
+ 'TESTOMATIO_ARTIFACTS_SIZE'
35
+ ];
36
+ }
37
+
38
+ resetConfig() {
39
+ this.config = undefined;
40
+ this.isEnabled = undefined;
41
+ }
42
+
43
+ getConfig() {
44
+ if (this.config) return this.config;
45
+ this.config = this.configKeys.reduce((acc, key) => {
46
+ acc[key] = process.env[key];
47
+ return acc;
48
+ }, {});
49
+ return this.config;
50
+ }
51
+
52
+ getMaskedConfig() {
53
+ return Object.fromEntries(
54
+ Object.entries(this.getConfig()).map(([key, value]) => [
55
+ key,
56
+ key === 'S3_SECRET_ACCESS_KEY' || key === 'S3_ACCESS_KEY_ID' ? '***' : value,
57
+ ]),
58
+ );
59
+ }
60
+
61
+ checkEnabled() {
62
+ if (this.isEnabled !== undefined) return this.isEnabled;
63
+
64
+ const { S3_BUCKET, TESTOMATIO_DISABLE_ARTIFACTS } = this.getConfig();
65
+ if (!S3_BUCKET) debug(`Upload is disabled because S3_BUCKET is not set`);
66
+ this.isEnabled = !!(S3_BUCKET && !TESTOMATIO_DISABLE_ARTIFACTS);
67
+
68
+ if (this.isEnabled) debug('S3 uploader is enabled');
69
+ debug(this.getMaskedConfig());
70
+
71
+ return this.isEnabled;
72
+ }
73
+
74
+ enableLogStorage() {
75
+ this.storeEnabled = true;
76
+ }
77
+
78
+ disbleLogStorage() {
79
+ this.storeEnabled = false;
80
+ }
81
+
82
+ async uploadToS3(Body, Key) {
83
+ const { S3_BUCKET, TESTOMATIO_PRIVATE_ARTIFACTS } = this.getConfig();
84
+ const ACL = TESTOMATIO_PRIVATE_ARTIFACTS ? 'private' : 'public-read';
85
+
86
+ if (!S3_BUCKET || !Body) {
87
+ console.log(APP_PREFIX, chalk.bold.red(`Failed uploading '${Key}'. Please check S3 credentials`), this.getMaskedConfig());
88
+ return;
89
+ }
90
+
91
+ debug('Uploading to S3:', Key);
92
+
93
+ const s3 = new S3(this.getS3Config());
94
+
95
+ try {
96
+ const upload = new Upload({
97
+ client: s3,
98
+ params: {
99
+ Bucket: S3_BUCKET,
100
+ Key,
101
+ Body,
102
+ ACL,
103
+ },
104
+ });
105
+
106
+ const link = await this.getS3LocationLink(upload);
107
+ this.totalUploaded++;
108
+ this.succesfulUploads[Key] = link;
109
+ return link;
110
+ } catch (e) {
111
+ this.failedUpload++;
112
+ debug('S3 uploading error:', e);
113
+ console.log(APP_PREFIX, "Upload failed:", e.message, this.getMaskedConfig());
114
+ }
115
+ }
116
+
117
+ readUploadedFiles(runId) {
118
+ const tempFilePath = this.#getUploadFilePath(runId);
119
+
120
+ debug('Reading file', tempFilePath);
121
+
122
+ if (!fs.existsSync(tempFilePath)) {
123
+ debug('File not found:', tempFilePath);
124
+ return [];
125
+ }
126
+
127
+ const stats = fs.statSync(tempFilePath);
128
+ const diff = (+new Date()) - (+stats.mtime);
129
+ const diffHours = diff / 1000 / 60 / 60;
130
+ if (diffHours > 3) {
131
+ console.log(APP_PREFIX, 'Artifacts file is too old, can\'t process artifacts. Please re-run the tests.');
132
+ return [];
133
+ }
134
+
135
+ const data = fs.readFileSync(tempFilePath, 'utf8');
136
+ const lines = data.split('\n').filter(Boolean);
137
+ return lines.map(line => JSON.parse(line));
138
+ }
139
+
140
+ #getUploadFilePath(runId, forceCreate = false) {
141
+ const tempFilePath = path.join(os.tmpdir(), `testomatio.run.${runId}.jsonl`);
142
+ if (!fs.existsSync(tempFilePath) || forceCreate) {
143
+ debug('Creating artifacts file:', tempFilePath);
144
+ fs.writeFileSync(tempFilePath, '');
145
+ // make symlink to 'testomatio.run.latest.jsonl' file
146
+ const latestFilePath = path.join(os.tmpdir(), 'testomatio.run.latest.jsonl');
147
+ if (fs.existsSync(latestFilePath)) {
148
+ fs.unlinkSync(latestFilePath);
149
+ }
150
+ fs.symlinkSync(tempFilePath, latestFilePath);
151
+
152
+ }
153
+ return tempFilePath;
154
+ }
155
+
156
+ storeUploadedFile(filePath, runId, rid, uploaded = false) {
157
+ if (!this.storeEnabled) return;
158
+
159
+ if (!filePath || !runId || !rid ) return;
160
+
161
+ const tempFilePath = this.#getUploadFilePath(runId);
162
+
163
+ const data = { rid, file: filePath, uploaded };
164
+ const jsonLine = JSON.stringify(data) + '\n';
165
+
166
+ fs.appendFileSync(tempFilePath, jsonLine);
167
+ }
168
+
169
+ getskippedUpload() {
170
+ return this.skippedUpload;
171
+ }
172
+
173
+ async uploadFileByPath(filePath, pathInS3) {
174
+ const [runId, rid] = pathInS3;
175
+
176
+ if (!this.isEnabled) {
177
+ this.storeUploadedFile(filePath, runId, rid, false);
178
+ this.skippedUpload++;
179
+ return;
180
+ }
181
+
182
+ const {
183
+ S3_BUCKET,
184
+ TESTOMATIO_ARTIFACTS_SIZE,
185
+ } = this.getConfig();
186
+
187
+ debug('Started upload', filePath, 'to', S3_BUCKET);
188
+
189
+ const isFileExist = await this.checkFileExists(filePath, 20, 500);
190
+
191
+ if (!isFileExist) {
192
+ this.failedUpload++;
193
+ console.error(chalk.yellow(`Artifacts file ${filePath} does not exist. Skipping...`));
194
+ return;
195
+ }
196
+
197
+ const fileSize = fs.statSync(filePath).size;
198
+ const fileSizeInMb = fileSize / (1024 * 1024);
199
+
200
+ if (TESTOMATIO_ARTIFACTS_SIZE && fileSizeInMb > parseInt(TESTOMATIO_ARTIFACTS_SIZE)) {
201
+ this.skippedUpload++;
202
+ console.error(chalk.yellow(`Artifacts file ${filePath} exceeds the maximum allowed size. Skipping...`));
203
+ return;
204
+ }
205
+ debug('File:', filePath, 'exists, size:', fileSizeInMb.toFixed(2), 'MB');
206
+
207
+ const fileStream = fs.createReadStream(filePath);
208
+ const Key = pathInS3.join('/');
209
+
210
+ const link = await this.uploadToS3(fileStream, Key);
211
+
212
+ this.storeUploadedFile(filePath, runId, rid, !!link);
213
+
214
+ return link;
215
+ }
216
+
217
+ async uploadFileAsBuffer(buffer, pathInS3) {
218
+ if (!this.isEnabled) return;
219
+
220
+ let Key = pathInS3.join('/');
221
+ const ext = this.#getFileExtBase64(buffer);
222
+
223
+ if (ext) {
224
+ Key = `${Key}.${ext}`;
225
+ }
226
+
227
+ return this.uploadToS3(buffer, Key);
228
+ }
229
+
230
+ async checkFileExists(filePath, attempts = 5, intervalMs = 500) {
231
+ return promiseRetry(
232
+ async (retry, number) => {
233
+ try {
234
+ fs.accessSync(filePath);
235
+ return true;
236
+ } catch (err) {
237
+ if (number === attempts) {
238
+ return false;
239
+ }
240
+ debug(`File not found, retrying (attempt ${number}/${attempts})`);
241
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
242
+ retry(err);
243
+ }
244
+ },
245
+ {
246
+ retries: attempts,
247
+ minTimeout: intervalMs,
248
+ maxTimeout: intervalMs,
249
+ }
250
+ );
251
+ }
252
+
253
+ async getS3LocationLink(out) {
254
+ const response = await out.done();
255
+
256
+ let s3Location = response?.Location;
257
+
258
+ if (!s3Location) {
259
+ s3Location = out?.singleUploadResult?.Location;
260
+ debug('Uploaded singleUploadResult.Location', s3Location);
261
+
262
+ if (!s3Location) {
263
+ throw new Error("Problems getting the S3 artifact's link. Please check S3 permissions!");
264
+ }
265
+ }
266
+
267
+ return s3Location;
268
+ }
269
+
270
+ #getFileExtBase64(str) {
271
+ const type = str.charAt(0);
272
+
273
+ return (
274
+ {
275
+ '/': 'jpg',
276
+ i: 'png',
277
+ R: 'gif',
278
+ U: 'webp',
279
+ }[type] || ''
280
+ );
281
+ }
282
+
283
+ getS3Config() {
284
+ const { S3_REGION, S3_SESSION_TOKEN, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, S3_FORCE_PATH_STYLE, S3_ENDPOINT } =
285
+ this.getConfig();
286
+
287
+ const cfg = {
288
+ region: S3_REGION,
289
+ credentials: {
290
+ accessKeyId: S3_ACCESS_KEY_ID,
291
+ secretAccessKey: S3_SECRET_ACCESS_KEY,
292
+ s3ForcePathStyle: S3_FORCE_PATH_STYLE,
293
+ },
294
+ };
295
+
296
+ if (S3_SESSION_TOKEN) {
297
+ cfg.credentials.sessionToken = S3_SESSION_TOKEN;
298
+ }
299
+
300
+ if (S3_ENDPOINT) {
301
+ cfg.endpoint = S3_ENDPOINT;
302
+ }
303
+
304
+ return cfg;
305
+ }
306
+ }
307
+
308
+ module.exports = S3Uploader;
@@ -1,4 +1,4 @@
1
- const { resetConfig } = require('../fileUploader');
1
+ const { resetConfig } = require('../uploader');
2
2
  const { APP_PREFIX } = require('../constants');
3
3
 
4
4
  /**
package/lib/xmlReader.js CHANGED
@@ -13,7 +13,7 @@ const {
13
13
  fetchIdFromCode,
14
14
  humanize,
15
15
  } = require('./utils/utils');
16
- const upload = require('./fileUploader');
16
+ const S3Uploader = require('./uploader');
17
17
  const pipesFactory = require('./pipe');
18
18
  const adapterFactory = require('./junit-adapter');
19
19
  const config = require('./config');
@@ -56,7 +56,7 @@ class XmlReader {
56
56
  this.tests = [];
57
57
  this.stats = {};
58
58
  this.stats.language = opts.lang?.toLowerCase();
59
- this.filesToUpload = {};
59
+ this.uploader = new S3Uploader();
60
60
 
61
61
  this.version = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json')).toString()).version;
62
62
  console.log(APP_PREFIX, `Testomatio Reporter v${this.version}`);
@@ -382,7 +382,7 @@ class XmlReader {
382
382
  if (!files.length) continue;
383
383
 
384
384
  const runId = this.runId || this.store.runId || Date.now().toString();
385
- test.artifacts = await Promise.all(files.map(f => upload.uploadFileByPath(f, runId)));
385
+ test.artifacts = await Promise.all(files.map(f => this.uploader.uploadFileByPath(f, [runId])));
386
386
  console.log(APP_PREFIX, `🗄️ Uploaded ${chalk.bold(`${files.length} artifacts`)} for test ${test.title}`);
387
387
  }
388
388
  }
@@ -398,7 +398,9 @@ class XmlReader {
398
398
 
399
399
  debug('Run', runParams);
400
400
 
401
- return Promise.all(this.pipes.map(p => p.createRun(runParams)));
401
+ await Promise.all(this.pipes.map(p => p.createRun(runParams)));
402
+
403
+ this.uploader.checkEnabled();
402
404
  }
403
405
 
404
406
  async uploadData() {