@testomatio/reporter 1.5.1 → 1.6.0-beta-2-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.
- package/lib/adapter/codecept.js +8 -7
- package/lib/adapter/playwright.js +26 -21
- package/lib/bin/cli.js +216 -0
- package/lib/bin/uploadArtifacts.js +86 -0
- package/lib/client.js +44 -32
- package/lib/pipe/testomatio.js +4 -2
- package/lib/uploader.js +312 -0
- package/lib/utils/pipe_utils.js +0 -2
- package/lib/xmlReader.js +6 -4
- package/package.json +5 -3
- package/lib/fileUploader.js +0 -306
package/lib/adapter/codecept.js
CHANGED
|
@@ -2,7 +2,6 @@ const debug = require('debug')('@testomatio/reporter:adapter:codeceptjs');
|
|
|
2
2
|
const chalk = require('chalk');
|
|
3
3
|
const TestomatClient = require('../client');
|
|
4
4
|
const { STATUS, APP_PREFIX, TESTOMAT_TMP_STORAGE_DIR } = require('../constants');
|
|
5
|
-
const upload = require('../fileUploader');
|
|
6
5
|
const { getTestomatIdFromTestTitle, fileSystem } = require('../utils/utils');
|
|
7
6
|
const { services } = require('../services');
|
|
8
7
|
|
|
@@ -128,10 +127,8 @@ function CodeceptReporter(config) {
|
|
|
128
127
|
// all tests were reported and we can upload videos
|
|
129
128
|
await Promise.all(reportTestPromises);
|
|
130
129
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
await uploadAttachments(client, traces, '📁 Uploading', 'trace');
|
|
134
|
-
}
|
|
130
|
+
await uploadAttachments(client, videos, '🎞️ Uploading', 'video');
|
|
131
|
+
await uploadAttachments(client, traces, '📁 Uploading', 'trace');
|
|
135
132
|
|
|
136
133
|
const status = failedTests.length === 0 ? STATUS.PASSED : STATUS.FAILED;
|
|
137
134
|
client.updateRunStatus(status);
|
|
@@ -304,13 +301,17 @@ function CodeceptReporter(config) {
|
|
|
304
301
|
}
|
|
305
302
|
|
|
306
303
|
async function uploadAttachments(client, attachments, messagePrefix, attachmentType) {
|
|
307
|
-
if (!attachments?.length) return;
|
|
304
|
+
if (!attachments?.length) return;
|
|
308
305
|
|
|
309
|
-
console.log(APP_PREFIX, `Attachments: ${messagePrefix} ${attachments.length} ${attachmentType} ...`);
|
|
306
|
+
if (client.uploader.isEnabled) console.log(APP_PREFIX, `Attachments: ${messagePrefix} ${attachments.length} ${attachmentType} ...`);
|
|
310
307
|
|
|
311
308
|
const promises = attachments.map(async attachment => {
|
|
312
309
|
const { rid, title, path, type } = attachment;
|
|
313
310
|
const file = { path, type, title };
|
|
311
|
+
|
|
312
|
+
// we are storing file if upload is disabled
|
|
313
|
+
if (!client.uploader.isEnabled) return client.uploader.storeUploadedFile(path, client.runId, rid, false);
|
|
314
|
+
|
|
314
315
|
return client.addTestRun(undefined, {
|
|
315
316
|
...stripExampleFromTitle(title),
|
|
316
317
|
rid,
|
|
@@ -6,7 +6,6 @@ const { v4: uuidv4 } = require('uuid');
|
|
|
6
6
|
const fs = require('fs');
|
|
7
7
|
const { APP_PREFIX, STATUS: Status, TESTOMAT_TMP_STORAGE_DIR } = require('../constants');
|
|
8
8
|
const TestomatioClient = require('../client');
|
|
9
|
-
const { isArtifactsEnabled } = require('../fileUploader');
|
|
10
9
|
const { getTestomatIdFromTestTitle, fileSystem } = require('../utils/utils');
|
|
11
10
|
// const debug = require('debug')('@testomatio/reporter:adapter:playwright');
|
|
12
11
|
const { services } = require('../services');
|
|
@@ -106,31 +105,37 @@ class PlaywrightReporter {
|
|
|
106
105
|
|
|
107
106
|
await Promise.all(reportTestPromises);
|
|
108
107
|
|
|
109
|
-
if (this.uploads.length
|
|
110
|
-
|
|
108
|
+
if (!this.uploads.length) return;
|
|
109
|
+
|
|
110
|
+
if (this.client.uploader.isEnabled) console.log(APP_PREFIX, `🎞️ Uploading ${this.uploads.length} files...`);
|
|
111
111
|
|
|
112
|
-
|
|
112
|
+
const promises = [];
|
|
113
113
|
|
|
114
|
-
|
|
115
|
-
|
|
114
|
+
for (const upload of this.uploads) {
|
|
115
|
+
const { rid, file, title } = upload;
|
|
116
116
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
title,
|
|
127
|
-
files,
|
|
128
|
-
file,
|
|
129
|
-
}),
|
|
130
|
-
);
|
|
117
|
+
const files = upload.files.map(attachment => ({
|
|
118
|
+
path: this.#getArtifactPath(attachment),
|
|
119
|
+
title,
|
|
120
|
+
type: attachment.contentType,
|
|
121
|
+
}));
|
|
122
|
+
|
|
123
|
+
if (!this.client.uploader.isEnabled) {
|
|
124
|
+
files.forEach(f => this.client.uploader.storeUploadedFile(f, this.client.runId, rid, false));
|
|
125
|
+
continue;
|
|
131
126
|
}
|
|
132
|
-
|
|
127
|
+
|
|
128
|
+
promises.push(
|
|
129
|
+
this.client.addTestRun(undefined, {
|
|
130
|
+
rid,
|
|
131
|
+
title,
|
|
132
|
+
files,
|
|
133
|
+
file,
|
|
134
|
+
}),
|
|
135
|
+
);
|
|
133
136
|
}
|
|
137
|
+
await Promise.all(promises);
|
|
138
|
+
|
|
134
139
|
|
|
135
140
|
await this.client.updateRunStatus(checkStatus(result.status));
|
|
136
141
|
}
|
package/lib/bin/cli.js
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const { program } = require('commander');
|
|
3
|
+
const spawn = require('cross-spawn');
|
|
4
|
+
const chalk = require('chalk');
|
|
5
|
+
const glob = require('glob');
|
|
6
|
+
const debug = require('debug')('@testomatio/reporter:cli');
|
|
7
|
+
const TestomatClient = require('../client');
|
|
8
|
+
const XmlReader = require('../xmlReader');
|
|
9
|
+
const { APP_PREFIX, STATUS } = require('../constants');
|
|
10
|
+
const { version } = require('../../package.json');
|
|
11
|
+
const config = require('../config');
|
|
12
|
+
|
|
13
|
+
console.log(chalk.cyan.bold(` 🤩 Testomat.io Reporter v${version}`));
|
|
14
|
+
|
|
15
|
+
program
|
|
16
|
+
.version(version)
|
|
17
|
+
.option('--env-file <envfile>', 'Load environment variables from env file')
|
|
18
|
+
.hook('preAction', (thisCommand) => {
|
|
19
|
+
const opts = thisCommand.opts();
|
|
20
|
+
if (opts.envFile) {
|
|
21
|
+
require('dotenv').config({ path: opts.envFile });
|
|
22
|
+
} else {
|
|
23
|
+
require('dotenv').config();
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
program
|
|
28
|
+
.command('start')
|
|
29
|
+
.description('Start a new run and return its ID')
|
|
30
|
+
.action(async () => {
|
|
31
|
+
console.log('Starting a new Run on Testomat.io...');
|
|
32
|
+
const apiKey = process.env['INPUT_TESTOMATIO-KEY'] || config.TESTOMATIO;
|
|
33
|
+
const client = new TestomatClient({ apiKey });
|
|
34
|
+
|
|
35
|
+
client.createRun().then(() => {
|
|
36
|
+
console.log(process.env.runId);
|
|
37
|
+
process.exit(0);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
program
|
|
42
|
+
.command('finish')
|
|
43
|
+
.description('Finish Run by its ID')
|
|
44
|
+
.action(async () => {
|
|
45
|
+
if (!process.env.TESTOMATIO_RUN) {
|
|
46
|
+
console.log('TESTOMATIO_RUN environment variable must be set.');
|
|
47
|
+
return process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
console.log('Finishing Run on Testomat.io...');
|
|
51
|
+
const apiKey = process.env['INPUT_TESTOMATIO-KEY'] || config.TESTOMATIO;
|
|
52
|
+
const client = new TestomatClient({ apiKey });
|
|
53
|
+
|
|
54
|
+
client.updateRunStatus(STATUS.FINISHED).then(() => {
|
|
55
|
+
console.log(chalk.yellow(`Run ${process.env.TESTOMATIO_RUN} was finished`));
|
|
56
|
+
process.exit(0);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
program
|
|
61
|
+
.command('run')
|
|
62
|
+
.description('Run tests with the specified command')
|
|
63
|
+
.argument('<command>', 'Test runner command')
|
|
64
|
+
.option('--filter <filter>', 'Additional execution filter')
|
|
65
|
+
.action(async (command, opts) => {
|
|
66
|
+
const apiKey = process.env['INPUT_TESTOMATIO-KEY'] || config.TESTOMATIO;
|
|
67
|
+
const title = process.env.TESTOMATIO_TITLE;
|
|
68
|
+
|
|
69
|
+
if (!command || !command.split) {
|
|
70
|
+
console.log(APP_PREFIX, `No command provided. Use -c option to launch a test runner.`);
|
|
71
|
+
return process.exit(255);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const client = new TestomatClient({ apiKey, title, parallel: true });
|
|
75
|
+
|
|
76
|
+
if (opts.filter) {
|
|
77
|
+
const [pipe, ...optsArray] = opts.filter.split(':');
|
|
78
|
+
const pipeOptions = optsArray.join(':');
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const tests = await client.prepareRun({ pipe, pipeOptions });
|
|
82
|
+
if (tests && tests.length > 0) {
|
|
83
|
+
command += ` --grep (${tests.join('|')})`;
|
|
84
|
+
}
|
|
85
|
+
} catch (err) {
|
|
86
|
+
console.log(APP_PREFIX, err);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
console.log(APP_PREFIX, `🚀 Running`, chalk.green(command));
|
|
91
|
+
|
|
92
|
+
const runTests = () => {
|
|
93
|
+
const testCmds = command.split(' ');
|
|
94
|
+
const cmd = spawn(testCmds[0], testCmds.slice(1), { stdio: 'inherit' });
|
|
95
|
+
|
|
96
|
+
cmd.on('close', code => {
|
|
97
|
+
const emoji = code === 0 ? '🟢' : '🔴';
|
|
98
|
+
console.log(APP_PREFIX, emoji, `Runner exited with ${chalk.bold(code)}`);
|
|
99
|
+
if (apiKey) {
|
|
100
|
+
const status = code === 0 ? 'passed' : 'failed';
|
|
101
|
+
client.updateRunStatus(status, true);
|
|
102
|
+
}
|
|
103
|
+
process.exit(code);
|
|
104
|
+
});
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
if (apiKey) {
|
|
108
|
+
client.createRun().then(runTests);
|
|
109
|
+
} else {
|
|
110
|
+
runTests();
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
program
|
|
115
|
+
.command('xml')
|
|
116
|
+
.description('Parse XML reports and upload to Testomat.io')
|
|
117
|
+
.argument('<pattern>', 'XML file pattern')
|
|
118
|
+
.option('-d, --dir <dir>', 'Project directory')
|
|
119
|
+
.option('--java-tests [java-path]', 'Load Java tests from path, by default: src/test/java')
|
|
120
|
+
.option('--lang <lang>', 'Language used (python, ruby, java)')
|
|
121
|
+
.option('--timelimit <time>', 'default time limit in seconds to kill a stuck process')
|
|
122
|
+
.action(async (pattern, opts) => {
|
|
123
|
+
if (!pattern.endsWith('.xml')) {
|
|
124
|
+
pattern += '.xml';
|
|
125
|
+
}
|
|
126
|
+
let { javaTests, lang } = opts;
|
|
127
|
+
if (javaTests === true) javaTests = 'src/test/java';
|
|
128
|
+
lang = lang?.toLowerCase();
|
|
129
|
+
const runReader = new XmlReader({ javaTests, lang });
|
|
130
|
+
const files = glob.sync(pattern, { cwd: opts.dir || process.cwd() });
|
|
131
|
+
if (!files.length) {
|
|
132
|
+
console.log(APP_PREFIX, `Report can't be created. No XML files found 😥`);
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
for (const file of files) {
|
|
137
|
+
console.log(APP_PREFIX, `Parsed ${file}`);
|
|
138
|
+
runReader.parse(file);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let timeoutTimer;
|
|
142
|
+
if (opts.timelimit) {
|
|
143
|
+
timeoutTimer = setTimeout(() => {
|
|
144
|
+
console.log(`⚠️ Reached timeout of ${opts.timelimit}s. Exiting... (Exit code is 0 to not fail the pipeline)`);
|
|
145
|
+
process.exit(0);
|
|
146
|
+
}, parseInt(opts.timelimit, 10) * 1000);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
await runReader.createRun();
|
|
151
|
+
await runReader.uploadData();
|
|
152
|
+
} catch (err) {
|
|
153
|
+
console.log(APP_PREFIX, 'Error updating status, skipping...', err);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (timeoutTimer) clearTimeout(timeoutTimer);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
program
|
|
160
|
+
.command('upload-artifacts')
|
|
161
|
+
.description('Upload artifacts to Testomat.io')
|
|
162
|
+
.option('--force', 'Re-upload artifacts even if they were uploaded before')
|
|
163
|
+
.action(async (opts) => {
|
|
164
|
+
const apiKey = config.TESTOMATIO;
|
|
165
|
+
|
|
166
|
+
process.env.TESTOMATIO_DISABLE_ARTIFACTS = '';
|
|
167
|
+
|
|
168
|
+
const client = new TestomatClient({
|
|
169
|
+
apiKey,
|
|
170
|
+
isBatchEnabled: false,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
let testruns = client.uploader.readUploadedFiles(process.env.TESTOMATIO_RUN);
|
|
174
|
+
const numTotalArtifacts = testruns.length;
|
|
175
|
+
|
|
176
|
+
debug('Found testruns:', testruns);
|
|
177
|
+
|
|
178
|
+
if (!opts.force) testruns = testruns.filter(tr => !tr.uploaded);
|
|
179
|
+
|
|
180
|
+
if (!testruns.length) {
|
|
181
|
+
console.log(APP_PREFIX, 'Total artifacts:', numTotalArtifacts);
|
|
182
|
+
if (numTotalArtifacts) {
|
|
183
|
+
console.log(APP_PREFIX, 'No new artifacts to upload');
|
|
184
|
+
console.log(APP_PREFIX, 'To re-upload artifacts run this command with --force flag');
|
|
185
|
+
}
|
|
186
|
+
process.exit(0);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const testrunsByRid = testruns.reduce((acc, { rid, file }) => {
|
|
190
|
+
if (!acc[rid]) {
|
|
191
|
+
acc[rid] = [];
|
|
192
|
+
}
|
|
193
|
+
if (!acc[rid].includes(file)) acc[rid].push(file);
|
|
194
|
+
return acc;
|
|
195
|
+
}, {});
|
|
196
|
+
|
|
197
|
+
await client.createRun();
|
|
198
|
+
client.uploader.checkEnabled();
|
|
199
|
+
client.uploader.disbleLogStorage();
|
|
200
|
+
|
|
201
|
+
for (const rid in testrunsByRid) {
|
|
202
|
+
const files = testrunsByRid[rid];
|
|
203
|
+
await client.addTestRun(undefined, { rid, files });
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
console.log(APP_PREFIX, client.uploader.totalUploaded, 'artifacts uploaded');
|
|
207
|
+
if (client.uploader.failedUpload) {
|
|
208
|
+
console.log(APP_PREFIX, client.uploader.failedUpload, 'artifacts failed to upload');
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
program.parse(process.argv);
|
|
213
|
+
|
|
214
|
+
if (!process.argv.slice(2).length) {
|
|
215
|
+
program.outputHelp();
|
|
216
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const program = require('commander');
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const debug = require('debug')('@testomatio/reporter:upload-cli');
|
|
5
|
+
const TestomatClient = require('../client');
|
|
6
|
+
const { APP_PREFIX } = require('../constants');
|
|
7
|
+
const { version } = require('../../package.json');
|
|
8
|
+
const config = require('../config');
|
|
9
|
+
|
|
10
|
+
console.log(chalk.cyan.bold(` 🤩 Testomat.io Reporter v${version}`));
|
|
11
|
+
|
|
12
|
+
program
|
|
13
|
+
.option('--env-file <envfile>', 'Load environment variables from env file')
|
|
14
|
+
.option('--force', 'Re-upload artifacts even if they were uploaded before')
|
|
15
|
+
.action(async opts => {
|
|
16
|
+
|
|
17
|
+
if (opts.envFile) {
|
|
18
|
+
require('dotenv').config(opts.envFile); // eslint-disable-line
|
|
19
|
+
} else {
|
|
20
|
+
// try to load from env file
|
|
21
|
+
require('dotenv').config(); // eslint-disable-line
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const apiKey = config.TESTOMATIO;
|
|
25
|
+
|
|
26
|
+
process.env.TESTOMATIO_DISABLE_ARTIFACTS = '';
|
|
27
|
+
// process.env.TESTOMATIO_ARTIFACTS_SIZE = null;
|
|
28
|
+
|
|
29
|
+
const client = new TestomatClient({
|
|
30
|
+
apiKey,
|
|
31
|
+
isBatchEnabled: false,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
let testruns = client.uploader.readUploadedFiles(process.env.TESTOMATIO_RUN);
|
|
35
|
+
|
|
36
|
+
const numTotalArtifacts = testruns.length;
|
|
37
|
+
|
|
38
|
+
debug('Found testruns:', testruns);
|
|
39
|
+
|
|
40
|
+
if (!opts.force) testruns = testruns.filter(tr => !tr.uploaded);
|
|
41
|
+
|
|
42
|
+
if (!testruns.length) {
|
|
43
|
+
console.log(APP_PREFIX, 'Total artifacts:', numTotalArtifacts);
|
|
44
|
+
if (numTotalArtifacts) {
|
|
45
|
+
console.log(APP_PREFIX, 'No new artifacts to upload');
|
|
46
|
+
console.log(APP_PREFIX, 'To re-upload artifacts run this command with --force flag');
|
|
47
|
+
}
|
|
48
|
+
process.exit(0);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const testrunsByRid = testruns.reduce((acc, { rid, file }) => {
|
|
52
|
+
if (!acc[rid]) {
|
|
53
|
+
acc[rid] = [];
|
|
54
|
+
}
|
|
55
|
+
if (!acc[rid].includes(file)) acc[rid].push(file);
|
|
56
|
+
return acc;
|
|
57
|
+
}, {});
|
|
58
|
+
|
|
59
|
+
let numArtifacts = 0;
|
|
60
|
+
|
|
61
|
+
// we need to obtain S3 credentials
|
|
62
|
+
await client.createRun();
|
|
63
|
+
|
|
64
|
+
client.uploader.checkEnabled();
|
|
65
|
+
client.uploader.disbleLogStorage();
|
|
66
|
+
|
|
67
|
+
for (const rid in testrunsByRid) {
|
|
68
|
+
const files = testrunsByRid[rid];
|
|
69
|
+
numArtifacts += files.length;
|
|
70
|
+
await client.addTestRun(undefined, {
|
|
71
|
+
rid,
|
|
72
|
+
files,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
console.log(APP_PREFIX, client.uploader.totalUploaded, 'artifacts uploaded');
|
|
77
|
+
if (client.uploader.failedUpload) {
|
|
78
|
+
console.log(APP_PREFIX, client.uploader.failedUpload, 'artifacts failed to upload');
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
if (process.argv.length <= 1) {
|
|
83
|
+
program.outputHelp();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
program.parse(process.argv);
|
package/lib/client.js
CHANGED
|
@@ -5,7 +5,7 @@ const { minimatch } = require('minimatch');
|
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const chalk = require('chalk');
|
|
7
7
|
const { randomUUID } = require('crypto');
|
|
8
|
-
const
|
|
8
|
+
const S3Uploader = require('./uploader');
|
|
9
9
|
const { APP_PREFIX, STATUS } = require('./constants');
|
|
10
10
|
const pipesFactory = require('./pipe');
|
|
11
11
|
const { glob } = require('glob');
|
|
@@ -26,14 +26,14 @@ class Client {
|
|
|
26
26
|
* @param {*} params
|
|
27
27
|
*/
|
|
28
28
|
constructor(params = {}) {
|
|
29
|
-
|
|
30
|
-
this.
|
|
31
|
-
this.pipes = pipesFactory(params,
|
|
29
|
+
this.pipeStore = {};
|
|
30
|
+
this.runId = randomUUID(); // will be replaced by real run id
|
|
31
|
+
this.pipes = pipesFactory(params, this.pipeStore);
|
|
32
32
|
this.queue = Promise.resolve();
|
|
33
|
-
this.totalUploaded = 0;
|
|
34
|
-
this.failedToUpload = 0;
|
|
35
33
|
this.version = JSON.parse(fs.readFileSync(join(__dirname, '..', 'package.json')).toString()).version;
|
|
36
34
|
this.executionList = Promise.resolve();
|
|
35
|
+
this.uploader = new S3Uploader();
|
|
36
|
+
this.uploader.checkEnabled();
|
|
37
37
|
|
|
38
38
|
console.log(APP_PREFIX, `Testomatio Reporter v${this.version}`);
|
|
39
39
|
}
|
|
@@ -104,7 +104,13 @@ class Client {
|
|
|
104
104
|
this.queue = this.queue
|
|
105
105
|
.then(() => Promise.all(this.pipes.map(p => p.createRun())))
|
|
106
106
|
.catch(err => console.log(APP_PREFIX, err))
|
|
107
|
+
.then(() => {
|
|
108
|
+
const runId = this.pipeStore?.runId;
|
|
109
|
+
if (runId) this.runId = runId;
|
|
110
|
+
this.uploader.checkEnabled();
|
|
111
|
+
})
|
|
107
112
|
.then(() => undefined); // fixes return type
|
|
113
|
+
|
|
108
114
|
// debug('Run', this.queue);
|
|
109
115
|
return this.queue;
|
|
110
116
|
}
|
|
@@ -169,24 +175,28 @@ class Client {
|
|
|
169
175
|
|
|
170
176
|
const uploadedFiles = [];
|
|
171
177
|
|
|
172
|
-
for (
|
|
173
|
-
|
|
178
|
+
for (let f of files) {
|
|
179
|
+
if (typeof f === 'object') {
|
|
180
|
+
if (!f.path) continue;
|
|
181
|
+
|
|
182
|
+
f = f.path;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
uploadedFiles.push(this.uploader.uploadFileByPath(f, [
|
|
186
|
+
this.runId,
|
|
187
|
+
rid,
|
|
188
|
+
path.basename(f)
|
|
189
|
+
]));
|
|
190
|
+
|
|
174
191
|
}
|
|
175
192
|
|
|
176
193
|
for (const [idx, buffer] of filesBuffers.entries()) {
|
|
177
194
|
const fileName = `${idx + 1}-${title.replace(/\s+/g, '-')}`;
|
|
178
|
-
uploadedFiles.push(
|
|
195
|
+
uploadedFiles.push(this.uploader.uploadFileAsBuffer(buffer, [this.runId, rid, fileName]));
|
|
179
196
|
}
|
|
180
197
|
|
|
181
198
|
const artifacts = (await Promise.all(uploadedFiles)).filter(n => !!n);
|
|
182
199
|
|
|
183
|
-
if (artifacts.length < uploadedFiles.length) {
|
|
184
|
-
const failedUploading = uploadedFiles.length - artifacts.length;
|
|
185
|
-
this.failedToUpload += failedUploading;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
this.totalUploaded += artifacts.length;
|
|
189
|
-
|
|
190
200
|
const data = {
|
|
191
201
|
rid,
|
|
192
202
|
files,
|
|
@@ -242,28 +252,30 @@ class Client {
|
|
|
242
252
|
this.queue = this.queue
|
|
243
253
|
.then(() => Promise.all(this.pipes.map(p => p.finishRun(runParams))))
|
|
244
254
|
.then(() => {
|
|
245
|
-
debug('TOTAL artifacts', this.totalUploaded);
|
|
246
|
-
|
|
247
|
-
debug(`${this.totalUploaded} artifacts are not uploaded, because artifacts uploading is not enabled`);
|
|
255
|
+
debug('TOTAL artifacts', this.uploader.totalUploaded);
|
|
256
|
+
debug(`${this.uploader.skippedUpload} artifacts skipped`);
|
|
248
257
|
|
|
249
|
-
if (this.totalUploaded &&
|
|
258
|
+
if (this.uploader.totalUploaded && this.uploader.isEnabled) {
|
|
250
259
|
console.log(
|
|
251
260
|
APP_PREFIX,
|
|
252
|
-
`🗄️ ${this.totalUploaded} artifacts ${
|
|
261
|
+
`🗄️ ${this.uploader.totalUploaded} artifacts ${
|
|
253
262
|
process.env.TESTOMATIO_PRIVATE_ARTIFACTS ? 'privately' : chalk.bold('publicly')
|
|
254
263
|
} uploaded to S3 bucket`,
|
|
255
|
-
);
|
|
256
|
-
|
|
257
|
-
if (this.failedToUpload > 0) {
|
|
258
|
-
console.log(
|
|
259
|
-
APP_PREFIX,
|
|
260
|
-
chalk.yellow(
|
|
261
|
-
`Some artifacts were not uploaded. ${this.failedToUpload} artifacts could not be uploaded.
|
|
262
|
-
Run tests with DEBUG="@testomatio/reporter:file-uploader" to see details"`,
|
|
263
|
-
),
|
|
264
|
-
);
|
|
265
|
-
}
|
|
264
|
+
);
|
|
266
265
|
}
|
|
266
|
+
|
|
267
|
+
if (this.uploader.failedUpload) {
|
|
268
|
+
console.log(APP_PREFIX, `${this.uploader.failedUpload} artifacts failed to upload`);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (this.uploader.isEnabled && this.uploader.skippedUpload) {
|
|
272
|
+
console.log(APP_PREFIX, `${chalk.bold(this.uploader.skippedUpload)} artifacts skipped to upload`);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (this.uploader.skippedUpload || this.uploader.failedUpload) {
|
|
276
|
+
console.log(APP_PREFIX, `Run "${chalk.magenta(`TESTOMATIO_RUN=${this.runId} npx upload-artifacts`)}" with valid S3 credentials to upload skipped & failed artifacts`);
|
|
277
|
+
}
|
|
278
|
+
|
|
267
279
|
})
|
|
268
280
|
.catch(err => console.log(APP_PREFIX, err));
|
|
269
281
|
|
package/lib/pipe/testomatio.js
CHANGED
|
@@ -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
|
-
|
|
40
|
+
|
|
41
41
|
if (!this.apiKey) {
|
|
42
42
|
return;
|
|
43
43
|
}
|
|
@@ -149,7 +149,6 @@ class TestomatioPipe {
|
|
|
149
149
|
*/
|
|
150
150
|
async createRun(params = {}) {
|
|
151
151
|
this.batch.isEnabled = params.isBatchEnabled ?? this.batch.isEnabled;
|
|
152
|
-
debug('Creating run...');
|
|
153
152
|
if (!this.isEnabled) return;
|
|
154
153
|
if (this.batch.isEnabled) this.batch.intervalFunction = setInterval(this.#batchUpload, this.batch.intervalTime);
|
|
155
154
|
|
|
@@ -191,12 +190,14 @@ class TestomatioPipe {
|
|
|
191
190
|
debug('Run params', JSON.stringify(runParams, null, 2));
|
|
192
191
|
|
|
193
192
|
if (this.runId) {
|
|
193
|
+
this.store.runId = this.runId;
|
|
194
194
|
debug(`Run with id ${this.runId} already created, updating...`);
|
|
195
195
|
const resp = await this.axios.put(`/api/reporter/${this.runId}`, runParams);
|
|
196
196
|
if (resp.data.artifacts) setS3Credentials(resp.data.artifacts);
|
|
197
197
|
return;
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
+
debug('Creating run...');
|
|
200
201
|
try {
|
|
201
202
|
const resp = await this.axios.post(`/api/reporter`, runParams, {
|
|
202
203
|
maxContentLength: Infinity,
|
|
@@ -412,6 +413,7 @@ class TestomatioPipe {
|
|
|
412
413
|
console.log(APP_PREFIX, `📊 ${notFinishedMessage}. Report URL: ${chalk.magenta(this.runUrl)}`);
|
|
413
414
|
console.log(APP_PREFIX, `🛬 Run to finish it: TESTOMATIO_RUN=${this.runId} npx start-test-run --finish`);
|
|
414
415
|
}
|
|
416
|
+
|
|
415
417
|
if (this.hasUnmatchedTests) {
|
|
416
418
|
console.log('');
|
|
417
419
|
// eslint-disable-next-line max-len
|
package/lib/uploader.js
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
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 = null) {
|
|
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 = null, forceCreate = false) {
|
|
141
|
+
if (!runId && !forceCreate) {
|
|
142
|
+
return path.join(os.tmpdir(), 'testomatio.run.latest.jsonl');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const tempFilePath = path.join(os.tmpdir(), `testomatio.run.${runId}.jsonl`);
|
|
146
|
+
if (!fs.existsSync(tempFilePath) || forceCreate) {
|
|
147
|
+
debug('Creating artifacts file:', tempFilePath);
|
|
148
|
+
fs.writeFileSync(tempFilePath, '');
|
|
149
|
+
// make symlink to 'testomatio.run.latest.jsonl' file
|
|
150
|
+
const latestFilePath = path.join(os.tmpdir(), 'testomatio.run.latest.jsonl');
|
|
151
|
+
if (fs.existsSync(latestFilePath)) {
|
|
152
|
+
fs.unlinkSync(latestFilePath);
|
|
153
|
+
}
|
|
154
|
+
fs.symlinkSync(tempFilePath, latestFilePath);
|
|
155
|
+
|
|
156
|
+
}
|
|
157
|
+
return tempFilePath;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
storeUploadedFile(filePath, runId, rid, uploaded = false) {
|
|
161
|
+
if (!this.storeEnabled) return;
|
|
162
|
+
|
|
163
|
+
if (!filePath || !runId || !rid ) return;
|
|
164
|
+
|
|
165
|
+
const tempFilePath = this.#getUploadFilePath(runId);
|
|
166
|
+
|
|
167
|
+
const data = { rid, file: filePath, uploaded };
|
|
168
|
+
const jsonLine = JSON.stringify(data) + '\n';
|
|
169
|
+
|
|
170
|
+
fs.appendFileSync(tempFilePath, jsonLine);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
getskippedUpload() {
|
|
174
|
+
return this.skippedUpload;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async uploadFileByPath(filePath, pathInS3) {
|
|
178
|
+
const [runId, rid] = pathInS3;
|
|
179
|
+
|
|
180
|
+
if (!this.isEnabled) {
|
|
181
|
+
this.storeUploadedFile(filePath, runId, rid, false);
|
|
182
|
+
this.skippedUpload++;
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const {
|
|
187
|
+
S3_BUCKET,
|
|
188
|
+
TESTOMATIO_ARTIFACTS_SIZE,
|
|
189
|
+
} = this.getConfig();
|
|
190
|
+
|
|
191
|
+
debug('Started upload', filePath, 'to', S3_BUCKET);
|
|
192
|
+
|
|
193
|
+
const isFileExist = await this.checkFileExists(filePath, 20, 500);
|
|
194
|
+
|
|
195
|
+
if (!isFileExist) {
|
|
196
|
+
this.failedUpload++;
|
|
197
|
+
console.error(chalk.yellow(`Artifacts file ${filePath} does not exist. Skipping...`));
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const fileSize = fs.statSync(filePath).size;
|
|
202
|
+
const fileSizeInMb = fileSize / (1024 * 1024);
|
|
203
|
+
|
|
204
|
+
if (TESTOMATIO_ARTIFACTS_SIZE && fileSizeInMb > parseInt(TESTOMATIO_ARTIFACTS_SIZE)) {
|
|
205
|
+
this.skippedUpload++;
|
|
206
|
+
console.error(chalk.yellow(`Artifacts file ${filePath} exceeds the maximum allowed size. Skipping...`));
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
debug('File:', filePath, 'exists, size:', fileSizeInMb.toFixed(2), 'MB');
|
|
210
|
+
|
|
211
|
+
const fileStream = fs.createReadStream(filePath);
|
|
212
|
+
const Key = pathInS3.join('/');
|
|
213
|
+
|
|
214
|
+
const link = await this.uploadToS3(fileStream, Key);
|
|
215
|
+
|
|
216
|
+
this.storeUploadedFile(filePath, runId, rid, !!link);
|
|
217
|
+
|
|
218
|
+
return link;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async uploadFileAsBuffer(buffer, pathInS3) {
|
|
222
|
+
if (!this.isEnabled) return;
|
|
223
|
+
|
|
224
|
+
let Key = pathInS3.join('/');
|
|
225
|
+
const ext = this.#getFileExtBase64(buffer);
|
|
226
|
+
|
|
227
|
+
if (ext) {
|
|
228
|
+
Key = `${Key}.${ext}`;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return this.uploadToS3(buffer, Key);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async checkFileExists(filePath, attempts = 5, intervalMs = 500) {
|
|
235
|
+
return promiseRetry(
|
|
236
|
+
async (retry, number) => {
|
|
237
|
+
try {
|
|
238
|
+
fs.accessSync(filePath);
|
|
239
|
+
return true;
|
|
240
|
+
} catch (err) {
|
|
241
|
+
if (number === attempts) {
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
debug(`File not found, retrying (attempt ${number}/${attempts})`);
|
|
245
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
246
|
+
retry(err);
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
retries: attempts,
|
|
251
|
+
minTimeout: intervalMs,
|
|
252
|
+
maxTimeout: intervalMs,
|
|
253
|
+
}
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async getS3LocationLink(out) {
|
|
258
|
+
const response = await out.done();
|
|
259
|
+
|
|
260
|
+
let s3Location = response?.Location;
|
|
261
|
+
|
|
262
|
+
if (!s3Location) {
|
|
263
|
+
s3Location = out?.singleUploadResult?.Location;
|
|
264
|
+
debug('Uploaded singleUploadResult.Location', s3Location);
|
|
265
|
+
|
|
266
|
+
if (!s3Location) {
|
|
267
|
+
throw new Error("Problems getting the S3 artifact's link. Please check S3 permissions!");
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return s3Location;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
#getFileExtBase64(str) {
|
|
275
|
+
const type = str.charAt(0);
|
|
276
|
+
|
|
277
|
+
return (
|
|
278
|
+
{
|
|
279
|
+
'/': 'jpg',
|
|
280
|
+
i: 'png',
|
|
281
|
+
R: 'gif',
|
|
282
|
+
U: 'webp',
|
|
283
|
+
}[type] || ''
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
getS3Config() {
|
|
288
|
+
const { S3_REGION, S3_SESSION_TOKEN, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, S3_FORCE_PATH_STYLE, S3_ENDPOINT } =
|
|
289
|
+
this.getConfig();
|
|
290
|
+
|
|
291
|
+
const cfg = {
|
|
292
|
+
region: S3_REGION,
|
|
293
|
+
credentials: {
|
|
294
|
+
accessKeyId: S3_ACCESS_KEY_ID,
|
|
295
|
+
secretAccessKey: S3_SECRET_ACCESS_KEY,
|
|
296
|
+
s3ForcePathStyle: S3_FORCE_PATH_STYLE,
|
|
297
|
+
},
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
if (S3_SESSION_TOKEN) {
|
|
301
|
+
cfg.credentials.sessionToken = S3_SESSION_TOKEN;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (S3_ENDPOINT) {
|
|
305
|
+
cfg.endpoint = S3_ENDPOINT;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return cfg;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
module.exports = S3Uploader;
|
package/lib/utils/pipe_utils.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
const { resetConfig } = require('../fileUploader');
|
|
2
1
|
const { APP_PREFIX } = require('../constants');
|
|
3
2
|
|
|
4
3
|
/**
|
|
@@ -17,7 +16,6 @@ function setS3Credentials(artifacts) {
|
|
|
17
16
|
if (artifacts.ENDPOINT) process.env.S3_ENDPOINT = artifacts.ENDPOINT;
|
|
18
17
|
if (artifacts.SESSION_TOKEN) process.env.S3_SESSION_TOKEN = artifacts.SESSION_TOKEN;
|
|
19
18
|
if (artifacts.presign) process.env.TESTOMATIO_PRIVATE_ARTIFACTS = '1';
|
|
20
|
-
resetConfig();
|
|
21
19
|
}
|
|
22
20
|
/**
|
|
23
21
|
* Generates mode request parameters based on the input params.
|
package/lib/xmlReader.js
CHANGED
|
@@ -13,7 +13,7 @@ const {
|
|
|
13
13
|
fetchIdFromCode,
|
|
14
14
|
humanize,
|
|
15
15
|
} = require('./utils/utils');
|
|
16
|
-
const
|
|
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.
|
|
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 =>
|
|
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
|
-
|
|
401
|
+
await Promise.all(this.pipes.map(p => p.createRun(runParams)));
|
|
402
|
+
|
|
403
|
+
this.uploader.checkEnabled();
|
|
402
404
|
}
|
|
403
405
|
|
|
404
406
|
async uploadData() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@testomatio/reporter",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0-beta-2-artifacts",
|
|
4
4
|
"description": "Testomatio Reporter Client",
|
|
5
5
|
"main": "./lib/reporter.js",
|
|
6
6
|
"typings": "typings/index.d.ts",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"axios-retry": "^3.9.1",
|
|
17
17
|
"callsite-record": "^4.1.4",
|
|
18
18
|
"chalk": "^4.1.0",
|
|
19
|
-
"commander": "^
|
|
19
|
+
"commander": "^12",
|
|
20
20
|
"cross-spawn": "^7.0.3",
|
|
21
21
|
"csv-writer": "^1.6.0",
|
|
22
22
|
"debug": "^4.3.4",
|
|
@@ -81,7 +81,9 @@
|
|
|
81
81
|
"puppeteer": "^22.15.0"
|
|
82
82
|
},
|
|
83
83
|
"bin": {
|
|
84
|
+
"@testomatio/reporter": "./lib/bin/cli.js",
|
|
84
85
|
"report-xml": "./lib/bin/reportXml.js",
|
|
85
|
-
"start-test-run": "./lib/bin/startTest.js"
|
|
86
|
+
"start-test-run": "./lib/bin/startTest.js",
|
|
87
|
+
"upload-artifacts": "./lib/bin/uploadArtifacts.js"
|
|
86
88
|
}
|
|
87
89
|
}
|
package/lib/fileUploader.js
DELETED
|
@@ -1,306 +0,0 @@
|
|
|
1
|
-
const debug = require('debug')('@testomatio/reporter:file-uploader');
|
|
2
|
-
const { S3 } = require('@aws-sdk/client-s3');
|
|
3
|
-
const { Upload } = require('@aws-sdk/lib-storage');
|
|
4
|
-
|
|
5
|
-
const fs = require('fs');
|
|
6
|
-
const util = require('util');
|
|
7
|
-
const path = require('path');
|
|
8
|
-
const promiseRetry = require('promise-retry');
|
|
9
|
-
|
|
10
|
-
const readFile = util.promisify(fs.readFile);
|
|
11
|
-
const stat = util.promisify(fs.stat);
|
|
12
|
-
const chalk = require('chalk');
|
|
13
|
-
const { randomUUID } = require('crypto');
|
|
14
|
-
|
|
15
|
-
const { APP_PREFIX } = require('./constants');
|
|
16
|
-
|
|
17
|
-
const keys = [
|
|
18
|
-
'S3_ENDPOINT',
|
|
19
|
-
'S3_REGION',
|
|
20
|
-
'S3_BUCKET',
|
|
21
|
-
'S3_ACCESS_KEY_ID',
|
|
22
|
-
'S3_SECRET_ACCESS_KEY',
|
|
23
|
-
'S3_SESSION_TOKEN',
|
|
24
|
-
'TESTOMATIO_DISABLE_ARTIFACTS',
|
|
25
|
-
'TESTOMATIO_PRIVATE_ARTIFACTS',
|
|
26
|
-
'S3_FORCE_PATH_STYLE',
|
|
27
|
-
];
|
|
28
|
-
|
|
29
|
-
let config;
|
|
30
|
-
|
|
31
|
-
function resetConfig() {
|
|
32
|
-
config = undefined;
|
|
33
|
-
isEnabled = undefined;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function getConfig() {
|
|
37
|
-
if (config) return config;
|
|
38
|
-
config = keys.reduce((acc, key) => {
|
|
39
|
-
acc[key] = process.env[key];
|
|
40
|
-
return acc;
|
|
41
|
-
}, {});
|
|
42
|
-
return config;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function getMaskedConfig() {
|
|
46
|
-
return Object.fromEntries(
|
|
47
|
-
Object.entries(getConfig()).map(([key, value]) => [
|
|
48
|
-
key,
|
|
49
|
-
key === 'S3_SECRET_ACCESS_KEY' || key === 'S3_ACCESS_KEY_ID' ? '***' : value,
|
|
50
|
-
]),
|
|
51
|
-
);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
let isEnabled;
|
|
55
|
-
|
|
56
|
-
const isArtifactsEnabled = () => {
|
|
57
|
-
if (isEnabled !== undefined) return isEnabled;
|
|
58
|
-
const { S3_BUCKET, TESTOMATIO_DISABLE_ARTIFACTS } = getConfig();
|
|
59
|
-
isEnabled = !!(S3_BUCKET && !TESTOMATIO_DISABLE_ARTIFACTS);
|
|
60
|
-
debug(`Upload is ${isEnabled ? 'enabled' : 'disabled'}`);
|
|
61
|
-
return isEnabled;
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
const _getFileExtBase64 = str => {
|
|
65
|
-
const type = str.charAt(0);
|
|
66
|
-
|
|
67
|
-
return (
|
|
68
|
-
{
|
|
69
|
-
'/': '.jpg',
|
|
70
|
-
i: '.png',
|
|
71
|
-
R: '.gif',
|
|
72
|
-
U: '.webp',
|
|
73
|
-
}[type] || ''
|
|
74
|
-
);
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
const _getS3Config = () => {
|
|
78
|
-
const { S3_REGION, S3_SESSION_TOKEN, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, S3_FORCE_PATH_STYLE, S3_ENDPOINT } =
|
|
79
|
-
getConfig();
|
|
80
|
-
|
|
81
|
-
const cfg = {
|
|
82
|
-
region: S3_REGION,
|
|
83
|
-
credentials: {
|
|
84
|
-
accessKeyId: S3_ACCESS_KEY_ID,
|
|
85
|
-
secretAccessKey: S3_SECRET_ACCESS_KEY,
|
|
86
|
-
s3ForcePathStyle: S3_FORCE_PATH_STYLE,
|
|
87
|
-
},
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
if (S3_SESSION_TOKEN) {
|
|
91
|
-
cfg.credentials.sessionToken = S3_SESSION_TOKEN;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (S3_ENDPOINT) {
|
|
95
|
-
cfg.endpoint = S3_ENDPOINT;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
return cfg;
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
const uploadUsingS3 = async (filePath, runId) => {
|
|
102
|
-
let ContentType;
|
|
103
|
-
let Key;
|
|
104
|
-
|
|
105
|
-
if (typeof filePath === 'object') {
|
|
106
|
-
ContentType = filePath?.type;
|
|
107
|
-
filePath = filePath?.path;
|
|
108
|
-
Key = filePath?.name;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const { TESTOMATIO_PRIVATE_ARTIFACTS, S3_BUCKET } = getConfig();
|
|
112
|
-
|
|
113
|
-
try {
|
|
114
|
-
debug('S3 config', getMaskedConfig());
|
|
115
|
-
debug('Started upload', filePath, 'to ', S3_BUCKET);
|
|
116
|
-
|
|
117
|
-
// Verification that the file was actually created: 20 attempts of 0.5 second => 10sec
|
|
118
|
-
const isFileExist = await checkFileExists(filePath, 20, 500);
|
|
119
|
-
|
|
120
|
-
if (!isFileExist) {
|
|
121
|
-
console.error(chalk.yellow(`Artifacts file ${filePath} does not exist. Skipping...`));
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
debug('File: ', filePath, ' exists');
|
|
126
|
-
|
|
127
|
-
const fileData = await readFile(filePath);
|
|
128
|
-
|
|
129
|
-
Key = `${runId}/${randomUUID()}-${Key || path.basename(filePath)}`;
|
|
130
|
-
|
|
131
|
-
const ACL = TESTOMATIO_PRIVATE_ARTIFACTS ? 'private' : 'public-read';
|
|
132
|
-
|
|
133
|
-
if (!S3_BUCKET || !fileData) {
|
|
134
|
-
console.log(
|
|
135
|
-
APP_PREFIX,
|
|
136
|
-
chalk.bold.red(`Failed uploading '${Key}'. Please check S3 credentials`),
|
|
137
|
-
getMaskedConfig(),
|
|
138
|
-
);
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const s3 = new S3(_getS3Config());
|
|
143
|
-
|
|
144
|
-
const params = {
|
|
145
|
-
Bucket: S3_BUCKET,
|
|
146
|
-
Key,
|
|
147
|
-
Body: fileData,
|
|
148
|
-
ContentType,
|
|
149
|
-
ACL,
|
|
150
|
-
};
|
|
151
|
-
|
|
152
|
-
const out = new Upload({
|
|
153
|
-
client: s3,
|
|
154
|
-
params,
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
const link = await getS3LocationLink(out);
|
|
158
|
-
|
|
159
|
-
debug(`Succesfully uploaded ${filePath} => ${S3_BUCKET}/${Key} | URL: ${link}`);
|
|
160
|
-
|
|
161
|
-
return link;
|
|
162
|
-
} catch (e) {
|
|
163
|
-
debug('S3 file uploading error: ', e);
|
|
164
|
-
|
|
165
|
-
console.log(APP_PREFIX, `To ${chalk.bold('disable')} artifact uploads set: TESTOMATIO_DISABLE_ARTIFACTS=1`);
|
|
166
|
-
|
|
167
|
-
if (!TESTOMATIO_PRIVATE_ARTIFACTS) {
|
|
168
|
-
console.log(APP_PREFIX, `To enable ${chalk.bold('PRIVATE')} uploads set: TESTOMATIO_PRIVATE_ARTIFACTS=1`);
|
|
169
|
-
} else {
|
|
170
|
-
console.log(
|
|
171
|
-
APP_PREFIX,
|
|
172
|
-
`To enable ${chalk.bold('PUBLIC')} uploads remove TESTOMATIO_PRIVATE_ARTIFACTS env variable`,
|
|
173
|
-
);
|
|
174
|
-
}
|
|
175
|
-
console.log(APP_PREFIX, '---------------');
|
|
176
|
-
}
|
|
177
|
-
};
|
|
178
|
-
|
|
179
|
-
const uploadUsingS3AsBuffer = async (buffer, fileName, runId) => {
|
|
180
|
-
const { S3_REGION, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, S3_ENDPOINT, TESTOMATIO_PRIVATE_ARTIFACTS, S3_BUCKET } =
|
|
181
|
-
getConfig();
|
|
182
|
-
|
|
183
|
-
const ACL = TESTOMATIO_PRIVATE_ARTIFACTS ? 'private' : 'public-read';
|
|
184
|
-
|
|
185
|
-
const fileExtension = _getFileExtBase64(buffer.toString('base64'));
|
|
186
|
-
const Key = `${runId}/${fileName}${fileExtension}`;
|
|
187
|
-
|
|
188
|
-
if (!S3_BUCKET || !buffer) {
|
|
189
|
-
console.log(APP_PREFIX, chalk.bold.red(`Failed uploading '${Key}'. Please check S3 credentials`), {
|
|
190
|
-
accessKeyId: S3_ACCESS_KEY_ID,
|
|
191
|
-
secretAccessKey: S3_SECRET_ACCESS_KEY ? '**** (hidden) ***' : '(empty)',
|
|
192
|
-
region: S3_REGION,
|
|
193
|
-
bucket: S3_BUCKET,
|
|
194
|
-
acl: ACL,
|
|
195
|
-
endpoint: S3_ENDPOINT,
|
|
196
|
-
});
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
const s3 = new S3(_getS3Config());
|
|
201
|
-
|
|
202
|
-
try {
|
|
203
|
-
const out = new Upload({
|
|
204
|
-
client: s3,
|
|
205
|
-
|
|
206
|
-
params: {
|
|
207
|
-
Bucket: S3_BUCKET,
|
|
208
|
-
Key,
|
|
209
|
-
Body: buffer,
|
|
210
|
-
ACL,
|
|
211
|
-
},
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
return await getS3LocationLink(out);
|
|
215
|
-
} catch (e) {
|
|
216
|
-
debug('S3 buffer uploading error: ', e);
|
|
217
|
-
|
|
218
|
-
console.log(APP_PREFIX, `To ${chalk.bold('disable')} artifact uploads set: TESTOMATIO_DISABLE_ARTIFACTS=1`);
|
|
219
|
-
|
|
220
|
-
if (!TESTOMATIO_PRIVATE_ARTIFACTS) {
|
|
221
|
-
console.log(APP_PREFIX, `To enable ${chalk.bold('PRIVATE')} uploads set: TESTOMATIO_PRIVATE_ARTIFACTS=1`);
|
|
222
|
-
} else {
|
|
223
|
-
console.log(
|
|
224
|
-
APP_PREFIX,
|
|
225
|
-
`To enable ${chalk.bold('PUBLIC')} uploads remove TESTOMATIO_PRIVATE_ARTIFACTS env variable`,
|
|
226
|
-
);
|
|
227
|
-
}
|
|
228
|
-
console.log(APP_PREFIX, '---------------');
|
|
229
|
-
}
|
|
230
|
-
};
|
|
231
|
-
|
|
232
|
-
const uploadFileByPath = async (filePath, runId) => {
|
|
233
|
-
try {
|
|
234
|
-
if (isArtifactsEnabled()) {
|
|
235
|
-
return uploadUsingS3(filePath, runId);
|
|
236
|
-
}
|
|
237
|
-
} catch (e) {
|
|
238
|
-
debug(e);
|
|
239
|
-
|
|
240
|
-
console.error(chalk.red('Error occurred while uploading artifacts! '), e);
|
|
241
|
-
}
|
|
242
|
-
};
|
|
243
|
-
|
|
244
|
-
const uploadFileAsBuffer = async (buffer, fileName, runId) => {
|
|
245
|
-
try {
|
|
246
|
-
if (isArtifactsEnabled()) {
|
|
247
|
-
return uploadUsingS3AsBuffer(buffer, fileName, runId);
|
|
248
|
-
}
|
|
249
|
-
} catch (e) {
|
|
250
|
-
debug(e);
|
|
251
|
-
|
|
252
|
-
console.error(chalk.red('Error occurred while uploading artifacts! '), e);
|
|
253
|
-
}
|
|
254
|
-
};
|
|
255
|
-
|
|
256
|
-
const checkFileExists = async (filePath, attempts = 5, intervalMs = 500) => {
|
|
257
|
-
const checkFile = async () => {
|
|
258
|
-
const fileStats = await stat(filePath);
|
|
259
|
-
if (fileStats.isFile()) {
|
|
260
|
-
return true;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
throw new Error('File not found');
|
|
264
|
-
};
|
|
265
|
-
|
|
266
|
-
try {
|
|
267
|
-
await promiseRetry(
|
|
268
|
-
{
|
|
269
|
-
retries: attempts,
|
|
270
|
-
minTimeout: intervalMs,
|
|
271
|
-
},
|
|
272
|
-
checkFile,
|
|
273
|
-
);
|
|
274
|
-
|
|
275
|
-
return true;
|
|
276
|
-
} catch (err) {
|
|
277
|
-
console.error(chalk.yellow(`File ${filePath} was not found or did not have time to be generated...`));
|
|
278
|
-
|
|
279
|
-
return false;
|
|
280
|
-
}
|
|
281
|
-
};
|
|
282
|
-
|
|
283
|
-
const getS3LocationLink = async out => {
|
|
284
|
-
const response = await out.done();
|
|
285
|
-
|
|
286
|
-
let s3Location = response?.Location;
|
|
287
|
-
|
|
288
|
-
if (!s3Location) {
|
|
289
|
-
// TODO: out: a fallback case - remove after deeper testing
|
|
290
|
-
s3Location = out?.singleUploadResult?.Location;
|
|
291
|
-
debug('Uploaded singleUploadResult.Location', s3Location);
|
|
292
|
-
|
|
293
|
-
if (!s3Location) {
|
|
294
|
-
throw new Error("Problems getting the S3 artifact's link. Please check S3 permissions!");
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
return s3Location;
|
|
299
|
-
};
|
|
300
|
-
|
|
301
|
-
module.exports = {
|
|
302
|
-
uploadFileByPath,
|
|
303
|
-
uploadFileAsBuffer,
|
|
304
|
-
isArtifactsEnabled,
|
|
305
|
-
resetConfig,
|
|
306
|
-
};
|