testbeats 2.0.1 → 2.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- > !IMPORTANT - This npm package has been renamed from [test-results-reporter](https://www.npmjs.com/package/test-results-reporter) to [testbeats](https://www.npmjs.com/package/testbeats). test-results-reporter will soon be phased out, and users are encouraged to transition to testbeats.
1
+ > This npm package has been renamed from [test-results-reporter](https://www.npmjs.com/package/test-results-reporter) to [testbeats](https://www.npmjs.com/package/testbeats). test-results-reporter will soon be phased out, and users are encouraged to transition to testbeats.
2
2
 
3
3
  <span align="center">
4
4
 
@@ -19,35 +19,23 @@
19
19
 
20
20
  <hr>
21
21
 
22
- ### Targets
23
-
24
- <img height="48" style="margin: 6px;" src="./assets/slack.png" alt="slack" /> <img height="48" style="margin: 6px;" src="./assets/teams.png" alt="teams" /> <img height="48" style="margin: 6px;" src="./assets/chat.png" alt="chat" />
25
-
26
- ### Extensions
27
-
28
- <img height="48" style="margin: 6px;" src="./assets/reportportal.jpeg" alt="reportportal" /> <img height="48" style="margin: 6px;" src="./assets/quickchart.png" alt="quickchart" /> <img height="48" style="margin: 6px;" src="./assets/hyperlink.png" alt="hyperlink" /> <img height="48" style="margin: 6px;" src="./assets/mentions.png" alt="mentions" />
29
-
30
- ### Test Results
31
-
32
- <img height="48" style="margin: 6px;" src="./assets/testng.png" alt="testng" /> <img height="48" style="margin: 6px;" src="./assets/junit.png" alt="junit" /> <img height="48" style="margin: 6px;" src="./assets/cucumber.png" alt="cucumber" /> <img height="48" style="margin: 6px;" src="./assets/mocha.png" alt="mocha" /> <img height="48" style="margin: 6px;" src="./assets/xunit.png" alt="xunit" /> <img height="48" style="margin: 6px;" src="./assets/jmeter.png" alt="jmeter" />
22
+ </span>
33
23
 
34
- <hr>
24
+ ### Get Started
35
25
 
36
- ## Sample Reports
26
+ TestBeats is a tool designed to streamline the process of publishing test results from various automation testing frameworks to communication platforms like **slack**, **teams** and more for easy access and collaboration. It unifies your test reporting to build quality insights and make faster decisions.
37
27
 
38
- <br>
28
+ Read more about the project at [https://testbeats.com](https://testbeats.com)
39
29
 
40
- ![teams-summary-report](https://github.com/test-results-reporter/testbeats/raw/main/assets/teams-qc.png)
30
+ ### Sample Reports
41
31
 
42
- ![slack-summary-report](https://github.com/test-results-reporter/testbeats/raw/main/assets/slack-report-portal-analysis.png)
32
+ #### Alerts in Slack
43
33
 
44
- <br />
34
+ ![testbeats-failure-summary](./assets/testbeats-slack-failure-summary.png)
45
35
 
46
- <hr >
36
+ #### Results in Portal
47
37
 
48
- # [Documentation](https://test-results-reporter.github.io/)
49
-
50
- </span>
38
+ ![testbeats-failure-summary](./assets/testbeats-failure-summary.png)
51
39
 
52
40
  <br />
53
41
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testbeats",
3
- "version": "2.0.1",
3
+ "version": "2.0.3",
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
  }
@@ -0,0 +1,51 @@
1
+ const request = require('phin-retry');
2
+
3
+ class BeatsApi {
4
+
5
+ /**
6
+ * @param {import('../index').PublishReport} config
7
+ */
8
+ constructor(config) {
9
+ this.config = config;
10
+ }
11
+
12
+ postTestRun(payload) {
13
+ return request.post({
14
+ url: `${this.getBaseUrl()}/api/core/v1/test-runs`,
15
+ headers: {
16
+ 'x-api-key': this.config.api_key
17
+ },
18
+ body: payload
19
+ });
20
+ }
21
+
22
+ /**
23
+ * @param {string} run_id
24
+ * @returns
25
+ */
26
+ getTestRun(run_id) {
27
+ return request.get({
28
+ url: `${this.getBaseUrl()}/api/core/v1/test-runs/key?id=${run_id}`,
29
+ headers: {
30
+ 'x-api-key': this.config.api_key
31
+ }
32
+ });
33
+ }
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
+
46
+ getBaseUrl() {
47
+ return process.env.TEST_BEATS_URL || "https://app.testbeats.com";
48
+ }
49
+ }
50
+
51
+ module.exports = { BeatsApi }
@@ -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 }
@@ -0,0 +1,181 @@
1
+ const { getCIInformation } = require('../helpers/ci');
2
+ const logger = require('../utils/logger');
3
+ const { BeatsApi } = require('./beats.api');
4
+ const { HOOK } = require('../helpers/constants');
5
+ const TestResult = require('test-results-parser/src/models/TestResult');
6
+ const { BeatsAttachments } = require('./beats.attachments');
7
+
8
+ class Beats {
9
+
10
+ /**
11
+ * @param {import('../index').PublishReport} config
12
+ * @param {TestResult} result
13
+ */
14
+ constructor(config, result) {
15
+ this.config = config;
16
+ this.result = result;
17
+ this.api = new BeatsApi(config);
18
+ this.test_run_id = '';
19
+ }
20
+
21
+ async publish() {
22
+ this.#setCIInfo();
23
+ this.#setProjectName();
24
+ this.#setRunName();
25
+ this.#setApiKey();
26
+ await this.#publishTestResults();
27
+ await this.#uploadAttachments();
28
+ this.#updateTitleLink();
29
+ await this.#attachFailureSummary();
30
+ }
31
+
32
+ #setCIInfo() {
33
+ this.ci = getCIInformation();
34
+ }
35
+
36
+ #setProjectName() {
37
+ this.config.project = this.config.project || process.env.TEST_BEATS_PROJECT || (this.ci && this.ci.repository_name) || 'demo-project';
38
+ }
39
+
40
+ #setApiKey() {
41
+ this.config.api_key = this.config.api_key || process.env.TEST_BEATS_API_KEY;
42
+ }
43
+
44
+ #setRunName() {
45
+ this.config.run = this.config.run || process.env.TEST_BEATS_RUN || (this.ci && this.ci.build_name) || 'demo-run';
46
+ }
47
+
48
+ async #publishTestResults() {
49
+ if (!this.config.api_key) {
50
+ logger.warn('😿 No API key provided, skipping publishing results to TestBeats Portal');
51
+ return;
52
+ }
53
+ logger.info("🚀 Publishing results to TestBeats Portal...");
54
+ try {
55
+ const payload = this.#getPayload();
56
+ const response = await this.api.postTestRun(payload);
57
+ this.test_run_id = response.id;
58
+ } catch (error) {
59
+ logger.error(`❌ Unable to publish results to TestBeats Portal: ${error.message}`, error);
60
+ }
61
+ }
62
+
63
+ #getPayload() {
64
+ const payload = {
65
+ project: this.config.project,
66
+ run: this.config.run,
67
+ ...this.result
68
+ }
69
+ if (this.ci) {
70
+ payload.ci_details = [this.ci];
71
+ }
72
+ return payload;
73
+ }
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
+
102
+ #updateTitleLink() {
103
+ if (!this.test_run_id) {
104
+ return;
105
+ }
106
+ if (!this.config.targets) {
107
+ return;
108
+ }
109
+ const link = `${this.api.getBaseUrl()}/reports/${this.test_run_id}`;
110
+ for (const target of this.config.targets) {
111
+ target.inputs.title_link = link;
112
+ }
113
+ }
114
+
115
+ async #attachFailureSummary() {
116
+ if (!this.test_run_id) {
117
+ return;
118
+ }
119
+ if (!this.config.targets) {
120
+ return;
121
+ }
122
+ if (this.result.status !== 'FAIL') {
123
+ return;
124
+ }
125
+ if (this.config.show_failure_summary === false) {
126
+ return;
127
+ }
128
+ const text = await this.#getFailureSummary();
129
+ if (!text) {
130
+ return;
131
+ }
132
+ const extension = this.#getAIFailureSummaryExtension(text);
133
+ for (const target of this.config.targets) {
134
+ target.extensions = target.extensions || [];
135
+ target.extensions.push(extension);
136
+ }
137
+ }
138
+
139
+ async #getFailureSummary() {
140
+ logger.info('✨ Fetching AI Failure Summary...');
141
+ let retry = 3;
142
+ while (retry >= 0) {
143
+ retry = retry - 1;
144
+ await new Promise(resolve => setTimeout(resolve, this.#getDelay()));
145
+ const test_run = await this.api.getTestRun(this.test_run_id);
146
+ const status = test_run && test_run.failure_summary_status;
147
+ switch (status) {
148
+ case 'COMPLETED':
149
+ return test_run.execution_metrics[0].failure_summary;
150
+ case 'FAILED':
151
+ logger.error(`❌ Failed to generate AI Failure Summary`);
152
+ return;
153
+ case 'SKIPPED':
154
+ logger.warn(`❗ Skipped generating AI Failure Summary`);
155
+ return;
156
+ }
157
+ logger.info(`🔄 AI Failure Summary not generated, retrying...`);
158
+ }
159
+ logger.warn(`🙈 AI Failure Summary not generated in given time`);
160
+ }
161
+
162
+ #getDelay() {
163
+ if (process.env.TEST_BEATS_DELAY) {
164
+ return parseInt(process.env.TEST_BEATS_DELAY);
165
+ }
166
+ return 3000;
167
+ }
168
+
169
+ #getAIFailureSummaryExtension(text) {
170
+ return {
171
+ name: 'ai-failure-summary',
172
+ hook: HOOK.AFTER_SUMMARY,
173
+ inputs: {
174
+ failure_summary: text
175
+ }
176
+ };
177
+ }
178
+
179
+ }
180
+
181
+ module.exports = { Beats }
@@ -1,174 +1,13 @@
1
- const request = require('phin-retry');
2
1
  const TestResult = require('test-results-parser/src/models/TestResult');
3
- const { getCIInformation } = require('../helpers/ci');
4
- const { HOOK } = require('../helpers/constants');
5
-
6
- function get_base_url() {
7
- return process.env.TEST_BEATS_URL || "https://app.testbeats.com";
8
- }
2
+ const { Beats } = require('./beats');
9
3
 
10
4
  /**
11
5
  * @param {import('../index').PublishReport} config
12
6
  * @param {TestResult} result
13
7
  */
14
8
  async function run(config, result) {
15
- init(config);
16
- if (isValid(config)) {
17
- const run_id = await publishTestResults(config, result);
18
- if (run_id) {
19
- attachTestBeatsReportHyperLink(config, run_id);
20
- await attachTestBeatsFailureSummary(config, result, run_id);
21
- }
22
- } else {
23
- console.warn('Missing testbeats config parameters');
24
- }
25
- }
26
-
27
- /**
28
- *
29
- * @param {import('../index').PublishReport} config
30
- */
31
- function init(config) {
32
- config.project = config.project || process.env.TEST_BEATS_PROJECT;
33
- config.run = config.run || process.env.TEST_BEATS_RUN;
34
- config.api_key = config.api_key || process.env.TEST_BEATS_API_KEY;
35
- }
36
-
37
- /**
38
- *
39
- * @param {import('../index').PublishReport} config
40
- */
41
- function isValid(config) {
42
- return config.project && config.run && config.api_key
43
- }
44
-
45
- /**
46
- * @param {import('../index').PublishReport} config
47
- * @param {TestResult} result
48
- */
49
- async function publishTestResults(config, result) {
50
- console.log("Publishing results to TestBeats");
51
- try {
52
- const payload = {
53
- project: config.project,
54
- run: config.run,
55
- ...result
56
- }
57
- const ci = getCIInformation();
58
- if (ci) {
59
- payload.ci_details = [ci];
60
- }
61
-
62
- const response = await request.post({
63
- url: `${get_base_url()}/api/core/v1/test-runs`,
64
- headers: {
65
- 'x-api-key': config.api_key
66
- },
67
- body: payload
68
- });
69
- return response.id;
70
- } catch (error) {
71
- console.log("Unable to publish results to TestBeats");
72
- console.log(error);
73
- }
74
- }
75
-
76
- /**
77
- * @param {import('../index').PublishReport} config
78
- * @param {string} run_id
79
- */
80
- function attachTestBeatsReportHyperLink(config, run_id) {
81
- const beats_link = get_test_beats_report_link(run_id);
82
- if (config.targets) {
83
- for (const target of config.targets) {
84
- target.inputs.title_link = beats_link;
85
- }
86
- }
87
- }
88
-
89
- /**
90
- * @param {import('../index').PublishReport} config
91
- * @param {TestResult} result
92
- * @param {string} run_id
93
- */
94
- async function attachTestBeatsFailureSummary(config, result, run_id) {
95
- if (result.status !== 'FAIL') {
96
- return;
97
- }
98
- if (config.show_failure_summary === false) {
99
- return;
100
- }
101
- try {
102
- await processFailureSummary(config, run_id);
103
- } catch (error) {
104
- console.log(error);
105
- console.log("error processing failure summary");
106
- }
107
- }
108
-
109
- async function processFailureSummary(config, run_id) {
110
- let retry = 3;
111
- while (retry > 0) {
112
- const test_run = await getTestRun(config, run_id);
113
- if (test_run && test_run.failure_summary_status) {
114
- if (test_run.failure_summary_status === 'COMPLETED') {
115
- addAIFailureSummaryExtension(config, test_run);
116
- return;
117
- } else if (test_run.failure_summary_status === 'FAILED') {
118
- console.log(`Test run failure summary failed`);
119
- return;
120
- } else if (test_run.failure_summary_status === 'SKIPPED') {
121
- console.log(`Test run failure summary failed`);
122
- return;
123
- }
124
- }
125
- console.log(`Test run failure summary not completed, retrying...`);
126
- await new Promise(resolve => setTimeout(resolve, 3000));
127
- retry = retry - 1;
128
- }
129
- }
130
-
131
- /**
132
- * @param {import('../index').PublishReport} config
133
- * @param {string} run_id
134
- */
135
- function getTestRun(config, run_id) {
136
- return request.get({
137
- url: `${get_base_url()}/api/core/v1/test-runs/key?id=${run_id}`,
138
- headers: {
139
- 'x-api-key': config.api_key
140
- }
141
- });
142
- }
143
-
144
- function getAIFailureSummaryExtension(test_run) {
145
- const execution_metric = test_run.execution_metrics[0];
146
- return {
147
- name: 'ai-failure-summary',
148
- hook: HOOK.AFTER_SUMMARY,
149
- inputs: {
150
- failure_summary: execution_metric.failure_summary
151
- }
152
- };
153
- }
154
-
155
- function addAIFailureSummaryExtension(config, test_run) {
156
- const extension = getAIFailureSummaryExtension(test_run);
157
- if (config.targets) {
158
- for (const target of config.targets) {
159
- target.extensions = target.extensions || [];
160
- target.extensions.push(extension);
161
- }
162
- }
163
- }
164
-
165
- /**
166
- *
167
- * @param {string} run_id
168
- * @returns
169
- */
170
- function get_test_beats_report_link(run_id) {
171
- return `${get_base_url()}/reports/${run_id}`;
9
+ const beats = new Beats(config, result);
10
+ await beats.publish();
172
11
  }
173
12
 
174
13
  module.exports = { run }
package/src/cli.js CHANGED
@@ -4,18 +4,38 @@ require('dotenv').config();
4
4
  const sade = require('sade');
5
5
 
6
6
  const prog = sade('testbeats');
7
- const publish = require('./commands/publish');
7
+ const { PublishCommand } = require('./commands/publish.command');
8
+ const logger = require('./utils/logger');
9
+ const { ConfigBuilder } = require('./utils/config.builder');
8
10
 
9
11
  prog
10
- .version('2.0.1')
11
- .option('-c, --config', 'Provide path to custom config', 'config.json');
12
+ .version('2.0.3')
13
+ .option('-c, --config', 'path to config file')
14
+ .option('-l, --logLevel', 'Log Level', "INFO")
15
+ .option('--slack', 'slack webhook url')
16
+ .option('--teams', 'teams webhook url')
17
+ .option('--chat', 'chat webhook url')
18
+ .option('--title', 'title of the test run')
19
+ .option('--junit', 'junit xml path')
20
+ .option('--testng', 'testng xml path')
21
+ .option('--cucumber', 'cucumber json path')
22
+ .option('--mocha', 'mocha json path')
23
+ .option('--nunit', 'nunit xml path')
24
+ .option('--xunit', 'xunit xml path')
25
+ .option('--mstest', 'mstest xml path')
26
+ .option('-ci-info', 'ci info extension')
27
+ .option('-chart-test-summary', 'chart test summary extension');
12
28
 
13
29
  prog.command('publish')
14
30
  .action(async (opts) => {
15
31
  try {
16
- await publish.run(opts);
32
+ logger.setLevel(opts.logLevel);
33
+ const config_builder = new ConfigBuilder(opts);
34
+ config_builder.build();
35
+ const publish_command = new PublishCommand(opts);
36
+ await publish_command.publish();
17
37
  } catch (error) {
18
- console.error(error);
38
+ logger.error(`Report publish failed: ${error.message}`);
19
39
  process.exit(1);
20
40
  }
21
41
  });
@@ -0,0 +1,188 @@
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 target_manager = require('../targets');
7
+ const logger = require('../utils/logger');
8
+ const { processData } = require('../helpers/helper');
9
+ const pkg = require('../../package.json');
10
+
11
+ class PublishCommand {
12
+
13
+ /**
14
+ * @param {import('../index').PublishOptions} opts
15
+ */
16
+ constructor(opts) {
17
+ this.opts = opts;
18
+ }
19
+
20
+ async publish() {
21
+ logger.info(`🥁 TestBeats v${pkg.version}`);
22
+ this.#validateOptions();
23
+ this.#setConfigFromFile();
24
+ this.#processConfig();
25
+ this.#validateConfig();
26
+ this.#processResults();
27
+ await this.#publishResults();
28
+ logger.info('✅ Results published successfully!');
29
+ }
30
+
31
+ #validateOptions() {
32
+ if (!this.opts) {
33
+ throw new Error('Missing publish options');
34
+ }
35
+ if (!this.opts.config) {
36
+ throw new Error('Missing publish config');
37
+ }
38
+ }
39
+
40
+ #setConfigFromFile() {
41
+ if (typeof this.opts.config === 'string') {
42
+ const cwd = process.cwd();
43
+ const file_path = path.join(cwd, this.opts.config);
44
+ try {
45
+ this.opts.config = require(path.join(cwd, this.opts.config));
46
+ } catch (error) {
47
+ throw new Error(`Config file not found: ${file_path}`);
48
+ }
49
+ }
50
+ }
51
+
52
+ #processConfig() {
53
+ const processed_config = processData(this.opts.config);
54
+ /**@type {import('../index').PublishConfig[]} */
55
+ this.configs = [];
56
+ if (processed_config.reports) {
57
+ for (const report of config.reports) {
58
+ this.configs.push(report);
59
+ }
60
+ } else {
61
+ this.configs.push(processed_config);
62
+ }
63
+ }
64
+
65
+ #validateConfig() {
66
+ logger.info("🛠️ Validating configuration...")
67
+ for (const config of this.configs) {
68
+ this.#validateResults(config);
69
+ this.#validateTargets(config);
70
+ }
71
+ }
72
+
73
+ /**
74
+ *
75
+ * @param {import('../index').PublishReport} config
76
+ */
77
+ #validateResults(config) {
78
+ logger.debug("Validating results...")
79
+ if (!config.results) {
80
+ throw new Error('Missing results properties in config');
81
+ }
82
+ if (!Array.isArray(config.results)) {
83
+ throw new Error(`'config.results' must be an array`);
84
+ }
85
+ if (!config.results.length) {
86
+ throw new Error('At least one result must be defined');
87
+ }
88
+ for (const result of config.results) {
89
+ if (!result.type) {
90
+ throw new Error('Missing result type');
91
+ }
92
+ if (result.type === 'custom') {
93
+ if (!result.result) {
94
+ throw new Error(`custom 'config.results[*].result' is missing`);
95
+ }
96
+ } else {
97
+ if (!result.files) {
98
+ throw new Error('Missing result files');
99
+ }
100
+ if (!Array.isArray(result.files)) {
101
+ throw new Error('result files must be an array');
102
+ }
103
+ if (!result.files.length) {
104
+ throw new Error('At least one result file must be defined');
105
+ }
106
+ }
107
+ }
108
+ logger.debug("Validating results - Successful!")
109
+ }
110
+
111
+ /**
112
+ *
113
+ * @param {import('../index').PublishReport} config
114
+ */
115
+ #validateTargets(config) {
116
+ logger.debug("Validating targets...")
117
+ if (!config.targets) {
118
+ logger.warn('⚠️ Targets are not defined in config');
119
+ return;
120
+ }
121
+ if (!Array.isArray(config.targets)) {
122
+ throw new Error('targets must be an array');
123
+ }
124
+ for (const target of config.targets) {
125
+ if (!target.name) {
126
+ throw new Error(`'config.targets[*].name' is missing`);
127
+ }
128
+ if (target.name === 'slack' || target.name === 'teams' || target.name === 'chat') {
129
+ if (!target.inputs) {
130
+ throw new Error(`missing inputs in ${target.name} target`);
131
+ }
132
+ }
133
+ if (target.inputs) {
134
+ const inputs = target.inputs;
135
+ if (target.name === 'slack' || target.name === 'teams' || target.name === 'chat') {
136
+ if (!inputs.url) {
137
+ throw new Error(`missing url in ${target.name} target inputs`);
138
+ }
139
+ if (typeof inputs.url !== 'string') {
140
+ throw new Error(`url in ${target.name} target inputs must be a string`);
141
+ }
142
+ if (!inputs.url.startsWith('http')) {
143
+ throw new Error(`url in ${target.name} target inputs must start with 'http' or 'https'`);
144
+ }
145
+ }
146
+ }
147
+ }
148
+ logger.debug("Validating targets - Successful!")
149
+ }
150
+
151
+ #processResults() {
152
+ logger.info('🧙 Processing results...');
153
+ this.results = [];
154
+ for (const config of this.configs) {
155
+ for (const result_options of config.results) {
156
+ if (result_options.type === 'custom') {
157
+ this.results.push(result_options.result);
158
+ } else if (result_options.type === 'jmeter') {
159
+ this.results.push(prp.parse(result_options));
160
+ } else {
161
+ this.results.push(trp.parse(result_options));
162
+ }
163
+ }
164
+ }
165
+ }
166
+
167
+ async #publishResults() {
168
+ for (const config of this.configs) {
169
+ for (let i = 0; i < this.results.length; i++) {
170
+ const result = this.results[i];
171
+ const global_extensions = config.extensions || [];
172
+ await beats.run(config, result);
173
+ if (config.targets) {
174
+ for (const target of config.targets) {
175
+ target.extensions = target.extensions || [];
176
+ target.extensions = global_extensions.concat(target.extensions);
177
+ await target_manager.run(target, result);
178
+ }
179
+ } else {
180
+ logger.warn('⚠️ No targets defined, skipping sending results to targets');
181
+ }
182
+ }
183
+ }
184
+ }
185
+
186
+ }
187
+
188
+ module.exports = { PublishCommand }
@@ -10,6 +10,7 @@ const ci_info = require('./ci-info');
10
10
  const ai_failure_summary = require('./ai-failure-summary');
11
11
  const { EXTENSION } = require('../helpers/constants');
12
12
  const { checkCondition } = require('../helpers/helper');
13
+ const logger = require('../utils/logger');
13
14
 
14
15
  async function run(options) {
15
16
  const { target, result, hook } = options;
@@ -25,9 +26,9 @@ async function run(options) {
25
26
  try {
26
27
  await extension_runner.run(options);
27
28
  } catch (error) {
28
- console.log('Failed to run extension');
29
- console.log(extension);
30
- console.log(error);
29
+ logger.error(`Failed to run extension: ${error.message}`);
30
+ logger.debug(`Extension details`, extension);
31
+ logger.debug(`Error: `, error);
31
32
  }
32
33
  }
33
34
  }
@@ -1,6 +1,7 @@
1
1
  const { getSuiteHistory, getLastLaunchByName, getLaunchDetails } = require('../helpers/report-portal');
2
2
  const { addChatExtension, addSlackExtension, addTeamsExtension } = require('../helpers/extension.helper');
3
3
  const { HOOK, STATUS } = require('../helpers/constants');
4
+ const logger = require('../utils/logger');
4
5
 
5
6
  async function getLaunchHistory(extension) {
6
7
  const { inputs, outputs } = extension;
@@ -71,8 +72,8 @@ async function run({ extension, target, payload }) {
71
72
  }
72
73
  }
73
74
  } catch (error) {
74
- console.log('Failed to get report portal history');
75
- console.log(error);
75
+ logger.error(`Failed to get report portal history: ${error.message}`);
76
+ logger.debug(`Error: ${error}`);
76
77
  }
77
78
  }
78
79
 
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 cmd_publish = require('./commands/publish');
1
+ const { PublishCommand } = require('./commands/publish.command');
2
2
 
3
3
  function publish(options) {
4
- return cmd_publish.run(options);
4
+ const publish_command = new PublishCommand(options);
5
+ return publish_command.publish();
5
6
  }
6
7
 
7
8
  function defineConfig(config) {
@@ -0,0 +1,108 @@
1
+ class ConfigBuilder {
2
+
3
+ /**
4
+ * @param {import('../index').CommandLineOptions} opts
5
+ */
6
+ constructor(opts) {
7
+ this.opts = opts;
8
+ }
9
+
10
+ build() {
11
+ if (!this.opts) {
12
+ return;
13
+ }
14
+ if (this.opts.config) {
15
+ return;
16
+ }
17
+
18
+ this.#buildConfig();
19
+ this.#buildBeats();
20
+ this.#buildResults();
21
+ this.#buildTargets();
22
+ this.#buildExtensions();
23
+
24
+ this.opts.config = this.config;
25
+ }
26
+
27
+ #buildConfig() {
28
+ /** @type {import('../index').PublishConfig} */
29
+ this.config = {};
30
+ }
31
+
32
+ #buildBeats() {
33
+ this.config.project = this.opts.project || this.config.project;
34
+ this.config.run = this.opts.run || this.config.run;
35
+ this.config.api_key = this.opts['api-key'] || this.config.api_key;
36
+ }
37
+
38
+ #buildResults() {
39
+ if (this.opts.junit) {
40
+ this.#addResults('junit', this.opts.junit);
41
+ }
42
+ if (this.opts.testng) {
43
+ this.#addResults('testng', this.opts.testng);
44
+ }
45
+ if (this.opts.cucumber) {
46
+ this.#addResults('cucumber', this.opts.cucumber);
47
+ }
48
+ if (this.opts.mocha) {
49
+ this.#addResults('mocha', this.opts.mocha);
50
+ }
51
+ if (this.opts.nunit) {
52
+ this.#addResults('nunit', this.opts.nunit);
53
+ }
54
+ if (this.opts.xunit) {
55
+ this.#addResults('xunit', this.opts.xunit);
56
+ }
57
+ if (this.opts.mstest) {
58
+ this.#addResults('mstest', this.opts.mstest);
59
+ }
60
+ }
61
+
62
+ /**
63
+ * @param {string} type
64
+ * @param {string} file
65
+ */
66
+ #addResults(type, file) {
67
+ this.config.results = [
68
+ {
69
+ type,
70
+ files: [file]
71
+ }
72
+ ]
73
+ }
74
+
75
+ #buildTargets() {
76
+ if (this.opts.slack) {
77
+ this.#addTarget('slack', this.opts.slack);
78
+ }
79
+ if (this.opts.teams) {
80
+ this.#addTarget('teams', this.opts.teams);
81
+ }
82
+ if (this.opts.chat) {
83
+ this.#addTarget('chat', this.opts.chat);
84
+ }
85
+ }
86
+
87
+ #addTarget(name, url) {
88
+ this.config.targets = this.config.targets || [];
89
+ this.config.targets.push({ name, inputs: { url, title: this.opts.title || '', only_failures: true } })
90
+ }
91
+
92
+ #buildExtensions() {
93
+ if (this.opts['ci-info']) {
94
+ this.#addExtension('ci-info');
95
+ }
96
+ if (this.opts['chart-test-summary']) {
97
+ this.#addExtension('quick-chart-test-summary');
98
+ }
99
+ }
100
+
101
+ #addExtension(name) {
102
+ this.config.extensions = this.config.extensions || [];
103
+ this.config.extensions.push({ name });
104
+ }
105
+
106
+ }
107
+
108
+ module.exports = { ConfigBuilder };
@@ -0,0 +1,98 @@
1
+ const trm = console;
2
+
3
+ const LEVEL_VERBOSE = 2;
4
+ const LEVEL_TRACE = 3;
5
+ const LEVEL_DEBUG = 4;
6
+ const LEVEL_INFO = 5;
7
+ const LEVEL_WARN = 6;
8
+ const LEVEL_ERROR = 7;
9
+ const LEVEL_SILENT = 8;
10
+
11
+ /**
12
+ * returns log level value
13
+ * @param {string} level - log level
14
+ */
15
+ function getLevelValue(level) {
16
+ const logLevel = level.toUpperCase();
17
+ switch (logLevel) {
18
+ case 'TRACE':
19
+ return LEVEL_TRACE;
20
+ case 'DEBUG':
21
+ return LEVEL_DEBUG;
22
+ case 'INFO':
23
+ return LEVEL_INFO;
24
+ case 'WARN':
25
+ return LEVEL_WARN;
26
+ case 'ERROR':
27
+ return LEVEL_ERROR;
28
+ case 'SILENT':
29
+ return LEVEL_SILENT;
30
+ case 'VERBOSE':
31
+ return LEVEL_VERBOSE;
32
+ default:
33
+ return LEVEL_INFO;
34
+ }
35
+ }
36
+
37
+ class Logger {
38
+
39
+ constructor() {
40
+ this.level = process.env.TESTBEATS_LOG_LEVEL || 'INFO';
41
+ this.levelValue = getLevelValue(this.level);
42
+ if (process.env.TESTBEATS_DISABLE_LOG_COLORS === 'true') {
43
+ options.disableColors = true;
44
+ }
45
+ }
46
+
47
+ /**
48
+ * sets log level
49
+ * @param {('TRACE'|'DEBUG'|'INFO'|'WARN'|'ERROR')} level - log level
50
+ */
51
+ setLevel(level) {
52
+ this.level = level;
53
+ this.levelValue = getLevelValue(this.level);
54
+ }
55
+
56
+ trace(...msg) {
57
+ if (this.levelValue <= LEVEL_TRACE) {
58
+ msg.forEach(m => trm.debug(m));
59
+ }
60
+ }
61
+
62
+ debug(...msg) {
63
+ if (this.levelValue <= LEVEL_DEBUG) {
64
+ msg.forEach(m => trm.debug(m));
65
+ }
66
+ }
67
+
68
+ info(...msg) {
69
+ if (this.levelValue <= LEVEL_INFO) {
70
+ msg.forEach(m => trm.info(m));
71
+ }
72
+ }
73
+
74
+ warn(...msg) {
75
+ if (this.levelValue <= LEVEL_WARN) {
76
+ msg.forEach(m => trm.warn(getMessage(m)));
77
+ }
78
+ }
79
+
80
+ error(...msg) {
81
+ if (this.levelValue <= LEVEL_ERROR) {
82
+ msg.forEach(m => trm.error(getMessage(m)));
83
+ }
84
+ }
85
+
86
+ }
87
+
88
+
89
+ function getMessage(msg) {
90
+ try {
91
+ return typeof msg === 'object' ? JSON.stringify(msg, null, 2) : msg;
92
+ } catch (_) {
93
+ return msg;
94
+ }
95
+ }
96
+
97
+
98
+ module.exports = new Logger();
@@ -1,157 +0,0 @@
1
- const path = require('path');
2
- const trp = require('test-results-parser');
3
- const prp = require('performance-results-parser');
4
-
5
- const { processData } = require('../helpers/helper');
6
- const beats = require('../beats');
7
- const target_manager = require('../targets');
8
-
9
- /**
10
- * @param {import('../index').PublishOptions} opts
11
- */
12
- async function run(opts) {
13
- if (!opts) {
14
- throw new Error('Missing publish options');
15
- }
16
- if (!opts.config) {
17
- throw new Error('Missing publish config');
18
- }
19
- if (typeof opts.config === 'string') {
20
- const cwd = process.cwd();
21
- const file_path = path.join(cwd, opts.config);
22
- try {
23
- opts.config = require(path.join(cwd, opts.config));
24
- } catch (error) {
25
- throw new Error(`Config file not found: ${file_path}`);
26
- }
27
- }
28
- const config = processData(opts.config);
29
- if (config.reports) {
30
- for (const report of config.reports) {
31
- validateConfig(report);
32
- await processReport(report);
33
- }
34
- } else {
35
- validateConfig(config);
36
- await processReport(config);
37
- }
38
- }
39
-
40
- /**
41
- *
42
- * @param {import('../index').PublishReport} report
43
- */
44
- async function processReport(report) {
45
- const parsed_results = [];
46
- for (const result_options of report.results) {
47
- if (result_options.type === 'custom') {
48
- parsed_results.push(result_options.result);
49
- } else if (result_options.type === 'jmeter') {
50
- parsed_results.push(prp.parse(result_options));
51
- } else {
52
- parsed_results.push(trp.parse(result_options));
53
- }
54
- }
55
-
56
- for (let i = 0; i < parsed_results.length; i++) {
57
- const result = parsed_results[i];
58
- await beats.run(report, result);
59
- if (report.targets) {
60
- for (const target of report.targets) {
61
- await target_manager.run(target, result);
62
- }
63
- } else {
64
- console.log('No targets defined, skipping sending results to targets');
65
- }
66
- }
67
- }
68
-
69
- /**
70
- *
71
- * @param {import('../index').PublishReport} config
72
- */
73
- function validateConfig(config) {
74
- if (!config) {
75
- throw new Error('Missing publish config');
76
- }
77
- validateResults(config);
78
- validateTargets(config);
79
- }
80
-
81
- /**
82
- *
83
- * @param {import('../index').PublishReport} config
84
- */
85
- function validateResults(config) {
86
- if (!config.results) {
87
- throw new Error('Missing results properties in config');
88
- }
89
- if (!Array.isArray(config.results)) {
90
- throw new Error('results must be an array');
91
- }
92
- if (!config.results.length) {
93
- throw new Error('At least one result must be defined');
94
- }
95
- for (const result of config.results) {
96
- if (!result.type) {
97
- throw new Error('Missing result type');
98
- }
99
- if (result.type === 'custom') {
100
- if (!result.result) {
101
- throw new Error('Missing result');
102
- }
103
- } else {
104
- if (!result.files) {
105
- throw new Error('Missing result files');
106
- }
107
- if (!Array.isArray(result.files)) {
108
- throw new Error('result files must be an array');
109
- }
110
- if (!result.files.length) {
111
- throw new Error('At least one result file must be defined');
112
- }
113
- }
114
- }
115
- }
116
-
117
- /**
118
- *
119
- * @param {import('../index').PublishReport} config
120
- */
121
- function validateTargets(config) {
122
- if (!config.targets) {
123
- console.warn('targets are not defined in config');
124
- return;
125
- }
126
- if (!Array.isArray(config.targets)) {
127
- throw new Error('targets must be an array');
128
- }
129
- for (const target of config.targets) {
130
- if (!target.name) {
131
- throw new Error('missing target name');
132
- }
133
- if (target.name === 'slack' || target.name === 'teams' || target.name === 'chat') {
134
- if (!target.inputs) {
135
- throw new Error(`missing inputs in ${target.name} target`);
136
- }
137
- }
138
- if (target.inputs) {
139
- const inputs = target.inputs;
140
- if (target.name === 'slack' || target.name === 'teams' || target.name === 'chat') {
141
- if (!inputs.url) {
142
- throw new Error(`missing url in ${target.name} target inputs`);
143
- }
144
- if (typeof inputs.url !== 'string') {
145
- throw new Error(`url in ${target.name} target inputs must be a string`);
146
- }
147
- if (!inputs.url.startsWith('http')) {
148
- throw new Error(`url in ${target.name} target inputs must start with 'http' or 'https'`);
149
- }
150
- }
151
- }
152
- }
153
- }
154
-
155
- module.exports = {
156
- run
157
- }