@testomatio/reporter 1.1.0-beta.mocha-create.9 → 1.1.0-rc.2
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 +11 -17
- package/lib/adapter/codecept.js +81 -6
- package/lib/adapter/cucumber/current.js +15 -8
- package/lib/adapter/cucumber/legacy.js +2 -1
- package/lib/adapter/cypress-plugin/index.js +46 -26
- package/lib/adapter/jest.js +46 -8
- package/lib/adapter/mocha.js +58 -5
- package/lib/adapter/playwright.js +21 -9
- package/lib/bin/startTest.js +2 -1
- package/lib/client.js +55 -46
- package/lib/config.js +5 -0
- package/lib/data-storage.js +203 -0
- package/lib/fileUploader.js +7 -4
- package/lib/junit-adapter/index.js +3 -2
- package/lib/pipe/testomatio.js +2 -1
- package/lib/reporter-functions.js +12 -9
- package/lib/reporter.js +8 -12
- package/lib/services/artifacts.js +57 -0
- package/lib/services/index.js +13 -0
- package/lib/{storages/key-value-storage.js → services/key-values.js} +19 -19
- package/lib/{storages → services}/logger.js +34 -21
- package/lib/utils/utils.js +13 -4
- package/lib/xmlReader.js +106 -104
- package/package.json +2 -2
- package/Changelog.md +0 -367
- package/lib/storages/artifact-storage.js +0 -70
- package/lib/storages/data-storage.js +0 -307
package/lib/client.js
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
const debug = require('debug')('@testomatio/reporter:client');
|
|
2
2
|
const createCallsiteRecord = require('callsite-record');
|
|
3
3
|
const { sep, join } = require('path');
|
|
4
|
-
const { minimatch } = require('minimatch')
|
|
4
|
+
const { minimatch } = require('minimatch');
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const chalk = require('chalk');
|
|
7
7
|
const { randomUUID } = require('crypto');
|
|
8
8
|
const upload = require('./fileUploader');
|
|
9
9
|
const { APP_PREFIX } = require('./constants');
|
|
10
10
|
const pipesFactory = require('./pipe');
|
|
11
|
-
const artifactStorage = require('./storages/artifact-storage');
|
|
12
11
|
|
|
13
12
|
/**
|
|
14
13
|
* @typedef {import('../types').TestData} TestData
|
|
@@ -36,7 +35,7 @@ class Client {
|
|
|
36
35
|
|
|
37
36
|
/**
|
|
38
37
|
* Asynchronously prepares the execution list for running tests through various pipes.
|
|
39
|
-
* Each pipe in the client is checked for enablement,
|
|
38
|
+
* Each pipe in the client is checked for enablement,
|
|
40
39
|
* and if all pipes are disabled, the function returns a resolved Promise.
|
|
41
40
|
* Otherwise, it executes the `prepareRun` method for each enabled pipe and collects the results.
|
|
42
41
|
* The results are then filtered to remove any undefined values.
|
|
@@ -46,7 +45,7 @@ class Client {
|
|
|
46
45
|
* @param {Object} params - The options for preparing the test execution list.
|
|
47
46
|
* @param {string} params.pipe - Name of the executed pipe.
|
|
48
47
|
* @param {string} params.pipeOptions - Filter option.
|
|
49
|
-
* @returns {Promise<any>} - A Promise that resolves to an
|
|
48
|
+
* @returns {Promise<any>} - A Promise that resolves to an
|
|
50
49
|
* array containing the prepared execution list,
|
|
51
50
|
* or resolves to undefined if no valid results are found or if all pipes are disabled.
|
|
52
51
|
*/
|
|
@@ -63,22 +62,22 @@ class Client {
|
|
|
63
62
|
if (!filterPipe.isEnabled) {
|
|
64
63
|
// TODO:for the future for the another pipes
|
|
65
64
|
console.warn(
|
|
66
|
-
APP_PREFIX,
|
|
67
|
-
`At the moment processing is available only for the "testomatio" key. Example: "testomatio:tag-name=xxx"
|
|
65
|
+
APP_PREFIX,
|
|
66
|
+
`At the moment processing is available only for the "testomatio" key. Example: "testomatio:tag-name=xxx"`,
|
|
68
67
|
);
|
|
69
68
|
return;
|
|
70
69
|
}
|
|
71
70
|
|
|
72
|
-
const results = await Promise.all(
|
|
73
|
-
({ pipe: p.toString(), result: await p.prepareRun(pipeOptions) })
|
|
74
|
-
)
|
|
75
|
-
|
|
71
|
+
const results = await Promise.all(
|
|
72
|
+
this.pipes.map(async p => ({ pipe: p.toString(), result: await p.prepareRun(pipeOptions) })),
|
|
73
|
+
);
|
|
74
|
+
|
|
76
75
|
const result = results.filter(p => p.pipe.includes('Testomatio'))[0]?.result;
|
|
77
76
|
|
|
78
77
|
if (!result || result.length === 0) {
|
|
79
78
|
return;
|
|
80
|
-
}
|
|
81
|
-
|
|
79
|
+
}
|
|
80
|
+
|
|
82
81
|
debug('Execution tests list', result);
|
|
83
82
|
|
|
84
83
|
return result;
|
|
@@ -135,41 +134,28 @@ class Client {
|
|
|
135
134
|
suite_title,
|
|
136
135
|
suite_id,
|
|
137
136
|
test_id,
|
|
137
|
+
manuallyAttachedArtifacts,
|
|
138
|
+
meta,
|
|
138
139
|
} = testData;
|
|
139
140
|
let { message = '' } = testData;
|
|
140
141
|
|
|
141
|
-
const uploadedFiles = [];
|
|
142
|
-
|
|
143
|
-
let stack = '';
|
|
144
142
|
|
|
143
|
+
let errorFormatted = '';
|
|
145
144
|
if (error) {
|
|
146
|
-
|
|
145
|
+
errorFormatted += this.formatError(error) || '';
|
|
147
146
|
message = error?.message;
|
|
148
147
|
}
|
|
149
|
-
if (steps) {
|
|
150
|
-
stack = this.formatSteps(stack, steps);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
stack += testData.stack || '';
|
|
154
148
|
|
|
155
|
-
//
|
|
156
|
-
|
|
157
|
-
const logger = require('./storages/logger');
|
|
158
|
-
const testLogs = logger.getLogs(test_id);
|
|
159
|
-
// debug(`Test logs for ${test_id}:\n`, testLogs);
|
|
160
|
-
if (stack) stack += '\n\n';
|
|
161
|
-
stack += testLogs ? `${chalk.bold('Logs:')}\n${testLogs}` : '';
|
|
149
|
+
// Attach logs
|
|
150
|
+
const fullLogs = this.formatLogs({ error: errorFormatted, steps, logs: testData.logs });
|
|
162
151
|
|
|
163
|
-
//
|
|
164
|
-
|
|
165
|
-
if (artifactFiles) files.push(...artifactFiles);
|
|
152
|
+
// add artifacts
|
|
153
|
+
if (manuallyAttachedArtifacts?.length) files.push(...manuallyAttachedArtifacts);
|
|
166
154
|
|
|
167
|
-
|
|
168
|
-
const keyValueStorage = require('./storages/key-value-storage');
|
|
169
|
-
const keyValues = keyValueStorage.get(test_id);
|
|
155
|
+
const uploadedFiles = [];
|
|
170
156
|
|
|
171
|
-
for (const
|
|
172
|
-
uploadedFiles.push(upload.uploadFileByPath(
|
|
157
|
+
for (const f of files) {
|
|
158
|
+
uploadedFiles.push(upload.uploadFileByPath(f, this.uuid));
|
|
173
159
|
}
|
|
174
160
|
|
|
175
161
|
for (const [idx, buffer] of filesBuffers.entries()) {
|
|
@@ -177,9 +163,16 @@ class Client {
|
|
|
177
163
|
uploadedFiles.push(upload.uploadFileAsBuffer(buffer, fileName, this.uuid));
|
|
178
164
|
}
|
|
179
165
|
|
|
180
|
-
const artifacts = await Promise.all(uploadedFiles);
|
|
166
|
+
const artifacts = (await Promise.all(uploadedFiles)).filter(n => !!n);
|
|
181
167
|
|
|
182
|
-
|
|
168
|
+
if (artifacts.length < uploadedFiles.length) {
|
|
169
|
+
console.log(
|
|
170
|
+
APP_PREFIX,
|
|
171
|
+
chalk.yellow(
|
|
172
|
+
`Some artifacts were not uploaded. ${artifacts.length}/${uploadedFiles.length} artifacts uploaded.`,
|
|
173
|
+
),
|
|
174
|
+
);
|
|
175
|
+
}
|
|
183
176
|
|
|
184
177
|
this.totalUploaded += uploadedFiles.filter(n => n).length;
|
|
185
178
|
|
|
@@ -187,7 +180,7 @@ class Client {
|
|
|
187
180
|
files,
|
|
188
181
|
steps,
|
|
189
182
|
status,
|
|
190
|
-
stack,
|
|
183
|
+
stack: fullLogs,
|
|
191
184
|
example,
|
|
192
185
|
file,
|
|
193
186
|
code,
|
|
@@ -198,9 +191,11 @@ class Client {
|
|
|
198
191
|
message,
|
|
199
192
|
run_time: parseFloat(time),
|
|
200
193
|
artifacts,
|
|
201
|
-
meta
|
|
194
|
+
meta,
|
|
202
195
|
};
|
|
203
196
|
|
|
197
|
+
debug('Adding test run...', data);
|
|
198
|
+
|
|
204
199
|
this.queue = this.queue.then(() =>
|
|
205
200
|
Promise.all(
|
|
206
201
|
this.pipes.map(async p => {
|
|
@@ -252,15 +247,27 @@ class Client {
|
|
|
252
247
|
return this.queue;
|
|
253
248
|
}
|
|
254
249
|
|
|
255
|
-
|
|
256
|
-
|
|
250
|
+
/**
|
|
251
|
+
* Returns the formatted stack including the stack trace, steps, and logs.
|
|
252
|
+
* @returns {string}
|
|
253
|
+
*/
|
|
254
|
+
formatLogs({ error, steps, logs }) {
|
|
255
|
+
error = error?.trim();
|
|
256
|
+
steps = steps?.trim();
|
|
257
|
+
logs = logs?.trim();
|
|
258
|
+
|
|
259
|
+
let testLogs = '';
|
|
260
|
+
if (steps) testLogs += `${chalk.bold.blue('################[ Steps ]################')}\n${steps}\n\n`;
|
|
261
|
+
if (logs) testLogs += `${chalk.bold.gray('################[ Logs ]################')}\n${logs}\n\n`;
|
|
262
|
+
if (error) testLogs += `${chalk.bold.red('################[ Failure ]################')}\n${error}`;
|
|
263
|
+
return testLogs;
|
|
257
264
|
}
|
|
258
265
|
|
|
259
266
|
formatError(error, message) {
|
|
260
267
|
if (!message) message = error.message;
|
|
261
268
|
if (error.inspect) message = error.inspect() || '';
|
|
262
269
|
|
|
263
|
-
let stack =
|
|
270
|
+
let stack = `${message}\n`;
|
|
264
271
|
|
|
265
272
|
// diffs for mocha, cypress, codeceptjs style
|
|
266
273
|
if (error.actual && error.expected) {
|
|
@@ -277,11 +284,11 @@ class Client {
|
|
|
277
284
|
const record = createCallsiteRecord({
|
|
278
285
|
forError: error,
|
|
279
286
|
isCallsiteFrame: frame => {
|
|
280
|
-
if (customFilter && minimatch(frame.getFileName(), customFilter)) return false;
|
|
287
|
+
if (customFilter && minimatch(frame.getFileName(), customFilter)) return false;
|
|
281
288
|
if (hasFrame) return false;
|
|
282
289
|
if (isNotInternalFrame(frame)) hasFrame = true;
|
|
283
290
|
return hasFrame;
|
|
284
|
-
}
|
|
291
|
+
},
|
|
285
292
|
});
|
|
286
293
|
if (record && !record.filename.startsWith('http')) {
|
|
287
294
|
stack += record.renderSync({ stackFilter: isNotInternalFrame });
|
|
@@ -294,9 +301,11 @@ class Client {
|
|
|
294
301
|
}
|
|
295
302
|
|
|
296
303
|
function isNotInternalFrame(frame) {
|
|
297
|
-
return
|
|
304
|
+
return (
|
|
305
|
+
frame.getFileName().includes(sep) &&
|
|
298
306
|
!frame.getFileName().includes('node_modules') &&
|
|
299
307
|
!frame.getFileName().includes('internal')
|
|
308
|
+
);
|
|
300
309
|
}
|
|
301
310
|
|
|
302
311
|
module.exports = Client;
|
package/lib/config.js
CHANGED
|
@@ -8,6 +8,10 @@ const debug = require('debug')('@testomatio/reporter:config');
|
|
|
8
8
|
const dotenv = require('dotenv');
|
|
9
9
|
const envFileVars = dotenv.config({ path: '.env' }).parsed; */
|
|
10
10
|
|
|
11
|
+
const TESTOMATIO = process.env.TESTOMATIO || process.env.TESTOMATIO_API_KEY || process.env.TESTOMATIO_TOKEN || '';
|
|
12
|
+
if (TESTOMATIO === 'undefined') console.error('TESTOMATIO is "undefined". Something went wrong. Contact dev team.');
|
|
13
|
+
process.env.TESTOMATIO = TESTOMATIO;
|
|
14
|
+
|
|
11
15
|
// select only TESTOMATIO related variables (only to print them in debug)
|
|
12
16
|
const testomatioEnvVars =
|
|
13
17
|
Object.keys(process.env)
|
|
@@ -22,3 +26,4 @@ debug('TESTOMATIO variables:', testomatioEnvVars);
|
|
|
22
26
|
const config = process.env;
|
|
23
27
|
|
|
24
28
|
module.exports = config;
|
|
29
|
+
module.exports.TESTOMATIO = TESTOMATIO;
|
|
@@ -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, jestHelpers } = require('./utils/utils');
|
|
8
|
+
|
|
9
|
+
class DataStorage {
|
|
10
|
+
static #instance;
|
|
11
|
+
|
|
12
|
+
context;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
*
|
|
16
|
+
* @returns {DataStorage}
|
|
17
|
+
*/
|
|
18
|
+
static getInstance() {
|
|
19
|
+
if (!this.#instance) {
|
|
20
|
+
this.#instance = new DataStorage();
|
|
21
|
+
}
|
|
22
|
+
return this.#instance;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
setContext(context) {
|
|
26
|
+
this.context = context;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Creates data storage instance as singleton
|
|
31
|
+
* Stores data to global variable or to file depending on what is applicable for current test runner (adapter)
|
|
32
|
+
* Recommend to use composition while using this class (instead of inheritance).
|
|
33
|
+
* ! Also the class which will use data storage should be singleton (to avoid data loss).
|
|
34
|
+
*/
|
|
35
|
+
constructor() {
|
|
36
|
+
// some frameworks use global variable to store data, some use file storage
|
|
37
|
+
this.isFileStorage = true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
#stringToFilename(str) {
|
|
41
|
+
// TODO: use md5 hash later
|
|
42
|
+
const validFilenameRegex = /[^a-zA-Z0-9_.-]/g;
|
|
43
|
+
// replace all characters not in the regex above with underscore, then duplicate underscores removed
|
|
44
|
+
let filename = str.replace(validFilenameRegex, '_').replace(/_{2,}/g, '_').substring(0, 255); // max filename length
|
|
45
|
+
// remove leading and trailing underscores
|
|
46
|
+
filename = filename.replace(/^_+|_+$/g, '');
|
|
47
|
+
return filename;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Puts any data to storage (file or global variable).
|
|
52
|
+
* If file: stores data as text, if global variable – stores as array of data.
|
|
53
|
+
* @param {'log' | 'artifact' | 'keyvalue'} dataType
|
|
54
|
+
* @param {*} data anything you want to store (string, object, array, etc)
|
|
55
|
+
* @param {*} context could be testId or any context (test name, suite name, including their IDs etc)
|
|
56
|
+
* suite name + test name is used by default
|
|
57
|
+
* @returns
|
|
58
|
+
*/
|
|
59
|
+
putData(dataType, data, context = null) {
|
|
60
|
+
if (!dataType || !data) return;
|
|
61
|
+
|
|
62
|
+
context = context || this.context || global.testomatioTestTitle || jestHelpers.getIdOfCurrentlyRunningTest();
|
|
63
|
+
if (!context) {
|
|
64
|
+
debug(`No context provided for "${dataType}" data:`, data);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
context = this.#stringToFilename(context);
|
|
68
|
+
|
|
69
|
+
if (this.isFileStorage) {
|
|
70
|
+
const dataDirPath = path.join(TESTOMAT_TMP_STORAGE_DIR, dataType);
|
|
71
|
+
fileSystem.createDir(dataDirPath);
|
|
72
|
+
this.#putDataToFile(dataType, data, context);
|
|
73
|
+
} else {
|
|
74
|
+
this.#putDataToGlobalVar(dataType, data, context);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Returns data, stored for specific test/context (or data which was stored without test id specified).
|
|
80
|
+
* This method will get data from global variable and/or from from file (previosly saved with put method).
|
|
81
|
+
*
|
|
82
|
+
* @param {'log' | 'artifact' | 'keyvalue'} dataType
|
|
83
|
+
* @param {string} context
|
|
84
|
+
* @returns {any []} array of data (any type), null (if no data found for context) or string (if data type is log)
|
|
85
|
+
*/
|
|
86
|
+
getData(dataType, context) {
|
|
87
|
+
// TODO: think if it could be useful
|
|
88
|
+
// context = context || this.context || global.testomatioTestTitle || jestHelpers.getIdOfCurrentlyRunningTest();
|
|
89
|
+
|
|
90
|
+
if (!context) {
|
|
91
|
+
debug(`Trying to get "${dataType}" data without context`);
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
context = this.#stringToFilename(context);
|
|
96
|
+
|
|
97
|
+
let testDataFromFile = [];
|
|
98
|
+
let testDataFromGlobalVar = [];
|
|
99
|
+
|
|
100
|
+
if (global?.testomatioDataStore) {
|
|
101
|
+
testDataFromGlobalVar = this.#getDataFromGlobalVar(dataType, context);
|
|
102
|
+
if (testDataFromGlobalVar) {
|
|
103
|
+
if (testDataFromGlobalVar.length) return testDataFromGlobalVar;
|
|
104
|
+
}
|
|
105
|
+
// don't return nothing if no data in global variable
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
testDataFromFile = this.#getDataFromFile(dataType, context);
|
|
109
|
+
|
|
110
|
+
if (testDataFromFile.length) {
|
|
111
|
+
return testDataFromFile;
|
|
112
|
+
}
|
|
113
|
+
debug(`No "${dataType}" data for context "${context}" in both file and global variable`);
|
|
114
|
+
|
|
115
|
+
// in case no data found for context
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* @param {'log' | 'artifact' | 'keyvalue'} dataType
|
|
121
|
+
* @param {string} context
|
|
122
|
+
* @returns aray of data (any type)
|
|
123
|
+
*/
|
|
124
|
+
#getDataFromGlobalVar(dataType, context) {
|
|
125
|
+
try {
|
|
126
|
+
if (global?.testomatioDataStore[dataType]) {
|
|
127
|
+
const testData = global.testomatioDataStore[dataType][context];
|
|
128
|
+
if (testData) debug(`"${dataType}" data for constext "${context}":`, testData.join(', '));
|
|
129
|
+
return testData || [];
|
|
130
|
+
}
|
|
131
|
+
// debug(`No ${this.dataType} data for context ${context} in <global> storage`);
|
|
132
|
+
return [];
|
|
133
|
+
} catch (e) {
|
|
134
|
+
// there could be no data, ignore
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* @param {'log' | 'artifact' | 'keyvalue'} dataType
|
|
140
|
+
* @param {*} context
|
|
141
|
+
* @returns array of data (any type)
|
|
142
|
+
*/
|
|
143
|
+
#getDataFromFile(dataType, context) {
|
|
144
|
+
const dataDirPath = path.join(TESTOMAT_TMP_STORAGE_DIR, dataType);
|
|
145
|
+
try {
|
|
146
|
+
const filepath = join(dataDirPath, `${dataType}_${context}`);
|
|
147
|
+
if (fs.existsSync(filepath)) {
|
|
148
|
+
const testDataAsText = fs.readFileSync(filepath, 'utf-8');
|
|
149
|
+
if (testDataAsText) debug(`"${dataType}" data for context "${context}":`, testDataAsText);
|
|
150
|
+
const testDataArr = testDataAsText?.split(os.EOL) || [];
|
|
151
|
+
return testDataArr;
|
|
152
|
+
}
|
|
153
|
+
// debug(`No ${this.dataType} data for ${context} in <file> storage`);
|
|
154
|
+
return [];
|
|
155
|
+
} catch (e) {
|
|
156
|
+
// there could be no data, ignore
|
|
157
|
+
}
|
|
158
|
+
return [];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Puts data to global variable. Unlike the file storage, stores data in array (file storage just append as string).
|
|
163
|
+
* @param {'log' | 'artifact' | 'keyvalue'} dataType
|
|
164
|
+
* @param {*} data
|
|
165
|
+
* @param {*} context
|
|
166
|
+
*/
|
|
167
|
+
#putDataToGlobalVar(dataType, data, context) {
|
|
168
|
+
debug('Saving data to global variable for ', context, ':', data);
|
|
169
|
+
if (!global.testomatioDataStore) global.testomatioDataStore = {};
|
|
170
|
+
if (!global.testomatioDataStore?.[dataType]) global.testomatioDataStore[dataType] = {};
|
|
171
|
+
|
|
172
|
+
if (!global.testomatioDataStore?.[dataType][context]) global.testomatioDataStore[dataType][context] = [];
|
|
173
|
+
global.testomatioDataStore[dataType][context].push(data);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Puts data to file. Unlike the global variable storage, stores data as string
|
|
178
|
+
* @param {'log' | 'artifact' | 'keyvalue'} dataType
|
|
179
|
+
* @param {*} data
|
|
180
|
+
* @param {string} context
|
|
181
|
+
* @returns
|
|
182
|
+
*/
|
|
183
|
+
#putDataToFile(dataType, data, context) {
|
|
184
|
+
const dataDirPath = path.join(TESTOMAT_TMP_STORAGE_DIR, dataType);
|
|
185
|
+
if (typeof data !== 'string') data = JSON.stringify(data);
|
|
186
|
+
const filename = `${dataType}_${context}`;
|
|
187
|
+
const filepath = join(dataDirPath, filename);
|
|
188
|
+
if (!fs.existsSync(dataDirPath)) fileSystem.createDir(dataDirPath);
|
|
189
|
+
debug(`Saving data to file for context "${context}" to ${filepath}. Data: ${JSON.stringify(data)}`);
|
|
190
|
+
|
|
191
|
+
// append new line if file already exists (in this case its definitely includes some data)
|
|
192
|
+
if (fs.existsSync(filepath)) {
|
|
193
|
+
fs.appendFileSync(filepath, os.EOL + data, 'utf-8');
|
|
194
|
+
} else {
|
|
195
|
+
fs.writeFileSync(filepath, data, 'utf-8');
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
module.exports.dataStorage = DataStorage.getInstance();
|
|
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
|
@@ -146,8 +146,13 @@ const uploadUsingS3 = async (filePath, runId) => {
|
|
|
146
146
|
params
|
|
147
147
|
});
|
|
148
148
|
|
|
149
|
-
|
|
150
|
-
|
|
149
|
+
|
|
150
|
+
const link = await getS3LocationLink(out);
|
|
151
|
+
|
|
152
|
+
debug(`Succesfully uploaded ${filePath} => ${S3_BUCKET}/${Key} | URL: ${link}`);
|
|
153
|
+
|
|
154
|
+
return link;
|
|
155
|
+
}
|
|
151
156
|
catch (e) {
|
|
152
157
|
debug('S3 file uploading error: ', e);
|
|
153
158
|
|
|
@@ -279,8 +284,6 @@ const getS3LocationLink = async (out) => {
|
|
|
279
284
|
|
|
280
285
|
let s3Location = response?.Location;
|
|
281
286
|
|
|
282
|
-
debug('Uploaded response.Location', s3Location);
|
|
283
|
-
|
|
284
287
|
if (!s3Location) {
|
|
285
288
|
// TODO: out: a fallback case - remove after deeper testing
|
|
286
289
|
s3Location = out?.singleUploadResult?.Location;
|
|
@@ -6,8 +6,7 @@ const RubyAdapter = require('./ruby');
|
|
|
6
6
|
const CSharpAdapter = require('./csharp');
|
|
7
7
|
|
|
8
8
|
function AdapterFactory(lang, opts) {
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
|
|
11
10
|
if (lang === 'java') {
|
|
12
11
|
return new JavaAdapter(opts);
|
|
13
12
|
}
|
|
@@ -23,6 +22,8 @@ function AdapterFactory(lang, opts) {
|
|
|
23
22
|
if (lang === 'c#' || lang === 'csharp') {
|
|
24
23
|
return new CSharpAdapter(opts);
|
|
25
24
|
}
|
|
25
|
+
|
|
26
|
+
return new Adapter(opts);
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
module.exports = AdapterFactory;
|
package/lib/pipe/testomatio.js
CHANGED
|
@@ -5,6 +5,7 @@ const JsonCycle = require('json-cycle');
|
|
|
5
5
|
const { APP_PREFIX, STATUS } = require('../constants');
|
|
6
6
|
const { isValidUrl, foundedTestLog } = require('../utils/utils');
|
|
7
7
|
const { parseFilterParams, generateFilterRequestParams, setS3Credentials, } = require('../utils/pipe_utils');
|
|
8
|
+
const { TESTOMATIO } = require('../config');
|
|
8
9
|
|
|
9
10
|
if (process.env.TESTOMATIO_RUN) {
|
|
10
11
|
process.env.runId = process.env.TESTOMATIO_RUN;
|
|
@@ -20,7 +21,7 @@ class TestomatioPipe {
|
|
|
20
21
|
constructor(params, store) {
|
|
21
22
|
this.isEnabled = false;
|
|
22
23
|
this.url = params.testomatioUrl || process.env.TESTOMATIO_URL || 'https://app.testomat.io';
|
|
23
|
-
this.apiKey = params.apiKey ||
|
|
24
|
+
this.apiKey = params.apiKey || TESTOMATIO;
|
|
24
25
|
debug('Testomatio Pipe: ', this.apiKey ? 'API KEY' : '*no api key*');
|
|
25
26
|
if (!this.apiKey) {
|
|
26
27
|
return;
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
const
|
|
2
|
-
const
|
|
3
|
-
|
|
1
|
+
const { services } = require('./services');
|
|
2
|
+
const { initPlaywrightForStorage } = require('./adapter/playwright');
|
|
3
|
+
|
|
4
|
+
if (process.env.PLAYWRIGHT_TEST_BASE_URL) {
|
|
5
|
+
initPlaywrightForStorage();
|
|
6
|
+
}
|
|
4
7
|
|
|
5
8
|
/**
|
|
6
9
|
* Stores path to file as artifact and uploads it to the S3 storage
|
|
@@ -8,7 +11,7 @@ const keyValueStorage = require('./storages/key-value-storage');
|
|
|
8
11
|
*/
|
|
9
12
|
function saveArtifact(data, context = null) {
|
|
10
13
|
if (!data) return;
|
|
11
|
-
|
|
14
|
+
services.artifacts.put(data, context);
|
|
12
15
|
}
|
|
13
16
|
|
|
14
17
|
/**
|
|
@@ -16,23 +19,23 @@ function saveArtifact(data, context = null) {
|
|
|
16
19
|
* @param {...any} args
|
|
17
20
|
*/
|
|
18
21
|
function logMessage(...args) {
|
|
19
|
-
logger.
|
|
22
|
+
services.logger.templateLiteralLog(...args);
|
|
20
23
|
}
|
|
21
24
|
|
|
22
25
|
/**
|
|
23
26
|
* Similar to "log" function but marks message in report as a step
|
|
24
|
-
* @param {*} message
|
|
27
|
+
* @param {*} message
|
|
25
28
|
*/
|
|
26
29
|
function addStep(message) {
|
|
27
|
-
logger.step(message);
|
|
30
|
+
services.logger.step(message);
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
/**
|
|
31
34
|
* Add key-value pair(s) to the test report
|
|
32
|
-
* @param {*} keyValue
|
|
35
|
+
* @param {*} keyValue
|
|
33
36
|
*/
|
|
34
37
|
function setKeyValue(keyValue) {
|
|
35
|
-
|
|
38
|
+
services.keyValues.put(keyValue);
|
|
36
39
|
}
|
|
37
40
|
|
|
38
41
|
module.exports = {
|
package/lib/reporter.js
CHANGED
|
@@ -1,23 +1,19 @@
|
|
|
1
|
-
const logger = require('./storages/logger');
|
|
2
1
|
const TestomatClient = require('./client');
|
|
3
2
|
const TRConstants = require('./constants');
|
|
4
3
|
|
|
5
|
-
const log = logger.templateLiteralLog.bind(logger);
|
|
6
|
-
const step = logger.step.bind(logger);
|
|
7
4
|
const reporterFunctions = require('./reporter-functions');
|
|
8
5
|
|
|
9
6
|
module.exports = {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
log,
|
|
13
|
-
step,
|
|
14
|
-
TestomatClient,
|
|
15
|
-
TRConstants,
|
|
16
|
-
};
|
|
7
|
+
// TODO: deprecate in future; use log or testomat.log
|
|
8
|
+
logger: reporterFunctions.log,
|
|
9
|
+
testomatioLogger: reporterFunctions.log,
|
|
17
10
|
|
|
18
|
-
module.exports.testomat = {
|
|
19
11
|
artifact: reporterFunctions.artifact,
|
|
20
12
|
log: reporterFunctions.log,
|
|
21
|
-
step: reporterFunctions.step,
|
|
22
13
|
meta: reporterFunctions.keyValue,
|
|
14
|
+
step: reporterFunctions.step,
|
|
15
|
+
|
|
16
|
+
TestomatClient,
|
|
17
|
+
TRConstants,
|
|
23
18
|
};
|
|
19
|
+
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const debug = require('debug')('@testomatio/reporter:services-artifacts');
|
|
2
|
+
const { dataStorage } = require('../data-storage');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Artifact storage is supposed to store file paths
|
|
6
|
+
*/
|
|
7
|
+
class ArtifactStorage {
|
|
8
|
+
static #instance;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Singleton
|
|
12
|
+
* @returns {ArtifactStorage}
|
|
13
|
+
*/
|
|
14
|
+
static getInstance() {
|
|
15
|
+
if (!this.#instance) {
|
|
16
|
+
this.#instance = new ArtifactStorage();
|
|
17
|
+
}
|
|
18
|
+
return this.#instance;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Stores path to file as artifact and uploads it to the S3 storage
|
|
23
|
+
* @param {string | {path: string, type: string, name: string}} data - path to file or object with path, type and name
|
|
24
|
+
* @param {*} context testId or test title
|
|
25
|
+
*/
|
|
26
|
+
put(data, context = null) {
|
|
27
|
+
if (!data) return;
|
|
28
|
+
debug('Save artifact:', data);
|
|
29
|
+
dataStorage.putData('artifact', data, context);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Returns list of artifacts to upload
|
|
34
|
+
* @param {*} context testId or test context from test runner
|
|
35
|
+
* @returns {(string | {path: string, type: string, name: string})[]}
|
|
36
|
+
*/
|
|
37
|
+
get(context) {
|
|
38
|
+
let artifacts = dataStorage.getData('artifact', context);
|
|
39
|
+
if (!artifacts || !artifacts.length) return [];
|
|
40
|
+
|
|
41
|
+
artifacts = artifacts.map(artifactData => {
|
|
42
|
+
// artifact could be an object ({type, path, name} props) or string (just path)
|
|
43
|
+
let artifact;
|
|
44
|
+
try {
|
|
45
|
+
artifact = JSON.parse(artifactData);
|
|
46
|
+
} catch (e) {
|
|
47
|
+
artifact = artifactData;
|
|
48
|
+
}
|
|
49
|
+
return artifact;
|
|
50
|
+
});
|
|
51
|
+
artifacts = artifacts.filter(artifact => !!artifact);
|
|
52
|
+
debug(`Artifacts for test ${context}:`, artifacts);
|
|
53
|
+
return artifacts.length ? artifacts : [];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports.artifactStorage = ArtifactStorage.getInstance();
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const { logger } = require('./logger');
|
|
2
|
+
const { artifactStorage } = require('./artifacts');
|
|
3
|
+
const { keyValueStorage } = require('./key-values');
|
|
4
|
+
const { dataStorage } = require('../data-storage');
|
|
5
|
+
|
|
6
|
+
module.exports.services = {
|
|
7
|
+
logger,
|
|
8
|
+
artifacts: artifactStorage,
|
|
9
|
+
keyValues: keyValueStorage,
|
|
10
|
+
setContext: context => {
|
|
11
|
+
dataStorage.setContext(context);
|
|
12
|
+
},
|
|
13
|
+
};
|