@testomatio/reporter 1.2.3-beta → 1.2.3-beta.upload-s3
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/README.md +61 -54
- package/lib/adapter/codecept.js +96 -35
- package/lib/adapter/cucumber/current.js +23 -16
- package/lib/adapter/cucumber/legacy.js +14 -13
- package/lib/adapter/cucumber.js +2 -2
- package/lib/adapter/cypress-plugin/index.js +52 -25
- package/lib/adapter/jasmine.js +1 -1
- package/lib/adapter/jest.js +49 -16
- package/lib/adapter/mocha.js +103 -51
- package/lib/adapter/playwright.js +80 -27
- package/lib/adapter/webdriver.js +1 -1
- package/lib/bin/reportXml.js +14 -13
- package/lib/bin/startTest.js +27 -6
- package/lib/client.js +192 -69
- package/lib/config.js +34 -0
- package/lib/constants.js +19 -7
- package/lib/data-storage.js +203 -0
- package/lib/fileUploader.js +135 -55
- package/lib/junit-adapter/adapter.js +0 -2
- package/lib/junit-adapter/csharp.js +3 -4
- package/lib/junit-adapter/index.js +3 -3
- package/lib/junit-adapter/java.js +35 -17
- package/lib/junit-adapter/javascript.js +1 -2
- package/lib/junit-adapter/python.js +12 -14
- package/lib/junit-adapter/ruby.js +1 -2
- package/lib/pipe/csv.js +5 -3
- package/lib/pipe/github.js +27 -39
- package/lib/pipe/gitlab.js +20 -24
- package/lib/pipe/html.js +354 -0
- package/lib/pipe/index.js +3 -1
- package/lib/pipe/testomatio.js +185 -56
- package/lib/reporter-functions.js +46 -0
- package/lib/reporter.js +11 -9
- package/lib/services/artifacts.js +57 -0
- package/lib/services/index.js +13 -0
- package/lib/services/key-values.js +58 -0
- package/lib/services/logger.js +311 -0
- package/lib/template/testomatio.hbs +1236 -0
- package/lib/utils/pipe_utils.js +129 -0
- package/lib/{util.js → utils/utils.js} +144 -12
- package/lib/xmlReader.js +211 -122
- package/package.json +18 -7
- package/lib/_ArtifactStorageOld.js +0 -142
- package/lib/artifactStorage.js +0 -25
- package/lib/dataStorage.js +0 -241
- package/lib/helpers.js +0 -34
- package/lib/logger.js +0 -293
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
const debug = require('debug')('@testomatio/reporter:storage');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const { join } = require('path');
|
|
6
|
+
const { TESTOMAT_TMP_STORAGE_DIR } = require('./constants');
|
|
7
|
+
const { fileSystem, testRunnerHelper } = require('./utils/utils');
|
|
8
|
+
const crypto = require('crypto');
|
|
9
|
+
|
|
10
|
+
class DataStorage {
|
|
11
|
+
static #instance;
|
|
12
|
+
|
|
13
|
+
context;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
*
|
|
17
|
+
* @returns {DataStorage}
|
|
18
|
+
*/
|
|
19
|
+
static getInstance() {
|
|
20
|
+
if (!this.#instance) {
|
|
21
|
+
this.#instance = new DataStorage();
|
|
22
|
+
}
|
|
23
|
+
return this.#instance;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
setContext(context) {
|
|
27
|
+
this.context = context;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Creates data storage instance as singleton
|
|
32
|
+
* Stores data to global variable or to file depending on what is applicable for current test runner (adapter)
|
|
33
|
+
* Recommend to use composition while using this class (instead of inheritance).
|
|
34
|
+
* ! Also the class which will use data storage should be singleton (to avoid data loss).
|
|
35
|
+
*/
|
|
36
|
+
constructor() {
|
|
37
|
+
// some frameworks use global variable to store data, some use file storage
|
|
38
|
+
this.isFileStorage = true;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Puts any data to storage (file or global variable).
|
|
43
|
+
* If file: stores data as text, if global variable – stores as array of data.
|
|
44
|
+
* @param {'log' | 'artifact' | 'keyvalue'} dataType
|
|
45
|
+
* @param {*} data anything you want to store (string, object, array, etc)
|
|
46
|
+
* @param {*} context could be testId or any context (test name, suite name, including their IDs etc)
|
|
47
|
+
* suite name + test name is used by default
|
|
48
|
+
* @returns
|
|
49
|
+
*/
|
|
50
|
+
putData(dataType, data, context = null) {
|
|
51
|
+
if (!dataType || !data) return;
|
|
52
|
+
|
|
53
|
+
context = context || this.context || testRunnerHelper.getNameOfCurrentlyRunningTest();
|
|
54
|
+
if (!context) {
|
|
55
|
+
debug(`No context provided for "${dataType}" data:`, data);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const contextHash = stringToMD5Hash(context);
|
|
59
|
+
|
|
60
|
+
if (this.isFileStorage) {
|
|
61
|
+
const dataDirPath = path.join(TESTOMAT_TMP_STORAGE_DIR, dataType);
|
|
62
|
+
fileSystem.createDir(dataDirPath);
|
|
63
|
+
this.#putDataToFile(dataType, data, contextHash);
|
|
64
|
+
} else {
|
|
65
|
+
this.#putDataToGlobalVar(dataType, data, contextHash);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Returns data, stored for specific test/context (or data which was stored without test id specified).
|
|
71
|
+
* This method will get data from global variable and/or from from file (previosly saved with put method).
|
|
72
|
+
*
|
|
73
|
+
* @param {'log' | 'artifact' | 'keyvalue'} dataType
|
|
74
|
+
* @param {string} context
|
|
75
|
+
* @returns {any []} array of data (any type), null (if no data found for context) or string (if data type is log)
|
|
76
|
+
*/
|
|
77
|
+
getData(dataType, context) {
|
|
78
|
+
// TODO: think if it could be useful
|
|
79
|
+
// context = context || this.context || testRunnerHelper.getNameOfCurrentlyRunningTest();
|
|
80
|
+
|
|
81
|
+
if (!context) {
|
|
82
|
+
debug(`Trying to get "${dataType}" data without context`);
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const contextHash = stringToMD5Hash(context);
|
|
87
|
+
|
|
88
|
+
let testDataFromFile = [];
|
|
89
|
+
let testDataFromGlobalVar = [];
|
|
90
|
+
|
|
91
|
+
if (global?.testomatioDataStore) {
|
|
92
|
+
testDataFromGlobalVar = this.#getDataFromGlobalVar(dataType, contextHash);
|
|
93
|
+
if (testDataFromGlobalVar) {
|
|
94
|
+
if (testDataFromGlobalVar.length) return testDataFromGlobalVar;
|
|
95
|
+
}
|
|
96
|
+
// don't return nothing if no data in global variable
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
testDataFromFile = this.#getDataFromFile(dataType, contextHash);
|
|
100
|
+
|
|
101
|
+
if (testDataFromFile.length) {
|
|
102
|
+
return testDataFromFile;
|
|
103
|
+
}
|
|
104
|
+
debug(`No "${dataType}" data for context "${contextHash}" in both file and global variable`);
|
|
105
|
+
|
|
106
|
+
// in case no data found for context
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* @param {'log' | 'artifact' | 'keyvalue'} dataType
|
|
112
|
+
* @param {string} context
|
|
113
|
+
* @returns aray of data (any type)
|
|
114
|
+
*/
|
|
115
|
+
#getDataFromGlobalVar(dataType, context) {
|
|
116
|
+
try {
|
|
117
|
+
if (global?.testomatioDataStore[dataType]) {
|
|
118
|
+
const testData = global.testomatioDataStore[dataType][context];
|
|
119
|
+
if (testData) debug(`"${dataType}" data for constext "${context}":`, testData.join(', '));
|
|
120
|
+
return testData || [];
|
|
121
|
+
}
|
|
122
|
+
// debug(`No ${this.dataType} data for context ${context} in <global> storage`);
|
|
123
|
+
return [];
|
|
124
|
+
} catch (e) {
|
|
125
|
+
// there could be no data, ignore
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* @param {'log' | 'artifact' | 'keyvalue'} dataType
|
|
131
|
+
* @param {*} context
|
|
132
|
+
* @returns array of data (any type)
|
|
133
|
+
*/
|
|
134
|
+
#getDataFromFile(dataType, context) {
|
|
135
|
+
const dataDirPath = path.join(TESTOMAT_TMP_STORAGE_DIR, dataType);
|
|
136
|
+
try {
|
|
137
|
+
const filepath = join(dataDirPath, `${dataType}_${context}`);
|
|
138
|
+
if (fs.existsSync(filepath)) {
|
|
139
|
+
const testDataAsText = fs.readFileSync(filepath, 'utf-8');
|
|
140
|
+
if (testDataAsText) debug(`"${dataType}" data for context "${context}":`, testDataAsText);
|
|
141
|
+
const testDataArr = testDataAsText?.split(os.EOL) || [];
|
|
142
|
+
return testDataArr;
|
|
143
|
+
}
|
|
144
|
+
// debug(`No ${this.dataType} data for ${context} in <file> storage`);
|
|
145
|
+
return [];
|
|
146
|
+
} catch (e) {
|
|
147
|
+
// there could be no data, ignore
|
|
148
|
+
}
|
|
149
|
+
return [];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Puts data to global variable. Unlike the file storage, stores data in array (file storage just append as string).
|
|
154
|
+
* @param {'log' | 'artifact' | 'keyvalue'} dataType
|
|
155
|
+
* @param {*} data
|
|
156
|
+
* @param {*} context
|
|
157
|
+
*/
|
|
158
|
+
#putDataToGlobalVar(dataType, data, context) {
|
|
159
|
+
debug('Saving data to global variable for ', context, ':', data);
|
|
160
|
+
if (!global.testomatioDataStore) global.testomatioDataStore = {};
|
|
161
|
+
if (!global.testomatioDataStore?.[dataType]) global.testomatioDataStore[dataType] = {};
|
|
162
|
+
|
|
163
|
+
if (!global.testomatioDataStore?.[dataType][context]) global.testomatioDataStore[dataType][context] = [];
|
|
164
|
+
global.testomatioDataStore[dataType][context].push(data);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Puts data to file. Unlike the global variable storage, stores data as string
|
|
169
|
+
* @param {'log' | 'artifact' | 'keyvalue'} dataType
|
|
170
|
+
* @param {*} data
|
|
171
|
+
* @param {string} context
|
|
172
|
+
* @returns
|
|
173
|
+
*/
|
|
174
|
+
#putDataToFile(dataType, data, context) {
|
|
175
|
+
const dataDirPath = path.join(TESTOMAT_TMP_STORAGE_DIR, dataType);
|
|
176
|
+
if (typeof data !== 'string') data = JSON.stringify(data);
|
|
177
|
+
const filename = `${dataType}_${context}`;
|
|
178
|
+
const filepath = join(dataDirPath, filename);
|
|
179
|
+
if (!fs.existsSync(dataDirPath)) fileSystem.createDir(dataDirPath);
|
|
180
|
+
debug(`Saving data to file for context "${context}" to ${filepath}. Data: ${JSON.stringify(data)}`);
|
|
181
|
+
|
|
182
|
+
// append new line if file already exists (in this case its definitely includes some data)
|
|
183
|
+
if (fs.existsSync(filepath)) {
|
|
184
|
+
fs.appendFileSync(filepath, os.EOL + data, 'utf-8');
|
|
185
|
+
} else {
|
|
186
|
+
fs.writeFileSync(filepath, data, 'utf-8');
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function stringToMD5Hash(str) {
|
|
192
|
+
const md5 = crypto.createHash('md5');
|
|
193
|
+
md5.update(str);
|
|
194
|
+
const hash = md5.digest('hex');
|
|
195
|
+
|
|
196
|
+
return hash;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
module.exports.dataStorage = DataStorage.getInstance();
|
|
200
|
+
module.exports.stringToMD5Hash = stringToMD5Hash;
|
|
201
|
+
|
|
202
|
+
// TODO: consider using fs promises instead of writeSync/appendFileSync to
|
|
203
|
+
// prevent blocking and improve performance (probably queue usage will be required)
|
package/lib/fileUploader.js
CHANGED
|
@@ -3,10 +3,15 @@ const { S3 } = require('@aws-sdk/client-s3');
|
|
|
3
3
|
const { Upload } = require('@aws-sdk/lib-storage');
|
|
4
4
|
|
|
5
5
|
const fs = require('fs');
|
|
6
|
+
const util = require('util');
|
|
6
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);
|
|
7
12
|
const chalk = require('chalk');
|
|
8
13
|
const { randomUUID } = require('crypto');
|
|
9
|
-
|
|
14
|
+
|
|
10
15
|
const { APP_PREFIX } = require('./constants');
|
|
11
16
|
|
|
12
17
|
const keys = [
|
|
@@ -15,9 +20,10 @@ const keys = [
|
|
|
15
20
|
'S3_BUCKET',
|
|
16
21
|
'S3_ACCESS_KEY_ID',
|
|
17
22
|
'S3_SECRET_ACCESS_KEY',
|
|
23
|
+
'S3_SESSION_TOKEN',
|
|
18
24
|
'TESTOMATIO_DISABLE_ARTIFACTS',
|
|
19
25
|
'TESTOMATIO_PRIVATE_ARTIFACTS',
|
|
20
|
-
'S3_FORCE_PATH_STYLE',
|
|
26
|
+
'S3_FORCE_PATH_STYLE',
|
|
21
27
|
];
|
|
22
28
|
|
|
23
29
|
let config;
|
|
@@ -37,8 +43,12 @@ function getConfig() {
|
|
|
37
43
|
}
|
|
38
44
|
|
|
39
45
|
function getMaskedConfig() {
|
|
40
|
-
return Object.fromEntries(
|
|
41
|
-
.map(([key, value]) => [
|
|
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
|
+
);
|
|
42
52
|
}
|
|
43
53
|
|
|
44
54
|
let isEnabled;
|
|
@@ -65,7 +75,7 @@ const _getFileExtBase64 = str => {
|
|
|
65
75
|
};
|
|
66
76
|
|
|
67
77
|
const _getS3Config = () => {
|
|
68
|
-
const { S3_REGION, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, S3_FORCE_PATH_STYLE, S3_ENDPOINT } = getConfig();
|
|
78
|
+
const { S3_REGION, S3_SESSION_TOKEN, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, S3_FORCE_PATH_STYLE, S3_ENDPOINT } = getConfig();
|
|
69
79
|
|
|
70
80
|
const cfg = {
|
|
71
81
|
region: S3_REGION,
|
|
@@ -73,15 +83,19 @@ const _getS3Config = () => {
|
|
|
73
83
|
accessKeyId: S3_ACCESS_KEY_ID,
|
|
74
84
|
secretAccessKey: S3_SECRET_ACCESS_KEY,
|
|
75
85
|
s3ForcePathStyle: S3_FORCE_PATH_STYLE,
|
|
76
|
-
}
|
|
86
|
+
},
|
|
77
87
|
};
|
|
78
88
|
|
|
89
|
+
if (S3_SESSION_TOKEN) {
|
|
90
|
+
cfg.credentials.sessionToken = S3_SESSION_TOKEN;
|
|
91
|
+
}
|
|
92
|
+
|
|
79
93
|
if (S3_ENDPOINT) {
|
|
80
94
|
cfg.endpoint = S3_ENDPOINT;
|
|
81
95
|
}
|
|
82
96
|
|
|
83
97
|
return cfg;
|
|
84
|
-
}
|
|
98
|
+
};
|
|
85
99
|
|
|
86
100
|
const uploadUsingS3 = async (filePath, runId) => {
|
|
87
101
|
let ContentType;
|
|
@@ -93,48 +107,62 @@ const uploadUsingS3 = async (filePath, runId) => {
|
|
|
93
107
|
Key = filePath.name;
|
|
94
108
|
}
|
|
95
109
|
|
|
96
|
-
|
|
97
|
-
console.error(chalk.yellow(`Artifacts file ${filePath} does not exist. Skipping...`));
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
110
|
+
const { TESTOMATIO_PRIVATE_ARTIFACTS, S3_BUCKET } = getConfig();
|
|
100
111
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
112
|
+
try {
|
|
113
|
+
debug('S3 config', getMaskedConfig());
|
|
114
|
+
debug('Started upload', filePath, 'to ', S3_BUCKET);
|
|
104
115
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const fileData = fs.readFileSync(filePath);
|
|
116
|
+
// Verification that the file was actually created: 20 attempts of 0.5 second => 10sec
|
|
117
|
+
const isFileExist = await checkFileExists(filePath, 20, 500);
|
|
109
118
|
|
|
110
|
-
|
|
111
|
-
|
|
119
|
+
if (!isFileExist) {
|
|
120
|
+
console.error(chalk.yellow(`Artifacts file ${filePath} does not exist. Skipping...`));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
112
123
|
|
|
113
|
-
|
|
124
|
+
debug('File: ', filePath, ' exists');
|
|
114
125
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
Key
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
126
|
+
const fileData = await readFile(filePath);
|
|
127
|
+
|
|
128
|
+
Key = `${runId}/${randomUUID()}-${Key || path.basename(filePath)}`;
|
|
129
|
+
|
|
130
|
+
const ACL = TESTOMATIO_PRIVATE_ARTIFACTS ? 'private' : 'public-read';
|
|
131
|
+
|
|
132
|
+
if (!S3_BUCKET || !fileData) {
|
|
133
|
+
console.log(
|
|
134
|
+
APP_PREFIX,
|
|
135
|
+
chalk.bold.red(`Failed uploading '${Key}'. Please check S3 credentials`),
|
|
136
|
+
getMaskedConfig(),
|
|
137
|
+
);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const s3 = new S3(_getS3Config());
|
|
142
|
+
|
|
143
|
+
const params = {
|
|
144
|
+
Bucket: S3_BUCKET,
|
|
145
|
+
Key,
|
|
146
|
+
Body: fileData,
|
|
147
|
+
ContentType,
|
|
148
|
+
ACL,
|
|
149
|
+
};
|
|
122
150
|
|
|
123
|
-
try {
|
|
124
151
|
const out = new Upload({
|
|
125
152
|
client: s3,
|
|
126
|
-
params
|
|
153
|
+
params,
|
|
127
154
|
});
|
|
128
155
|
|
|
129
|
-
await out
|
|
130
|
-
|
|
156
|
+
const link = await getS3LocationLink(out);
|
|
157
|
+
|
|
158
|
+
debug(`Succesfully uploaded ${filePath} => ${S3_BUCKET}/${Key} | URL: ${link}`);
|
|
131
159
|
|
|
132
|
-
return
|
|
160
|
+
return link;
|
|
133
161
|
} catch (e) {
|
|
134
|
-
|
|
135
|
-
console.log(APP_PREFIX, chalk.red(`Failed uploading '${Key}'. Please check S3 credentials`), getMaskedConfig());
|
|
162
|
+
debug('S3 file uploading error: ', e);
|
|
136
163
|
|
|
137
164
|
console.log(APP_PREFIX, `To ${chalk.bold('disable')} artifact uploads set: TESTOMATIO_DISABLE_ARTIFACTS=1`);
|
|
165
|
+
|
|
138
166
|
if (!TESTOMATIO_PRIVATE_ARTIFACTS) {
|
|
139
167
|
console.log(APP_PREFIX, `To enable ${chalk.bold('PRIVATE')} uploads set: TESTOMATIO_PRIVATE_ARTIFACTS=1`);
|
|
140
168
|
} else {
|
|
@@ -144,20 +172,30 @@ const fileData = fs.readFileSync(filePath);
|
|
|
144
172
|
);
|
|
145
173
|
}
|
|
146
174
|
console.log(APP_PREFIX, '---------------');
|
|
147
|
-
}
|
|
175
|
+
}
|
|
148
176
|
};
|
|
149
177
|
|
|
150
178
|
const uploadUsingS3AsBuffer = async (buffer, fileName, runId) => {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
S3_REGION, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, S3_ENDPOINT, TESTOMATIO_PRIVATE_ARTIFACTS, S3_BUCKET
|
|
154
|
-
} = getConfig();
|
|
179
|
+
const { S3_REGION, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, S3_ENDPOINT, TESTOMATIO_PRIVATE_ARTIFACTS, S3_BUCKET } =
|
|
180
|
+
getConfig();
|
|
155
181
|
|
|
156
182
|
const ACL = TESTOMATIO_PRIVATE_ARTIFACTS ? 'private' : 'public-read';
|
|
157
183
|
|
|
158
184
|
const fileExtension = _getFileExtBase64(buffer.toString('base64'));
|
|
159
185
|
const Key = `${runId}/${fileName}${fileExtension}`;
|
|
160
186
|
|
|
187
|
+
if (!S3_BUCKET || !buffer) {
|
|
188
|
+
console.log(APP_PREFIX, chalk.bold.red(`Failed uploading '${Key}'. Please check S3 credentials`), {
|
|
189
|
+
accessKeyId: S3_ACCESS_KEY_ID,
|
|
190
|
+
secretAccessKey: S3_SECRET_ACCESS_KEY ? '**** (hidden) ***' : '(empty)',
|
|
191
|
+
region: S3_REGION,
|
|
192
|
+
bucket: S3_BUCKET,
|
|
193
|
+
acl: ACL,
|
|
194
|
+
endpoint: S3_ENDPOINT,
|
|
195
|
+
});
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
161
199
|
const s3 = new S3(_getS3Config());
|
|
162
200
|
|
|
163
201
|
try {
|
|
@@ -169,22 +207,15 @@ const uploadUsingS3AsBuffer = async (buffer, fileName, runId) => {
|
|
|
169
207
|
Key,
|
|
170
208
|
Body: buffer,
|
|
171
209
|
ACL,
|
|
172
|
-
}
|
|
210
|
+
},
|
|
173
211
|
});
|
|
174
|
-
await out.done();
|
|
175
212
|
|
|
176
|
-
return out
|
|
213
|
+
return await getS3LocationLink(out);
|
|
177
214
|
} catch (e) {
|
|
178
|
-
|
|
179
|
-
accessKeyId: S3_ACCESS_KEY_ID,
|
|
180
|
-
secretAccessKey: S3_SECRET_ACCESS_KEY ? '**** (hidden) ***' : '(empty)',
|
|
181
|
-
region: S3_REGION,
|
|
182
|
-
bucket: S3_BUCKET,
|
|
183
|
-
acl: ACL,
|
|
184
|
-
endpoint: S3_ENDPOINT,
|
|
185
|
-
});
|
|
215
|
+
debug('S3 buffer uploading error: ', e);
|
|
186
216
|
|
|
187
217
|
console.log(APP_PREFIX, `To ${chalk.bold('disable')} artifact uploads set: TESTOMATIO_DISABLE_ARTIFACTS=1`);
|
|
218
|
+
|
|
188
219
|
if (!TESTOMATIO_PRIVATE_ARTIFACTS) {
|
|
189
220
|
console.log(APP_PREFIX, `To enable ${chalk.bold('PRIVATE')} uploads set: TESTOMATIO_PRIVATE_ARTIFACTS=1`);
|
|
190
221
|
} else {
|
|
@@ -203,7 +234,9 @@ const uploadFileByPath = async (filePath, runId) => {
|
|
|
203
234
|
return uploadUsingS3(filePath, runId);
|
|
204
235
|
}
|
|
205
236
|
} catch (e) {
|
|
206
|
-
|
|
237
|
+
debug(e);
|
|
238
|
+
|
|
239
|
+
console.error(chalk.red('Error occurred while uploading artifacts! '), e);
|
|
207
240
|
}
|
|
208
241
|
};
|
|
209
242
|
|
|
@@ -213,13 +246,60 @@ const uploadFileAsBuffer = async (buffer, fileName, runId) => {
|
|
|
213
246
|
return uploadUsingS3AsBuffer(buffer, fileName, runId);
|
|
214
247
|
}
|
|
215
248
|
} catch (e) {
|
|
216
|
-
|
|
249
|
+
debug(e);
|
|
250
|
+
|
|
251
|
+
console.error(chalk.red('Error occurred while uploading artifacts! '), e);
|
|
217
252
|
}
|
|
218
253
|
};
|
|
219
254
|
|
|
255
|
+
const checkFileExists = async (filePath, attempts = 5, intervalMs = 500) => {
|
|
256
|
+
const checkFile = async () => {
|
|
257
|
+
const fileStats = await stat(filePath);
|
|
258
|
+
if (fileStats.isFile()) {
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
throw new Error('File not found');
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
try {
|
|
266
|
+
await promiseRetry(
|
|
267
|
+
{
|
|
268
|
+
retries: attempts,
|
|
269
|
+
minTimeout: intervalMs,
|
|
270
|
+
},
|
|
271
|
+
checkFile,
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
return true;
|
|
275
|
+
} catch (err) {
|
|
276
|
+
console.error(chalk.yellow(`File ${filePath} was not found or did not have time to be generated...`));
|
|
277
|
+
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const getS3LocationLink = async out => {
|
|
283
|
+
const response = await out.done();
|
|
284
|
+
|
|
285
|
+
let s3Location = response?.Location;
|
|
286
|
+
|
|
287
|
+
if (!s3Location) {
|
|
288
|
+
// TODO: out: a fallback case - remove after deeper testing
|
|
289
|
+
s3Location = out?.singleUploadResult?.Location;
|
|
290
|
+
debug('Uploaded singleUploadResult.Location', s3Location);
|
|
291
|
+
|
|
292
|
+
if (!s3Location) {
|
|
293
|
+
throw new Error("Problems getting the S3 artifact's link. Please check S3 permissions!");
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return s3Location;
|
|
298
|
+
};
|
|
299
|
+
|
|
220
300
|
module.exports = {
|
|
221
|
-
uploadFileByPath
|
|
222
|
-
uploadFileAsBuffer
|
|
301
|
+
uploadFileByPath,
|
|
302
|
+
uploadFileAsBuffer,
|
|
223
303
|
isArtifactsEnabled,
|
|
224
304
|
resetConfig,
|
|
225
305
|
};
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
const Adapter = require('./adapter');
|
|
2
2
|
|
|
3
3
|
class CSharpAdapter extends Adapter {
|
|
4
|
-
|
|
5
4
|
formatTest(t) {
|
|
6
5
|
const title = t.title.replace(/\(.*?\)/, '').trim();
|
|
7
6
|
const example = t.title.match(/\((.*?)\)/);
|
|
8
|
-
if (example) t.example = { ...example[1].split(',')};
|
|
9
|
-
const suite = t.suite_title.split('.')
|
|
7
|
+
if (example) t.example = { ...example[1].split(',') };
|
|
8
|
+
const suite = t.suite_title.split('.');
|
|
10
9
|
t.suite_title = suite.pop();
|
|
11
10
|
t.file = suite.join('/');
|
|
12
11
|
t.title = title.trim();
|
|
@@ -14,4 +13,4 @@ class CSharpAdapter extends Adapter {
|
|
|
14
13
|
}
|
|
15
14
|
}
|
|
16
15
|
|
|
17
|
-
module.exports = CSharpAdapter;
|
|
16
|
+
module.exports = CSharpAdapter;
|
|
@@ -6,8 +6,6 @@ const RubyAdapter = require('./ruby');
|
|
|
6
6
|
const CSharpAdapter = require('./csharp');
|
|
7
7
|
|
|
8
8
|
function AdapterFactory(lang, opts) {
|
|
9
|
-
if (!lang) return new Adapter(opts);
|
|
10
|
-
|
|
11
9
|
if (lang === 'java') {
|
|
12
10
|
return new JavaAdapter(opts);
|
|
13
11
|
}
|
|
@@ -23,6 +21,8 @@ function AdapterFactory(lang, opts) {
|
|
|
23
21
|
if (lang === 'c#' || lang === 'csharp') {
|
|
24
22
|
return new CSharpAdapter(opts);
|
|
25
23
|
}
|
|
24
|
+
|
|
25
|
+
return new Adapter(opts);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
module.exports = AdapterFactory;
|
|
28
|
+
module.exports = AdapterFactory;
|
|
@@ -2,39 +2,57 @@ const path = require('path');
|
|
|
2
2
|
const Adapter = require('./adapter');
|
|
3
3
|
|
|
4
4
|
class JavaAdapter extends Adapter {
|
|
5
|
-
|
|
6
5
|
getFilePath(t) {
|
|
7
|
-
const fileName = namespaceToFileName(t.suite_title)
|
|
6
|
+
const fileName = namespaceToFileName(t.suite_title);
|
|
8
7
|
return this.opts.javaTests + path.sep + fileName;
|
|
9
8
|
}
|
|
10
9
|
|
|
11
10
|
formatTest(t) {
|
|
12
|
-
const fileParts = t.suite_title.split('.')
|
|
13
|
-
const example = t.title.match(/\[(.*)\]/)?.[1];
|
|
14
|
-
if (example) t.example = { "#": example }
|
|
11
|
+
const fileParts = t.suite_title.split('.');
|
|
15
12
|
|
|
16
13
|
t.file = namespaceToFileName(t.suite_title);
|
|
17
14
|
t.title = t.title.split('(')[0];
|
|
18
|
-
|
|
15
|
+
|
|
16
|
+
// detect params
|
|
17
|
+
const paramMatches = t.title.match(/\[(.*?)\]/g);
|
|
18
|
+
|
|
19
|
+
if (paramMatches) {
|
|
20
|
+
const params = paramMatches.map((_match, index) => `param${index + 1}`);
|
|
21
|
+
if (params.length === 1) params[0] = 'param';
|
|
22
|
+
let paramIndex = 0;
|
|
23
|
+
|
|
24
|
+
t.title = t.title.replace(/: \[(.*?)\]/g, () => {
|
|
25
|
+
if (params.length < 2) return `\${param}`;
|
|
26
|
+
const paramName = params[paramIndex] || `param${paramIndex + 1}`;
|
|
27
|
+
paramIndex++;
|
|
28
|
+
return `\${${paramName}}`;
|
|
29
|
+
});
|
|
30
|
+
const example = {};
|
|
31
|
+
paramMatches.forEach((match, index) => {
|
|
32
|
+
example[params[index]] = match.replace(/[[\]]/g, '');
|
|
33
|
+
});
|
|
34
|
+
t.example = example;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
t.suite_title = fileParts[fileParts.length - 1].replace(/\$/g, ' | ');
|
|
19
38
|
return t;
|
|
20
39
|
}
|
|
21
40
|
|
|
22
|
-
formatStack(t) {
|
|
23
|
-
|
|
41
|
+
// formatStack(t) {
|
|
42
|
+
// const stack = super.formatStack(t);
|
|
24
43
|
|
|
25
|
-
|
|
44
|
+
// const file = t.suite_title.split('.');
|
|
26
45
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
46
|
+
// const fileLine = `at .*${file[file.length - 1]}\.java:(\\d+)` // eslint-disable-line no-useless-escape
|
|
47
|
+
// const regexp = new RegExp(fileLine,"g")
|
|
48
|
+
// return stack.replace(regexp, `${this.opts.javaTests}${path.sep}${namespaceToFileName(t.suite_title)}:$1:`);
|
|
49
|
+
// }
|
|
31
50
|
}
|
|
32
51
|
|
|
33
52
|
function namespaceToFileName(fileName) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
53
|
+
const fileParts = fileName.split('.');
|
|
54
|
+
fileParts[fileParts.length - 1] = fileParts[fileParts.length - 1]?.replace(/\$.*/, '');
|
|
55
|
+
return `${fileParts.join(path.sep)}.java`;
|
|
38
56
|
}
|
|
39
57
|
|
|
40
58
|
module.exports = JavaAdapter;
|
|
@@ -3,12 +3,11 @@ const path = require('path');
|
|
|
3
3
|
const Adapter = require('./adapter');
|
|
4
4
|
|
|
5
5
|
class JavaScriptAdapter extends Adapter {
|
|
6
|
-
|
|
7
6
|
formatStack(t) {
|
|
8
7
|
let stack = super.formatStack(t);
|
|
9
8
|
|
|
10
9
|
try {
|
|
11
|
-
const error = new Error(stack.split('\n')[0])
|
|
10
|
+
const error = new Error(stack.split('\n')[0]);
|
|
12
11
|
error.stack = stack;
|
|
13
12
|
const record = createCallsiteRecord({
|
|
14
13
|
forError: error,
|