@testomatio/reporter 0.8.0-beta.9 → 0.8.2-beta.1
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/Changelog.md +251 -0
- package/README.md +77 -7
- package/lib/ArtifactStorage.js +131 -0
- package/lib/adapter/codecept.js +25 -24
- package/lib/adapter/cucumber/current.js +16 -12
- package/lib/adapter/cucumber/legacy.js +14 -10
- package/lib/adapter/cypress-plugin/index.js +7 -7
- package/lib/adapter/jasmine.js +5 -4
- package/lib/adapter/jest.js +7 -11
- package/lib/adapter/mocha.js +69 -18
- package/lib/adapter/playwright.js +28 -31
- package/lib/adapter/webdriver.js +2 -1
- package/lib/bin/reportXml.js +5 -2
- package/lib/bin/startTest.js +2 -2
- package/lib/client.js +73 -49
- package/lib/constants.js +17 -2
- package/lib/fileUploader.js +103 -64
- package/lib/junit-adapter/csharp.js +0 -1
- package/lib/junit-adapter/index.js +3 -2
- package/lib/pipe/csv.js +133 -0
- package/lib/pipe/github.js +137 -73
- package/lib/pipe/gitlab.js +229 -0
- package/lib/pipe/index.js +27 -12
- package/lib/pipe/testomatio.js +71 -44
- package/lib/reporter.js +2 -0
- package/lib/util.js +46 -13
- package/lib/xmlReader.js +47 -31
- package/package.json +12 -3
- package/testcafe/index.js +0 -61
- package/testcafe/package.json +0 -14
package/lib/client.js
CHANGED
|
@@ -1,29 +1,30 @@
|
|
|
1
|
-
const
|
|
1
|
+
const debug = require('debug')('@testomatio/reporter:client');
|
|
2
2
|
const createCallsiteRecord = require('callsite-record');
|
|
3
3
|
const { sep, join } = require('path');
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const chalk = require('chalk');
|
|
6
|
+
const { randomUUID } = require('crypto');
|
|
6
7
|
const upload = require('./fileUploader');
|
|
7
|
-
const {
|
|
8
|
+
const { APP_PREFIX } = require('./constants');
|
|
8
9
|
const pipesFactory = require('./pipe');
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {import('../types').TestData} TestData
|
|
13
|
+
* @typedef {import('../types').RunStatus} RunStatus
|
|
14
|
+
*/
|
|
11
15
|
|
|
12
|
-
|
|
13
|
-
class TestomatClient {
|
|
16
|
+
class Client {
|
|
14
17
|
/**
|
|
15
18
|
* Create a Testomat client instance
|
|
16
19
|
*
|
|
17
20
|
* @param {*} params
|
|
18
21
|
*/
|
|
19
22
|
constructor(params = {}) {
|
|
20
|
-
this.title = params.title || process.env.TESTOMATIO_TITLE;
|
|
21
|
-
this.env = TESTOMATIO_ENV;
|
|
22
23
|
this.parallel = params.parallel;
|
|
23
|
-
const store = {}
|
|
24
|
+
const store = {};
|
|
25
|
+
this.uuid = randomUUID();
|
|
24
26
|
this.pipes = pipesFactory(params, store);
|
|
25
27
|
this.queue = Promise.resolve();
|
|
26
|
-
this.axios = axios.create();
|
|
27
28
|
this.totalUploaded = 0;
|
|
28
29
|
this.version = JSON.parse(fs.readFileSync(join(__dirname, '..', 'package.json')).toString()).version;
|
|
29
30
|
console.log(APP_PREFIX, `Testomatio Reporter v${this.version}`);
|
|
@@ -32,11 +33,11 @@ class TestomatClient {
|
|
|
32
33
|
/**
|
|
33
34
|
* Used to create a new Test run
|
|
34
35
|
*
|
|
35
|
-
* @returns {Promise} - resolves to Run id which should be used to update / add test
|
|
36
|
+
* @returns {Promise<void>} - resolves to Run id which should be used to update / add test
|
|
36
37
|
*/
|
|
37
38
|
createRun() {
|
|
38
39
|
// all pipes disabled, skipping
|
|
39
|
-
if (!this.pipes.filter(p => p.isEnabled).length) return;
|
|
40
|
+
if (!this.pipes.filter(p => p.isEnabled).length) return Promise.resolve();
|
|
40
41
|
|
|
41
42
|
const runParams = {
|
|
42
43
|
title: this.title,
|
|
@@ -46,27 +47,39 @@ class TestomatClient {
|
|
|
46
47
|
|
|
47
48
|
global.testomatioArtifacts = [];
|
|
48
49
|
|
|
49
|
-
this.queue = this.queue
|
|
50
|
-
|
|
50
|
+
this.queue = this.queue
|
|
51
|
+
.then(() => Promise.all(this.pipes.map(p => p.createRun(runParams))))
|
|
52
|
+
.catch(err => console.log(APP_PREFIX, err))
|
|
53
|
+
.then(() => undefined); // fixes return type
|
|
54
|
+
debug('Run', this.queue);
|
|
51
55
|
return this.queue;
|
|
52
56
|
}
|
|
53
57
|
|
|
54
58
|
/**
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* @
|
|
59
|
+
* Updates test status and its data
|
|
60
|
+
*
|
|
61
|
+
* @param {string|undefined} status
|
|
62
|
+
* @param {TestData} [testData]
|
|
63
|
+
* @param {string[]} [storeArtifacts]
|
|
64
|
+
* @returns {Promise<void>}
|
|
58
65
|
*/
|
|
59
|
-
async addTestRun(
|
|
66
|
+
async addTestRun(status, testData, storeArtifacts = []) {
|
|
60
67
|
// all pipes disabled, skipping
|
|
61
68
|
if (!this.pipes.filter(p => p.isEnabled).length) return;
|
|
62
69
|
|
|
70
|
+
if (!testData) testData = {
|
|
71
|
+
title: 'Unknown test',
|
|
72
|
+
suite_title: 'Unknown suite',
|
|
73
|
+
}
|
|
74
|
+
|
|
63
75
|
const {
|
|
64
|
-
error =
|
|
76
|
+
error = null,
|
|
65
77
|
time = '',
|
|
66
78
|
example = null,
|
|
67
79
|
files = [],
|
|
68
80
|
filesBuffers = [],
|
|
69
81
|
steps,
|
|
82
|
+
code = null,
|
|
70
83
|
title,
|
|
71
84
|
suite_title,
|
|
72
85
|
suite_id,
|
|
@@ -76,33 +89,37 @@ class TestomatClient {
|
|
|
76
89
|
|
|
77
90
|
const uploadedFiles = [];
|
|
78
91
|
|
|
79
|
-
if (testId) testData.test_id = testId;
|
|
80
|
-
|
|
81
92
|
let stack = '';
|
|
82
93
|
|
|
83
94
|
if (error) {
|
|
84
|
-
stack = this.formatError(error);
|
|
85
|
-
message = error
|
|
95
|
+
stack = this.formatError(error) || '';
|
|
96
|
+
message = error?.message;
|
|
86
97
|
}
|
|
87
98
|
if (steps) {
|
|
88
99
|
stack = this.formatSteps(stack, steps);
|
|
89
100
|
}
|
|
90
101
|
|
|
102
|
+
if (Array.isArray(storeArtifacts) && storeArtifacts.length) {
|
|
103
|
+
debug("CLIENT storeArtifact", storeArtifacts);
|
|
104
|
+
files.push(...storeArtifacts);
|
|
105
|
+
}
|
|
106
|
+
|
|
91
107
|
if (Array.isArray(global.testomatioArtifacts)) {
|
|
108
|
+
debug("CLIENT global[testomatioArtifacts]", global.testomatioArtifacts);
|
|
92
109
|
files.push(...global.testomatioArtifacts);
|
|
93
110
|
global.testomatioArtifacts = [];
|
|
94
111
|
}
|
|
95
112
|
|
|
96
113
|
for (const file of files) {
|
|
97
|
-
uploadedFiles.push(upload.uploadFileByPath(file, this.
|
|
114
|
+
uploadedFiles.push(upload.uploadFileByPath(file, this.uuid));
|
|
98
115
|
}
|
|
99
116
|
|
|
100
117
|
for (const [idx, buffer] of filesBuffers.entries()) {
|
|
101
118
|
const fileName = `${idx + 1}-${title.replace(/\s+/g, '-')}`;
|
|
102
|
-
uploadedFiles.push(upload.uploadFileAsBuffer(buffer, fileName, this.
|
|
119
|
+
uploadedFiles.push(upload.uploadFileAsBuffer(buffer, fileName, this.uuid));
|
|
103
120
|
}
|
|
104
121
|
|
|
105
|
-
const artifacts = await Promise.all(uploadedFiles)
|
|
122
|
+
const artifacts = await Promise.all(uploadedFiles);
|
|
106
123
|
|
|
107
124
|
global.testomatioArtifacts = [];
|
|
108
125
|
|
|
@@ -114,7 +131,8 @@ class TestomatClient {
|
|
|
114
131
|
status,
|
|
115
132
|
stack,
|
|
116
133
|
example,
|
|
117
|
-
|
|
134
|
+
code,
|
|
135
|
+
title,
|
|
118
136
|
suite_title,
|
|
119
137
|
suite_id,
|
|
120
138
|
test_id,
|
|
@@ -123,36 +141,42 @@ class TestomatClient {
|
|
|
123
141
|
artifacts,
|
|
124
142
|
};
|
|
125
143
|
|
|
126
|
-
this.queue = this.queue
|
|
144
|
+
this.queue = this.queue
|
|
145
|
+
.then(() => Promise.all(this.pipes.map(p => p.addTest(data))))
|
|
146
|
+
.catch(err => console.log(APP_PREFIX, err))
|
|
147
|
+
.then(() => undefined);
|
|
127
148
|
return this.queue;
|
|
128
149
|
}
|
|
129
150
|
|
|
130
151
|
/**
|
|
131
|
-
* Update run status
|
|
132
152
|
*
|
|
133
|
-
*
|
|
153
|
+
* Updates the status of the current test run and finishes the run.
|
|
154
|
+
* @param {RunStatus} status - The status of the current test run. Must be one of "passed", "failed", or "finished"
|
|
155
|
+
* @param {boolean} [isParallel] - Whether the current test run was executed in parallel with other tests.
|
|
156
|
+
* @returns {Promise<void>} - A Promise that resolves when finishes the run.
|
|
134
157
|
*/
|
|
135
|
-
updateRunStatus(status, isParallel) {
|
|
158
|
+
updateRunStatus(status, isParallel = false) {
|
|
136
159
|
// all pipes disabled, skipping
|
|
137
|
-
if (!this.pipes.filter(p => p.isEnabled).length) return;
|
|
160
|
+
if (!this.pipes.filter(p => p.isEnabled).length) return Promise.resolve();
|
|
161
|
+
|
|
162
|
+
const runParams = { status, parallel: isParallel };
|
|
163
|
+
|
|
164
|
+
this.queue = this.queue
|
|
165
|
+
.then(() => Promise.all(this.pipes.map(p => p.finishRun(runParams))))
|
|
166
|
+
.then(() => {
|
|
167
|
+
debug("TOTAL uploaded files", this.totalUploaded);
|
|
168
|
+
|
|
169
|
+
if (upload.isArtifactsEnabled() && this.totalUploaded > 0) {
|
|
170
|
+
console.log(
|
|
171
|
+
APP_PREFIX,
|
|
172
|
+
`🗄️ Total ${this.totalUploaded} artifacts ${
|
|
173
|
+
process.env.TESTOMATIO_PRIVATE_ARTIFACTS ? 'privately' : chalk.bold('publicly')
|
|
174
|
+
} uploaded to S3 bucket `,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
})
|
|
178
|
+
.catch(err => console.log(APP_PREFIX, err));
|
|
138
179
|
|
|
139
|
-
let statusEvent;
|
|
140
|
-
if (status === FINISHED) statusEvent = 'finish';
|
|
141
|
-
if (status === PASSED) statusEvent = 'pass';
|
|
142
|
-
if (status === FAILED) statusEvent = 'fail';
|
|
143
|
-
if (isParallel) statusEvent += '_parallel';
|
|
144
|
-
const runParams = { status, statusEvent }
|
|
145
|
-
|
|
146
|
-
this.queue = this.queue.then(() => Promise.all(this.pipes.map(p => p.finishRun(runParams))));
|
|
147
|
-
|
|
148
|
-
if (upload.isArtifactsEnabled && this.totalUploaded > 0) {
|
|
149
|
-
console.log(
|
|
150
|
-
APP_PREFIX,
|
|
151
|
-
`🗄️ Total ${this.totalUploaded} artifacts ${
|
|
152
|
-
process.env.TESTOMATIO_PRIVATE_ARTIFACTS ? 'privately' : chalk.bold('publicly')
|
|
153
|
-
} uploaded to S3 bucket `,
|
|
154
|
-
);
|
|
155
|
-
}
|
|
156
180
|
return this.queue;
|
|
157
181
|
}
|
|
158
182
|
|
|
@@ -193,4 +217,4 @@ class TestomatClient {
|
|
|
193
217
|
}
|
|
194
218
|
}
|
|
195
219
|
|
|
196
|
-
module.exports =
|
|
220
|
+
module.exports = Client;
|
package/lib/constants.js
CHANGED
|
@@ -1,11 +1,26 @@
|
|
|
1
1
|
const chalk = require('chalk');
|
|
2
2
|
|
|
3
3
|
const APP_PREFIX = chalk.gray('[TESTOMATIO]');
|
|
4
|
+
const TESTOMAT_ARTIFACT_SUFFIX = "testomatio_artifact_";
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
const CSV_HEADERS = [
|
|
7
|
+
{ id: 'suite_title', title: 'Suite_title' },
|
|
8
|
+
{ id: 'title', title: 'Title' },
|
|
9
|
+
{ id: 'status', title: 'Status' },
|
|
10
|
+
{ id: 'message', title: 'Message' },
|
|
11
|
+
{ id: 'stack', title: 'Stack' },
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
const STATUS = {
|
|
6
15
|
PASSED: 'passed',
|
|
7
16
|
FAILED: 'failed',
|
|
8
17
|
SKIPPED: 'skipped',
|
|
9
18
|
FINISHED: 'finished',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
module.exports = {
|
|
10
22
|
APP_PREFIX,
|
|
11
|
-
|
|
23
|
+
TESTOMAT_ARTIFACT_SUFFIX,
|
|
24
|
+
CSV_HEADERS,
|
|
25
|
+
STATUS,
|
|
26
|
+
}
|
package/lib/fileUploader.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
const
|
|
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
|
+
|
|
2
5
|
const fs = require('fs');
|
|
3
6
|
const path = require('path');
|
|
4
7
|
const chalk = require('chalk');
|
|
@@ -6,18 +9,42 @@ const { randomUUID } = require('crypto');
|
|
|
6
9
|
const memoize = require('lodash.memoize');
|
|
7
10
|
const { APP_PREFIX } = require('./constants');
|
|
8
11
|
|
|
9
|
-
const
|
|
10
|
-
S3_ENDPOINT,
|
|
11
|
-
S3_REGION,
|
|
12
|
-
S3_BUCKET,
|
|
13
|
-
S3_ACCESS_KEY_ID,
|
|
14
|
-
S3_SECRET_ACCESS_KEY,
|
|
15
|
-
TESTOMATIO_DISABLE_ARTIFACTS,
|
|
16
|
-
TESTOMATIO_PRIVATE_ARTIFACTS,
|
|
17
|
-
S3_FORCE_PATH_STYLE,
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
12
|
+
const keys = [
|
|
13
|
+
'S3_ENDPOINT',
|
|
14
|
+
'S3_REGION',
|
|
15
|
+
'S3_BUCKET',
|
|
16
|
+
'S3_ACCESS_KEY_ID',
|
|
17
|
+
'S3_SECRET_ACCESS_KEY',
|
|
18
|
+
'TESTOMATIO_DISABLE_ARTIFACTS',
|
|
19
|
+
'TESTOMATIO_PRIVATE_ARTIFACTS',
|
|
20
|
+
'S3_FORCE_PATH_STYLE',
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
let config;
|
|
24
|
+
|
|
25
|
+
function getConfig() {
|
|
26
|
+
if (config) return config;
|
|
27
|
+
config = keys.reduce((acc, key) => {
|
|
28
|
+
acc[key] = process.env[key];
|
|
29
|
+
return acc;
|
|
30
|
+
}, {});
|
|
31
|
+
return config;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function getMaskedConfig() {
|
|
35
|
+
const config = getConfig();
|
|
36
|
+
return Object.fromEntries(Object.entries(config).map(([key, value]) => [key, key === 'S3_SECRET_ACCESS_KEY' || key === 'S3_ACCESS_KEY_ID' ? '***' : value]));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let isEnabled;
|
|
40
|
+
|
|
41
|
+
const isArtifactsEnabled = () => {
|
|
42
|
+
if (isEnabled !== undefined) return isEnabled;
|
|
43
|
+
const { S3_BUCKET, TESTOMATIO_DISABLE_ARTIFACTS } = getConfig();
|
|
44
|
+
isEnabled = !!(S3_BUCKET && !TESTOMATIO_DISABLE_ARTIFACTS);
|
|
45
|
+
debug(`Upload is ${isEnabled ? 'enabled' : 'disabled'}`);
|
|
46
|
+
return isEnabled;
|
|
47
|
+
};
|
|
21
48
|
|
|
22
49
|
const _getFileExtBase64 = str => {
|
|
23
50
|
const type = str.charAt(0);
|
|
@@ -32,6 +59,25 @@ const _getFileExtBase64 = str => {
|
|
|
32
59
|
);
|
|
33
60
|
};
|
|
34
61
|
|
|
62
|
+
const _getS3Config = () => {
|
|
63
|
+
const { S3_REGION, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, S3_FORCE_PATH_STYLE, S3_ENDPOINT } = getConfig();
|
|
64
|
+
|
|
65
|
+
const cfg = {
|
|
66
|
+
region: S3_REGION,
|
|
67
|
+
credentials: {
|
|
68
|
+
accessKeyId: S3_ACCESS_KEY_ID,
|
|
69
|
+
secretAccessKey: S3_SECRET_ACCESS_KEY,
|
|
70
|
+
s3ForcePathStyle: S3_FORCE_PATH_STYLE,
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
if (S3_ENDPOINT) {
|
|
75
|
+
cfg.endpoint = S3_ENDPOINT;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return cfg;
|
|
79
|
+
}
|
|
80
|
+
|
|
35
81
|
const uploadUsingS3 = async (filePath, runId) => {
|
|
36
82
|
let ContentType;
|
|
37
83
|
let Key;
|
|
@@ -42,49 +88,45 @@ const uploadUsingS3 = async (filePath, runId) => {
|
|
|
42
88
|
Key = filePath.name;
|
|
43
89
|
}
|
|
44
90
|
|
|
45
|
-
const config = {
|
|
46
|
-
region: S3_REGION,
|
|
47
|
-
accessKeyId: S3_ACCESS_KEY_ID,
|
|
48
|
-
secretAccessKey: S3_SECRET_ACCESS_KEY,
|
|
49
|
-
s3ForcePathStyle: S3_FORCE_PATH_STYLE,
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
if (S3_ENDPOINT) {
|
|
53
|
-
config.endpoint = new AWS.Endpoint(S3_ENDPOINT);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const s3 = new AWS.S3(config);
|
|
57
91
|
if (!fs.existsSync(filePath)) {
|
|
58
92
|
console.error(chalk.yellow(`Artifacts file ${filePath} does not exist. Skipping...`));
|
|
59
93
|
return;
|
|
60
94
|
}
|
|
95
|
+
|
|
96
|
+
const {
|
|
97
|
+
TESTOMATIO_PRIVATE_ARTIFACTS, S3_BUCKET
|
|
98
|
+
} = getConfig();
|
|
99
|
+
|
|
100
|
+
debug('S3 config', getMaskedConfig());
|
|
101
|
+
debug('Uploading', filePath, 'to', S3_BUCKET);
|
|
61
102
|
|
|
62
|
-
|
|
103
|
+
const fileData = fs.readFileSync(filePath);
|
|
63
104
|
|
|
64
105
|
Key = `${runId}/${randomUUID()}-${Key || path.basename(filePath)}`;
|
|
65
106
|
const ACL = TESTOMATIO_PRIVATE_ARTIFACTS ? 'private' : 'public-read';
|
|
66
107
|
|
|
108
|
+
const s3 = new S3(_getS3Config());
|
|
109
|
+
|
|
110
|
+
const params = {
|
|
111
|
+
Bucket: S3_BUCKET,
|
|
112
|
+
Key,
|
|
113
|
+
Body: fileData,
|
|
114
|
+
ACL,
|
|
115
|
+
};
|
|
116
|
+
|
|
67
117
|
try {
|
|
68
|
-
const out =
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
})
|
|
76
|
-
.promise();
|
|
118
|
+
const out = new Upload({
|
|
119
|
+
client: s3,
|
|
120
|
+
params
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
await out.done();
|
|
124
|
+
debug('Uploaded', out.singleUploadResult.Location)
|
|
77
125
|
|
|
78
|
-
return out.Location;
|
|
126
|
+
return out.singleUploadResult.Location;
|
|
79
127
|
} catch (e) {
|
|
80
|
-
console.log(
|
|
81
|
-
|
|
82
|
-
secretAccessKey: S3_SECRET_ACCESS_KEY ? '**** (hidden) ***' : '(empty)',
|
|
83
|
-
region: S3_REGION,
|
|
84
|
-
bucket: S3_BUCKET,
|
|
85
|
-
acl: ACL,
|
|
86
|
-
endpoint: S3_ENDPOINT,
|
|
87
|
-
});
|
|
128
|
+
console.log(e);
|
|
129
|
+
console.log(APP_PREFIX, chalk.bold.red(`Failed uploading '${Key}'. Please check S3 credentials`), getMaskedConfig());
|
|
88
130
|
|
|
89
131
|
console.log(APP_PREFIX, `To ${chalk.bold('disable')} artifact uploads set: TESTOMATIO_DISABLE_ARTIFACTS=1`);
|
|
90
132
|
if (!TESTOMATIO_PRIVATE_ARTIFACTS) {
|
|
@@ -96,39 +138,36 @@ const uploadUsingS3 = async (filePath, runId) => {
|
|
|
96
138
|
);
|
|
97
139
|
}
|
|
98
140
|
console.log(APP_PREFIX, '---------------');
|
|
99
|
-
}
|
|
141
|
+
}
|
|
100
142
|
};
|
|
101
143
|
|
|
102
144
|
const uploadUsingS3AsBuffer = async (buffer, fileName, runId) => {
|
|
103
|
-
const config = {
|
|
104
|
-
region: S3_REGION,
|
|
105
|
-
accessKeyId: S3_ACCESS_KEY_ID,
|
|
106
|
-
secretAccessKey: S3_SECRET_ACCESS_KEY,
|
|
107
|
-
s3ForcePathStyle: S3_FORCE_PATH_STYLE,
|
|
108
|
-
};
|
|
109
145
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const s3 = new AWS.S3(config);
|
|
146
|
+
const {
|
|
147
|
+
S3_REGION, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, S3_ENDPOINT, TESTOMATIO_PRIVATE_ARTIFACTS, S3_BUCKET
|
|
148
|
+
} = getConfig();
|
|
115
149
|
|
|
116
150
|
const ACL = TESTOMATIO_PRIVATE_ARTIFACTS ? 'private' : 'public-read';
|
|
117
151
|
|
|
118
152
|
const fileExtension = _getFileExtBase64(buffer.toString('base64'));
|
|
119
153
|
const Key = `${runId}/${fileName}${fileExtension}`;
|
|
120
154
|
|
|
155
|
+
const s3 = new S3(_getS3Config());
|
|
156
|
+
|
|
121
157
|
try {
|
|
122
|
-
const out =
|
|
123
|
-
|
|
158
|
+
const out = new Upload({
|
|
159
|
+
client: s3,
|
|
160
|
+
|
|
161
|
+
params: {
|
|
124
162
|
Bucket: S3_BUCKET,
|
|
125
163
|
Key,
|
|
126
164
|
Body: buffer,
|
|
127
165
|
ACL,
|
|
128
|
-
}
|
|
129
|
-
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
await out.done();
|
|
130
169
|
|
|
131
|
-
return out.Location;
|
|
170
|
+
return out.singleUploadResult.Location;
|
|
132
171
|
} catch (e) {
|
|
133
172
|
console.log(APP_PREFIX, chalk.bold.red(`Failed uploading '${Key}'. Please check S3 credentials`), {
|
|
134
173
|
accessKeyId: S3_ACCESS_KEY_ID,
|
|
@@ -154,7 +193,7 @@ const uploadUsingS3AsBuffer = async (buffer, fileName, runId) => {
|
|
|
154
193
|
|
|
155
194
|
const uploadFileByPath = async (filePath, runId) => {
|
|
156
195
|
try {
|
|
157
|
-
if (isArtifactsEnabled) {
|
|
196
|
+
if (isArtifactsEnabled()) {
|
|
158
197
|
return uploadUsingS3(filePath, runId);
|
|
159
198
|
}
|
|
160
199
|
} catch (e) {
|
|
@@ -164,7 +203,7 @@ const uploadFileByPath = async (filePath, runId) => {
|
|
|
164
203
|
|
|
165
204
|
const uploadFileAsBuffer = async (buffer, fileName, runId) => {
|
|
166
205
|
try {
|
|
167
|
-
if (isArtifactsEnabled) {
|
|
206
|
+
if (isArtifactsEnabled()) {
|
|
168
207
|
return uploadUsingS3AsBuffer(buffer, fileName, runId);
|
|
169
208
|
}
|
|
170
209
|
} catch (e) {
|
|
@@ -175,5 +214,5 @@ const uploadFileAsBuffer = async (buffer, fileName, runId) => {
|
|
|
175
214
|
module.exports = {
|
|
176
215
|
uploadFileByPath: memoize(uploadFileByPath),
|
|
177
216
|
uploadFileAsBuffer: memoize(uploadFileAsBuffer),
|
|
178
|
-
isArtifactsEnabled,
|
|
217
|
+
isArtifactsEnabled: memoize(isArtifactsEnabled),
|
|
179
218
|
};
|
|
@@ -5,8 +5,7 @@ const PythonAdapter = require('./python');
|
|
|
5
5
|
const RubyAdapter = require('./ruby');
|
|
6
6
|
const CSharpAdapter = require('./csharp');
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
module.exports = function (lang, opts) {
|
|
8
|
+
function AdapterFactory(lang, opts) {
|
|
10
9
|
if (!lang) return new Adapter(opts);
|
|
11
10
|
|
|
12
11
|
if (lang === 'java') {
|
|
@@ -25,3 +24,5 @@ module.exports = function (lang, opts) {
|
|
|
25
24
|
return new CSharpAdapter(opts);
|
|
26
25
|
}
|
|
27
26
|
}
|
|
27
|
+
|
|
28
|
+
module.exports = AdapterFactory;
|
package/lib/pipe/csv.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
const debug = require('debug')('@testomatio/reporter:pipe:csv');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const csvWriter = require('csv-writer');
|
|
5
|
+
const chalk = require('chalk');
|
|
6
|
+
const merge = require('lodash.merge');
|
|
7
|
+
const { isSameTest, getCurrentDateTime } = require('../util');
|
|
8
|
+
const { CSV_HEADERS } = require('../constants');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {import('../../types').Pipe} Pipe
|
|
12
|
+
* @typedef {import('../../types').TestData} TestData
|
|
13
|
+
* @class CsvPipe
|
|
14
|
+
* @implements {Pipe}
|
|
15
|
+
*/
|
|
16
|
+
class CsvPipe {
|
|
17
|
+
|
|
18
|
+
constructor(params, store) {
|
|
19
|
+
this.store = store || {};
|
|
20
|
+
this.title = params.title || process.env.TESTOMATIO_TITLE;
|
|
21
|
+
this.isEnabled = true;
|
|
22
|
+
this.results = [];
|
|
23
|
+
|
|
24
|
+
this.outputDir = 'export';
|
|
25
|
+
this.csvFilename = process.env.TESTOMATIO_CSV_FILENAME;
|
|
26
|
+
this.isCsvSave = false;
|
|
27
|
+
|
|
28
|
+
if (this.csvFilename !== undefined && this.csvFilename.split('.').length > 0) {
|
|
29
|
+
this.isCsvSave = true;
|
|
30
|
+
|
|
31
|
+
if (this.csvFilename.split('.')[0] === 'report') {
|
|
32
|
+
this.outputFile = path.resolve(process.cwd(), this.outputDir, 'report.csv');
|
|
33
|
+
} else {
|
|
34
|
+
this.outputFile = path.resolve(
|
|
35
|
+
process.cwd(),
|
|
36
|
+
this.outputDir,
|
|
37
|
+
`${getCurrentDateTime()}_${this.csvFilename.split('.')[0]}.csv`,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async createRun() {
|
|
44
|
+
// empty
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
updateRun() {}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Create a folder that will contain the exported files
|
|
51
|
+
*/
|
|
52
|
+
checkExportDir() {
|
|
53
|
+
if (!fs.existsSync(this.outputDir)) {
|
|
54
|
+
return fs.mkdirSync(this.outputDir);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Save data to the csv file.
|
|
60
|
+
* @param {Object} data - data that will be added to the CSV file.
|
|
61
|
+
* Example: [{suite_title: "Suite #1", test: "Test-case-1", message: "Test msg"}]
|
|
62
|
+
* @param {Object} headers - csv file headers. Example: [{ id: 'suite_title', title: 'Suite_title' }]
|
|
63
|
+
*/
|
|
64
|
+
async saveToCsv(data, headers) {
|
|
65
|
+
debug('Data', data);
|
|
66
|
+
// First, we check whether the export directory exists: if yes - OK, no - create it.
|
|
67
|
+
this.checkExportDir();
|
|
68
|
+
|
|
69
|
+
if (!this.outputFile) {
|
|
70
|
+
console.log(this, chalk.yellow(`CSV file is not set, ignoring`));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
console.log(this, chalk.yellow(`The test results will be added to the csv. It will take some time...`));
|
|
75
|
+
// Create csv writer object
|
|
76
|
+
const writer = csvWriter.createObjectCsvWriter({
|
|
77
|
+
path: this.outputFile,
|
|
78
|
+
header: headers,
|
|
79
|
+
});
|
|
80
|
+
// Save csv file based on the current data
|
|
81
|
+
try {
|
|
82
|
+
await writer.writeRecords(data);
|
|
83
|
+
console.log(this, chalk.green(`Recording completed! You can check the result in file = ${this.outputFile}`));
|
|
84
|
+
} catch (e) {
|
|
85
|
+
console.log('Unknown error: ', e);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Add test data to the result array for saving. As a result of this function, we get a result object to save.
|
|
91
|
+
* @param {Object} test - object which includes each test entry.
|
|
92
|
+
*/
|
|
93
|
+
addTest(test) {
|
|
94
|
+
if (!this.isEnabled) return;
|
|
95
|
+
|
|
96
|
+
const index = this.results.findIndex(t => isSameTest(t, test));
|
|
97
|
+
// update if they were already added
|
|
98
|
+
if (index >= 0) {
|
|
99
|
+
this.results[index] = merge(this.results[index], test);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const { suite_title, title, status, message, stack } = test;
|
|
104
|
+
|
|
105
|
+
this.results.push({
|
|
106
|
+
suite_title,
|
|
107
|
+
title,
|
|
108
|
+
status,
|
|
109
|
+
message,
|
|
110
|
+
stack,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* @param {{ tests?: TestData[] }} runParams
|
|
116
|
+
* @returns {Promise<void>}
|
|
117
|
+
*/
|
|
118
|
+
async finishRun(runParams) {
|
|
119
|
+
if (!this.isEnabled) return;
|
|
120
|
+
|
|
121
|
+
if (runParams.tests) runParams.tests.forEach(t => this.addTest(t));
|
|
122
|
+
// Save results based on the default headers
|
|
123
|
+
if (this.isCsvSave) {
|
|
124
|
+
this.saveToCsv(this.results, CSV_HEADERS);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
toString() {
|
|
129
|
+
return 'csv exporter';
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
module.exports = CsvPipe;
|