testbeats 2.0.4 → 2.0.6

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
@@ -13,6 +13,8 @@
13
13
  ![Downloads](https://img.shields.io/npm/dt/test-results-reporter?logo=npm&label=downloads-old)
14
14
  ![Downloads](https://img.shields.io/npm/dt/testbeats?logo=npm)
15
15
  ![Size](https://img.shields.io/bundlephobia/minzip/testbeats)
16
+ ![NodeJs](https://img.shields.io/badge/NodeJS-%3E%3D14.0.0-brightgreen?logo=node.js)
17
+
16
18
 
17
19
  [![Stars](https://img.shields.io/github/stars/test-results-reporter/testbeats?style=social)](https://github.com/test-results-reporter/testbeats/stargazers)
18
20
  ![Downloads](https://img.shields.io/github/downloads/test-results-reporter/testbeats/total?logo=github)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testbeats",
3
- "version": "2.0.4",
3
+ "version": "2.0.6",
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",
@@ -64,5 +64,9 @@
64
64
  "mocha-multi-reporters": "^1.5.1",
65
65
  "pactum": "^3.2.3",
66
66
  "pkg": "^5.8.0"
67
- }
67
+ },
68
+ "engines": {
69
+ "node": ">=14.0.0"
70
+ },
71
+ "engineStrict": true
68
72
  }
@@ -16,6 +16,7 @@ class Beats {
16
16
  this.result = result;
17
17
  this.api = new BeatsApi(config);
18
18
  this.test_run_id = '';
19
+ this.test_run = null;
19
20
  }
20
21
 
21
22
  async publish() {
@@ -27,6 +28,7 @@ class Beats {
27
28
  await this.#uploadAttachments();
28
29
  this.#updateTitleLink();
29
30
  await this.#attachFailureSummary();
31
+ await this.#attachSmartAnalysis();
30
32
  }
31
33
 
32
34
  #setCIInfo() {
@@ -47,7 +49,7 @@ class Beats {
47
49
 
48
50
  async #publishTestResults() {
49
51
  if (!this.config.api_key) {
50
- logger.warn('😿 No API key provided, skipping publishing results to TestBeats Portal');
52
+ logger.warn('😿 No API key provided, skipping publishing results to TestBeats Portal...');
51
53
  return;
52
54
  }
53
55
  logger.info("🚀 Publishing results to TestBeats Portal...");
@@ -125,55 +127,77 @@ class Beats {
125
127
  if (this.config.show_failure_summary === false) {
126
128
  return;
127
129
  }
128
- const text = await this.#getFailureSummary();
129
- if (!text) {
130
+ try {
131
+ logger.info('✨ Fetching AI Failure Summary...');
132
+ await this.#setTestRun(' AI Failure Summary', 'failure_summary_status');
133
+ this.config.extensions.push({
134
+ name: 'ai-failure-summary',
135
+ hook: HOOK.AFTER_SUMMARY,
136
+ inputs: {
137
+ data: this.test_run
138
+ }
139
+ });
140
+ } catch (error) {
141
+ logger.error(`❌ Unable to attach failure summary: ${error.message}`, error);
142
+ }
143
+ }
144
+
145
+ async #attachSmartAnalysis() {
146
+ if (!this.test_run_id) {
130
147
  return;
131
148
  }
132
- const extension = this.#getAIFailureSummaryExtension(text);
133
- for (const target of this.config.targets) {
134
- target.extensions = target.extensions || [];
135
- target.extensions.push(extension);
149
+ if (!this.config.targets) {
150
+ return;
151
+ }
152
+ if (this.config.show_smart_analysis === false) {
153
+ return;
154
+ }
155
+ try {
156
+ logger.info('🤓 Fetching Smart Analysis...');
157
+ await this.#setTestRun('Smart Analysis', 'smart_analysis_status');
158
+ this.config.extensions.push({
159
+ name: 'smart-analysis',
160
+ hook: HOOK.AFTER_SUMMARY,
161
+ inputs: {
162
+ data: this.test_run
163
+ }
164
+ });
165
+ } catch (error) {
166
+ logger.error(`❌ Unable to attach smart analysis: ${error.message}`, error);
167
+ }
168
+ }
169
+
170
+ #getDelay() {
171
+ if (process.env.TEST_BEATS_DELAY) {
172
+ return parseInt(process.env.TEST_BEATS_DELAY);
136
173
  }
174
+ return 3000;
137
175
  }
138
176
 
139
- async #getFailureSummary() {
140
- logger.info('✨ Fetching AI Failure Summary...');
177
+ async #setTestRun(text, wait_for = 'smart_analysis_status') {
178
+ if (this.test_run && this.test_run[wait_for] === 'COMPLETED') {
179
+ return;
180
+ }
141
181
  let retry = 3;
142
182
  while (retry >= 0) {
143
183
  retry = retry - 1;
144
184
  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;
185
+ this.test_run = await this.api.getTestRun(this.test_run_id);
186
+ const status = this.test_run && this.test_run[wait_for];
147
187
  switch (status) {
148
188
  case 'COMPLETED':
149
- return test_run.execution_metrics[0].failure_summary;
189
+ logger.debug(`☑️ ${text} generated successfully`);
190
+ return;
150
191
  case 'FAILED':
151
- logger.error(`❌ Failed to generate AI Failure Summary`);
192
+ logger.error(`❌ Failed to generate ${text}`);
152
193
  return;
153
194
  case 'SKIPPED':
154
- logger.warn(`❗ Skipped generating AI Failure Summary`);
195
+ logger.warn(`❗ Skipped generating ${text}`);
155
196
  return;
156
197
  }
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);
198
+ logger.info(`🔄 ${text} not generated, retrying...`);
165
199
  }
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
- };
200
+ logger.warn(`🙈 ${text} not generated in given time`);
177
201
  }
178
202
 
179
203
  }
@@ -0,0 +1,19 @@
1
+ export type IBeatExecutionMetric = {
2
+ id: string
3
+ created_at: string
4
+ updated_at: string
5
+ newly_failed: number
6
+ always_failing: number
7
+ recurring_failures: number
8
+ recovered: number
9
+ added: number
10
+ removed: number
11
+ flaky: number
12
+ failure_summary: any
13
+ failure_summary_provider: any
14
+ failure_summary_model: any
15
+ status: string
16
+ status_message: any
17
+ test_run_id: string
18
+ org_id: string
19
+ }
package/src/cli.js CHANGED
@@ -11,6 +11,9 @@ prog
11
11
  .version('2.0.4')
12
12
  .option('-c, --config', 'path to config file')
13
13
  .option('-l, --logLevel', 'Log Level', "INFO")
14
+ .option('--api-key', 'api key')
15
+ .option('--project', 'project name')
16
+ .option('--run', 'run name')
14
17
  .option('--slack', 'slack webhook url')
15
18
  .option('--teams', 'teams webhook url')
16
19
  .option('--chat', 'chat webhook url')
@@ -1,6 +1,7 @@
1
1
  const path = require('path');
2
2
  const trp = require('test-results-parser');
3
3
  const prp = require('performance-results-parser');
4
+ const os = require('os');
4
5
 
5
6
  const beats = require('../beats');
6
7
  const { ConfigBuilder } = require('../utils/config.builder');
@@ -8,6 +9,7 @@ const target_manager = require('../targets');
8
9
  const logger = require('../utils/logger');
9
10
  const { processData } = require('../helpers/helper');
10
11
  const pkg = require('../../package.json');
12
+ const { MIN_NODE_VERSION } = require('../helpers/constants');
11
13
 
12
14
  class PublishCommand {
13
15
 
@@ -20,6 +22,8 @@ class PublishCommand {
20
22
 
21
23
  async publish() {
22
24
  logger.info(`🥁 TestBeats v${pkg.version}`);
25
+
26
+ this.#validateEnvDetails();
23
27
  this.#buildConfig();
24
28
  this.#validateOptions();
25
29
  this.#setConfigFromFile();
@@ -30,6 +34,20 @@ class PublishCommand {
30
34
  logger.info('✅ Results published successfully!');
31
35
  }
32
36
 
37
+ #validateEnvDetails() {
38
+ try {
39
+ const current_major_version = parseInt(process.version.split('.')[0].replace('v', ''));
40
+ if (current_major_version >= MIN_NODE_VERSION) {
41
+ logger.info(`💻 NodeJS: ${process.version}, OS: ${os.platform()}, Version: ${os.release()}, Arch: ${os.machine()}`);
42
+ return;
43
+ }
44
+ } catch (error) {
45
+ logger.warn(`⚠️ Unable to verify NodeJS version: ${error.message}`);
46
+ return;
47
+ }
48
+ throw new Error(`❌ Supported NodeJS version is >= v${MIN_NODE_VERSION}. Current version is ${process.version}`)
49
+ }
50
+
33
51
  #buildConfig() {
34
52
  const config_builder = new ConfigBuilder(this.opts);
35
53
  config_builder.build();
@@ -52,8 +70,7 @@ class PublishCommand {
52
70
  const config_json = require(path.join(cwd, this.opts.config));
53
71
  this.opts.config = config_json;
54
72
  } 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}`);
73
+ throw new Error(`Failed to read config file: '${file_path}' with error: '${error.message}'`);
57
74
  }
58
75
  }
59
76
  }
@@ -63,7 +80,7 @@ class PublishCommand {
63
80
  /**@type {import('../index').PublishConfig[]} */
64
81
  this.configs = [];
65
82
  if (processed_config.reports) {
66
- for (const report of config.reports) {
83
+ for (const report of processed_config.reports) {
67
84
  this.configs.push(report);
68
85
  }
69
86
  } else {
@@ -72,7 +89,7 @@ class PublishCommand {
72
89
  }
73
90
 
74
91
  #validateConfig() {
75
- logger.info("🛠️ Validating configuration...")
92
+ logger.info("🚓 Validating configuration...")
76
93
  for (const config of this.configs) {
77
94
  this.#validateResults(config);
78
95
  this.#validateTargets(config);
@@ -177,12 +194,12 @@ class PublishCommand {
177
194
  for (const config of this.configs) {
178
195
  for (let i = 0; i < this.results.length; i++) {
179
196
  const result = this.results[i];
180
- const global_extensions = config.extensions || [];
197
+ config.extensions = config.extensions || [];
181
198
  await beats.run(config, result);
182
199
  if (config.targets) {
183
200
  for (const target of config.targets) {
184
201
  target.extensions = target.extensions || [];
185
- target.extensions = global_extensions.concat(target.extensions);
202
+ target.extensions = config.extensions.concat(target.extensions);
186
203
  await target_manager.run(target, result);
187
204
  }
188
205
  } else {
@@ -194,4 +211,4 @@ class PublishCommand {
194
211
 
195
212
  }
196
213
 
197
- module.exports = { PublishCommand }
214
+ module.exports = { PublishCommand }
@@ -0,0 +1,43 @@
1
+ const { BaseExtension } = require('./base.extension');
2
+ const { STATUS, HOOK } = require("../helpers/constants");
3
+
4
+
5
+ class AIFailureSummaryExtension extends BaseExtension {
6
+
7
+ constructor(target, extension, result, payload, root_payload) {
8
+ super(target, extension, result, payload, root_payload);
9
+ this.#setDefaultOptions();
10
+ this.#setDefaultInputs();
11
+ this.updateExtensionInputs();
12
+ }
13
+
14
+ run() {
15
+ this.#setText();
16
+ this.attach();
17
+ }
18
+
19
+ #setDefaultOptions() {
20
+ this.default_options.hook = HOOK.AFTER_SUMMARY,
21
+ this.default_options.condition = STATUS.PASS_OR_FAIL;
22
+ }
23
+
24
+ #setDefaultInputs() {
25
+ this.default_inputs.title = 'AI Failure Summary ✨';
26
+ this.default_inputs.title_link = '';
27
+ }
28
+
29
+ #setText() {
30
+ const data = this.extension.inputs.data;
31
+ if (!data) {
32
+ return;
33
+ }
34
+
35
+ /**
36
+ * @type {import('../beats/beats.types').IBeatExecutionMetric}
37
+ */
38
+ const execution_metrics = data.execution_metrics[0];
39
+ this.text = execution_metrics.failure_summary;
40
+ }
41
+ }
42
+
43
+ module.exports = { AIFailureSummaryExtension }
@@ -0,0 +1,91 @@
1
+ const logger = require('../utils/logger');
2
+ const { addChatExtension, addSlackExtension, addTeamsExtension } = require('../helpers/extension.helper');
3
+
4
+ class BaseExtension {
5
+
6
+ /**
7
+ *
8
+ * @param {import('..').Target} target
9
+ * @param {import('..').Extension} extension
10
+ * @param {import('..').TestResult} result
11
+ * @param {any} payload
12
+ * @param {any} root_payload
13
+ */
14
+ constructor(target, extension, result, payload, root_payload) {
15
+ this.target = target;
16
+ this.extension = extension;
17
+ this.result = result;
18
+ this.payload = payload;
19
+ this.root_payload = root_payload;
20
+
21
+ this.text = '';
22
+
23
+ /**
24
+ * @type {import('..').ExtensionInputs}
25
+ */
26
+ this.default_inputs = {};
27
+
28
+ /**
29
+ * @type {import('..').IExtensionDefaultOptions}
30
+ */
31
+ this.default_options = {};
32
+ }
33
+
34
+ updateExtensionInputs() {
35
+ this.extension.inputs = Object.assign({}, this.default_inputs, this.extension.inputs);
36
+ switch (this.target.name) {
37
+ case 'teams':
38
+ this.extension.inputs = Object.assign({}, { separator: true }, this.extension.inputs);
39
+ break;
40
+ case 'slack':
41
+ this.extension.inputs = Object.assign({}, { separator: false }, this.extension.inputs);
42
+ break;
43
+ case 'chat':
44
+ this.extension.inputs = Object.assign({}, { separator: true }, this.extension.inputs);
45
+ break;
46
+ default:
47
+ break;
48
+ }
49
+ }
50
+
51
+ attach() {
52
+ if (!this.text) {
53
+ logger.debug(`⚠️ Extension '${this.extension.name}' has no text. Skipping.`);
54
+ return;
55
+ }
56
+
57
+ switch (this.target.name) {
58
+ case 'teams':
59
+ addTeamsExtension({ payload: this.payload, extension: this.extension, text: this.text });
60
+ break;
61
+ case 'slack':
62
+ addSlackExtension({ payload: this.payload, extension: this.extension, text: this.text });
63
+ break;
64
+ case 'chat':
65
+ addChatExtension({ payload: this.payload, extension: this.extension, text: this.text });
66
+ break;
67
+ default:
68
+ break;
69
+ }
70
+ }
71
+
72
+ /**
73
+ * @param {string[]} texts
74
+ */
75
+ mergeTexts(texts) {
76
+ const _texts = texts.filter(text => !!text);
77
+ switch (this.target.name) {
78
+ case 'teams':
79
+ return _texts.join('\n\n');
80
+ case 'slack':
81
+ return _texts.join('\n');
82
+ case 'chat':
83
+ return _texts.join('<br>');
84
+ default:
85
+ break;
86
+ }
87
+ }
88
+
89
+ }
90
+
91
+ module.exports = { BaseExtension }
@@ -0,0 +1,81 @@
1
+ const { BaseExtension } = require("./base.extension");
2
+ const { getCIInformation } = require('../helpers/ci');
3
+ const { getMetaDataText } = require("../helpers/metadata.helper");
4
+ const { STATUS, HOOK } = require("../helpers/constants");
5
+
6
+ class CIInfoExtension extends BaseExtension {
7
+
8
+ constructor(target, extension, result, payload, root_payload) {
9
+ super(target, extension, result, payload, root_payload);
10
+ this.#setDefaultOptions();
11
+ this.#setDefaultInputs();
12
+ this.updateExtensionInputs();
13
+
14
+ this.ci = null;
15
+ this.repository_elements = [];
16
+ this.build_elements = [];
17
+ }
18
+
19
+ #setDefaultOptions() {
20
+ this.default_options.hook = HOOK.AFTER_SUMMARY,
21
+ this.default_options.condition = STATUS.PASS_OR_FAIL;
22
+ }
23
+
24
+ #setDefaultInputs() {
25
+ this.default_inputs.title = '';
26
+ this.default_inputs.title_link = '';
27
+ this.default_inputs.show_repository = true;
28
+ this.default_inputs.show_repository_branch = true;
29
+ this.default_inputs.show_build = true;
30
+ }
31
+
32
+ async run() {
33
+ this.ci = getCIInformation();
34
+
35
+ this.setRepositoryElements();
36
+ this.setBuildElements();
37
+
38
+ const repository_text = await getMetaDataText({ elements: this.repository_elements, target: this.target, extension: this.extension, result: this.result, default_condition: this.default_options.condition });
39
+ const build_text = await getMetaDataText({ elements: this.build_elements, target: this.target, extension: this.extension, result: this.result, default_condition: this.default_options.condition });
40
+ this.text = this.mergeTexts([repository_text, build_text]);
41
+ this.attach();
42
+ }
43
+
44
+ setRepositoryElements() {
45
+ if (!this.ci) {
46
+ return;
47
+ }
48
+
49
+ if (this.extension.inputs.show_repository && this.ci.repository_url && this.ci.repository_name) {
50
+ this.repository_elements.push({ label: 'Repository', key: this.ci.repository_name, value: this.ci.repository_url, type: 'hyperlink' });
51
+ }
52
+ if (this.extension.inputs.show_repository_branch && this.ci.repository_ref) {
53
+ if (this.ci.repository_ref.includes('refs/pull')) {
54
+ const pr_url = this.ci.repository_url + this.ci.repository_ref.replace('refs/pull/', '/pull/');
55
+ const pr_name = this.ci.repository_ref.replace('refs/pull/', '').replace('/merge', '');
56
+ this.repository_elements.push({ label: 'Pull Request', key: pr_name, value: pr_url, type: 'hyperlink' });
57
+ } else {
58
+ const branch_url = this.ci.repository_url + this.ci.repository_ref.replace('refs/heads/', '/tree/');
59
+ const branch_name = this.ci.repository_ref.replace('refs/heads/', '');
60
+ this.repository_elements.push({ label: 'Branch', key: branch_name, value: branch_url, type: 'hyperlink' });
61
+ }
62
+ }
63
+ }
64
+
65
+ setBuildElements() {
66
+ if (!this.ci) {
67
+ return;
68
+ }
69
+
70
+ if (this.extension.inputs.show_build && this.ci.build_url) {
71
+ const name = (this.ci.build_name || 'Build') + (this.ci.build_number ? ` #${this.ci.build_number}` : '');
72
+ this.build_elements.push({ label: 'Build', key: name, value: this.ci.build_url, type: 'hyperlink' });
73
+ }
74
+ if (this.extension.inputs.data) {
75
+ this.build_elements = this.build_elements.concat(this.extension.inputs.data);
76
+ }
77
+ }
78
+
79
+ }
80
+
81
+ module.exports = { CIInfoExtension };
@@ -0,0 +1,12 @@
1
+ export type ICIInfo = {
2
+ ci: string
3
+ git: string
4
+ repository_url: string
5
+ repository_name: string
6
+ repository_ref: string
7
+ repository_commit_sha: string
8
+ build_url: string
9
+ build_number: string
10
+ build_name: string
11
+ user: string
12
+ }
@@ -6,8 +6,9 @@ const qc_test_summary = require('./quick-chart-test-summary');
6
6
  const percy_analysis = require('./percy-analysis');
7
7
  const custom = require('./custom');
8
8
  const metadata = require('./metadata');
9
- const ci_info = require('./ci-info');
10
- const ai_failure_summary = require('./ai-failure-summary');
9
+ const { AIFailureSummaryExtension } = require('./ai-failure-summary.extension');
10
+ const { SmartAnalysisExtension } = require('./smart-analysis.extension');
11
+ const { CIInfoExtension } = require('./ci-info.extension');
11
12
  const { EXTENSION } = require('../helpers/constants');
12
13
  const { checkCondition } = require('../helpers/helper');
13
14
  const logger = require('../utils/logger');
@@ -17,7 +18,7 @@ async function run(options) {
17
18
  const extensions = target.extensions || [];
18
19
  for (let i = 0; i < extensions.length; i++) {
19
20
  const extension = extensions[i];
20
- const extension_runner = getExtensionRunner(extension);
21
+ const extension_runner = getExtensionRunner(extension, options);
21
22
  const extension_options = Object.assign({}, extension_runner.default_options, extension);
22
23
  if (extension_options.hook === hook) {
23
24
  if (await checkCondition({ condition: extension_options.condition, result, target, extension })) {
@@ -35,7 +36,7 @@ async function run(options) {
35
36
  }
36
37
  }
37
38
 
38
- function getExtensionRunner(extension) {
39
+ function getExtensionRunner(extension, options) {
39
40
  switch (extension.name) {
40
41
  case EXTENSION.HYPERLINKS:
41
42
  return hyperlinks;
@@ -54,9 +55,11 @@ function getExtensionRunner(extension) {
54
55
  case EXTENSION.METADATA:
55
56
  return metadata;
56
57
  case EXTENSION.CI_INFO:
57
- return ci_info;
58
+ return new CIInfoExtension(options.target, extension, options.result, options.payload, options.root_payload);
58
59
  case EXTENSION.AI_FAILURE_SUMMARY:
59
- return ai_failure_summary;
60
+ return new AIFailureSummaryExtension(options.target, extension, options.result, options.payload, options.root_payload);
61
+ case EXTENSION.SMART_ANALYSIS:
62
+ return new SmartAnalysisExtension(options.target, extension, options.result, options.payload, options.root_payload);
60
63
  default:
61
64
  return require(extension.name);
62
65
  }
@@ -87,7 +87,7 @@ function setPayloadWithMSTeamsEntities(payload) {
87
87
  }
88
88
 
89
89
  const default_options = {
90
- hook: HOOK.END,
90
+ hook: HOOK.AFTER_SUMMARY,
91
91
  condition: STATUS.FAIL
92
92
  }
93
93
 
@@ -0,0 +1,62 @@
1
+ const { BaseExtension } = require('./base.extension');
2
+ const { STATUS, HOOK } = require("../helpers/constants");
3
+
4
+ class SmartAnalysisExtension extends BaseExtension {
5
+
6
+ constructor(target, extension, result, payload, root_payload) {
7
+ super(target, extension, result, payload, root_payload);
8
+ this.#setDefaultOptions();
9
+ this.#setDefaultInputs();
10
+ this.updateExtensionInputs();
11
+ }
12
+
13
+ run() {
14
+ this.#setText();
15
+ this.attach();
16
+ }
17
+
18
+ #setDefaultOptions() {
19
+ this.default_options.hook = HOOK.AFTER_SUMMARY,
20
+ this.default_options.condition = STATUS.PASS_OR_FAIL;
21
+ }
22
+
23
+ #setDefaultInputs() {
24
+ this.default_inputs.title = 'Smart Analysis';
25
+ this.default_inputs.title_link = '';
26
+ }
27
+
28
+ #setText() {
29
+ const data = this.extension.inputs.data;
30
+
31
+ if (!data) {
32
+ return;
33
+ }
34
+
35
+ /**
36
+ * @type {import('../beats/beats.types').IBeatExecutionMetric}
37
+ */
38
+ const execution_metrics = data.execution_metrics[0];
39
+
40
+ const smart_analysis = [];
41
+ if (execution_metrics.newly_failed) {
42
+ smart_analysis.push(`⭕ NF: ${execution_metrics.newly_failed}`);
43
+ }
44
+ if (execution_metrics.always_failing) {
45
+ smart_analysis.push(`🔴 AF: ${execution_metrics.always_failing}`);
46
+ }
47
+ if (execution_metrics.recurring_failures) {
48
+ smart_analysis.push(`🟠 RF: ${execution_metrics.recurring_failures}`);
49
+ }
50
+ if (execution_metrics.flaky) {
51
+ smart_analysis.push(`🟡 FL: ${execution_metrics.flaky}`);
52
+ }
53
+ if (execution_metrics.recovered) {
54
+ smart_analysis.push(`🟢 RC: ${execution_metrics.recovered}`);
55
+ }
56
+
57
+ this.text = smart_analysis.join(' | ');
58
+ }
59
+
60
+ }
61
+
62
+ module.exports = { SmartAnalysisExtension };
package/src/helpers/ci.js CHANGED
@@ -1,5 +1,8 @@
1
1
  const ENV = process.env;
2
2
 
3
+ /**
4
+ * @returns {import('../extensions/extensions').ICIInfo}
5
+ */
3
6
  function getCIInformation() {
4
7
  if (ENV.GITHUB_ACTIONS) {
5
8
  return getGitHubActionsInformation();
@@ -21,6 +21,7 @@ const TARGET = Object.freeze({
21
21
 
22
22
  const EXTENSION = Object.freeze({
23
23
  AI_FAILURE_SUMMARY: 'ai-failure-summary',
24
+ SMART_ANALYSIS: 'smart-analysis',
24
25
  HYPERLINKS: 'hyperlinks',
25
26
  MENTIONS: 'mentions',
26
27
  REPORT_PORTAL_ANALYSIS: 'report-portal-analysis',
@@ -37,10 +38,13 @@ const URLS = Object.freeze({
37
38
  QUICK_CHART: 'https://quickchart.io'
38
39
  });
39
40
 
41
+ const MIN_NODE_VERSION = 14;
42
+
40
43
  module.exports = Object.freeze({
41
44
  STATUS,
42
45
  HOOK,
43
46
  TARGET,
44
47
  EXTENSION,
45
- URLS
48
+ URLS,
49
+ MIN_NODE_VERSION
46
50
  });
@@ -31,7 +31,7 @@ function processText(raw) {
31
31
  return raw;
32
32
  }
33
33
 
34
- /**
34
+ /**
35
35
  * @returns {import('../index').PublishConfig }
36
36
  */
37
37
  function processData(data) {
@@ -72,9 +72,9 @@ function getResultText({ result }) {
72
72
  }
73
73
 
74
74
  /**
75
- *
75
+ *
76
76
  * @param {object} param0
77
- * @param {string | Function} param0.condition
77
+ * @param {string | Function} param0.condition
78
78
  */
79
79
  async function checkCondition({ condition, result, target, extension }) {
80
80
  if (typeof condition === 'function') {
@@ -1,4 +1,17 @@
1
- const { checkCondition } = require('./helper')
1
+ const { checkCondition } = require('./helper');
2
+
3
+ function getMetaDataText(params) {
4
+ switch (params.target.name) {
5
+ case 'teams':
6
+ return getTeamsMetaDataText(params);
7
+ case 'slack':
8
+ return getSlackMetaDataText(params);
9
+ case 'chat':
10
+ return getChatMetaDataText(params);
11
+ default:
12
+ return '';
13
+ }
14
+ }
2
15
 
3
16
  /**
4
17
  * Asynchronously generates metadata text for slack.
@@ -109,6 +122,7 @@ function get_url({ url, target, extension, result}) {
109
122
  }
110
123
 
111
124
  module.exports = {
125
+ getMetaDataText,
112
126
  getSlackMetaDataText,
113
127
  getTeamsMetaDataText,
114
128
  getChatMetaDataText
package/src/index.d.ts CHANGED
@@ -25,6 +25,7 @@ export interface ExtensionInputs {
25
25
  title?: string;
26
26
  title_link?: string;
27
27
  separator?: boolean;
28
+ data?: any;
28
29
  }
29
30
 
30
31
  export interface ReportPortalAnalysisInputs extends ExtensionInputs {
@@ -227,6 +228,7 @@ export interface PublishReport {
227
228
  project?: string;
228
229
  run?: string;
229
230
  show_failure_summary?: boolean;
231
+ show_smart_analysis?: boolean;
230
232
  targets?: Target[];
231
233
  extensions?: Extension[];
232
234
  results?: ParseOptions[] | PerformanceParseOptions[] | CustomResultOptions[];
@@ -265,5 +267,10 @@ export interface CommandLineOptions {
265
267
  mstest?: string;
266
268
  }
267
269
 
270
+ export type IExtensionDefaultOptions = {
271
+ hook: Hook
272
+ condition: Condition
273
+ }
274
+
268
275
  export function publish(options: PublishOptions): Promise<any>
269
276
  export function defineConfig(config: PublishConfig): PublishConfig
@@ -1,72 +0,0 @@
1
- const { STATUS, HOOK } = require("../helpers/constants");
2
- const { addChatExtension, addSlackExtension, addTeamsExtension } = require('../helpers/extension.helper');
3
-
4
- /**
5
- * @param {object} param0
6
- * @param {import('..').Target} param0.target
7
- * @param {import('..').MetadataExtension} param0.extension
8
- */
9
- async function run({ target, extension, result, payload, root_payload }) {
10
- extension.inputs = Object.assign({}, default_inputs, extension.inputs);
11
- if (target.name === 'teams') {
12
- extension.inputs = Object.assign({}, default_inputs_teams, extension.inputs);
13
- await attachForTeams({ target, extension, payload, result });
14
- } else if (target.name === 'slack') {
15
- extension.inputs = Object.assign({}, default_inputs_slack, extension.inputs);
16
- await attachForSlack({ target, extension, payload, result });
17
- } else if (target.name === 'chat') {
18
- extension.inputs = Object.assign({}, default_inputs_chat, extension.inputs);
19
- await attachForChat({ target, extension, payload, result });
20
- }
21
- }
22
-
23
- /**
24
- * @param {object} param0
25
- * @param {import('..').MetadataExtension} param0.extension
26
- */
27
- async function attachForTeams({ target, extension, payload, result }) {
28
- const text = extension.inputs.failure_summary
29
- if (text) {
30
- addTeamsExtension({ payload, extension, text });
31
- }
32
- }
33
-
34
- async function attachForSlack({ target, extension, payload, result }) {
35
- const text = extension.inputs.failure_summary
36
- if (text) {
37
- addSlackExtension({ payload, extension, text });
38
- }
39
- }
40
-
41
- async function attachForChat({ target, extension, payload, result }) {
42
- const text = extension.inputs.failure_summary
43
- if (text) {
44
- addChatExtension({ payload, extension, text });
45
- }
46
- }
47
-
48
- const default_options = {
49
- hook: HOOK.AFTER_SUMMARY,
50
- condition: STATUS.FAIL,
51
- }
52
-
53
- const default_inputs = {
54
- title: 'AI Failure Summary ✨'
55
- }
56
-
57
- const default_inputs_teams = {
58
- separator: true
59
- }
60
-
61
- const default_inputs_slack = {
62
- separator: false
63
- }
64
-
65
- const default_inputs_chat = {
66
- separator: true
67
- }
68
-
69
- module.exports = {
70
- run,
71
- default_options
72
- }
@@ -1,134 +0,0 @@
1
- const { STATUS, HOOK } = require("../helpers/constants");
2
- const { getCIInformation } = require('../helpers/ci');
3
- const { addTeamsExtension, addSlackExtension, addChatExtension } = require('../helpers/extension.helper');
4
- const { getTeamsMetaDataText, getSlackMetaDataText, getChatMetaDataText } = require('../helpers/metadata.helper');
5
-
6
- /**
7
- *
8
- * @param {object} param0 - the payload object
9
- * @param {import('..').Extension} param0.extension - The result object
10
- *
11
- */
12
- async function run({ target, extension, payload, result }) {
13
- extension.inputs = Object.assign({}, default_inputs, extension.inputs);
14
- if (target.name === 'teams') {
15
- extension.inputs = Object.assign({}, default_inputs_teams, extension.inputs);
16
- const text = await get_text({ target, extension, result });
17
- if (text) {
18
- addTeamsExtension({ payload, extension, text });
19
- }
20
- } else if (target.name === 'slack') {
21
- extension.inputs = Object.assign({}, default_inputs_slack, extension.inputs);
22
- const text = await get_text({ target, extension, result });
23
- if (text) {
24
- addSlackExtension({ payload, extension, text });
25
- }
26
- } else if (target.name === 'chat') {
27
- extension.inputs = Object.assign({}, default_inputs_chat, extension.inputs);
28
- const text = await get_text({ target, extension, result });
29
- if (text) {
30
- addChatExtension({ payload, extension, text });
31
- }
32
- }
33
- }
34
-
35
- /**
36
- *
37
- * @param {import('..').CIInfoInputs} inputs
38
- */
39
- function get_repository_elements(inputs) {
40
- const elements = [];
41
- const ci = getCIInformation();
42
- if (inputs.show_repository && ci && ci.repository_url && ci.repository_name) {
43
- elements.push({ label: 'Repository', key: ci.repository_name, value: ci.repository_url, type: 'hyperlink' });
44
- }
45
- if (inputs.show_repository_branch && ci && ci.repository_ref) {
46
- if (ci.repository_ref.includes('refs/pull')) {
47
- const pr_url = ci.repository_url + ci.repository_ref.replace('refs/pull/', '/pull/');
48
- const pr_name = ci.repository_ref.replace('refs/pull/', '').replace('/merge', '');
49
- elements.push({ label: 'Pull Request', key: pr_name, value: pr_url, type: 'hyperlink' });
50
- } else {
51
- const branch_url = ci.repository_url + ci.repository_ref.replace('refs/heads/', '/tree/');
52
- const branch_name = ci.repository_ref.replace('refs/heads/', '');
53
- elements.push({ label: 'Branch', key: branch_name, value: branch_url, type: 'hyperlink' });
54
- }
55
- }
56
- return elements;
57
- }
58
-
59
- /**
60
- *
61
- * @param {import('..').CIInfoInputs} inputs
62
- */
63
- function get_build_elements(inputs) {
64
- let elements = [];
65
- const ci = getCIInformation();
66
- if (inputs.show_build && ci && ci.build_url) {
67
- const name = (ci.build_name || 'Build') + (ci.build_number ? ` #${ci.build_number}` : '');
68
- elements.push({ label: 'Build', key: name, value: ci.build_url, type: 'hyperlink' });
69
- }
70
- if (inputs.data) {
71
- elements = elements.concat(inputs.data);
72
- }
73
- return elements;
74
- }
75
-
76
- async function get_text({ target, extension, result }) {
77
- const repository_elements = get_repository_elements(extension.inputs);
78
- const build_elements = get_build_elements(extension.inputs);
79
- if (target.name === 'teams') {
80
- const repository_text = await getTeamsMetaDataText({ elements: repository_elements, target, extension, result, default_condition: default_options.condition });
81
- const build_text = await getTeamsMetaDataText({ elements: build_elements, target, extension, result, default_condition: default_options.condition });
82
- if (build_text) {
83
- return `${repository_text ? `${repository_text}\n\n` : '' }${build_text}`;
84
- } else {
85
- return repository_text;
86
- }
87
- } else if (target.name === 'slack') {
88
- const repository_text = await getSlackMetaDataText({ elements: repository_elements, target, extension, result, default_condition: default_options.condition });
89
- const build_text = await getSlackMetaDataText({ elements: build_elements, target, extension, result, default_condition: default_options.condition });
90
- if (build_text) {
91
- return `${repository_text ? `${repository_text}\n` : '' }${build_text}`;
92
- } else {
93
- return repository_text;
94
- }
95
- } else if (target.name === 'chat') {
96
- const repository_text = await getChatMetaDataText({ elements: repository_elements, target, extension, result, default_condition: default_options.condition });
97
- const build_text = await getChatMetaDataText({ elements: build_elements, target, extension, result, default_condition: default_options.condition });
98
- if (build_text) {
99
- return `${repository_text ? `${repository_text}<br>` : '' }${build_text}`;
100
- } else {
101
- return repository_text;
102
- }
103
- }
104
- }
105
-
106
- const default_options = {
107
- hook: HOOK.AFTER_SUMMARY,
108
- condition: STATUS.PASS_OR_FAIL,
109
- }
110
-
111
- const default_inputs = {
112
- title: '',
113
- show_repository: true,
114
- show_repository_branch: true,
115
- show_build: true,
116
- }
117
-
118
- const default_inputs_teams = {
119
-
120
- separator: true
121
- }
122
-
123
- const default_inputs_slack = {
124
- separator: false
125
- }
126
-
127
- const default_inputs_chat = {
128
- separator: true
129
- }
130
-
131
- module.exports = {
132
- run,
133
- default_options
134
- }