testbeats 2.0.2 → 2.0.4
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/package.json +5 -2
- package/src/beats/beats.api.js +11 -0
- package/src/beats/beats.attachments.js +100 -0
- package/src/beats/beats.js +30 -0
- package/src/cli.js +19 -6
- package/src/commands/publish.command.js +197 -0
- package/src/index.d.ts +22 -0
- package/src/index.js +3 -2
- package/src/utils/config.builder.js +117 -0
- package/src/commands/publish.js +0 -168
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "testbeats",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.4",
|
|
4
4
|
"description": "Publish test results to Microsoft Teams, Google Chat, Slack and InfluxDB",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"types": "./src/index.d.ts",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"/src"
|
|
12
12
|
],
|
|
13
13
|
"scripts": {
|
|
14
|
-
"test": "c8 mocha test",
|
|
14
|
+
"test": "c8 mocha test --reporter mocha-multi-reporters --reporter-options configFile=mocha.report.json",
|
|
15
15
|
"build": "pkg --out-path dist ."
|
|
16
16
|
},
|
|
17
17
|
"repository": {
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"async-retry": "^1.3.3",
|
|
50
50
|
"dotenv": "^14.3.2",
|
|
51
|
+
"form-data-lite": "^1.0.3",
|
|
51
52
|
"influxdb-lite": "^1.0.0",
|
|
52
53
|
"performance-results-parser": "latest",
|
|
53
54
|
"phin-retry": "^1.0.3",
|
|
@@ -59,6 +60,8 @@
|
|
|
59
60
|
"devDependencies": {
|
|
60
61
|
"c8": "^7.12.0",
|
|
61
62
|
"mocha": "^10.1.0",
|
|
63
|
+
"mocha-junit-reporter": "^2.2.1",
|
|
64
|
+
"mocha-multi-reporters": "^1.5.1",
|
|
62
65
|
"pactum": "^3.2.3",
|
|
63
66
|
"pkg": "^5.8.0"
|
|
64
67
|
}
|
package/src/beats/beats.api.js
CHANGED
|
@@ -32,6 +32,17 @@ class BeatsApi {
|
|
|
32
32
|
});
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
uploadAttachments(headers, payload) {
|
|
36
|
+
return request.post({
|
|
37
|
+
url: `${this.getBaseUrl()}/api/core/v1/test-cases/attachments`,
|
|
38
|
+
headers: {
|
|
39
|
+
'x-api-key': this.config.api_key,
|
|
40
|
+
...headers
|
|
41
|
+
},
|
|
42
|
+
body: payload
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
35
46
|
getBaseUrl() {
|
|
36
47
|
return process.env.TEST_BEATS_URL || "https://app.testbeats.com";
|
|
37
48
|
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const FormData = require('form-data-lite');
|
|
4
|
+
const TestResult = require('test-results-parser/src/models/TestResult');
|
|
5
|
+
const { BeatsApi } = require('./beats.api');
|
|
6
|
+
const logger = require('../utils/logger');
|
|
7
|
+
|
|
8
|
+
const MAX_ATTACHMENTS_PER_REQUEST = 5;
|
|
9
|
+
const MAX_ATTACHMENTS_PER_RUN = 20;
|
|
10
|
+
const MAX_ATTACHMENT_SIZE = 1024 * 1024;
|
|
11
|
+
|
|
12
|
+
class BeatsAttachments {
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {import('../index').PublishReport} config
|
|
16
|
+
* @param {TestResult} result
|
|
17
|
+
*/
|
|
18
|
+
constructor(config, result, test_run_id) {
|
|
19
|
+
this.config = config;
|
|
20
|
+
this.result = result;
|
|
21
|
+
this.api = new BeatsApi(config);
|
|
22
|
+
this.test_run_id = test_run_id;
|
|
23
|
+
this.failed_test_cases = [];
|
|
24
|
+
this.attachments = [];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async upload() {
|
|
28
|
+
this.#setAllFailedTestCases();
|
|
29
|
+
this.#setAttachments();
|
|
30
|
+
await this.#uploadAttachments();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
#setAllFailedTestCases() {
|
|
34
|
+
for (const suite of this.result.suites) {
|
|
35
|
+
for (const test of suite.cases) {
|
|
36
|
+
if (test.status === 'FAIL') {
|
|
37
|
+
this.failed_test_cases.push(test);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
#setAttachments() {
|
|
44
|
+
for (const test_case of this.failed_test_cases) {
|
|
45
|
+
for (const attachment of test_case.attachments) {
|
|
46
|
+
this.attachments.push(attachment);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async #uploadAttachments() {
|
|
52
|
+
if (this.attachments.length === 0) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
logger.info(`⏳ Uploading ${this.attachments.length} attachments...`);
|
|
56
|
+
const result_file = this.config.results[0].files[0];
|
|
57
|
+
const result_file_dir = path.dirname(result_file);
|
|
58
|
+
try {
|
|
59
|
+
let count = 0;
|
|
60
|
+
const size = MAX_ATTACHMENTS_PER_REQUEST;
|
|
61
|
+
for (let i = 0; i < this.attachments.length; i += size) {
|
|
62
|
+
if (count >= MAX_ATTACHMENTS_PER_RUN) {
|
|
63
|
+
logger.warn('⚠️ Maximum number of attachments per run reached. Skipping remaining attachments.');
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
const attachments_subset = this.attachments.slice(i, i + size);
|
|
67
|
+
const form = new FormData();
|
|
68
|
+
form.append('test_run_id', this.test_run_id);
|
|
69
|
+
const file_images = []
|
|
70
|
+
for (const attachment of attachments_subset) {
|
|
71
|
+
const attachment_path = path.join(result_file_dir, attachment.path);
|
|
72
|
+
const stats = fs.statSync(attachment_path);
|
|
73
|
+
if (stats.size > MAX_ATTACHMENT_SIZE) {
|
|
74
|
+
logger.warn(`⚠️ Attachment ${attachment.path} is too big (${stats.size} bytes). Allowed size is ${MAX_ATTACHMENT_SIZE} bytes.`);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
form.append('images', fs.readFileSync(attachment_path), { filename: path.basename(attachment_path), filepath: attachment_path });
|
|
78
|
+
file_images.push({
|
|
79
|
+
file_name: attachment.name,
|
|
80
|
+
file_path: attachment.path,
|
|
81
|
+
});
|
|
82
|
+
count += 1;
|
|
83
|
+
}
|
|
84
|
+
if (file_images.length === 0) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
form.append('file_images', JSON.stringify(file_images));
|
|
88
|
+
await this.api.uploadAttachments(form.getHeaders(), form.getBuffer());
|
|
89
|
+
logger.info(`🏞️ Uploaded ${count} attachments`);
|
|
90
|
+
}
|
|
91
|
+
} catch (error) {
|
|
92
|
+
logger.error(`❌ Unable to upload attachments: ${error.message}`, error);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
module.exports = { BeatsAttachments }
|
package/src/beats/beats.js
CHANGED
|
@@ -2,6 +2,8 @@ const { getCIInformation } = require('../helpers/ci');
|
|
|
2
2
|
const logger = require('../utils/logger');
|
|
3
3
|
const { BeatsApi } = require('./beats.api');
|
|
4
4
|
const { HOOK } = require('../helpers/constants');
|
|
5
|
+
const TestResult = require('test-results-parser/src/models/TestResult');
|
|
6
|
+
const { BeatsAttachments } = require('./beats.attachments');
|
|
5
7
|
|
|
6
8
|
class Beats {
|
|
7
9
|
|
|
@@ -22,6 +24,7 @@ class Beats {
|
|
|
22
24
|
this.#setRunName();
|
|
23
25
|
this.#setApiKey();
|
|
24
26
|
await this.#publishTestResults();
|
|
27
|
+
await this.#uploadAttachments();
|
|
25
28
|
this.#updateTitleLink();
|
|
26
29
|
await this.#attachFailureSummary();
|
|
27
30
|
}
|
|
@@ -69,6 +72,33 @@ class Beats {
|
|
|
69
72
|
return payload;
|
|
70
73
|
}
|
|
71
74
|
|
|
75
|
+
async #uploadAttachments() {
|
|
76
|
+
if (!this.test_run_id) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (this.result.status !== 'FAIL') {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
const attachments = new BeatsAttachments(this.config, this.result, this.test_run_id);
|
|
84
|
+
await attachments.upload();
|
|
85
|
+
} catch (error) {
|
|
86
|
+
logger.error(`❌ Unable to upload attachments: ${error.message}`, error);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
#getAllFailedTestCases() {
|
|
91
|
+
const test_cases = [];
|
|
92
|
+
for (const suite of this.result.suites) {
|
|
93
|
+
for (const test of suite.cases) {
|
|
94
|
+
if (test.status === 'FAIL') {
|
|
95
|
+
test_cases.push(test);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return test_cases;
|
|
100
|
+
}
|
|
101
|
+
|
|
72
102
|
#updateTitleLink() {
|
|
73
103
|
if (!this.test_run_id) {
|
|
74
104
|
return;
|
package/src/cli.js
CHANGED
|
@@ -4,20 +4,33 @@ require('dotenv').config();
|
|
|
4
4
|
const sade = require('sade');
|
|
5
5
|
|
|
6
6
|
const prog = sade('testbeats');
|
|
7
|
-
const
|
|
7
|
+
const { PublishCommand } = require('./commands/publish.command');
|
|
8
8
|
const logger = require('./utils/logger');
|
|
9
9
|
|
|
10
10
|
prog
|
|
11
|
-
.version('2.0.
|
|
12
|
-
.option('-c, --config', '
|
|
13
|
-
.option('-l, --logLevel', 'Log Level', "INFO")
|
|
11
|
+
.version('2.0.4')
|
|
12
|
+
.option('-c, --config', 'path to config file')
|
|
13
|
+
.option('-l, --logLevel', 'Log Level', "INFO")
|
|
14
|
+
.option('--slack', 'slack webhook url')
|
|
15
|
+
.option('--teams', 'teams webhook url')
|
|
16
|
+
.option('--chat', 'chat webhook url')
|
|
17
|
+
.option('--title', 'title of the test run')
|
|
18
|
+
.option('--junit', 'junit xml path')
|
|
19
|
+
.option('--testng', 'testng xml path')
|
|
20
|
+
.option('--cucumber', 'cucumber json path')
|
|
21
|
+
.option('--mocha', 'mocha json path')
|
|
22
|
+
.option('--nunit', 'nunit xml path')
|
|
23
|
+
.option('--xunit', 'xunit xml path')
|
|
24
|
+
.option('--mstest', 'mstest xml path')
|
|
25
|
+
.option('-ci-info', 'ci info extension')
|
|
26
|
+
.option('-chart-test-summary', 'chart test summary extension');
|
|
14
27
|
|
|
15
28
|
prog.command('publish')
|
|
16
29
|
.action(async (opts) => {
|
|
17
30
|
try {
|
|
18
31
|
logger.setLevel(opts.logLevel);
|
|
19
|
-
|
|
20
|
-
await publish
|
|
32
|
+
const publish_command = new PublishCommand(opts);
|
|
33
|
+
await publish_command.publish();
|
|
21
34
|
} catch (error) {
|
|
22
35
|
logger.error(`Report publish failed: ${error.message}`);
|
|
23
36
|
process.exit(1);
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const trp = require('test-results-parser');
|
|
3
|
+
const prp = require('performance-results-parser');
|
|
4
|
+
|
|
5
|
+
const beats = require('../beats');
|
|
6
|
+
const { ConfigBuilder } = require('../utils/config.builder');
|
|
7
|
+
const target_manager = require('../targets');
|
|
8
|
+
const logger = require('../utils/logger');
|
|
9
|
+
const { processData } = require('../helpers/helper');
|
|
10
|
+
const pkg = require('../../package.json');
|
|
11
|
+
|
|
12
|
+
class PublishCommand {
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {import('../index').PublishOptions} opts
|
|
16
|
+
*/
|
|
17
|
+
constructor(opts) {
|
|
18
|
+
this.opts = opts;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async publish() {
|
|
22
|
+
logger.info(`🥁 TestBeats v${pkg.version}`);
|
|
23
|
+
this.#buildConfig();
|
|
24
|
+
this.#validateOptions();
|
|
25
|
+
this.#setConfigFromFile();
|
|
26
|
+
this.#processConfig();
|
|
27
|
+
this.#validateConfig();
|
|
28
|
+
this.#processResults();
|
|
29
|
+
await this.#publishResults();
|
|
30
|
+
logger.info('✅ Results published successfully!');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
#buildConfig() {
|
|
34
|
+
const config_builder = new ConfigBuilder(this.opts);
|
|
35
|
+
config_builder.build();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
#validateOptions() {
|
|
39
|
+
if (!this.opts) {
|
|
40
|
+
throw new Error('Missing publish options');
|
|
41
|
+
}
|
|
42
|
+
if (!this.opts.config) {
|
|
43
|
+
throw new Error('Missing publish config');
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
#setConfigFromFile() {
|
|
48
|
+
if (typeof this.opts.config === 'string') {
|
|
49
|
+
const cwd = process.cwd();
|
|
50
|
+
const file_path = path.join(cwd, this.opts.config);
|
|
51
|
+
try {
|
|
52
|
+
const config_json = require(path.join(cwd, this.opts.config));
|
|
53
|
+
this.opts.config = config_json;
|
|
54
|
+
} catch (error) {
|
|
55
|
+
logger.error({ error }, `Failed to read config file: '${file_path}' with error: '${error.message}'`);
|
|
56
|
+
throw new Error(`Config file not found: ${file_path}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
#processConfig() {
|
|
62
|
+
const processed_config = processData(this.opts.config);
|
|
63
|
+
/**@type {import('../index').PublishConfig[]} */
|
|
64
|
+
this.configs = [];
|
|
65
|
+
if (processed_config.reports) {
|
|
66
|
+
for (const report of config.reports) {
|
|
67
|
+
this.configs.push(report);
|
|
68
|
+
}
|
|
69
|
+
} else {
|
|
70
|
+
this.configs.push(processed_config);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
#validateConfig() {
|
|
75
|
+
logger.info("🛠️ Validating configuration...")
|
|
76
|
+
for (const config of this.configs) {
|
|
77
|
+
this.#validateResults(config);
|
|
78
|
+
this.#validateTargets(config);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
*
|
|
84
|
+
* @param {import('../index').PublishReport} config
|
|
85
|
+
*/
|
|
86
|
+
#validateResults(config) {
|
|
87
|
+
logger.debug("Validating results...")
|
|
88
|
+
if (!config.results) {
|
|
89
|
+
throw new Error('Missing results properties in config');
|
|
90
|
+
}
|
|
91
|
+
if (!Array.isArray(config.results)) {
|
|
92
|
+
throw new Error(`'config.results' must be an array`);
|
|
93
|
+
}
|
|
94
|
+
if (!config.results.length) {
|
|
95
|
+
throw new Error('At least one result must be defined');
|
|
96
|
+
}
|
|
97
|
+
for (const result of config.results) {
|
|
98
|
+
if (!result.type) {
|
|
99
|
+
throw new Error('Missing result type');
|
|
100
|
+
}
|
|
101
|
+
if (result.type === 'custom') {
|
|
102
|
+
if (!result.result) {
|
|
103
|
+
throw new Error(`custom 'config.results[*].result' is missing`);
|
|
104
|
+
}
|
|
105
|
+
} else {
|
|
106
|
+
if (!result.files) {
|
|
107
|
+
throw new Error('Missing result files');
|
|
108
|
+
}
|
|
109
|
+
if (!Array.isArray(result.files)) {
|
|
110
|
+
throw new Error('result files must be an array');
|
|
111
|
+
}
|
|
112
|
+
if (!result.files.length) {
|
|
113
|
+
throw new Error('At least one result file must be defined');
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
logger.debug("Validating results - Successful!")
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
*
|
|
122
|
+
* @param {import('../index').PublishReport} config
|
|
123
|
+
*/
|
|
124
|
+
#validateTargets(config) {
|
|
125
|
+
logger.debug("Validating targets...")
|
|
126
|
+
if (!config.targets) {
|
|
127
|
+
logger.warn('⚠️ Targets are not defined in config');
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (!Array.isArray(config.targets)) {
|
|
131
|
+
throw new Error('targets must be an array');
|
|
132
|
+
}
|
|
133
|
+
for (const target of config.targets) {
|
|
134
|
+
if (!target.name) {
|
|
135
|
+
throw new Error(`'config.targets[*].name' is missing`);
|
|
136
|
+
}
|
|
137
|
+
if (target.name === 'slack' || target.name === 'teams' || target.name === 'chat') {
|
|
138
|
+
if (!target.inputs) {
|
|
139
|
+
throw new Error(`missing inputs in ${target.name} target`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (target.inputs) {
|
|
143
|
+
const inputs = target.inputs;
|
|
144
|
+
if (target.name === 'slack' || target.name === 'teams' || target.name === 'chat') {
|
|
145
|
+
if (!inputs.url) {
|
|
146
|
+
throw new Error(`missing url in ${target.name} target inputs`);
|
|
147
|
+
}
|
|
148
|
+
if (typeof inputs.url !== 'string') {
|
|
149
|
+
throw new Error(`url in ${target.name} target inputs must be a string`);
|
|
150
|
+
}
|
|
151
|
+
if (!inputs.url.startsWith('http')) {
|
|
152
|
+
throw new Error(`url in ${target.name} target inputs must start with 'http' or 'https'`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
logger.debug("Validating targets - Successful!")
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
#processResults() {
|
|
161
|
+
logger.info('🧙 Processing results...');
|
|
162
|
+
this.results = [];
|
|
163
|
+
for (const config of this.configs) {
|
|
164
|
+
for (const result_options of config.results) {
|
|
165
|
+
if (result_options.type === 'custom') {
|
|
166
|
+
this.results.push(result_options.result);
|
|
167
|
+
} else if (result_options.type === 'jmeter') {
|
|
168
|
+
this.results.push(prp.parse(result_options));
|
|
169
|
+
} else {
|
|
170
|
+
this.results.push(trp.parse(result_options));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async #publishResults() {
|
|
177
|
+
for (const config of this.configs) {
|
|
178
|
+
for (let i = 0; i < this.results.length; i++) {
|
|
179
|
+
const result = this.results[i];
|
|
180
|
+
const global_extensions = config.extensions || [];
|
|
181
|
+
await beats.run(config, result);
|
|
182
|
+
if (config.targets) {
|
|
183
|
+
for (const target of config.targets) {
|
|
184
|
+
target.extensions = target.extensions || [];
|
|
185
|
+
target.extensions = global_extensions.concat(target.extensions);
|
|
186
|
+
await target_manager.run(target, result);
|
|
187
|
+
}
|
|
188
|
+
} else {
|
|
189
|
+
logger.warn('⚠️ No targets defined, skipping sending results to targets');
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
module.exports = { PublishCommand }
|
package/src/index.d.ts
CHANGED
|
@@ -228,6 +228,7 @@ export interface PublishReport {
|
|
|
228
228
|
run?: string;
|
|
229
229
|
show_failure_summary?: boolean;
|
|
230
230
|
targets?: Target[];
|
|
231
|
+
extensions?: Extension[];
|
|
231
232
|
results?: ParseOptions[] | PerformanceParseOptions[] | CustomResultOptions[];
|
|
232
233
|
}
|
|
233
234
|
|
|
@@ -236,6 +237,7 @@ export interface PublishConfig {
|
|
|
236
237
|
project?: string;
|
|
237
238
|
run?: string;
|
|
238
239
|
targets?: Target[];
|
|
240
|
+
extensions?: Extension[];
|
|
239
241
|
results?: ParseOptions[] | PerformanceParseOptions[] | CustomResultOptions[];
|
|
240
242
|
}
|
|
241
243
|
|
|
@@ -243,5 +245,25 @@ export interface PublishOptions {
|
|
|
243
245
|
config: string | PublishConfig;
|
|
244
246
|
}
|
|
245
247
|
|
|
248
|
+
export interface CommandLineOptions {
|
|
249
|
+
config?: string;
|
|
250
|
+
project?: string;
|
|
251
|
+
run?: string;
|
|
252
|
+
'api-key'?: string;
|
|
253
|
+
slack?: string;
|
|
254
|
+
teams?: string;
|
|
255
|
+
chat?: string;
|
|
256
|
+
title?: string;
|
|
257
|
+
'ci-info'?: boolean;
|
|
258
|
+
'chart-test-summary'?: boolean;
|
|
259
|
+
junit?: string;
|
|
260
|
+
testng?: string;
|
|
261
|
+
cucumber?: string;
|
|
262
|
+
mocha?: string;
|
|
263
|
+
nunit?: string;
|
|
264
|
+
xunit?: string;
|
|
265
|
+
mstest?: string;
|
|
266
|
+
}
|
|
267
|
+
|
|
246
268
|
export function publish(options: PublishOptions): Promise<any>
|
|
247
269
|
export function defineConfig(config: PublishConfig): PublishConfig
|
package/src/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
const
|
|
1
|
+
const { PublishCommand } = require('./commands/publish.command');
|
|
2
2
|
|
|
3
3
|
function publish(options) {
|
|
4
|
-
|
|
4
|
+
const publish_command = new PublishCommand(options);
|
|
5
|
+
return publish_command.publish();
|
|
5
6
|
}
|
|
6
7
|
|
|
7
8
|
function defineConfig(config) {
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const logger = require('./logger');
|
|
3
|
+
|
|
4
|
+
class ConfigBuilder {
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {import('../index').CommandLineOptions} opts
|
|
8
|
+
*/
|
|
9
|
+
constructor(opts) {
|
|
10
|
+
this.opts = opts;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
build() {
|
|
14
|
+
if (!this.opts) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
if (typeof this.opts.config === 'object') {
|
|
18
|
+
return
|
|
19
|
+
}
|
|
20
|
+
if (this.opts.config && typeof this.opts.config === 'string') {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
logger.info('🏗 Building config...')
|
|
25
|
+
this.#buildConfig();
|
|
26
|
+
this.#buildBeats();
|
|
27
|
+
this.#buildResults();
|
|
28
|
+
this.#buildTargets();
|
|
29
|
+
this.#buildExtensions();
|
|
30
|
+
|
|
31
|
+
logger.debug(`🛠️ Generated Config: \n${JSON.stringify(this.config, null, 2)}`);
|
|
32
|
+
|
|
33
|
+
this.opts.config = this.config;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
#buildConfig() {
|
|
37
|
+
/** @type {import('../index').PublishConfig} */
|
|
38
|
+
this.config = {};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
#buildBeats() {
|
|
42
|
+
this.config.project = this.opts.project || this.config.project;
|
|
43
|
+
this.config.run = this.opts.run || this.config.run;
|
|
44
|
+
this.config.api_key = this.opts['api-key'] || this.config.api_key;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
#buildResults() {
|
|
48
|
+
if (this.opts.junit) {
|
|
49
|
+
this.#addResults('junit', this.opts.junit);
|
|
50
|
+
}
|
|
51
|
+
if (this.opts.testng) {
|
|
52
|
+
this.#addResults('testng', this.opts.testng);
|
|
53
|
+
}
|
|
54
|
+
if (this.opts.cucumber) {
|
|
55
|
+
this.#addResults('cucumber', this.opts.cucumber);
|
|
56
|
+
}
|
|
57
|
+
if (this.opts.mocha) {
|
|
58
|
+
this.#addResults('mocha', this.opts.mocha);
|
|
59
|
+
}
|
|
60
|
+
if (this.opts.nunit) {
|
|
61
|
+
this.#addResults('nunit', this.opts.nunit);
|
|
62
|
+
}
|
|
63
|
+
if (this.opts.xunit) {
|
|
64
|
+
this.#addResults('xunit', this.opts.xunit);
|
|
65
|
+
}
|
|
66
|
+
if (this.opts.mstest) {
|
|
67
|
+
this.#addResults('mstest', this.opts.mstest);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @param {string} type
|
|
73
|
+
* @param {string} file
|
|
74
|
+
*/
|
|
75
|
+
#addResults(type, file) {
|
|
76
|
+
this.config.results = [
|
|
77
|
+
{
|
|
78
|
+
type,
|
|
79
|
+
files: [path.join(file)]
|
|
80
|
+
}
|
|
81
|
+
]
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
#buildTargets() {
|
|
85
|
+
if (this.opts.slack) {
|
|
86
|
+
this.#addTarget('slack', this.opts.slack);
|
|
87
|
+
}
|
|
88
|
+
if (this.opts.teams) {
|
|
89
|
+
this.#addTarget('teams', this.opts.teams);
|
|
90
|
+
}
|
|
91
|
+
if (this.opts.chat) {
|
|
92
|
+
this.#addTarget('chat', this.opts.chat);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
#addTarget(name, url) {
|
|
97
|
+
this.config.targets = this.config.targets || [];
|
|
98
|
+
this.config.targets.push({ name, inputs: { url, title: this.opts.title || '', only_failures: true } })
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
#buildExtensions() {
|
|
102
|
+
if (this.opts['ci-info']) {
|
|
103
|
+
this.#addExtension('ci-info');
|
|
104
|
+
}
|
|
105
|
+
if (this.opts['chart-test-summary']) {
|
|
106
|
+
this.#addExtension('quick-chart-test-summary');
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
#addExtension(name) {
|
|
111
|
+
this.config.extensions = this.config.extensions || [];
|
|
112
|
+
this.config.extensions.push({ name });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
module.exports = { ConfigBuilder };
|
package/src/commands/publish.js
DELETED
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
const path = require('path');
|
|
2
|
-
const trp = require('test-results-parser');
|
|
3
|
-
const prp = require('performance-results-parser');
|
|
4
|
-
|
|
5
|
-
const pkg = require('../../package.json');
|
|
6
|
-
const { processData } = require('../helpers/helper');
|
|
7
|
-
const beats = require('../beats');
|
|
8
|
-
const target_manager = require('../targets');
|
|
9
|
-
const logger = require('../utils/logger');
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* @param {import('../index').PublishOptions} opts
|
|
13
|
-
*/
|
|
14
|
-
async function run(opts) {
|
|
15
|
-
logger.info(`💡 TestBeats v${pkg.version}`);
|
|
16
|
-
if (!opts) {
|
|
17
|
-
throw new Error('Missing publish options');
|
|
18
|
-
}
|
|
19
|
-
if (!opts.config) {
|
|
20
|
-
throw new Error('Missing publish config');
|
|
21
|
-
}
|
|
22
|
-
if (typeof opts.config === 'string') {
|
|
23
|
-
const cwd = process.cwd();
|
|
24
|
-
const file_path = path.join(cwd, opts.config);
|
|
25
|
-
try {
|
|
26
|
-
opts.config = require(path.join(cwd, opts.config));
|
|
27
|
-
} catch (error) {
|
|
28
|
-
throw new Error(`Config file not found: ${file_path}`);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
const config = processData(opts.config);
|
|
32
|
-
if (config.reports) {
|
|
33
|
-
for (const report of config.reports) {
|
|
34
|
-
validateConfig(report);
|
|
35
|
-
await processReport(report);
|
|
36
|
-
}
|
|
37
|
-
} else {
|
|
38
|
-
validateConfig(config);
|
|
39
|
-
await processReport(config);
|
|
40
|
-
}
|
|
41
|
-
logger.info('✅ Results published successfully!');
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
*
|
|
46
|
-
* @param {import('../index').PublishReport} report
|
|
47
|
-
*/
|
|
48
|
-
async function processReport(report) {
|
|
49
|
-
logger.debug("processReport: Started")
|
|
50
|
-
const parsed_results = [];
|
|
51
|
-
for (const result_options of report.results) {
|
|
52
|
-
if (result_options.type === 'custom') {
|
|
53
|
-
parsed_results.push(result_options.result);
|
|
54
|
-
} else if (result_options.type === 'jmeter') {
|
|
55
|
-
parsed_results.push(prp.parse(result_options));
|
|
56
|
-
} else {
|
|
57
|
-
parsed_results.push(trp.parse(result_options));
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
for (let i = 0; i < parsed_results.length; i++) {
|
|
62
|
-
const result = parsed_results[i];
|
|
63
|
-
await beats.run(report, result);
|
|
64
|
-
if (report.targets) {
|
|
65
|
-
for (const target of report.targets) {
|
|
66
|
-
await target_manager.run(target, result);
|
|
67
|
-
}
|
|
68
|
-
} else {
|
|
69
|
-
logger.warn('No targets defined, skipping sending results to targets');
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
logger.debug("processReport: Ended")
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
*
|
|
77
|
-
* @param {import('../index').PublishReport} config
|
|
78
|
-
*/
|
|
79
|
-
function validateConfig(config) {
|
|
80
|
-
logger.info("🛠️ Validating configuration...")
|
|
81
|
-
if (!config) {
|
|
82
|
-
throw new Error('Missing configuration');
|
|
83
|
-
}
|
|
84
|
-
validateResults(config);
|
|
85
|
-
validateTargets(config);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
*
|
|
90
|
-
* @param {import('../index').PublishReport} config
|
|
91
|
-
*/
|
|
92
|
-
function validateResults(config) {
|
|
93
|
-
logger.debug("Validating results...")
|
|
94
|
-
if (!config.results) {
|
|
95
|
-
throw new Error('Missing results properties in config');
|
|
96
|
-
}
|
|
97
|
-
if (!Array.isArray(config.results)) {
|
|
98
|
-
throw new Error('results must be an array');
|
|
99
|
-
}
|
|
100
|
-
if (!config.results.length) {
|
|
101
|
-
throw new Error('At least one result must be defined');
|
|
102
|
-
}
|
|
103
|
-
for (const result of config.results) {
|
|
104
|
-
if (!result.type) {
|
|
105
|
-
throw new Error('Missing result type');
|
|
106
|
-
}
|
|
107
|
-
if (result.type === 'custom') {
|
|
108
|
-
if (!result.result) {
|
|
109
|
-
throw new Error('Missing result');
|
|
110
|
-
}
|
|
111
|
-
} else {
|
|
112
|
-
if (!result.files) {
|
|
113
|
-
throw new Error('Missing result files');
|
|
114
|
-
}
|
|
115
|
-
if (!Array.isArray(result.files)) {
|
|
116
|
-
throw new Error('result files must be an array');
|
|
117
|
-
}
|
|
118
|
-
if (!result.files.length) {
|
|
119
|
-
throw new Error('At least one result file must be defined');
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
logger.debug("Validating results - Successful!")
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
*
|
|
128
|
-
* @param {import('../index').PublishReport} config
|
|
129
|
-
*/
|
|
130
|
-
function validateTargets(config) {
|
|
131
|
-
logger.debug("Validating targets...")
|
|
132
|
-
if (!config.targets) {
|
|
133
|
-
logger.warn('⚠️ Targets are not defined in config');
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
if (!Array.isArray(config.targets)) {
|
|
137
|
-
throw new Error('targets must be an array');
|
|
138
|
-
}
|
|
139
|
-
for (const target of config.targets) {
|
|
140
|
-
if (!target.name) {
|
|
141
|
-
throw new Error('missing target name');
|
|
142
|
-
}
|
|
143
|
-
if (target.name === 'slack' || target.name === 'teams' || target.name === 'chat') {
|
|
144
|
-
if (!target.inputs) {
|
|
145
|
-
throw new Error(`missing inputs in ${target.name} target`);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
if (target.inputs) {
|
|
149
|
-
const inputs = target.inputs;
|
|
150
|
-
if (target.name === 'slack' || target.name === 'teams' || target.name === 'chat') {
|
|
151
|
-
if (!inputs.url) {
|
|
152
|
-
throw new Error(`missing url in ${target.name} target inputs`);
|
|
153
|
-
}
|
|
154
|
-
if (typeof inputs.url !== 'string') {
|
|
155
|
-
throw new Error(`url in ${target.name} target inputs must be a string`);
|
|
156
|
-
}
|
|
157
|
-
if (!inputs.url.startsWith('http')) {
|
|
158
|
-
throw new Error(`url in ${target.name} target inputs must start with 'http' or 'https'`);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
logger.debug("Validating targets - Successful!")
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
module.exports = {
|
|
167
|
-
run
|
|
168
|
-
}
|