@testomatio/reporter 1.0.0 → 1.0.1-6.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 +92 -1
- package/README.md +20 -10
- package/lib/{ArtifactStorage.js → _ArtifactStorageOld.js} +16 -5
- package/lib/adapter/codecept.js +59 -39
- package/lib/adapter/cucumber/current.js +95 -59
- package/lib/adapter/cucumber/legacy.js +25 -11
- package/lib/adapter/cypress-plugin/index.js +1 -1
- package/lib/adapter/jasmine.js +1 -1
- package/lib/adapter/jest.js +30 -2
- package/lib/adapter/mocha.js +24 -24
- package/lib/adapter/playwright.js +18 -11
- package/lib/adapter/webdriver.js +1 -1
- package/lib/artifactStorage.js +25 -0
- package/lib/bin/reportXml.js +1 -0
- package/lib/bin/startTest.js +25 -4
- package/lib/client.js +114 -39
- package/lib/constants.js +5 -0
- package/lib/dataStorage.js +232 -0
- package/lib/junit-adapter/java.js +27 -9
- package/lib/logger.js +319 -0
- package/lib/pipe/csv.js +4 -1
- package/lib/pipe/github.js +24 -37
- package/lib/pipe/gitlab.js +5 -16
- package/lib/pipe/testomatio.js +138 -34
- package/lib/reporter.js +9 -1
- package/lib/utils/pipe_utils.js +135 -0
- package/lib/utils/utils.js +310 -0
- package/lib/xmlReader.js +132 -44
- package/package.json +9 -6
- package/lib/util.js +0 -184
package/lib/bin/startTest.js
CHANGED
|
@@ -13,9 +13,11 @@ program
|
|
|
13
13
|
.option('--launch', 'Start a new run and return its ID')
|
|
14
14
|
.option('--finish', 'Finish Run by its ID')
|
|
15
15
|
.option("--env-file <envfile>", "Load environment variables from env file")
|
|
16
|
-
.
|
|
16
|
+
.option("--filter <filter>", "Additional execution filter")
|
|
17
|
+
.action(async (opts) => {
|
|
18
|
+
const { launch, finish, filter } = opts;
|
|
19
|
+
let { command } = opts;
|
|
17
20
|
|
|
18
|
-
const { command, launch, finish } = opts;
|
|
19
21
|
if (opts.envFile) require('dotenv').config(opts.envFile); // eslint-disable-line
|
|
20
22
|
|
|
21
23
|
const apiKey = process.env['INPUT_TESTOMATIO-KEY'] || process.env.TESTOMATIO;
|
|
@@ -51,6 +53,27 @@ program
|
|
|
51
53
|
|
|
52
54
|
let exitCode = 0;
|
|
53
55
|
|
|
56
|
+
const client = new TestomatClient({ apiKey, title, parallel: true });
|
|
57
|
+
|
|
58
|
+
if(filter) {
|
|
59
|
+
const [pipe, ...optsArray] = filter.split(":");
|
|
60
|
+
const pipeOptions = optsArray.join(":");
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const tests = await client.prepareRun({pipe, pipeOptions});
|
|
64
|
+
|
|
65
|
+
if(!tests || tests.length === 0) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const grep = ` --grep (${tests.join('|')})`;
|
|
70
|
+
command += grep;
|
|
71
|
+
}
|
|
72
|
+
catch(err) {
|
|
73
|
+
console.log(APP_PREFIX, err);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
54
77
|
if (!command.split) {
|
|
55
78
|
process.exitCode = 255;
|
|
56
79
|
console.log(APP_PREFIX, `No command provided. Use -c option to launch a test runner.`);
|
|
@@ -73,8 +96,6 @@ program
|
|
|
73
96
|
return;
|
|
74
97
|
}
|
|
75
98
|
|
|
76
|
-
const client = new TestomatClient({ apiKey, title, parallel: true });
|
|
77
|
-
|
|
78
99
|
client.createRun().then(() => {
|
|
79
100
|
const cmd = spawn(testCmds[0], testCmds.slice(1), { stdio: 'inherit' });
|
|
80
101
|
|
package/lib/client.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
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
5
|
const fs = require('fs');
|
|
5
6
|
const chalk = require('chalk');
|
|
6
7
|
const { randomUUID } = require('crypto');
|
|
@@ -11,7 +12,7 @@ const pipesFactory = require('./pipe');
|
|
|
11
12
|
/**
|
|
12
13
|
* @typedef {import('../types').TestData} TestData
|
|
13
14
|
* @typedef {import('../types').RunStatus} RunStatus
|
|
14
|
-
* @typedef {import('../types').PipeResult} PipeResult
|
|
15
|
+
* @typedef {import('../types').PipeResult} PipeResult
|
|
15
16
|
*/
|
|
16
17
|
|
|
17
18
|
class Client {
|
|
@@ -21,57 +22,109 @@ class Client {
|
|
|
21
22
|
* @param {*} params
|
|
22
23
|
*/
|
|
23
24
|
constructor(params = {}) {
|
|
24
|
-
this.parallel = params.parallel;
|
|
25
25
|
const store = {};
|
|
26
26
|
this.uuid = randomUUID();
|
|
27
27
|
this.pipes = pipesFactory(params, store);
|
|
28
28
|
this.queue = Promise.resolve();
|
|
29
29
|
this.totalUploaded = 0;
|
|
30
30
|
this.version = JSON.parse(fs.readFileSync(join(__dirname, '..', 'package.json')).toString()).version;
|
|
31
|
+
this.executionList = Promise.resolve();
|
|
32
|
+
|
|
31
33
|
console.log(APP_PREFIX, `Testomatio Reporter v${this.version}`);
|
|
32
34
|
}
|
|
33
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Asynchronously prepares the execution list for running tests through various pipes.
|
|
38
|
+
* Each pipe in the client is checked for enablement,
|
|
39
|
+
* and if all pipes are disabled, the function returns a resolved Promise.
|
|
40
|
+
* Otherwise, it executes the `prepareRun` method for each enabled pipe and collects the results.
|
|
41
|
+
* The results are then filtered to remove any undefined values.
|
|
42
|
+
* If no valid results are found, the function returns undefined.
|
|
43
|
+
* Otherwise, it returns the first non-empty array from the filtered results.
|
|
44
|
+
*
|
|
45
|
+
* @param {Object} params - The options for preparing the test execution list.
|
|
46
|
+
* @param {string} params.pipe - Name of the executed pipe.
|
|
47
|
+
* @param {string} params.pipeOptions - Filter option.
|
|
48
|
+
* @returns {Promise<any>} - A Promise that resolves to an
|
|
49
|
+
* array containing the prepared execution list,
|
|
50
|
+
* or resolves to undefined if no valid results are found or if all pipes are disabled.
|
|
51
|
+
*/
|
|
52
|
+
async prepareRun(params) {
|
|
53
|
+
const { pipe, pipeOptions } = params;
|
|
54
|
+
// all pipes disabled, skipping
|
|
55
|
+
if (!this.pipes.some(p => p.isEnabled)) {
|
|
56
|
+
return Promise.resolve();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const filterPipe = this.pipes.find(p => p.constructor.name.toLowerCase() === `${pipe.toLowerCase()}pipe`);
|
|
61
|
+
|
|
62
|
+
if (!filterPipe.isEnabled) {
|
|
63
|
+
// TODO:for the future for the another pipes
|
|
64
|
+
console.warn(
|
|
65
|
+
APP_PREFIX,
|
|
66
|
+
`At the moment processing is available only for the "testomatio" key. Example: "testomatio:tag-name=xxx"`
|
|
67
|
+
);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const results = await Promise.all(this.pipes.map(async p =>
|
|
72
|
+
({ pipe: p.toString(), result: await p.prepareRun(pipeOptions) })
|
|
73
|
+
));
|
|
74
|
+
|
|
75
|
+
const result = results.filter(p => p.pipe.includes('Testomatio'))[0]?.result;
|
|
76
|
+
|
|
77
|
+
if (!result || result.length === 0) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
debug('Execution tests list', result);
|
|
82
|
+
|
|
83
|
+
return result;
|
|
84
|
+
} catch (err) {
|
|
85
|
+
console.error(APP_PREFIX, err);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
34
89
|
/**
|
|
35
90
|
* Used to create a new Test run
|
|
36
91
|
*
|
|
37
92
|
* @returns {Promise<any>} - resolves to Run id which should be used to update / add test
|
|
38
93
|
*/
|
|
39
94
|
createRun() {
|
|
95
|
+
debug('Creating run...');
|
|
40
96
|
// all pipes disabled, skipping
|
|
41
|
-
if (!this.pipes
|
|
42
|
-
|
|
43
|
-
const runParams = {
|
|
44
|
-
title: this.title,
|
|
45
|
-
parallel: this.parallel,
|
|
46
|
-
env: this.env,
|
|
47
|
-
};
|
|
97
|
+
if (!this.pipes?.filter(p => p.isEnabled).length) return Promise.resolve();
|
|
48
98
|
|
|
49
99
|
global.testomatioArtifacts = [];
|
|
100
|
+
if (!global.testomatioDataStore) global.testomatioDataStore = {};
|
|
50
101
|
|
|
51
102
|
this.queue = this.queue
|
|
52
|
-
.then(() => Promise.all(this.pipes.map(p => p.createRun(
|
|
103
|
+
.then(() => Promise.all(this.pipes.map(p => p.createRun())))
|
|
53
104
|
.catch(err => console.log(APP_PREFIX, err))
|
|
54
105
|
.then(() => undefined); // fixes return type
|
|
55
|
-
debug('Run', this.queue);
|
|
106
|
+
// debug('Run', this.queue);
|
|
56
107
|
return this.queue;
|
|
57
108
|
}
|
|
58
109
|
|
|
59
110
|
/**
|
|
60
111
|
* Updates test status and its data
|
|
61
|
-
*
|
|
112
|
+
*
|
|
62
113
|
* @param {string|undefined} status
|
|
63
114
|
* @param {TestData} [testData]
|
|
64
115
|
* @param {string[]} [storeArtifacts]
|
|
65
116
|
* @returns {Promise<PipeResult[]>}
|
|
66
117
|
*/
|
|
67
118
|
async addTestRun(status, testData, storeArtifacts = []) {
|
|
119
|
+
debug('Adding test run for test', testData?.test_id || 'unknown test');
|
|
68
120
|
// all pipes disabled, skipping
|
|
69
|
-
if (!this.pipes
|
|
121
|
+
if (!this.pipes?.filter(p => p.isEnabled).length) return [];
|
|
70
122
|
|
|
71
|
-
if (!testData)
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
123
|
+
if (!testData)
|
|
124
|
+
testData = {
|
|
125
|
+
title: 'Unknown test',
|
|
126
|
+
suite_title: 'Unknown suite',
|
|
127
|
+
};
|
|
75
128
|
|
|
76
129
|
const {
|
|
77
130
|
error = null,
|
|
@@ -100,13 +153,22 @@ class Client {
|
|
|
100
153
|
stack = this.formatSteps(stack, steps);
|
|
101
154
|
}
|
|
102
155
|
|
|
156
|
+
stack += testData.stack || '';
|
|
157
|
+
|
|
158
|
+
// in some cases (e.g. using cucumber) logger instance becomes empty object, if import at the top of the file
|
|
159
|
+
const logger = require('./logger');
|
|
160
|
+
const testLogs = logger.getLogs(test_id);
|
|
161
|
+
// debug(`Test logs for ${test_id}:\n`, testLogs);
|
|
162
|
+
if (stack) stack += '\n\n';
|
|
163
|
+
stack += testLogs ? `${chalk.bold('Logs:')}\n${testLogs}` : '';
|
|
164
|
+
|
|
103
165
|
if (Array.isArray(storeArtifacts) && storeArtifacts.length) {
|
|
104
|
-
debug(
|
|
166
|
+
debug('CLIENT storeArtifact', storeArtifacts);
|
|
105
167
|
files.push(...storeArtifacts);
|
|
106
168
|
}
|
|
107
169
|
|
|
108
170
|
if (Array.isArray(global.testomatioArtifacts)) {
|
|
109
|
-
debug(
|
|
171
|
+
debug('CLIENT global[testomatioArtifacts]', global.testomatioArtifacts);
|
|
110
172
|
files.push(...global.testomatioArtifacts);
|
|
111
173
|
global.testomatioArtifacts = [];
|
|
112
174
|
}
|
|
@@ -142,17 +204,19 @@ class Client {
|
|
|
142
204
|
artifacts,
|
|
143
205
|
};
|
|
144
206
|
|
|
145
|
-
this.queue = this.queue
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
207
|
+
this.queue = this.queue.then(() =>
|
|
208
|
+
Promise.all(
|
|
209
|
+
this.pipes.map(async p => {
|
|
210
|
+
try {
|
|
211
|
+
const result = await p.addTest(data);
|
|
212
|
+
return { pipe: p.toString(), result };
|
|
213
|
+
} catch (err) {
|
|
214
|
+
console.log(APP_PREFIX, p.toString(), err);
|
|
215
|
+
}
|
|
216
|
+
}),
|
|
217
|
+
),
|
|
218
|
+
);
|
|
219
|
+
|
|
156
220
|
return this.queue;
|
|
157
221
|
}
|
|
158
222
|
|
|
@@ -164,16 +228,17 @@ class Client {
|
|
|
164
228
|
* @returns {Promise<any>} - A Promise that resolves when finishes the run.
|
|
165
229
|
*/
|
|
166
230
|
updateRunStatus(status, isParallel = false) {
|
|
231
|
+
debug('Updating run status...');
|
|
167
232
|
// all pipes disabled, skipping
|
|
168
|
-
if (!this.pipes
|
|
233
|
+
if (!this.pipes?.filter(p => p.isEnabled).length) return Promise.resolve();
|
|
169
234
|
|
|
170
235
|
const runParams = { status, parallel: isParallel };
|
|
171
236
|
|
|
172
237
|
this.queue = this.queue
|
|
173
238
|
.then(() => Promise.all(this.pipes.map(p => p.finishRun(runParams))))
|
|
174
239
|
.then(() => {
|
|
175
|
-
debug(
|
|
176
|
-
|
|
240
|
+
debug('TOTAL uploaded files', this.totalUploaded);
|
|
241
|
+
|
|
177
242
|
if (upload.isArtifactsEnabled() && this.totalUploaded > 0) {
|
|
178
243
|
console.log(
|
|
179
244
|
APP_PREFIX,
|
|
@@ -206,17 +271,21 @@ class Client {
|
|
|
206
271
|
stack += '\n\n';
|
|
207
272
|
}
|
|
208
273
|
|
|
274
|
+
const customFilter = process.env.TESTOMATIO_STACK_IGNORE;
|
|
275
|
+
|
|
209
276
|
try {
|
|
277
|
+
let hasFrame = false;
|
|
210
278
|
const record = createCallsiteRecord({
|
|
211
279
|
forError: error,
|
|
280
|
+
isCallsiteFrame: frame => {
|
|
281
|
+
if (customFilter && minimatch(frame.getFileName(), customFilter)) return false;
|
|
282
|
+
if (hasFrame) return false;
|
|
283
|
+
if (isNotInternalFrame(frame)) hasFrame = true;
|
|
284
|
+
return hasFrame;
|
|
285
|
+
}
|
|
212
286
|
});
|
|
213
287
|
if (record && !record.filename.startsWith('http')) {
|
|
214
|
-
stack += record.renderSync({
|
|
215
|
-
stackFilter: frame =>
|
|
216
|
-
frame.getFileName().indexOf(sep) > -1 &&
|
|
217
|
-
frame.getFileName().indexOf('node_modules') < 0 &&
|
|
218
|
-
frame.getFileName().indexOf('internal') < 0,
|
|
219
|
-
});
|
|
288
|
+
stack += record.renderSync({ stackFilter: isNotInternalFrame });
|
|
220
289
|
}
|
|
221
290
|
return stack;
|
|
222
291
|
} catch (e) {
|
|
@@ -225,4 +294,10 @@ class Client {
|
|
|
225
294
|
}
|
|
226
295
|
}
|
|
227
296
|
|
|
297
|
+
function isNotInternalFrame(frame) {
|
|
298
|
+
return frame.getFileName().includes(sep) &&
|
|
299
|
+
!frame.getFileName().includes('node_modules') &&
|
|
300
|
+
!frame.getFileName().includes('internal')
|
|
301
|
+
}
|
|
302
|
+
|
|
228
303
|
module.exports = Client;
|
package/lib/constants.js
CHANGED
|
@@ -3,6 +3,10 @@ const chalk = require('chalk');
|
|
|
3
3
|
const APP_PREFIX = chalk.gray('[TESTOMATIO]');
|
|
4
4
|
const TESTOMAT_ARTIFACT_SUFFIX = "testomatio_artifact_";
|
|
5
5
|
|
|
6
|
+
const TESTOMAT_TMP_STORAGE = {
|
|
7
|
+
mainDir: "testomatio_tmp",
|
|
8
|
+
}
|
|
9
|
+
|
|
6
10
|
const CSV_HEADERS = [
|
|
7
11
|
{ id: 'suite_title', title: 'Suite_title' },
|
|
8
12
|
{ id: 'title', title: 'Title' },
|
|
@@ -21,6 +25,7 @@ const STATUS = {
|
|
|
21
25
|
module.exports = {
|
|
22
26
|
APP_PREFIX,
|
|
23
27
|
TESTOMAT_ARTIFACT_SUFFIX,
|
|
28
|
+
TESTOMAT_TMP_STORAGE,
|
|
24
29
|
CSV_HEADERS,
|
|
25
30
|
STATUS,
|
|
26
31
|
}
|
|
@@ -0,0 +1,232 @@
|
|
|
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 JestReporter = require('./adapter/jest');
|
|
7
|
+
const { TESTOMAT_TMP_STORAGE } = require('./constants');
|
|
8
|
+
const { fileSystem } = require('./utils/utils');
|
|
9
|
+
const getTestIdFromTestTitle = require('./utils/utils').parseTest;
|
|
10
|
+
|
|
11
|
+
class DataStorage {
|
|
12
|
+
/**
|
|
13
|
+
* Creates data storage instance for specific data type.
|
|
14
|
+
* Stores data to global variable or to file depending on what is applicable for current test runner
|
|
15
|
+
* (running environment).
|
|
16
|
+
* @param {*} dataType storage type (like 'log', 'artifact'); could be any string, used only to define file path
|
|
17
|
+
*/
|
|
18
|
+
constructor(dataType) {
|
|
19
|
+
this.dataType = dataType || 'data';
|
|
20
|
+
this.isFileStorage = true;
|
|
21
|
+
this.#refreshStorageType();
|
|
22
|
+
|
|
23
|
+
this.dataDirPath = path.join(TESTOMAT_TMP_STORAGE.mainDir, this.dataType);
|
|
24
|
+
fileSystem.createDir(this.dataDirPath);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Try to define the running environment. Not 100% reliable and used as additional check
|
|
29
|
+
* @returns jest | mocha | ...
|
|
30
|
+
*/
|
|
31
|
+
getRunningEnviroment() {
|
|
32
|
+
// jest
|
|
33
|
+
if (process.env.JEST_WORKER_ID) return 'jest';
|
|
34
|
+
|
|
35
|
+
if (global.codeceptjs) return 'codeceptjs';
|
|
36
|
+
|
|
37
|
+
// 'cucumber:current', 'cucumber:legacy'
|
|
38
|
+
if (global.testomatioRunningEnvironment) return global.testomatioRunningEnvironment;
|
|
39
|
+
|
|
40
|
+
if (process.env.PLAYWRIGHT_TEST_BASE_URL || process.env.TEST_WORKER_INDEX) return 'playwright';
|
|
41
|
+
|
|
42
|
+
// mocha - can't detect
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Puts any data to storage (file or global variable)
|
|
48
|
+
* @param {*} data anything you want to store
|
|
49
|
+
* @param {*} context could be testId or any context (test, suite, etc) which is used to define testId
|
|
50
|
+
* @returns
|
|
51
|
+
*/
|
|
52
|
+
putData(data, context = null) {
|
|
53
|
+
this.#refreshStorageType();
|
|
54
|
+
|
|
55
|
+
let testId = this.#tryToRetrieveTestId(context) || null;
|
|
56
|
+
|
|
57
|
+
if (this.runningEnvironment === 'codeceptjs') {
|
|
58
|
+
this.isFileStorage = false;
|
|
59
|
+
testId = testId ?? global.testomatioDataStore?.currentlyRunningTestId;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (this.runningEnvironment === 'jest') {
|
|
63
|
+
testId = testId ?? JestReporter.getIdOfCurrentlyRunningTest();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// logs in playwright are gathered by pw framework itself
|
|
67
|
+
if (this.runningEnvironment === 'playwright' && this.dataType === 'log') return;
|
|
68
|
+
|
|
69
|
+
// get id from global store
|
|
70
|
+
if (global.testomatioDataStore && global.testomatioDataStore.currentlyRunningTestId)
|
|
71
|
+
testId = testId ?? global.testomatioDataStore?.currentlyRunningTestId;
|
|
72
|
+
|
|
73
|
+
if (!testId && global?.currentlyRunningTestTitle)
|
|
74
|
+
testId = this.#tryToRetrieveTestId(global.currentlyRunningTestTitle);
|
|
75
|
+
|
|
76
|
+
if (!testId) {
|
|
77
|
+
debug(`No test id provided for ${this.dataType} data: ${data}`);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (this.isFileStorage) {
|
|
82
|
+
this.#putDataToFile(data, testId);
|
|
83
|
+
} else {
|
|
84
|
+
this.#putDataToGlobalVar(data, testId);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Returns data, stored for specific testId (or data which was stored without test id specified).
|
|
90
|
+
* This method will get data from global variable. But if it is not available, it will try to get data from file.
|
|
91
|
+
* Thus, good approach is to remove file storage folder before each test run (and after, for sure).
|
|
92
|
+
*
|
|
93
|
+
* Defining the execution environment is not guaranteed! Is used only as additional check.
|
|
94
|
+
* @param {*} context could be testId or any context (test, suite, etc) which is used to define testId
|
|
95
|
+
* @returns
|
|
96
|
+
*/
|
|
97
|
+
getData(context) {
|
|
98
|
+
this.#refreshStorageType();
|
|
99
|
+
|
|
100
|
+
let testId = null;
|
|
101
|
+
if (typeof context === 'string') {
|
|
102
|
+
testId = this.#tryToRetrieveTestId(context) || context;
|
|
103
|
+
} else {
|
|
104
|
+
// TODO: derive testId from context
|
|
105
|
+
// testId = context...
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!testId) {
|
|
109
|
+
debug(`Cannot get test id from passed context:`, context);
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
let testData = '';
|
|
114
|
+
|
|
115
|
+
if (global?.testomatioDataStore) {
|
|
116
|
+
testData = this.#getDataFromGlobalVar(testId);
|
|
117
|
+
// these frameworks use global variable storage
|
|
118
|
+
if (testData && ['cucumber', 'codeceptjs'].includes(this.runningEnvironment)) return testData;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/* condition is removed for mocha
|
|
122
|
+
mocha has created a global storage, but just in some cases, generally it is not available
|
|
123
|
+
*/
|
|
124
|
+
// if (this.isFileStorage || !testData) {
|
|
125
|
+
// testData = this.#getDataFromFile(testId);
|
|
126
|
+
// if (testData) return testData;
|
|
127
|
+
// }
|
|
128
|
+
|
|
129
|
+
const testDataFromFile = this.#getDataFromFile(testId);
|
|
130
|
+
testData += testDataFromFile || '';
|
|
131
|
+
|
|
132
|
+
debug(`No ${this.dataType} data for test id ${testId} in both file and global variable`);
|
|
133
|
+
return testData || '';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* This method is named as "try" because it does not guarantee that test id could be retrieved.
|
|
138
|
+
* Context could be anything (test, suite, string, etc) which is used to define testId.
|
|
139
|
+
* Or it could represent any other entity (which does not contain test id).
|
|
140
|
+
* @param {*} context
|
|
141
|
+
*/
|
|
142
|
+
#tryToRetrieveTestId(context) {
|
|
143
|
+
if (!context) return null;
|
|
144
|
+
this.#refreshStorageType();
|
|
145
|
+
|
|
146
|
+
if (this.runningEnvironment === 'playwright' || context?.title) {
|
|
147
|
+
// context is testInfo
|
|
148
|
+
const testId = getTestIdFromTestTitle(context.title);
|
|
149
|
+
if (testId) return testId;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (typeof context === 'string') {
|
|
153
|
+
const testId = getTestIdFromTestTitle(context);
|
|
154
|
+
if (testId) return testId;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Refreshes storage type (file or global variable) depending on current environment
|
|
162
|
+
* This method should be run on each attempt to put or get data from/to storage
|
|
163
|
+
* Because storage instance is created before the test runner is started. And storage type could be changed
|
|
164
|
+
*/
|
|
165
|
+
#refreshStorageType() {
|
|
166
|
+
this.runningEnvironment = this.getRunningEnviroment();
|
|
167
|
+
|
|
168
|
+
// some test frameworks do not persist global variables, thus file storage is used for them (by default)
|
|
169
|
+
if (['playwright', 'codeceptjs'].includes(this.runningEnvironment)) this.isFileStorage = false;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
#getDataFromGlobalVar(testId) {
|
|
173
|
+
try {
|
|
174
|
+
if (global?.testomatioDataStore[this.dataType]) {
|
|
175
|
+
const testData = global.testomatioDataStore[this.dataType][testId];
|
|
176
|
+
debug(`Data for test id ${testId}:\n${testData}`);
|
|
177
|
+
return testData || '';
|
|
178
|
+
}
|
|
179
|
+
debug(`No ${this.dataType} data for test id ${testId} in <global> storage`);
|
|
180
|
+
return '';
|
|
181
|
+
} catch (e) {
|
|
182
|
+
// there could be no data, ignore
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
#getDataFromFile(testId) {
|
|
187
|
+
try {
|
|
188
|
+
const filepath = join(this.dataDirPath, `${this.dataType}_${testId}`);
|
|
189
|
+
if (fs.existsSync(filepath)) {
|
|
190
|
+
const testData = fs.readFileSync(filepath, 'utf-8');
|
|
191
|
+
debug(`Data for test id ${testId}:\n${testData}`);
|
|
192
|
+
return testData || '';
|
|
193
|
+
}
|
|
194
|
+
debug(`No ${this.dataType} data for test id ${testId} in <file> storage`);
|
|
195
|
+
return '';
|
|
196
|
+
} catch (e) {
|
|
197
|
+
// there could be no data, ignore
|
|
198
|
+
}
|
|
199
|
+
return '';
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
#putDataToGlobalVar(data, testId) {
|
|
203
|
+
debug('Saving data to global variable for test', testId, ':\n', data, '\n');
|
|
204
|
+
if (!global.testomatioDataStore) global.testomatioDataStore = {};
|
|
205
|
+
if (!global.testomatioDataStore?.[this.dataType]) global.testomatioDataStore[this.dataType] = {};
|
|
206
|
+
global.testomatioDataStore?.[this.dataType][testId] // eslint-disable-line no-unused-expressions
|
|
207
|
+
? (global.testomatioDataStore[this.dataType][testId] += `\n${data}`)
|
|
208
|
+
: (global.testomatioDataStore[this.dataType][testId] = data);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
#putDataToFile(data, testId) {
|
|
212
|
+
if (typeof data !== 'string') data = JSON.stringify(data);
|
|
213
|
+
const filename = `${this.dataType}_${testId}`;
|
|
214
|
+
const filepath = join(this.dataDirPath, filename);
|
|
215
|
+
if (!fs.existsSync(this.dataDirPath)) fileSystem.createDir(this.dataDirPath);
|
|
216
|
+
debug('Saving data to file for test', testId, 'to', filepath, ':\n', data, '\n');
|
|
217
|
+
|
|
218
|
+
// TODO: handle multiple invocations of JSON.stringify.
|
|
219
|
+
// UPD: not actual because decided not to use it - it created extra wrapping quotes "" and // (escaped slashes)
|
|
220
|
+
fs.appendFileSync(filepath, data + os.EOL, 'utf-8');
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
module.exports.DataStorage = DataStorage;
|
|
225
|
+
|
|
226
|
+
// TODO: consider using fs promises instead of writeSync/appendFileSync to
|
|
227
|
+
// prevent blocking and improve performance (probably queue usage will be required)
|
|
228
|
+
|
|
229
|
+
// TODO: rewrite client - everything regarding storing artifacts
|
|
230
|
+
// TODO: try to define adapter inside client
|
|
231
|
+
// TODO: use .env
|
|
232
|
+
// TODO: ability to intercept multiple loggers (upd: no need)
|
|
@@ -10,24 +10,42 @@ class JavaAdapter extends Adapter {
|
|
|
10
10
|
|
|
11
11
|
formatTest(t) {
|
|
12
12
|
const fileParts = t.suite_title.split('.')
|
|
13
|
-
const example = t.title.match(/\[(.*)\]/)?.[1];
|
|
14
|
-
if (example) t.example = { "#": example }
|
|
15
13
|
|
|
16
14
|
t.file = namespaceToFileName(t.suite_title);
|
|
17
15
|
t.title = t.title.split('(')[0];
|
|
16
|
+
|
|
17
|
+
// detect params
|
|
18
|
+
const paramMatches = t.title.match(/\[(.*?)\]/g);
|
|
19
|
+
|
|
20
|
+
if (paramMatches) {
|
|
21
|
+
const params = paramMatches.map((_match, index) => `param${index + 1}`);
|
|
22
|
+
if (params.length === 1) params[0] = 'param';
|
|
23
|
+
let paramIndex = 0;
|
|
24
|
+
|
|
25
|
+
t.title = t.title.replace(/: \[(.*?)\]/g, () => {
|
|
26
|
+
if (params.length < 2) return `\${param}`
|
|
27
|
+
const paramName = params[paramIndex] || `param${paramIndex + 1}`;
|
|
28
|
+
paramIndex++;
|
|
29
|
+
return `\${${paramName}}`;
|
|
30
|
+
});
|
|
31
|
+
const example = {};
|
|
32
|
+
paramMatches.forEach((match, index) => { example[params[index]] = match.replace(/[[\]]/g, '') });
|
|
33
|
+
t.example = example;
|
|
34
|
+
}
|
|
35
|
+
|
|
18
36
|
t.suite_title = fileParts[fileParts.length - 1].replace(/\$/g, ' | ')
|
|
19
37
|
return t;
|
|
20
38
|
}
|
|
21
39
|
|
|
22
|
-
formatStack(t) {
|
|
23
|
-
|
|
40
|
+
// formatStack(t) {
|
|
41
|
+
// const stack = super.formatStack(t);
|
|
24
42
|
|
|
25
|
-
|
|
43
|
+
// const file = t.suite_title.split('.');
|
|
26
44
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
45
|
+
// const fileLine = `at .*${file[file.length - 1]}\.java:(\\d+)` // eslint-disable-line no-useless-escape
|
|
46
|
+
// const regexp = new RegExp(fileLine,"g")
|
|
47
|
+
// return stack.replace(regexp, `${this.opts.javaTests}${path.sep}${namespaceToFileName(t.suite_title)}:$1:`);
|
|
48
|
+
// }
|
|
31
49
|
}
|
|
32
50
|
|
|
33
51
|
function namespaceToFileName(fileName) {
|