@testomatio/reporter 2.0.0-beta-esm → 2.0.1-beta-esm
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.d.ts +2 -0
- package/lib/adapter/codecept.js +31 -24
- package/lib/adapter/cucumber/current.d.ts +14 -0
- package/lib/adapter/cucumber/legacy.d.ts +0 -0
- package/lib/adapter/cucumber.d.ts +2 -0
- package/lib/adapter/cypress-plugin/index.d.ts +2 -0
- package/lib/adapter/cypress-plugin/index.js +11 -9
- package/lib/adapter/jasmine.d.ts +11 -0
- package/lib/adapter/jest.d.ts +13 -0
- package/lib/adapter/mocha.d.ts +2 -0
- package/lib/adapter/mocha.js +4 -3
- package/lib/adapter/playwright.d.ts +14 -0
- package/lib/adapter/playwright.js +58 -33
- package/lib/adapter/vitest.d.ts +35 -0
- package/lib/adapter/vitest.js +6 -6
- package/lib/adapter/webdriver.d.ts +24 -0
- package/lib/adapter/webdriver.js +34 -6
- package/lib/bin/cli.d.ts +2 -0
- package/lib/bin/cli.js +228 -0
- package/lib/bin/reportXml.d.ts +2 -0
- package/lib/bin/reportXml.js +11 -9
- package/lib/bin/startTest.d.ts +2 -0
- package/lib/bin/startTest.js +9 -5
- package/lib/bin/uploadArtifacts.d.ts +2 -0
- package/lib/bin/uploadArtifacts.js +81 -0
- package/lib/client.d.ts +76 -0
- package/lib/client.js +111 -45
- package/lib/config.d.ts +1 -0
- package/lib/constants.d.ts +25 -0
- package/lib/constants.js +5 -1
- package/lib/data-storage.d.ts +34 -0
- package/lib/data-storage.js +2 -2
- package/lib/junit-adapter/adapter.d.ts +9 -0
- package/lib/junit-adapter/csharp.d.ts +4 -0
- package/lib/junit-adapter/index.d.ts +3 -0
- package/lib/junit-adapter/java.d.ts +5 -0
- package/lib/junit-adapter/javascript.d.ts +4 -0
- package/lib/junit-adapter/python.d.ts +5 -0
- package/lib/junit-adapter/ruby.d.ts +4 -0
- package/lib/output.d.ts +11 -0
- package/lib/package.json +3 -1
- package/lib/pipe/bitbucket.d.ts +23 -0
- package/lib/pipe/bitbucket.js +2 -2
- package/lib/pipe/csv.d.ts +47 -0
- package/lib/pipe/csv.js +2 -2
- package/lib/pipe/debug.d.ts +29 -0
- package/lib/pipe/debug.js +108 -0
- package/lib/pipe/github.d.ts +30 -0
- package/lib/pipe/github.js +2 -2
- package/lib/pipe/gitlab.d.ts +23 -0
- package/lib/pipe/gitlab.js +2 -2
- package/lib/pipe/html.d.ts +34 -0
- package/lib/pipe/html.js +8 -1
- package/lib/pipe/index.d.ts +1 -0
- package/lib/pipe/index.js +3 -3
- package/lib/pipe/testomatio.d.ts +70 -0
- package/lib/pipe/testomatio.js +50 -30
- package/lib/reporter-functions.d.ts +34 -0
- package/lib/reporter-functions.js +17 -7
- package/lib/reporter.d.ts +232 -0
- package/lib/reporter.js +19 -33
- package/lib/services/artifacts.d.ts +33 -0
- package/lib/services/index.d.ts +9 -0
- package/lib/services/key-values.d.ts +27 -0
- package/lib/services/key-values.js +1 -1
- package/lib/services/logger.d.ts +64 -0
- package/lib/template/testomatio.hbs +651 -1366
- package/lib/uploader.d.ts +60 -0
- package/lib/uploader.js +312 -0
- package/lib/utils/pipe_utils.d.ts +41 -0
- package/lib/utils/pipe_utils.js +3 -5
- package/lib/utils/utils.d.ts +45 -0
- package/lib/utils/utils.js +69 -2
- package/lib/xmlReader.d.ts +92 -0
- package/lib/xmlReader.js +22 -12
- package/package.json +15 -9
- package/src/adapter/codecept.js +30 -24
- package/src/adapter/cypress-plugin/index.js +5 -3
- package/src/adapter/mocha.cjs +1 -1
- package/src/adapter/mocha.js +4 -3
- package/src/adapter/playwright.js +59 -31
- package/src/adapter/vitest.js +6 -6
- package/src/adapter/webdriver.js +41 -10
- package/src/bin/cli.js +280 -0
- package/src/bin/reportXml.js +15 -8
- package/src/bin/startTest.js +7 -3
- package/src/bin/uploadArtifacts.js +90 -0
- package/src/client.js +137 -56
- package/src/constants.js +5 -1
- package/src/data-storage.js +2 -2
- package/src/pipe/bitbucket.js +2 -2
- package/src/pipe/csv.js +3 -3
- package/src/pipe/debug.js +104 -0
- package/src/pipe/github.js +2 -3
- package/src/pipe/gitlab.js +6 -6
- package/src/pipe/html.js +11 -3
- package/src/pipe/index.js +5 -7
- package/src/pipe/testomatio.js +72 -67
- package/src/reporter-functions.js +18 -7
- package/src/reporter.cjs_decprecated +21 -0
- package/src/reporter.js +20 -11
- package/src/services/key-values.js +1 -1
- package/src/services/logger.js +4 -2
- package/src/template/testomatio.hbs +651 -1366
- package/src/uploader.js +371 -0
- package/src/utils/pipe_utils.js +4 -12
- package/src/utils/utils.js +48 -6
- package/src/xmlReader.js +26 -15
- package/lib/adapter/jasmine/jasmine.js +0 -63
- package/lib/adapter/mocha/mocha.js +0 -125
- package/lib/fileUploader.js +0 -245
- package/lib/utils/chalk.js +0 -10
- package/src/fileUploader.js +0 -307
- package/src/reporter.cjs +0 -22
- package/src/utils/chalk.js +0 -13
package/src/uploader.js
ADDED
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
import createDebugMessages from 'debug';
|
|
2
|
+
import { S3 } from '@aws-sdk/client-s3';
|
|
3
|
+
import { Upload } from '@aws-sdk/lib-storage';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import os from 'os';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import promiseRetry from 'promise-retry';
|
|
8
|
+
import pc from 'picocolors';
|
|
9
|
+
import { APP_PREFIX } from './constants.js';
|
|
10
|
+
import { filesize as prettyBytes } from 'filesize';
|
|
11
|
+
|
|
12
|
+
const debug = createDebugMessages('@testomatio/reporter:file-uploader');
|
|
13
|
+
|
|
14
|
+
export class S3Uploader {
|
|
15
|
+
constructor() {
|
|
16
|
+
this.isEnabled = undefined;
|
|
17
|
+
this.storeEnabled = true;
|
|
18
|
+
this.config = undefined;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @type {{path: string, size: number}[]}
|
|
22
|
+
*/
|
|
23
|
+
this.skippedUploads = [];
|
|
24
|
+
this.failedUploads = [];
|
|
25
|
+
/**
|
|
26
|
+
* @type {{path: string, size: number, link: string}[]}
|
|
27
|
+
*/
|
|
28
|
+
this.successfulUploads = [];
|
|
29
|
+
|
|
30
|
+
this.configKeys = [
|
|
31
|
+
'S3_ENDPOINT',
|
|
32
|
+
'S3_REGION',
|
|
33
|
+
'S3_BUCKET',
|
|
34
|
+
'S3_ACCESS_KEY_ID',
|
|
35
|
+
'S3_SECRET_ACCESS_KEY',
|
|
36
|
+
'S3_SESSION_TOKEN',
|
|
37
|
+
'S3_FORCE_PATH_STYLE',
|
|
38
|
+
'TESTOMATIO_DISABLE_ARTIFACTS',
|
|
39
|
+
'TESTOMATIO_PRIVATE_ARTIFACTS',
|
|
40
|
+
'TESTOMATIO_ARTIFACT_MAX_SIZE_MB',
|
|
41
|
+
];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
resetConfig() {
|
|
45
|
+
this.config = undefined;
|
|
46
|
+
this.isEnabled = undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
*
|
|
51
|
+
* @returns {Record<string, string>}
|
|
52
|
+
*/
|
|
53
|
+
getConfig() {
|
|
54
|
+
if (this.config) return this.config;
|
|
55
|
+
this.config = this.configKeys.reduce((acc, key) => {
|
|
56
|
+
acc[key] = process.env[key];
|
|
57
|
+
return acc;
|
|
58
|
+
}, {});
|
|
59
|
+
return this.config;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
getMaskedConfig() {
|
|
63
|
+
return Object.fromEntries(
|
|
64
|
+
Object.entries(this.getConfig()).map(([key, value]) => [
|
|
65
|
+
key,
|
|
66
|
+
key === 'S3_SECRET_ACCESS_KEY' || key === 'S3_ACCESS_KEY_ID' ? '***' : value,
|
|
67
|
+
]),
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
checkEnabled() {
|
|
72
|
+
if (this.isEnabled !== undefined) return this.isEnabled;
|
|
73
|
+
|
|
74
|
+
const { S3_BUCKET, TESTOMATIO_DISABLE_ARTIFACTS } = this.getConfig();
|
|
75
|
+
if (!S3_BUCKET) debug(`Artifacts uploading is disabled because S3_BUCKET is not set`);
|
|
76
|
+
this.isEnabled = !!(S3_BUCKET && !TESTOMATIO_DISABLE_ARTIFACTS);
|
|
77
|
+
|
|
78
|
+
if (this.isEnabled) debug('S3 uploader is enabled');
|
|
79
|
+
debug(this.getMaskedConfig());
|
|
80
|
+
|
|
81
|
+
return this.isEnabled;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
enableLogStorage() {
|
|
85
|
+
this.storeEnabled = true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
disableLogStorage() {
|
|
89
|
+
this.storeEnabled = false;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
*
|
|
94
|
+
* @param {*} Body
|
|
95
|
+
* @param {*} Key
|
|
96
|
+
* @param {{path: string, size?: number}} file
|
|
97
|
+
* @returns
|
|
98
|
+
*/
|
|
99
|
+
async #uploadToS3(Body, Key, file) {
|
|
100
|
+
const { S3_BUCKET, TESTOMATIO_PRIVATE_ARTIFACTS } = this.getConfig();
|
|
101
|
+
const ACL = TESTOMATIO_PRIVATE_ARTIFACTS ? 'private' : 'public-read';
|
|
102
|
+
|
|
103
|
+
if (!S3_BUCKET || !Body) {
|
|
104
|
+
console.log(
|
|
105
|
+
APP_PREFIX,
|
|
106
|
+
pc.bold(pc.red(`Failed uploading '${Key}'. Please check S3 credentials`)),
|
|
107
|
+
this.getMaskedConfig(),
|
|
108
|
+
);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
debug('Uploading to S3:', Key);
|
|
113
|
+
|
|
114
|
+
const s3Config = this.#getS3Config();
|
|
115
|
+
const s3 = new S3(s3Config);
|
|
116
|
+
const params = {
|
|
117
|
+
Bucket: S3_BUCKET,
|
|
118
|
+
Key,
|
|
119
|
+
Body,
|
|
120
|
+
};
|
|
121
|
+
// disable ACL for I AM roles
|
|
122
|
+
if (!s3Config.credentials.sessionToken) {
|
|
123
|
+
params.ACL = ACL;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
const upload = new Upload({ client: s3, params });
|
|
128
|
+
|
|
129
|
+
const link = await this.getS3LocationLink(upload);
|
|
130
|
+
this.successfulUploads.push({ path: file.path, size: file.size, link });
|
|
131
|
+
debug(`📤 Uploaded artifact. File: ${file.path}, size: ${prettyBytes(file.size)}, link: ${link}`);
|
|
132
|
+
return link;
|
|
133
|
+
} catch (e) {
|
|
134
|
+
this.failedUploads.push({ path: file.path, size: file.size });
|
|
135
|
+
debug('S3 uploading error:', e);
|
|
136
|
+
console.log(APP_PREFIX, 'Upload failed:', e.message, '\nConfig:\n', this.getMaskedConfig());
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Returns an array of uploaded files
|
|
142
|
+
*
|
|
143
|
+
* @returns {{rid: string, file: string, uploaded: boolean}[]}
|
|
144
|
+
*/
|
|
145
|
+
readUploadedFiles(runId) {
|
|
146
|
+
const tempFilePath = this.#getFilePathWithUploadsList(runId);
|
|
147
|
+
|
|
148
|
+
debug('Reading file', tempFilePath);
|
|
149
|
+
|
|
150
|
+
if (!fs.existsSync(tempFilePath)) {
|
|
151
|
+
debug('File not found:', tempFilePath);
|
|
152
|
+
return [];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const stats = fs.statSync(tempFilePath);
|
|
156
|
+
debug('Artifacts file stats:', +stats.mtime);
|
|
157
|
+
debug('Current time:', +new Date());
|
|
158
|
+
const diff = +new Date() - +stats.mtime;
|
|
159
|
+
debug('Diff:', diff);
|
|
160
|
+
const diffHours = diff / 1000 / 60 / 60;
|
|
161
|
+
debug('Diff hours:', diffHours);
|
|
162
|
+
if (diffHours > 3) {
|
|
163
|
+
console.log(APP_PREFIX, "Artifacts file is too old, can't process artifacts. Please re-run the tests.");
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const data = fs.readFileSync(tempFilePath, 'utf8');
|
|
168
|
+
debug('Artifacts file contents:', data);
|
|
169
|
+
const lines = data.split('\n').filter(Boolean);
|
|
170
|
+
return lines.map(line => JSON.parse(line));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
#getFilePathWithUploadsList(runId) {
|
|
174
|
+
const tempFilePath = path.join(os.tmpdir(), `testomatio.run.${runId}.json`);
|
|
175
|
+
if (!fs.existsSync(tempFilePath)) {
|
|
176
|
+
debug('Creating artifacts file:', tempFilePath);
|
|
177
|
+
fs.writeFileSync(tempFilePath, '');
|
|
178
|
+
}
|
|
179
|
+
return tempFilePath;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
storeUploadedFile(filePath, runId, rid, uploaded = false) {
|
|
183
|
+
if (!this.storeEnabled) return;
|
|
184
|
+
|
|
185
|
+
if (!filePath || !runId || !rid) return;
|
|
186
|
+
|
|
187
|
+
const tempFilePath = this.#getFilePathWithUploadsList(runId);
|
|
188
|
+
|
|
189
|
+
if (typeof filePath === 'object') {
|
|
190
|
+
filePath = filePath.path;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (typeof filePath === 'string' && !path.isAbsolute(filePath)) {
|
|
194
|
+
filePath = path.join(process.cwd(), filePath);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const data = { rid, file: filePath, uploaded };
|
|
198
|
+
const jsonLine = `${JSON.stringify(data)}\n`;
|
|
199
|
+
fs.appendFileSync(tempFilePath, jsonLine);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* @param {*} filePath
|
|
204
|
+
* @param {*} pathInS3 contains runId, rid and filename
|
|
205
|
+
* @returns
|
|
206
|
+
*/
|
|
207
|
+
async uploadFileByPath(filePath, pathInS3) {
|
|
208
|
+
// sometimes artifacts uploading started before createRun function completion
|
|
209
|
+
this.isEnabled = this.isEnabled ?? this.checkEnabled();
|
|
210
|
+
|
|
211
|
+
const [runId, rid] = pathInS3;
|
|
212
|
+
|
|
213
|
+
if (!filePath) return;
|
|
214
|
+
|
|
215
|
+
let fileSize = null;
|
|
216
|
+
let fileSizeInMb = null;
|
|
217
|
+
|
|
218
|
+
try {
|
|
219
|
+
// file may not exist
|
|
220
|
+
fileSize = fs.statSync(filePath).size;
|
|
221
|
+
fileSizeInMb = Number((fileSize / (1024 * 1024)).toFixed(2));
|
|
222
|
+
} catch (e) {
|
|
223
|
+
debug(`File ${filePath} does not exist`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (!this.isEnabled) {
|
|
227
|
+
this.storeUploadedFile(filePath, runId, rid, false);
|
|
228
|
+
this.skippedUploads.push({ path: filePath, size: fileSize });
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const { S3_BUCKET, TESTOMATIO_ARTIFACT_MAX_SIZE_MB } = this.getConfig();
|
|
233
|
+
|
|
234
|
+
debug('Started upload', filePath, 'to', S3_BUCKET);
|
|
235
|
+
|
|
236
|
+
const isFileExist = await this.checkArtifactExistsInFileSystem(filePath, 20, 500);
|
|
237
|
+
|
|
238
|
+
if (!isFileExist) {
|
|
239
|
+
console.error(pc.yellow(`Artifacts file ${filePath} does not exist. Skipping...`));
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// skipping artifact only if: 1. storing to file is enabled, 2. max size is set and 3. file size exceeds the limit
|
|
244
|
+
if (
|
|
245
|
+
this.storeEnabled &&
|
|
246
|
+
TESTOMATIO_ARTIFACT_MAX_SIZE_MB &&
|
|
247
|
+
fileSizeInMb > parseFloat(TESTOMATIO_ARTIFACT_MAX_SIZE_MB)
|
|
248
|
+
) {
|
|
249
|
+
const skippedArtifact = { path: filePath, size: fileSize };
|
|
250
|
+
this.storeUploadedFile(filePath, runId, rid, false);
|
|
251
|
+
this.skippedUploads.push(skippedArtifact);
|
|
252
|
+
debug(pc.yellow(`Artifacts file ${JSON.stringify(skippedArtifact)} exceeds the maximum allowed size. Skipping.`));
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
debug('File:', filePath, 'exists, size:', prettyBytes(fileSize));
|
|
256
|
+
|
|
257
|
+
const fileStream = fs.createReadStream(filePath);
|
|
258
|
+
const Key = pathInS3.join('/');
|
|
259
|
+
|
|
260
|
+
const link = await this.#uploadToS3(fileStream, Key, { path: filePath, size: fileSize });
|
|
261
|
+
|
|
262
|
+
this.storeUploadedFile(filePath, runId, rid, !!link);
|
|
263
|
+
|
|
264
|
+
return link;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* @param {Buffer} buffer
|
|
269
|
+
* @param {string[]} pathInS3
|
|
270
|
+
* @returns
|
|
271
|
+
*/
|
|
272
|
+
async uploadFileAsBuffer(buffer, pathInS3) {
|
|
273
|
+
if (!this.isEnabled) return;
|
|
274
|
+
|
|
275
|
+
let Key = pathInS3.join('/');
|
|
276
|
+
const ext = this.#getFileExtBase64(buffer);
|
|
277
|
+
|
|
278
|
+
if (ext) {
|
|
279
|
+
Key = `${Key}.${ext}`;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return this.#uploadToS3(buffer, Key, { path: Key });
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async checkArtifactExistsInFileSystem(filePath, attempts = 5, intervalMs = 500) {
|
|
286
|
+
return promiseRetry(
|
|
287
|
+
async (retry, number) => {
|
|
288
|
+
try {
|
|
289
|
+
fs.accessSync(filePath);
|
|
290
|
+
return true;
|
|
291
|
+
} catch (err) {
|
|
292
|
+
if (number === attempts) {
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
debug(`File not found, retrying (attempt ${number}/${attempts})`);
|
|
296
|
+
await new Promise(resolve => {
|
|
297
|
+
setTimeout(resolve, intervalMs);
|
|
298
|
+
});
|
|
299
|
+
retry(err);
|
|
300
|
+
}
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
retries: attempts,
|
|
304
|
+
minTimeout: intervalMs,
|
|
305
|
+
maxTimeout: intervalMs,
|
|
306
|
+
},
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async getS3LocationLink(out) {
|
|
311
|
+
const response = await out.done();
|
|
312
|
+
|
|
313
|
+
let s3Location = response?.Location?.trim();
|
|
314
|
+
|
|
315
|
+
if (!s3Location) {
|
|
316
|
+
s3Location = out?.singleUploadResult?.Location;
|
|
317
|
+
debug('Uploaded singleUploadResult.Location', s3Location);
|
|
318
|
+
|
|
319
|
+
if (!s3Location) {
|
|
320
|
+
throw new Error("Problems getting the S3 artifact's link. Please check S3 permissions!");
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Normalize the URL
|
|
325
|
+
if (!s3Location.startsWith('http')) {
|
|
326
|
+
s3Location = `https://${s3Location}`;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return s3Location;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
#getFileExtBase64(str) {
|
|
333
|
+
const type = str.charAt(0);
|
|
334
|
+
|
|
335
|
+
return (
|
|
336
|
+
{
|
|
337
|
+
'/': 'jpg',
|
|
338
|
+
i: 'png',
|
|
339
|
+
R: 'gif',
|
|
340
|
+
U: 'webp',
|
|
341
|
+
}[type] || ''
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
#getS3Config() {
|
|
346
|
+
const { S3_REGION, S3_SESSION_TOKEN, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, S3_FORCE_PATH_STYLE, S3_ENDPOINT } =
|
|
347
|
+
this.getConfig();
|
|
348
|
+
|
|
349
|
+
const cfg = {
|
|
350
|
+
region: S3_REGION,
|
|
351
|
+
credentials: {
|
|
352
|
+
accessKeyId: S3_ACCESS_KEY_ID,
|
|
353
|
+
secretAccessKey: S3_SECRET_ACCESS_KEY,
|
|
354
|
+
},
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
if (S3_FORCE_PATH_STYLE) {
|
|
358
|
+
cfg.forcePathStyle = !['false', '0'].includes(String(S3_FORCE_PATH_STYLE || '').toLowerCase());
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (S3_SESSION_TOKEN) {
|
|
362
|
+
cfg.credentials.sessionToken = S3_SESSION_TOKEN;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (S3_ENDPOINT) {
|
|
366
|
+
cfg.endpoint = S3_ENDPOINT;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return cfg;
|
|
370
|
+
}
|
|
371
|
+
}
|
package/src/utils/pipe_utils.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { upload } from '../fileUploader.js';
|
|
2
1
|
import { APP_PREFIX } from '../constants.js';
|
|
3
2
|
|
|
4
3
|
/**
|
|
@@ -8,16 +7,16 @@ import { APP_PREFIX } from '../constants.js';
|
|
|
8
7
|
function setS3Credentials(artifacts) {
|
|
9
8
|
if (!Object.keys(artifacts).length) return;
|
|
10
9
|
|
|
11
|
-
console.log(APP_PREFIX, 'S3
|
|
10
|
+
console.log(APP_PREFIX, 'S3 credentials obtained from Testomat.io...');
|
|
12
11
|
|
|
13
12
|
if (artifacts.ACCESS_KEY_ID) process.env.S3_ACCESS_KEY_ID = artifacts.ACCESS_KEY_ID;
|
|
14
13
|
if (artifacts.SECRET_ACCESS_KEY) process.env.S3_SECRET_ACCESS_KEY = artifacts.SECRET_ACCESS_KEY;
|
|
15
14
|
if (artifacts.REGION) process.env.S3_REGION = artifacts.REGION;
|
|
16
15
|
if (artifacts.BUCKET) process.env.S3_BUCKET = artifacts.BUCKET;
|
|
17
|
-
if (artifacts.ENDPOINT) process.env.S3_ENDPOINT = artifacts.ENDPOINT;
|
|
18
16
|
if (artifacts.SESSION_TOKEN) process.env.S3_SESSION_TOKEN = artifacts.SESSION_TOKEN;
|
|
19
17
|
if (artifacts.presign) process.env.TESTOMATIO_PRIVATE_ARTIFACTS = '1';
|
|
20
|
-
|
|
18
|
+
// endpoint is not received from the server; and shuld be empty if IAM used (credentails obtained from the testomat)
|
|
19
|
+
process.env.S3_ENDPOINT = artifacts.ENDPOINT || '';
|
|
21
20
|
}
|
|
22
21
|
|
|
23
22
|
/**
|
|
@@ -117,11 +116,4 @@ function fullName(t) {
|
|
|
117
116
|
return line;
|
|
118
117
|
}
|
|
119
118
|
|
|
120
|
-
export {
|
|
121
|
-
updateFilterType,
|
|
122
|
-
parseFilterParams,
|
|
123
|
-
generateFilterRequestParams,
|
|
124
|
-
setS3Credentials,
|
|
125
|
-
statusEmoji,
|
|
126
|
-
fullName,
|
|
127
|
-
};
|
|
119
|
+
export { updateFilterType, parseFilterParams, generateFilterRequestParams, setS3Credentials, statusEmoji, fullName };
|
package/src/utils/utils.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { URL } from 'url';
|
|
2
|
-
import { sep, basename } from 'path';
|
|
2
|
+
import path, { sep, basename } from 'path';
|
|
3
3
|
import pc from 'picocolors';
|
|
4
4
|
import fs from 'fs';
|
|
5
5
|
import isValid from 'is-valid-path';
|
|
6
6
|
import createDebugMessages from 'debug';
|
|
7
|
+
import os from 'os';
|
|
7
8
|
|
|
8
9
|
const debug = createDebugMessages('@testomatio/reporter:util');
|
|
9
10
|
|
|
@@ -320,7 +321,46 @@ const testRunnerHelper = {
|
|
|
320
321
|
},
|
|
321
322
|
};
|
|
322
323
|
|
|
324
|
+
function storeRunId(runId) {
|
|
325
|
+
if (!runId || runId === 'undefined') return;
|
|
326
|
+
const filePath = path.join(os.tmpdir(), `testomatio.latest.run`);
|
|
327
|
+
fs.writeFileSync(filePath, runId);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function readLatestRunId() {
|
|
331
|
+
try {
|
|
332
|
+
const filePath = path.join(os.tmpdir(), `testomatio.latest.run`);
|
|
333
|
+
const stats = fs.statSync(filePath);
|
|
334
|
+
const diff = +new Date() - +stats.mtime;
|
|
335
|
+
const diffHours = diff / 1000 / 60 / 60;
|
|
336
|
+
if (diffHours > 1) return;
|
|
337
|
+
|
|
338
|
+
return fs.readFileSync(filePath)?.toString()?.trim();
|
|
339
|
+
} catch (e) {
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function formatStep(step, shift = 0) {
|
|
345
|
+
const prefix = ' '.repeat(shift);
|
|
346
|
+
|
|
347
|
+
const lines = [];
|
|
348
|
+
|
|
349
|
+
if (step.error) {
|
|
350
|
+
lines.push(`${prefix}${pc.red(step.title)} ${pc.gray(`${step.duration}ms`)}`);
|
|
351
|
+
} else {
|
|
352
|
+
lines.push(`${prefix}${step.title} ${pc.gray(`${step.duration}ms`)}`);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
for (const child of step.steps || []) {
|
|
356
|
+
lines.push(...formatStep(child, shift + 2));
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return lines;
|
|
360
|
+
}
|
|
361
|
+
|
|
323
362
|
export {
|
|
363
|
+
ansiRegExp,
|
|
324
364
|
isSameTest,
|
|
325
365
|
fetchSourceCode,
|
|
326
366
|
fetchSourceCodeFromStackTrace,
|
|
@@ -328,14 +368,16 @@ export {
|
|
|
328
368
|
fetchIdFromOutput,
|
|
329
369
|
fetchFilesFromStackTrace,
|
|
330
370
|
fileSystem,
|
|
371
|
+
foundedTestLog,
|
|
372
|
+
formatStep,
|
|
331
373
|
getCurrentDateTime,
|
|
332
|
-
specificTestInfo,
|
|
333
|
-
isValidUrl,
|
|
334
|
-
ansiRegExp,
|
|
335
374
|
getTestomatIdFromTestTitle,
|
|
336
|
-
parseSuite,
|
|
337
375
|
humanize,
|
|
376
|
+
isValidUrl,
|
|
377
|
+
parseSuite,
|
|
378
|
+
readLatestRunId,
|
|
338
379
|
removeColorCodes,
|
|
339
|
-
|
|
380
|
+
specificTestInfo,
|
|
381
|
+
storeRunId,
|
|
340
382
|
testRunnerHelper,
|
|
341
383
|
};
|
package/src/xmlReader.js
CHANGED
|
@@ -14,10 +14,10 @@ import {
|
|
|
14
14
|
fetchIdFromCode,
|
|
15
15
|
humanize,
|
|
16
16
|
} from './utils/utils.js';
|
|
17
|
-
import {pipesFactory} from './pipe/index.js';
|
|
17
|
+
import { pipesFactory } from './pipe/index.js';
|
|
18
18
|
import adapterFactory from './junit-adapter/index.js';
|
|
19
|
-
import {config} from './config.js';
|
|
20
|
-
import {
|
|
19
|
+
import { config } from './config.js';
|
|
20
|
+
import { S3Uploader } from './uploader.js';
|
|
21
21
|
|
|
22
22
|
// @ts-ignore this line will be removed in compiled code, because __dirname is defined in commonjs
|
|
23
23
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -26,7 +26,8 @@ const debug = createDebugMessages('@testomatio/reporter:xml');
|
|
|
26
26
|
const ridRunId = randomUUID();
|
|
27
27
|
|
|
28
28
|
const TESTOMATIO_URL = process.env.TESTOMATIO_URL || 'https://app.testomat.io';
|
|
29
|
-
const { TESTOMATIO_RUNGROUP_TITLE, TESTOMATIO_TITLE, TESTOMATIO_ENV, TESTOMATIO_RUN } =
|
|
29
|
+
const { TESTOMATIO_RUNGROUP_TITLE, TESTOMATIO_TITLE, TESTOMATIO_ENV, TESTOMATIO_RUN, TESTOMATIO_MARK_DETACHED } =
|
|
30
|
+
process.env;
|
|
30
31
|
|
|
31
32
|
const options = {
|
|
32
33
|
ignoreDeclaration: true,
|
|
@@ -46,6 +47,7 @@ class XmlReader {
|
|
|
46
47
|
title: TESTOMATIO_TITLE,
|
|
47
48
|
env: TESTOMATIO_ENV,
|
|
48
49
|
group_title: TESTOMATIO_RUNGROUP_TITLE,
|
|
50
|
+
detach: TESTOMATIO_MARK_DETACHED,
|
|
49
51
|
// batch uploading is implemented for xml already
|
|
50
52
|
isBatchEnabled: false,
|
|
51
53
|
};
|
|
@@ -61,7 +63,7 @@ class XmlReader {
|
|
|
61
63
|
this.tests = [];
|
|
62
64
|
this.stats = {};
|
|
63
65
|
this.stats.language = opts.lang?.toLowerCase();
|
|
64
|
-
this.
|
|
66
|
+
this.uploader = new S3Uploader();
|
|
65
67
|
|
|
66
68
|
// @ts-ignore
|
|
67
69
|
const packageJsonPath = path.resolve(__dirname, '..', 'package.json');
|
|
@@ -122,17 +124,25 @@ class XmlReader {
|
|
|
122
124
|
const hasFailures = resultTests.filter(t => t.status === 'failed').length > 0;
|
|
123
125
|
const status = failures > 0 || errors > 0 || hasFailures ? 'failed' : 'passed';
|
|
124
126
|
|
|
127
|
+
const time = testsuite.time || 0;
|
|
128
|
+
// debug('time', jsonSuite, time)
|
|
129
|
+
if (time) {
|
|
130
|
+
if (!this.stats.duration) this.stats.duration = 0;
|
|
131
|
+
this.stats.duration += parseFloat(time);
|
|
132
|
+
}
|
|
133
|
+
|
|
125
134
|
this.tests = this.tests.concat(resultTests);
|
|
126
135
|
|
|
127
136
|
return {
|
|
128
|
-
status,
|
|
129
137
|
create_tests: true,
|
|
138
|
+
duration: parseFloat(time),
|
|
139
|
+
failed_count: parseInt(failures, 10),
|
|
130
140
|
name,
|
|
131
|
-
tests_count: parseInt(tests, 10),
|
|
132
141
|
passed_count: parseInt(tests, 10) - parseInt(failures, 10),
|
|
133
|
-
failed_count: parseInt(failures, 10),
|
|
134
142
|
skipped_count: 0,
|
|
143
|
+
status,
|
|
135
144
|
tests: resultTests,
|
|
145
|
+
tests_count: parseInt(tests, 10),
|
|
136
146
|
};
|
|
137
147
|
}
|
|
138
148
|
|
|
@@ -300,6 +310,7 @@ class XmlReader {
|
|
|
300
310
|
calculateStats() {
|
|
301
311
|
this.stats = {
|
|
302
312
|
...this.stats,
|
|
313
|
+
detach: this.requestParams.detach,
|
|
303
314
|
status: 'passed',
|
|
304
315
|
create_tests: true,
|
|
305
316
|
tests_count: 0,
|
|
@@ -389,7 +400,7 @@ class XmlReader {
|
|
|
389
400
|
if (!files.length) continue;
|
|
390
401
|
|
|
391
402
|
const runId = this.runId || this.store.runId || Date.now().toString();
|
|
392
|
-
test.artifacts = await Promise.all(files.map(f =>
|
|
403
|
+
test.artifacts = await Promise.all(files.map(f => this.uploader.uploadFileByPath(f, [runId])));
|
|
393
404
|
console.log(APP_PREFIX, `🗄️ Uploaded ${pc.bold(`${files.length} artifacts`)} for test ${test.title}`);
|
|
394
405
|
}
|
|
395
406
|
}
|
|
@@ -406,7 +417,9 @@ class XmlReader {
|
|
|
406
417
|
debug('Run', runParams);
|
|
407
418
|
this.pipes = this.pipes || (await this.pipesPromise);
|
|
408
419
|
|
|
409
|
-
|
|
420
|
+
const run = await Promise.all(this.pipes.map(p => p.createRun(runParams)));
|
|
421
|
+
this.uploader.checkEnabled();
|
|
422
|
+
return run;
|
|
410
423
|
}
|
|
411
424
|
|
|
412
425
|
async uploadData() {
|
|
@@ -417,18 +430,16 @@ class XmlReader {
|
|
|
417
430
|
this.formatErrors();
|
|
418
431
|
this.formatTests();
|
|
419
432
|
|
|
420
|
-
debug('Uploading data', {
|
|
421
|
-
...this.stats,
|
|
422
|
-
tests: this.tests,
|
|
423
|
-
});
|
|
424
|
-
|
|
425
433
|
const dataString = {
|
|
426
434
|
...this.stats,
|
|
427
435
|
api_key: this.requestParams.apiKey,
|
|
428
436
|
status: 'finished',
|
|
437
|
+
duration: this.stats.duration,
|
|
429
438
|
tests: this.tests,
|
|
430
439
|
};
|
|
431
440
|
|
|
441
|
+
debug('Uploading data', dataString);
|
|
442
|
+
|
|
432
443
|
this.pipes = this.pipes || (await this.pipesPromise);
|
|
433
444
|
return Promise.all(this.pipes.map(p => p.finishRun(dataString)));
|
|
434
445
|
}
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.JasmineReporter = void 0;
|
|
7
|
-
const client_js_1 = __importDefault(require("../../client.js"));
|
|
8
|
-
const utils_js_1 = require("../../utils/utils.js");
|
|
9
|
-
const constants_js_1 = require("../../constants.js");
|
|
10
|
-
class JasmineReporter {
|
|
11
|
-
constructor(options) {
|
|
12
|
-
this.testTimeMap = {};
|
|
13
|
-
this.client = new client_js_1.default({ apiKey: options?.apiKey });
|
|
14
|
-
this.client.createRun();
|
|
15
|
-
}
|
|
16
|
-
getDuration(test) {
|
|
17
|
-
if (this.testTimeMap[test.id]) {
|
|
18
|
-
return Date.now() - this.testTimeMap[test.id];
|
|
19
|
-
}
|
|
20
|
-
return 0;
|
|
21
|
-
}
|
|
22
|
-
specStarted(result) {
|
|
23
|
-
this.testTimeMap[result.id] = Date.now();
|
|
24
|
-
}
|
|
25
|
-
specDone(result) {
|
|
26
|
-
if (!this.client)
|
|
27
|
-
return;
|
|
28
|
-
const title = result.description;
|
|
29
|
-
const { status } = result;
|
|
30
|
-
let errorMessage = '';
|
|
31
|
-
for (let i = 0; i < result.failedExpectations.length; i += 1) {
|
|
32
|
-
errorMessage = `${errorMessage}Failure: ${result.failedExpectations[i].message}\n`;
|
|
33
|
-
errorMessage = `${errorMessage}\n ${result.failedExpectations[i].stack}`;
|
|
34
|
-
}
|
|
35
|
-
console.log(`${title} : ${constants_js_1.STATUS.PASSED}`);
|
|
36
|
-
console.log(errorMessage);
|
|
37
|
-
const testId = (0, utils_js_1.getTestomatIdFromTestTitle)(title);
|
|
38
|
-
errorMessage = errorMessage.replace((0, utils_js_1.ansiRegExp)(), '');
|
|
39
|
-
this.client.addTestRun(status, {
|
|
40
|
-
error: result.failedExpectations[0],
|
|
41
|
-
message: errorMessage,
|
|
42
|
-
test_id: testId,
|
|
43
|
-
title,
|
|
44
|
-
time: this.getDuration(result),
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
jasmineDone(suiteInfo, done) {
|
|
48
|
-
if (!this.client)
|
|
49
|
-
return;
|
|
50
|
-
const { overallStatus } = suiteInfo;
|
|
51
|
-
const status = overallStatus === 'failed' ? constants_js_1.STATUS.FAILED : constants_js_1.STATUS.PASSED;
|
|
52
|
-
// @ts-ignore
|
|
53
|
-
this.client.updateRunStatus(status).then(() => done);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
exports.JasmineReporter = JasmineReporter;
|
|
57
|
-
module.exports = JasmineReporter;
|
|
58
|
-
|
|
59
|
-
module.exports.JasmineReporter = JasmineReporter;
|
|
60
|
-
|
|
61
|
-
module.exports.JasmineReporter = JasmineReporter;
|
|
62
|
-
|
|
63
|
-
module.exports.exports = exports;
|