@testomatio/reporter 1.0.8 → 1.0.9-beta.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/lib/adapter/mocha.js +23 -19
- package/lib/bin/startTest.js +25 -4
- package/lib/client.js +51 -0
- package/lib/dataStorage.js +37 -26
- package/lib/junit-adapter/java.js +27 -9
- package/lib/logger.js +61 -1
- package/lib/pipe/csv.js +2 -0
- package/lib/pipe/github.js +3 -15
- package/lib/pipe/gitlab.js +3 -15
- package/lib/pipe/misc.js +135 -0
- package/lib/pipe/testomatio.js +56 -22
- package/lib/util.js +57 -1
- package/lib/xmlReader.js +84 -16
- package/package.json +1 -1
package/lib/adapter/mocha.js
CHANGED
|
@@ -3,15 +3,18 @@ const Mocha = require('mocha');
|
|
|
3
3
|
const debug = require('debug')('@testomatio/reporter:adapter:mocha');
|
|
4
4
|
const chalk = require('chalk');
|
|
5
5
|
const TestomatClient = require('../client');
|
|
6
|
-
const { STATUS } = require('../constants');
|
|
7
|
-
const { parseTest, specificTestInfo } = require('../util');
|
|
6
|
+
const { STATUS, TESTOMAT_TMP_STORAGE } = require('../constants');
|
|
7
|
+
const { parseTest, specificTestInfo, fileSystem } = require('../util');
|
|
8
8
|
const ArtifactStorage = require('../_ArtifactStorageOld');
|
|
9
9
|
|
|
10
|
-
const { EVENT_RUN_BEGIN, EVENT_RUN_END, EVENT_TEST_FAIL, EVENT_TEST_PASS, EVENT_TEST_PENDING } =
|
|
10
|
+
const { EVENT_RUN_BEGIN, EVENT_RUN_END, EVENT_TEST_FAIL, EVENT_TEST_PASS, EVENT_TEST_PENDING } =
|
|
11
|
+
Mocha.Runner.constants;
|
|
11
12
|
|
|
12
13
|
function MochaReporter(runner, opts) {
|
|
13
14
|
Mocha.reporters.Base.call(this, runner);
|
|
14
|
-
let passes = 0;
|
|
15
|
+
let passes = 0;
|
|
16
|
+
let failures = 0;
|
|
17
|
+
let skipped = 0;
|
|
15
18
|
let artifactStore;
|
|
16
19
|
|
|
17
20
|
const apiKey = opts?.reporterOptions?.apiKey || process.env.TESTOMATIO;
|
|
@@ -25,33 +28,33 @@ function MochaReporter(runner, opts) {
|
|
|
25
28
|
runner.on(EVENT_RUN_BEGIN, () => {
|
|
26
29
|
client.createRun();
|
|
27
30
|
|
|
28
|
-
const params = {
|
|
29
|
-
toFile: true
|
|
31
|
+
const params = {
|
|
32
|
+
toFile: true,
|
|
30
33
|
};
|
|
31
|
-
|
|
32
|
-
artifactStore = runner._workerReporter !== undefined
|
|
33
|
-
|
|
34
|
-
|
|
34
|
+
|
|
35
|
+
artifactStore = runner._workerReporter !== undefined ? new ArtifactStorage(params) : new ArtifactStorage();
|
|
36
|
+
|
|
37
|
+
fileSystem.clearDir(TESTOMAT_TMP_STORAGE.mainDir);
|
|
35
38
|
});
|
|
36
39
|
|
|
37
|
-
runner.on(EVENT_TEST_PASS, async
|
|
40
|
+
runner.on(EVENT_TEST_PASS, async test => {
|
|
38
41
|
passes += 1;
|
|
39
42
|
console.log(chalk.bold.green('✔'), test.fullTitle());
|
|
40
43
|
const testId = parseTest(test.title);
|
|
41
44
|
|
|
42
|
-
const specificTest =
|
|
45
|
+
const specificTest = specificTestInfo(test);
|
|
43
46
|
const content = await artifactStore.artifactByTestName(specificTest);
|
|
44
47
|
|
|
45
48
|
debug(`test=${specificTest} content = `, content);
|
|
46
49
|
|
|
47
50
|
client.addTestRun(
|
|
48
|
-
STATUS.PASSED,
|
|
51
|
+
STATUS.PASSED,
|
|
49
52
|
{
|
|
50
53
|
test_id: testId,
|
|
51
54
|
title: test.title,
|
|
52
55
|
time: test.duration,
|
|
53
56
|
},
|
|
54
|
-
content
|
|
57
|
+
content,
|
|
55
58
|
);
|
|
56
59
|
});
|
|
57
60
|
|
|
@@ -66,24 +69,25 @@ function MochaReporter(runner, opts) {
|
|
|
66
69
|
});
|
|
67
70
|
});
|
|
68
71
|
|
|
69
|
-
runner.on(EVENT_TEST_FAIL, async(test, err) => {
|
|
72
|
+
runner.on(EVENT_TEST_FAIL, async (test, err) => {
|
|
70
73
|
failures += 1;
|
|
71
74
|
console.log(chalk.bold.red('✖'), test.fullTitle(), chalk.gray(err.message));
|
|
72
75
|
const testId = parseTest(test.title);
|
|
73
76
|
|
|
74
|
-
const specificTest =
|
|
77
|
+
const specificTest = specificTestInfo(test);
|
|
75
78
|
const content = await artifactStore.artifactByTestName(specificTest);
|
|
76
79
|
|
|
77
80
|
debug(`fail test=${specificTest} content = `, content);
|
|
78
81
|
|
|
79
82
|
client.addTestRun(
|
|
80
|
-
STATUS.FAILED,
|
|
83
|
+
STATUS.FAILED,
|
|
84
|
+
{
|
|
81
85
|
error: err,
|
|
82
86
|
test_id: testId,
|
|
83
87
|
title: test.title,
|
|
84
88
|
time: test.duration,
|
|
85
|
-
},
|
|
86
|
-
content
|
|
89
|
+
},
|
|
90
|
+
content,
|
|
87
91
|
);
|
|
88
92
|
});
|
|
89
93
|
|
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 opts = optsArray.join(":");
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const tests = await client.prepareRun({pipe, opts});
|
|
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
|
@@ -27,9 +27,60 @@ class Client {
|
|
|
27
27
|
this.queue = Promise.resolve();
|
|
28
28
|
this.totalUploaded = 0;
|
|
29
29
|
this.version = JSON.parse(fs.readFileSync(join(__dirname, '..', 'package.json')).toString()).version;
|
|
30
|
+
this.executionList = Promise.resolve();
|
|
31
|
+
|
|
30
32
|
console.log(APP_PREFIX, `Testomatio Reporter v${this.version}`);
|
|
31
33
|
}
|
|
32
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Asynchronously prepares the execution list for running tests through various pipes.
|
|
37
|
+
* Each pipe in the client is checked for enablement,
|
|
38
|
+
* and if all pipes are disabled, the function returns a resolved Promise.
|
|
39
|
+
* Otherwise, it executes the `prepareRun` method for each enabled pipe and collects the results.
|
|
40
|
+
* The results are then filtered to remove any undefined values.
|
|
41
|
+
* If no valid results are found, the function returns undefined.
|
|
42
|
+
* Otherwise, it returns the first non-empty array from the filtered results.
|
|
43
|
+
*
|
|
44
|
+
* @param {Object} params - The options for preparing the test execution list.
|
|
45
|
+
* @param {string} params.pipe - Name of the executed pipe.
|
|
46
|
+
* @param {string} params.opts - Filter option.
|
|
47
|
+
* @returns {Promise<any>} - A Promise that resolves to an
|
|
48
|
+
* array containing the prepared execution list,
|
|
49
|
+
* or resolves to undefined if no valid results are found or if all pipes are disabled.
|
|
50
|
+
*/
|
|
51
|
+
async prepareRun(params) {
|
|
52
|
+
const { pipe, opts } = params;
|
|
53
|
+
// all pipes disabled, skipping
|
|
54
|
+
if (!this.pipes.some(p => p.isEnabled)) {
|
|
55
|
+
return Promise.resolve();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
if (pipe.toLowerCase() !== "testomatio") {
|
|
60
|
+
//TODO: for the future for the another pipes
|
|
61
|
+
console.warn(APP_PREFIX, `At the moment processing is available only for the "testomatio" key. Example: "testomatio:tag-name=xxx"`)
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const results = await Promise.all(this.pipes.map(async p => {
|
|
66
|
+
return { pipe: p.toString(), result: await p.prepareRun(opts) };
|
|
67
|
+
}));
|
|
68
|
+
|
|
69
|
+
const result = results.filter(p => p.pipe.includes('Testomatio'))[0]?.result;
|
|
70
|
+
|
|
71
|
+
if (!result || result.length === 0) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
debug('Execution tests list', result);
|
|
76
|
+
|
|
77
|
+
return result;
|
|
78
|
+
} catch (err) {
|
|
79
|
+
console.error(APP_PREFIX, err);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
33
84
|
/**
|
|
34
85
|
* Used to create a new Test run
|
|
35
86
|
*
|
package/lib/dataStorage.js
CHANGED
|
@@ -11,7 +11,8 @@ const getTestIdFromTestTitle = require('./util').parseTest;
|
|
|
11
11
|
class DataStorage {
|
|
12
12
|
/**
|
|
13
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
|
|
14
|
+
* Stores data to global variable or to file depending on what is applicable for current test runner
|
|
15
|
+
* (running environment).
|
|
15
16
|
* dataType: 'log' | 'artifact' | ...
|
|
16
17
|
* @param {*} dataType storage type (like 'log', 'artifact'); could be any string, used only to define file path
|
|
17
18
|
*/
|
|
@@ -19,7 +20,7 @@ class DataStorage {
|
|
|
19
20
|
// if (!dataType) throw new Error('Data type is required when creating data storage');
|
|
20
21
|
this.dataType = dataType || 'data';
|
|
21
22
|
this.dataDirName = this.dataType;
|
|
22
|
-
|
|
23
|
+
this.isFileStorage = true;
|
|
23
24
|
this._refreshStorageType();
|
|
24
25
|
|
|
25
26
|
// dir is created in any case (not to recheck its existence every time)
|
|
@@ -37,14 +38,6 @@ class DataStorage {
|
|
|
37
38
|
// jest
|
|
38
39
|
if (process.env.JEST_WORKER_ID) return 'jest';
|
|
39
40
|
|
|
40
|
-
// mocha
|
|
41
|
-
try {
|
|
42
|
-
// @ts-expect-error mocha is defined only in mocha environment
|
|
43
|
-
if (typeof mocha !== 'undefined') return 'mocha';
|
|
44
|
-
} catch (e) {
|
|
45
|
-
// ignore
|
|
46
|
-
}
|
|
47
|
-
|
|
48
41
|
// codeceptjs
|
|
49
42
|
// @ts-expect-error codeceptjs is defined only in codeceptjs environment
|
|
50
43
|
if (global.codeceptjs) return 'codeceptjs';
|
|
@@ -55,6 +48,8 @@ class DataStorage {
|
|
|
55
48
|
|
|
56
49
|
if (process.env.PLAYWRIGHT_TEST_BASE_URL) return 'playwright';
|
|
57
50
|
|
|
51
|
+
// mocha - can't detect
|
|
52
|
+
|
|
58
53
|
return null;
|
|
59
54
|
}
|
|
60
55
|
|
|
@@ -69,15 +64,24 @@ class DataStorage {
|
|
|
69
64
|
|
|
70
65
|
let testId = this._tryToRetrieveTestId(context) || null;
|
|
71
66
|
|
|
72
|
-
if (this.runningEnvironment === 'codeceptjs')
|
|
73
|
-
|
|
74
|
-
|
|
67
|
+
if (this.runningEnvironment === 'codeceptjs') {
|
|
68
|
+
this.isFileStorage = false;
|
|
69
|
+
testId = testId ?? global.testomatioDataStore?.currentlyRunningTestId;
|
|
70
|
+
}
|
|
71
|
+
if (this.runningEnvironment === 'jest') {
|
|
72
|
+
testId = testId ?? JestReporter.getIdOfCurrentlyRunningTest();
|
|
73
|
+
this.isFileStorage = true;
|
|
74
|
+
}
|
|
75
|
+
// logs in playwright are gathered by pw framework itself
|
|
75
76
|
if (this.runningEnvironment === 'playwright' && this.dataType === 'log') return;
|
|
76
77
|
|
|
77
78
|
// get id from global store
|
|
78
79
|
if (global.testomatioDataStore && global.testomatioDataStore.currentlyRunningTestId)
|
|
79
80
|
testId = testId ?? global.testomatioDataStore?.currentlyRunningTestId;
|
|
80
81
|
|
|
82
|
+
if (!testId && global?.currentlyRunningTestTitle)
|
|
83
|
+
testId = this._tryToRetrieveTestId(global.currentlyRunningTestTitle);
|
|
84
|
+
|
|
81
85
|
// if testId is not provided, data is be saved to `{dataType}_other` file;
|
|
82
86
|
if (!testId) {
|
|
83
87
|
debug(`No test id provided for ${this.dataType} data: ${data}`);
|
|
@@ -116,17 +120,26 @@ class DataStorage {
|
|
|
116
120
|
return null;
|
|
117
121
|
}
|
|
118
122
|
|
|
119
|
-
let testData =
|
|
123
|
+
let testData = '';
|
|
120
124
|
|
|
121
125
|
if (global?.testomatioDataStore) {
|
|
122
126
|
testData = this._getDataFromGlobalVar(testId);
|
|
123
|
-
|
|
127
|
+
// these frameworks use global variable storage
|
|
128
|
+
if (testData && ['cucumber', 'codeceptjs'].includes(this.runningEnvironment)) return testData;
|
|
124
129
|
}
|
|
125
130
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
131
|
+
/* condition is removed for mocha
|
|
132
|
+
mocha has created a global storage, but just in some cases, generally it is not available
|
|
133
|
+
*/
|
|
134
|
+
// if (this.isFileStorage || !testData) {
|
|
135
|
+
// testData = this._getDataFromFile(testId);
|
|
136
|
+
// if (testData) return testData;
|
|
137
|
+
// }
|
|
138
|
+
|
|
139
|
+
const testDataFromFile = this._getDataFromFile(testId);
|
|
140
|
+
testData += testDataFromFile || '';
|
|
141
|
+
|
|
142
|
+
if (testData) return testData;
|
|
130
143
|
|
|
131
144
|
debug(`No ${this.dataType} data for test id ${testId} in both file and global variable`);
|
|
132
145
|
return testData;
|
|
@@ -162,7 +175,6 @@ class DataStorage {
|
|
|
162
175
|
* Because storage instance is created before the test runner is started. And storage type could be changed
|
|
163
176
|
*/
|
|
164
177
|
_refreshStorageType() {
|
|
165
|
-
this.isFileStorage = !global.testomatioDataStore;
|
|
166
178
|
/*
|
|
167
179
|
FYI:
|
|
168
180
|
If this storage instance is used within test runner, it works fine.
|
|
@@ -174,8 +186,8 @@ class DataStorage {
|
|
|
174
186
|
*/
|
|
175
187
|
this.runningEnvironment = this.getRunningEnviroment();
|
|
176
188
|
|
|
177
|
-
// some test frameworks do not persist global variables, thus file storage is used for them
|
|
178
|
-
if (this.runningEnvironment
|
|
189
|
+
// some test frameworks do not persist global variables, thus file storage is used for them (by default)
|
|
190
|
+
if (['playwright', 'codeceptjs'].includes(this.runningEnvironment)) this.isFileStorage = false;
|
|
179
191
|
}
|
|
180
192
|
|
|
181
193
|
_getDataFromGlobalVar(testId) {
|
|
@@ -186,7 +198,7 @@ class DataStorage {
|
|
|
186
198
|
return testData;
|
|
187
199
|
}
|
|
188
200
|
debug(`No ${this.dataType} data for test id ${testId} in <global> storage`);
|
|
189
|
-
return
|
|
201
|
+
return '';
|
|
190
202
|
} catch (e) {
|
|
191
203
|
// there could be no data, ignore
|
|
192
204
|
}
|
|
@@ -201,11 +213,11 @@ class DataStorage {
|
|
|
201
213
|
return testData;
|
|
202
214
|
}
|
|
203
215
|
debug(`No ${this.dataType} data for test id ${testId} in <file> storage`);
|
|
204
|
-
return
|
|
216
|
+
return '';
|
|
205
217
|
} catch (e) {
|
|
206
218
|
// there could be no data, ignore
|
|
207
219
|
}
|
|
208
|
-
return
|
|
220
|
+
return '';
|
|
209
221
|
}
|
|
210
222
|
|
|
211
223
|
_putDataToGlobalVar(data, testId) {
|
|
@@ -238,7 +250,6 @@ module.exports.DataStorage = DataStorage;
|
|
|
238
250
|
// TODO: use .env
|
|
239
251
|
// TODO: ability to intercept multiple loggers (upd: no need)
|
|
240
252
|
|
|
241
|
-
|
|
242
253
|
/* Cypress
|
|
243
254
|
Parallelization is only available if using Cypress Dashboard??
|
|
244
255
|
*/
|
|
@@ -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) {
|
package/lib/logger.js
CHANGED
|
@@ -53,6 +53,20 @@ class Logger {
|
|
|
53
53
|
}
|
|
54
54
|
},
|
|
55
55
|
};
|
|
56
|
+
|
|
57
|
+
// add beforeEach hook for mocha. it does not override existing hook, just add new one
|
|
58
|
+
try {
|
|
59
|
+
// @ts-ignore
|
|
60
|
+
if (!beforeEach) return;
|
|
61
|
+
// @ts-ignore
|
|
62
|
+
beforeEach(function () {
|
|
63
|
+
if (this.currentTest?.__mocha_id__) {
|
|
64
|
+
global.testTitle = this.currentTest.fullTitle();
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
} catch (e) {
|
|
68
|
+
// ignore
|
|
69
|
+
}
|
|
56
70
|
}
|
|
57
71
|
|
|
58
72
|
/**
|
|
@@ -115,6 +129,10 @@ class Logger {
|
|
|
115
129
|
_log(strings, ...args) {
|
|
116
130
|
// entity which is used to define testId
|
|
117
131
|
let context = null;
|
|
132
|
+
|
|
133
|
+
// get testId for mocha
|
|
134
|
+
context = global.testTitle ?? null;
|
|
135
|
+
|
|
118
136
|
// last argument could contain testId
|
|
119
137
|
const testId = this._helpers.parseLastArgToGetTestId(...args);
|
|
120
138
|
if (testId) {
|
|
@@ -152,7 +170,7 @@ class Logger {
|
|
|
152
170
|
}
|
|
153
171
|
|
|
154
172
|
/**
|
|
155
|
-
* This function is a wrapper for
|
|
173
|
+
* This function is a wrapper for each logging methods (log, warn, error etc) (not to repeat the same code)
|
|
156
174
|
* @param {*} argsArray
|
|
157
175
|
* @param {*} level
|
|
158
176
|
* @returns
|
|
@@ -162,6 +180,10 @@ class Logger {
|
|
|
162
180
|
|
|
163
181
|
// entity which is used to define testId
|
|
164
182
|
let context = null;
|
|
183
|
+
|
|
184
|
+
// get context for mocha
|
|
185
|
+
context = global.testTitle ?? null;
|
|
186
|
+
|
|
165
187
|
// last argument could contain testId
|
|
166
188
|
const testId = this._helpers.parseLastArgToGetTestId(...argsArray);
|
|
167
189
|
if (testId) {
|
|
@@ -299,3 +321,41 @@ module.exports = logger;
|
|
|
299
321
|
|
|
300
322
|
// TODO: parse passed arguments as {level: 'str', message: 'str'} because some loggers use such syntax;
|
|
301
323
|
// upd: did not face such loggers, but still could be useful
|
|
324
|
+
|
|
325
|
+
/* Cypress
|
|
326
|
+
There is no listener like "after:test" in cypress, only "after:spec" is available.
|
|
327
|
+
Thus, cannot separate logs even when I gather them (because I don't know when the test is done, just know about suite).
|
|
328
|
+
Also there is no easy way to access the message from cy.log() function.
|
|
329
|
+
(Using testomatio logger – logger.log() is not convenient because Cypress chains its commands,
|
|
330
|
+
thus such command will interrupt the chain.)
|
|
331
|
+
|
|
332
|
+
(Could not implement intercepting of cy.log('message'));
|
|
333
|
+
I found the only ability to get any logs using .task('log', 'message') (this is custom, not default cypress command)
|
|
334
|
+
and then intercept it with:
|
|
335
|
+
on('task', {
|
|
336
|
+
log (message) {
|
|
337
|
+
console.log(message)
|
|
338
|
+
return null
|
|
339
|
+
}
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
but:
|
|
343
|
+
1) it does not solve problem with getting current running testId;
|
|
344
|
+
2) leads to warning "Warning: Multiple attempts to register the following task(s):".)
|
|
345
|
+
|
|
346
|
+
My way to get test id:
|
|
347
|
+
add cypress command to save test title to file)):
|
|
348
|
+
Cypress.Commands.add('writeTestTitleToFile', () => {
|
|
349
|
+
const testTitle = cy.state('runnable').title;
|
|
350
|
+
cy.writeFile('testomatio_test_title', testTitle);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
Finally, in the test it will look like:
|
|
354
|
+
cy
|
|
355
|
+
.writeTestTitleToFile() // <<<
|
|
356
|
+
.task('log', 'This is a log message from the test') // <<<
|
|
357
|
+
|
|
358
|
+
.get('element)
|
|
359
|
+
.type('text')
|
|
360
|
+
.click()
|
|
361
|
+
*/
|
package/lib/pipe/csv.js
CHANGED
package/lib/pipe/github.js
CHANGED
|
@@ -6,6 +6,7 @@ const merge = require('lodash.merge');
|
|
|
6
6
|
const { Octokit } = require('@octokit/rest');
|
|
7
7
|
const { APP_PREFIX } = require('../constants');
|
|
8
8
|
const { ansiRegExp, isSameTest } = require('../util');
|
|
9
|
+
const { statusEmoji, fullName } = require('./misc');
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* @typedef {import('../../types').Pipe} Pipe
|
|
@@ -37,6 +38,8 @@ class GitHubPipe {
|
|
|
37
38
|
debug('GitHub Pipe: Enabled');
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
async prepareRun(opts) {}
|
|
42
|
+
|
|
40
43
|
async createRun() {}
|
|
41
44
|
|
|
42
45
|
addTest(test) {
|
|
@@ -182,21 +185,6 @@ class GitHubPipe {
|
|
|
182
185
|
}
|
|
183
186
|
}
|
|
184
187
|
|
|
185
|
-
function statusEmoji(status) {
|
|
186
|
-
if (status === 'passed') return '🟢';
|
|
187
|
-
if (status === 'failed') return '🔴';
|
|
188
|
-
if (status === 'skipped') return '🟡';
|
|
189
|
-
return '';
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
function fullName(t) {
|
|
193
|
-
let line = '';
|
|
194
|
-
if (t.suite_title) line = `${t.suite_title}: `;
|
|
195
|
-
line += `**${t.title}**`;
|
|
196
|
-
if (t.example) line += ` \`[${Object.values(t.example)}]\``;
|
|
197
|
-
return line;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
188
|
async function deletePreviousReport(octokit, owner, repo, issue, hiddenCommentData) {
|
|
201
189
|
if (process.env.GH_KEEP_OUTDATED_REPORTS) return;
|
|
202
190
|
|
package/lib/pipe/gitlab.js
CHANGED
|
@@ -6,6 +6,7 @@ const merge = require('lodash.merge');
|
|
|
6
6
|
const path = require('path');
|
|
7
7
|
const { APP_PREFIX } = require('../constants');
|
|
8
8
|
const { ansiRegExp, isSameTest } = require('../util');
|
|
9
|
+
const { statusEmoji, fullName } = require('./misc');
|
|
9
10
|
|
|
10
11
|
//! GITLAB_PAT environment variable is required for this functionality to work
|
|
11
12
|
//! and your pipeline trigger should be merge_request
|
|
@@ -46,6 +47,8 @@ class GitLabPipe {
|
|
|
46
47
|
debug('GitLab Pipe: Enabled');
|
|
47
48
|
}
|
|
48
49
|
|
|
50
|
+
async prepareRun(opts) {}
|
|
51
|
+
|
|
49
52
|
async createRun() {}
|
|
50
53
|
|
|
51
54
|
addTest(test) {
|
|
@@ -179,21 +182,6 @@ class GitLabPipe {
|
|
|
179
182
|
updateRun() {}
|
|
180
183
|
}
|
|
181
184
|
|
|
182
|
-
function statusEmoji(status) {
|
|
183
|
-
if (status === 'passed') return '🟢';
|
|
184
|
-
if (status === 'failed') return '🔴';
|
|
185
|
-
if (status === 'skipped') return '🟡';
|
|
186
|
-
return '';
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function fullName(t) {
|
|
190
|
-
let line = '';
|
|
191
|
-
if (t.suite_title) line = `${t.suite_title}: `;
|
|
192
|
-
line += `**${t.title}**`;
|
|
193
|
-
if (t.example) line += ` \`[${Object.values(t.example)}]\``;
|
|
194
|
-
return line;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
185
|
async function deletePreviousReport(axiosInstance, commentsRequestURL, hiddenCommentData, token) {
|
|
198
186
|
if (process.env.GITLAB_KEEP_OUTDATED_REPORTS) return;
|
|
199
187
|
|
package/lib/pipe/misc.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
const { resetConfig } = require('../fileUploader');
|
|
2
|
+
const { APP_PREFIX } = require('../constants');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Set S3 credentials from the provided artifacts object.
|
|
6
|
+
* @param {Object} artifacts - The artifacts object containing S3 credentials.
|
|
7
|
+
*/
|
|
8
|
+
function setS3Credentials(artifacts) {
|
|
9
|
+
if (!Object.keys(artifacts).length) return;
|
|
10
|
+
|
|
11
|
+
console.log(APP_PREFIX, 'S3 were credentials obtained from Testomat.io...');
|
|
12
|
+
|
|
13
|
+
if (artifacts.ACCESS_KEY_ID) process.env.S3_ACCESS_KEY_ID = artifacts.ACCESS_KEY_ID;
|
|
14
|
+
if (artifacts.SECRET_ACCESS_KEY) process.env.S3_SECRET_ACCESS_KEY = artifacts.SECRET_ACCESS_KEY;
|
|
15
|
+
if (artifacts.REGION) process.env.S3_REGION = artifacts.REGION;
|
|
16
|
+
if (artifacts.BUCKET) process.env.S3_BUCKET = artifacts.BUCKET;
|
|
17
|
+
if (artifacts.ENDPOINT) process.env.S3_ENDPOINT = artifacts.ENDPOINT;
|
|
18
|
+
if (artifacts.presign) process.env.TESTOMATIO_PRIVATE_ARTIFACTS = '1';
|
|
19
|
+
resetConfig();
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Generates mode request parameters based on the input params.
|
|
23
|
+
* @param {Object} params - The input parameters for the request.
|
|
24
|
+
* @param {string} params.type - The type of the request (e.g., "tag").
|
|
25
|
+
* @param {string} params.id - The ID associated with the request.
|
|
26
|
+
* @param {string} params.apiKey - The API key for authentication.
|
|
27
|
+
* @returns {Object|null} - An object containing the generated request parameters, or null if the type is invalid.
|
|
28
|
+
*/
|
|
29
|
+
function generateFilterRequestParams(params) {
|
|
30
|
+
const { type, id, apiKey } = params;
|
|
31
|
+
|
|
32
|
+
if (!type) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!id) {
|
|
37
|
+
console.error(APP_PREFIX, `Please make sure your settings "${type.toUpperCase()}"= "${id}" is correct!`);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
params: {
|
|
43
|
+
type,
|
|
44
|
+
id: encodeURIComponent(id),
|
|
45
|
+
api_key: apiKey
|
|
46
|
+
},
|
|
47
|
+
responseType: "json"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Parse filter parameters from a string in the format "type=id".
|
|
53
|
+
* @param {string} opts - The input string containing the filter parameters.
|
|
54
|
+
* @returns {Object} An object containing the parsed filter parameters.
|
|
55
|
+
* The object has properties "type" and "id".
|
|
56
|
+
*/
|
|
57
|
+
function parseFilterParams(opts) {
|
|
58
|
+
const [type, id] = opts.split("=");
|
|
59
|
+
const validType = updateFilterType(type);
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
type: validType,
|
|
63
|
+
id
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Update and validate the filter type.
|
|
69
|
+
* @param {string} type - The original filter type.
|
|
70
|
+
* @returns {string|undefined} The updated and validated filter type.
|
|
71
|
+
* Returns undefined if the type is not valid.
|
|
72
|
+
*/
|
|
73
|
+
function updateFilterType(type) {
|
|
74
|
+
const typeLowerCase = type.toLowerCase();
|
|
75
|
+
|
|
76
|
+
const filterTypes = [
|
|
77
|
+
"tag-name",
|
|
78
|
+
"plan-id",
|
|
79
|
+
"label",
|
|
80
|
+
"jira-ticket",
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
const filterApi = [
|
|
84
|
+
"tag",
|
|
85
|
+
"plan",
|
|
86
|
+
"label",
|
|
87
|
+
"jira",
|
|
88
|
+
// "ims-issue", //TODO: WIP
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
if (!filterTypes.includes(typeLowerCase)) {
|
|
92
|
+
console.log(APP_PREFIX, `❗❗❗ Invalid "filter=${type}" start settings! Available option list: ${filterTypes}`);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const index = filterTypes.indexOf(typeLowerCase);
|
|
97
|
+
|
|
98
|
+
return (index !== -1)
|
|
99
|
+
? filterApi[index]
|
|
100
|
+
: undefined
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Return an emoji based on the provided status.
|
|
105
|
+
* @param {string} status - The status value ('passed', 'failed', or 'skipped').
|
|
106
|
+
* @returns {string} - An emoji corresponding to the provided status.
|
|
107
|
+
*/
|
|
108
|
+
function statusEmoji(status) {
|
|
109
|
+
if (status === 'passed') return '🟢';
|
|
110
|
+
if (status === 'failed') return '🔴';
|
|
111
|
+
if (status === 'skipped') return '🟡';
|
|
112
|
+
return '';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Generate a full name string based on the provided test object.
|
|
117
|
+
* @param {object} t - The test object.
|
|
118
|
+
* @returns {string} - A formatted full name string for the test object.
|
|
119
|
+
*/
|
|
120
|
+
function fullName(t) {
|
|
121
|
+
let line = '';
|
|
122
|
+
if (t.suite_title) line = `${t.suite_title}: `;
|
|
123
|
+
line += `**${t.title}**`;
|
|
124
|
+
if (t.example) line += ` \`[${Object.values(t.example)}]\``;
|
|
125
|
+
return line;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
module.exports = {
|
|
129
|
+
updateFilterType,
|
|
130
|
+
parseFilterParams,
|
|
131
|
+
generateFilterRequestParams,
|
|
132
|
+
setS3Credentials,
|
|
133
|
+
statusEmoji,
|
|
134
|
+
fullName
|
|
135
|
+
};
|
package/lib/pipe/testomatio.js
CHANGED
|
@@ -3,8 +3,8 @@ const chalk = require('chalk');
|
|
|
3
3
|
const axios = require('axios');
|
|
4
4
|
const JsonCycle = require('json-cycle');
|
|
5
5
|
const { APP_PREFIX, STATUS } = require('../constants');
|
|
6
|
-
const { isValidUrl } = require('../util');
|
|
7
|
-
const {
|
|
6
|
+
const { isValidUrl, foundedTestLog } = require('../util');
|
|
7
|
+
const { parseFilterParams, generateFilterRequestParams, setS3Credentials, } = require('./misc');
|
|
8
8
|
|
|
9
9
|
const { TESTOMATIO_RUN } = process.env;
|
|
10
10
|
if (TESTOMATIO_RUN) {
|
|
@@ -33,7 +33,12 @@ class TestomatioPipe {
|
|
|
33
33
|
this.sharedRun = !!process.env.TESTOMATIO_SHARED_RUN;
|
|
34
34
|
this.groupTitle = params.groupTitle || process.env.TESTOMATIO_RUNGROUP_TITLE;
|
|
35
35
|
this.env = process.env.TESTOMATIO_ENV;
|
|
36
|
-
|
|
36
|
+
|
|
37
|
+
this.axios = axios.create({
|
|
38
|
+
baseURL: `${this.url.trim()}`,
|
|
39
|
+
timeout: 20000
|
|
40
|
+
});
|
|
41
|
+
|
|
37
42
|
this.isEnabled = true;
|
|
38
43
|
// do not finish this run (for parallel testing)
|
|
39
44
|
this.proceed = process.env.TESTOMATIO_PROCEED;
|
|
@@ -47,6 +52,50 @@ class TestomatioPipe {
|
|
|
47
52
|
}
|
|
48
53
|
}
|
|
49
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Asynchronously prepares and retrieves the Testomat.io test grepList based on the provided options.
|
|
57
|
+
* @param {Object} opts - The options for preparing the test grepList.
|
|
58
|
+
* @returns {Promise<string[]>} - An array containing the retrieved
|
|
59
|
+
* test grepList, or an empty array if no tests are found or the request is disabled.
|
|
60
|
+
* @throws {Error} - Throws an error if there was a problem while making the request.
|
|
61
|
+
*/
|
|
62
|
+
async prepareRun(opts) {
|
|
63
|
+
if (!this.isEnabled) return [];
|
|
64
|
+
|
|
65
|
+
const { type, id } = parseFilterParams(opts);
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const q = generateFilterRequestParams({
|
|
69
|
+
type,
|
|
70
|
+
id,
|
|
71
|
+
apiKey: this.apiKey.trim()
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
if (!q) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const resp = await this.axios.get('/api/test_grep', q);
|
|
79
|
+
const { data } = resp;
|
|
80
|
+
|
|
81
|
+
if (Array.isArray(data?.tests) && data?.tests?.length > 0) {
|
|
82
|
+
foundedTestLog(APP_PREFIX, data.tests);
|
|
83
|
+
return data.tests;
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
console.log(APP_PREFIX, `⛔ No tests found for your --filter --> ${type}=${id}`);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
console.error(
|
|
92
|
+
APP_PREFIX,
|
|
93
|
+
`🚩 Error getting Testomat.io test grepList: ${err}`,
|
|
94
|
+
);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
50
99
|
/**
|
|
51
100
|
* @returns Promise<void>
|
|
52
101
|
*/
|
|
@@ -84,13 +133,13 @@ class TestomatioPipe {
|
|
|
84
133
|
);
|
|
85
134
|
|
|
86
135
|
if (this.runId) {
|
|
87
|
-
const resp = await this.axios.put(
|
|
136
|
+
const resp = await this.axios.put(`/api/reporter/${this.runId}`, runParams);
|
|
88
137
|
if (resp.data.artifacts) setS3Credentials(resp.data.artifacts);
|
|
89
138
|
return;
|
|
90
139
|
}
|
|
91
140
|
|
|
92
141
|
try {
|
|
93
|
-
const resp = await this.axios.post(
|
|
142
|
+
const resp = await this.axios.post(`/api/reporter`, runParams, {
|
|
94
143
|
maxContentLength: Infinity,
|
|
95
144
|
maxBodyLength: Infinity,
|
|
96
145
|
});
|
|
@@ -121,7 +170,7 @@ class TestomatioPipe {
|
|
|
121
170
|
data.create = this.createNewTests;
|
|
122
171
|
const json = JsonCycle.stringify(data);
|
|
123
172
|
|
|
124
|
-
return this.axios.post(
|
|
173
|
+
return this.axios.post(`/api/reporter/${this.runId}/testrun`, json, {
|
|
125
174
|
maxContentLength: Infinity,
|
|
126
175
|
maxBodyLength: Infinity,
|
|
127
176
|
headers: {
|
|
@@ -169,7 +218,7 @@ class TestomatioPipe {
|
|
|
169
218
|
|
|
170
219
|
try {
|
|
171
220
|
if (this.runId && !this.proceed) {
|
|
172
|
-
await this.axios.put(
|
|
221
|
+
await this.axios.put(`/api/reporter/${this.runId}`, {
|
|
173
222
|
api_key: this.apiKey,
|
|
174
223
|
status_event,
|
|
175
224
|
tests: params.tests,
|
|
@@ -209,18 +258,3 @@ class TestomatioPipe {
|
|
|
209
258
|
}
|
|
210
259
|
|
|
211
260
|
module.exports = TestomatioPipe;
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
function setS3Credentials(artifacts) {
|
|
215
|
-
if (!Object.keys(artifacts).length) return;
|
|
216
|
-
|
|
217
|
-
console.log(APP_PREFIX, 'S3 were credentials obtained from Testomat.io...');
|
|
218
|
-
|
|
219
|
-
if (artifacts.ACCESS_KEY_ID) process.env.S3_ACCESS_KEY_ID = artifacts.ACCESS_KEY_ID;
|
|
220
|
-
if (artifacts.SECRET_ACCESS_KEY) process.env.S3_SECRET_ACCESS_KEY = artifacts.SECRET_ACCESS_KEY;
|
|
221
|
-
if (artifacts.REGION) process.env.S3_REGION = artifacts.REGION;
|
|
222
|
-
if (artifacts.BUCKET) process.env.S3_BUCKET = artifacts.BUCKET;
|
|
223
|
-
if (artifacts.ENDPOINT) process.env.S3_ENDPOINT = artifacts.ENDPOINT;
|
|
224
|
-
if (artifacts.presign) process.env.TESTOMATIO_PRIVATE_ARTIFACTS = '1';
|
|
225
|
-
resetConfig();
|
|
226
|
-
}
|
package/lib/util.js
CHANGED
|
@@ -192,6 +192,60 @@ const fileSystem = {
|
|
|
192
192
|
}
|
|
193
193
|
};
|
|
194
194
|
|
|
195
|
+
|
|
196
|
+
const foundedTestLog = (app, tests) => {
|
|
197
|
+
const n = tests.length;
|
|
198
|
+
|
|
199
|
+
return (n === 1)
|
|
200
|
+
? console.log(app, `✅ We found only one test!`)
|
|
201
|
+
: console.log(app, `✅ We found ${n} tests!`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const humanize = (text) => {
|
|
205
|
+
text = decamelize(text);
|
|
206
|
+
return text.replace(/_./g, match => ` ${ match.charAt(1).toUpperCase()}`)
|
|
207
|
+
.trim()
|
|
208
|
+
.replace(/^(.)|\s(.)/g, ($1) => $1.toUpperCase()).trim()
|
|
209
|
+
.replace(/\sA\s/g, ' a ') // replace a|the
|
|
210
|
+
.replace(/\sThe\s/g, ' the ') // replace a|the
|
|
211
|
+
.replace(/^Test\s/, '')
|
|
212
|
+
.replace(/^Should\s/, '')
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* From https://github.com/sindresorhus/decamelize/blob/main/index.js
|
|
217
|
+
* @param {*} text
|
|
218
|
+
* @returns
|
|
219
|
+
*/
|
|
220
|
+
const decamelize = (text) => {
|
|
221
|
+
const separator = '_';
|
|
222
|
+
const replacement = `$1${separator}$2`;
|
|
223
|
+
|
|
224
|
+
// Split lowercase sequences followed by uppercase character.
|
|
225
|
+
// `dataForUSACounties` → `data_For_USACounties`
|
|
226
|
+
// `myURLstring → `my_URLstring`
|
|
227
|
+
let decamelized = text.replace(
|
|
228
|
+
/([\p{Lowercase_Letter}\d])(\p{Uppercase_Letter})/gu,
|
|
229
|
+
replacement,
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
// Lowercase all single uppercase characters. As we
|
|
233
|
+
// want to preserve uppercase sequences, we cannot
|
|
234
|
+
// simply lowercase the separated string at the end.
|
|
235
|
+
// `data_For_USACounties` → `data_for_USACounties`
|
|
236
|
+
decamelized = decamelized.replace(
|
|
237
|
+
/((?<![\p{Uppercase_Letter}\d])[\p{Uppercase_Letter}\d](?![\p{Uppercase_Letter}\d]))/gu,
|
|
238
|
+
$0 => $0.toLowerCase(),
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
// Remaining uppercase sequences will be separated from lowercase sequences.
|
|
242
|
+
// `data_For_USACounties` → `data_for_USA_counties`
|
|
243
|
+
return decamelized.replace(
|
|
244
|
+
/(\p{Uppercase_Letter}+)(\p{Uppercase_Letter}\p{Lowercase_Letter}+)/gu,
|
|
245
|
+
(_, $1, $2) => $1 + separator + $2.toLowerCase(),
|
|
246
|
+
);
|
|
247
|
+
};
|
|
248
|
+
|
|
195
249
|
module.exports = {
|
|
196
250
|
isSameTest,
|
|
197
251
|
fetchSourceCode,
|
|
@@ -204,4 +258,6 @@ module.exports = {
|
|
|
204
258
|
ansiRegExp,
|
|
205
259
|
parseTest,
|
|
206
260
|
parseSuite,
|
|
207
|
-
|
|
261
|
+
humanize,
|
|
262
|
+
foundedTestLog
|
|
263
|
+
}
|
package/lib/xmlReader.js
CHANGED
|
@@ -8,7 +8,7 @@ const { fetchFilesFromStackTrace, fetchSourceCode, fetchSourceCodeFromStackTrace
|
|
|
8
8
|
const upload = require('./fileUploader');
|
|
9
9
|
const pipesFactory = require('./pipe');
|
|
10
10
|
const adapterFactory = require('./junit-adapter');
|
|
11
|
-
|
|
11
|
+
const { humanize } = require('./util')
|
|
12
12
|
|
|
13
13
|
const TESTOMATIO_URL = process.env.TESTOMATIO_URL || "https://app.testomat.io";
|
|
14
14
|
const TESTOMATIO = process.env.TESTOMATIO; // key?
|
|
@@ -35,7 +35,7 @@ class XmlReader {
|
|
|
35
35
|
group_title: TESTOMATIO_RUNGROUP_TITLE,
|
|
36
36
|
};
|
|
37
37
|
this.runId = opts.runId || TESTOMATIO_RUN;
|
|
38
|
-
this.adapter = adapterFactory(
|
|
38
|
+
this.adapter = adapterFactory(opts.lang?.toLowerCase(), opts)
|
|
39
39
|
if (!this.adapter) throw new Error('XML adapter for this format not found');
|
|
40
40
|
|
|
41
41
|
this.opts = opts || {};
|
|
@@ -53,8 +53,12 @@ class XmlReader {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
connectAdapter() {
|
|
56
|
-
if (this.opts.javaTests)
|
|
57
|
-
|
|
56
|
+
if (this.opts.javaTests) {
|
|
57
|
+
this.adapter = adapterFactory('java', this.opts);
|
|
58
|
+
return this.adapter;
|
|
59
|
+
}
|
|
60
|
+
this.adapter = adapterFactory(this.stats.language, this.opts);
|
|
61
|
+
return this.adapter;
|
|
58
62
|
}
|
|
59
63
|
|
|
60
64
|
parse(fileName) {
|
|
@@ -70,6 +74,8 @@ class XmlReader {
|
|
|
70
74
|
return this.processTRX(jsonResult);
|
|
71
75
|
} else if (jsonResult['test-run']) {
|
|
72
76
|
return this.processNUnit(jsonResult['test-run']);
|
|
77
|
+
} else if (jsonResult.assemblies) {
|
|
78
|
+
return this.processXUnit(jsonResult.assemblies);
|
|
73
79
|
} else {
|
|
74
80
|
console.log(jsonResult)
|
|
75
81
|
throw new Error("Format can't be parsed")
|
|
@@ -190,6 +196,79 @@ class XmlReader {
|
|
|
190
196
|
};
|
|
191
197
|
}
|
|
192
198
|
|
|
199
|
+
processXUnit(assemblies) {
|
|
200
|
+
const tests = [];
|
|
201
|
+
|
|
202
|
+
assemblies = Array.isArray(assemblies.assembly) ? assemblies.assembly : [assemblies.assembly];
|
|
203
|
+
|
|
204
|
+
assemblies.forEach(assembly => {
|
|
205
|
+
const { collection } = assembly;
|
|
206
|
+
|
|
207
|
+
const suites = Array.isArray(collection) ? collection : [collection];
|
|
208
|
+
|
|
209
|
+
suites.forEach(suite => {
|
|
210
|
+
const { test } = suite;
|
|
211
|
+
if (!test) return;
|
|
212
|
+
const cases = Array.isArray(test) ? test : [test];
|
|
213
|
+
cases.forEach(testCase => {
|
|
214
|
+
const { type, time, result } = testCase;
|
|
215
|
+
|
|
216
|
+
let message = '';
|
|
217
|
+
let stack = '';
|
|
218
|
+
|
|
219
|
+
if (testCase.failure) {
|
|
220
|
+
message = testCase.failure.message;
|
|
221
|
+
stack = testCase.failure['stack-trace']
|
|
222
|
+
}
|
|
223
|
+
if (testCase.reason) {
|
|
224
|
+
message = testCase.reason.message;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
let status = STATUS.PASSED;
|
|
228
|
+
if (result === 'Pass') status = STATUS.PASSED;
|
|
229
|
+
if (result === 'Fail') status = STATUS.FAILED;
|
|
230
|
+
if (result === 'Skip') status = STATUS.SKIPPED;
|
|
231
|
+
|
|
232
|
+
const pathParts = type.split('.');
|
|
233
|
+
const suite_title = pathParts[pathParts.length - 1];
|
|
234
|
+
const file = pathParts.slice(0, -1).join('/');
|
|
235
|
+
const title = testCase.method || testCase.name.split('.').pop();
|
|
236
|
+
const run_time = parseFloat(time) * 1000;
|
|
237
|
+
|
|
238
|
+
tests.push({
|
|
239
|
+
create: true,
|
|
240
|
+
stack,
|
|
241
|
+
message,
|
|
242
|
+
file,
|
|
243
|
+
status,
|
|
244
|
+
title,
|
|
245
|
+
suite_title,
|
|
246
|
+
run_time,
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
const hasFailures = tests.filter(t => t.status === STATUS.FAILED).length > 0;
|
|
254
|
+
const status = hasFailures ? STATUS.FAILED : STATUS.PASSED;
|
|
255
|
+
|
|
256
|
+
this.tests = tests;
|
|
257
|
+
|
|
258
|
+
debug(tests);
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
status,
|
|
262
|
+
create_tests: true,
|
|
263
|
+
name: 'xUnit',
|
|
264
|
+
tests_count: tests.length,
|
|
265
|
+
passed_count: tests.filter(t => t.status === STATUS.PASSED).length,
|
|
266
|
+
failed_count: tests.filter(t => t.status === STATUS.FAILED).length,
|
|
267
|
+
skipped_count: tests.filter(t => t.status === STATUS.SKIPPED).length,
|
|
268
|
+
tests,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
193
272
|
calculateStats() {
|
|
194
273
|
this.stats = {
|
|
195
274
|
...this.stats,
|
|
@@ -241,18 +320,7 @@ class XmlReader {
|
|
|
241
320
|
|
|
242
321
|
this.adapter.formatTest(t)
|
|
243
322
|
|
|
244
|
-
t.title = t.title
|
|
245
|
-
// insert a space before all caps
|
|
246
|
-
.replace(/([A-Z])/g, ' $1')
|
|
247
|
-
// _ chars to spaces
|
|
248
|
-
.replace(/_/g, ' ')
|
|
249
|
-
// uppercase the first character
|
|
250
|
-
.replace(/^(.)|\s(.)/g, $1 => $1.toLowerCase())
|
|
251
|
-
|
|
252
|
-
// remove standard prefixes
|
|
253
|
-
.replace(/^test\s/, '')
|
|
254
|
-
.replace(/^should\s/, '')
|
|
255
|
-
.trim();
|
|
323
|
+
t.title = humanize(t.title);
|
|
256
324
|
});
|
|
257
325
|
}
|
|
258
326
|
|