testbeats 2.1.2 → 2.1.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testbeats",
3
- "version": "2.1.2",
3
+ "version": "2.1.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",
@@ -47,7 +47,7 @@
47
47
  "homepage": "https://testbeats.com",
48
48
  "dependencies": {
49
49
  "async-retry": "^1.3.3",
50
- "dotenv": "^14.3.2",
50
+ "dotenv": "^16.4.5",
51
51
  "form-data-lite": "^1.0.3",
52
52
  "influxdb-lite": "^1.0.0",
53
53
  "performance-results-parser": "latest",
@@ -58,12 +58,12 @@
58
58
  "test-results-parser": "0.2.5"
59
59
  },
60
60
  "devDependencies": {
61
- "c8": "^7.12.0",
62
- "mocha": "^10.1.0",
61
+ "c8": "^10.1.2",
62
+ "mocha": "^10.7.3",
63
63
  "mocha-junit-reporter": "^2.2.1",
64
64
  "mocha-multi-reporters": "^1.5.1",
65
- "pactum": "^3.2.3",
66
- "pkg": "^5.8.0"
65
+ "pactum": "^3.7.1",
66
+ "pkg": "^5.8.1"
67
67
  },
68
68
  "engines": {
69
69
  "node": ">=14.0.0"
@@ -4,10 +4,11 @@ const FormData = require('form-data-lite');
4
4
  const TestResult = require('test-results-parser/src/models/TestResult');
5
5
  const { BeatsApi } = require('./beats.api');
6
6
  const logger = require('../utils/logger');
7
+ const TestAttachment = require('test-results-parser/src/models/TestAttachment');
7
8
 
8
9
  const MAX_ATTACHMENTS_PER_REQUEST = 5;
9
10
  const MAX_ATTACHMENTS_PER_RUN = 20;
10
- const MAX_ATTACHMENT_SIZE = 1024 * 1024;
11
+ const MAX_ATTACHMENT_SIZE = 2 * 1024 * 1024;
11
12
 
12
13
  class BeatsAttachments {
13
14
 
@@ -53,8 +54,6 @@ class BeatsAttachments {
53
54
  return;
54
55
  }
55
56
  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
57
  try {
59
58
  let count = 0;
60
59
  const size = MAX_ATTACHMENTS_PER_REQUEST;
@@ -68,7 +67,11 @@ class BeatsAttachments {
68
67
  form.append('test_run_id', this.test_run_id);
69
68
  const file_images = []
70
69
  for (const attachment of attachments_subset) {
71
- const attachment_path = path.join(result_file_dir, attachment.path);
70
+ const attachment_path = this.#getAttachmentFilePath(attachment);
71
+ if (!attachment_path) {
72
+ logger.warn(`⚠️ Unable to find attachment ${attachment.path}`);
73
+ continue;
74
+ }
72
75
  const stats = fs.statSync(attachment_path);
73
76
  if (stats.size > MAX_ATTACHMENT_SIZE) {
74
77
  logger.warn(`⚠️ Attachment ${attachment.path} is too big (${stats.size} bytes). Allowed size is ${MAX_ATTACHMENT_SIZE} bytes.`);
@@ -93,6 +96,33 @@ class BeatsAttachments {
93
96
  }
94
97
  }
95
98
 
99
+ /**
100
+ *
101
+ * @param {TestAttachment} attachment
102
+ */
103
+ #getAttachmentFilePath(attachment) {
104
+ const result_file = this.config.results[0].files[0];
105
+ const result_file_dir = path.dirname(result_file);
106
+ const relative_attachment_path = path.join(result_file_dir, attachment.path);
107
+ const raw_attachment_path = attachment.path;
108
+
109
+ try {
110
+ fs.statSync(relative_attachment_path);
111
+ return relative_attachment_path;
112
+ } catch {
113
+ // nothing
114
+ }
115
+
116
+ try {
117
+ fs.statSync(raw_attachment_path);
118
+ return raw_attachment_path;
119
+ } catch {
120
+ // nothing
121
+ }
122
+
123
+ return null;
124
+ }
125
+
96
126
 
97
127
 
98
128
  }
@@ -1,7 +1,7 @@
1
1
  const { getCIInformation } = require('../helpers/ci');
2
2
  const logger = require('../utils/logger');
3
3
  const { BeatsApi } = require('./beats.api');
4
- const { HOOK } = require('../helpers/constants');
4
+ const { HOOK, PROCESS_STATUS } = require('../helpers/constants');
5
5
  const TestResult = require('test-results-parser/src/models/TestResult');
6
6
  const { BeatsAttachments } = require('./beats.attachments');
7
7
 
@@ -31,9 +31,7 @@ class Beats {
31
31
  await this.#publishTestResults();
32
32
  await this.#uploadAttachments();
33
33
  this.#updateTitleLink();
34
- await this.#attachFailureSummary();
35
- await this.#attachSmartAnalysis();
36
- await this.#attachErrorClusters();
34
+ await this.#attachExtensions();
37
35
  }
38
36
 
39
37
  #setCIInfo() {
@@ -104,13 +102,20 @@ class Beats {
104
102
  }
105
103
  }
106
104
 
107
- async #attachFailureSummary() {
105
+ async #attachExtensions() {
108
106
  if (!this.test_run_id) {
109
107
  return;
110
108
  }
111
109
  if (!this.config.targets) {
112
110
  return;
113
111
  }
112
+ await this.#attachFailureSummary();
113
+ await this.#attachFailureAnalysis();
114
+ await this.#attachSmartAnalysis();
115
+ await this.#attachErrorClusters();
116
+ }
117
+
118
+ async #attachFailureSummary() {
114
119
  if (this.result.status !== 'FAIL') {
115
120
  return;
116
121
  }
@@ -132,13 +137,29 @@ class Beats {
132
137
  }
133
138
  }
134
139
 
135
- async #attachSmartAnalysis() {
136
- if (!this.test_run_id) {
140
+ async #attachFailureAnalysis() {
141
+ if (this.result.status !== 'FAIL') {
137
142
  return;
138
143
  }
139
- if (!this.config.targets) {
144
+ if (this.config.show_failure_analysis === false) {
140
145
  return;
141
146
  }
147
+ try {
148
+ logger.info('🪄 Fetching Failure Analysis...');
149
+ await this.#setTestRun('Failure Analysis Status', 'failure_analysis_status');
150
+ this.config.extensions.push({
151
+ name: 'failure-analysis',
152
+ hook: HOOK.AFTER_SUMMARY,
153
+ inputs: {
154
+ data: this.test_run
155
+ }
156
+ });
157
+ } catch (error) {
158
+ logger.error(`❌ Unable to attach failure analysis: ${error.message}`, error);
159
+ }
160
+ }
161
+
162
+ async #attachSmartAnalysis() {
142
163
  if (this.config.show_smart_analysis === false) {
143
164
  return;
144
165
  }
@@ -165,7 +186,7 @@ class Beats {
165
186
  }
166
187
 
167
188
  async #setTestRun(text, wait_for = 'smart_analysis_status') {
168
- if (this.test_run && this.test_run[wait_for] === 'COMPLETED') {
189
+ if (this.test_run && this.test_run[wait_for] === PROCESS_STATUS.COMPLETED) {
169
190
  return;
170
191
  }
171
192
  let retry = 3;
@@ -175,13 +196,13 @@ class Beats {
175
196
  this.test_run = await this.api.getTestRun(this.test_run_id);
176
197
  const status = this.test_run && this.test_run[wait_for];
177
198
  switch (status) {
178
- case 'COMPLETED':
199
+ case PROCESS_STATUS.COMPLETED:
179
200
  logger.debug(`☑️ ${text} generated successfully`);
180
201
  return;
181
- case 'FAILED':
202
+ case PROCESS_STATUS.FAILED:
182
203
  logger.error(`❌ Failed to generate ${text}`);
183
204
  return;
184
- case 'SKIPPED':
205
+ case PROCESS_STATUS.SKIPPED:
185
206
  logger.warn(`❗ Skipped generating ${text}`);
186
207
  return;
187
208
  }
@@ -191,12 +212,6 @@ class Beats {
191
212
  }
192
213
 
193
214
  async #attachErrorClusters() {
194
- if (!this.test_run_id) {
195
- return;
196
- }
197
- if (!this.config.targets) {
198
- return;
199
- }
200
215
  if (this.result.status !== 'FAIL') {
201
216
  return;
202
217
  }
@@ -9,6 +9,12 @@ export type IBeatExecutionMetric = {
9
9
  added: number
10
10
  removed: number
11
11
  flaky: number
12
+ product_bugs: number
13
+ environment_issues: number
14
+ automation_bugs: number
15
+ not_a_defects: number
16
+ to_investigate: number
17
+ auto_analysed: number
12
18
  failure_summary: any
13
19
  failure_summary_provider: any
14
20
  failure_summary_model: any
package/src/cli.js CHANGED
@@ -6,9 +6,10 @@ const sade = require('sade');
6
6
  const prog = sade('testbeats');
7
7
  const { PublishCommand } = require('./commands/publish.command');
8
8
  const logger = require('./utils/logger');
9
+ const pkg = require('../package.json');
9
10
 
10
11
  prog
11
- .version('2.0.4')
12
+ .version(pkg.version)
12
13
  .option('-c, --config', 'path to config file')
13
14
  .option('-l, --logLevel', 'Log Level', "INFO")
14
15
  .option('--api-key', 'api key')
@@ -40,4 +41,4 @@ prog.command('publish')
40
41
  }
41
42
  });
42
43
 
43
- prog.parse(process.argv);
44
+ prog.parse(process.argv);
@@ -0,0 +1,58 @@
1
+ const { BaseExtension } = require('./base.extension');
2
+ const { STATUS, HOOK } = require("../helpers/constants");
3
+
4
+ class FailureAnalysisExtension 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
+ #setDefaultOptions() {
14
+ this.default_options.hook = HOOK.AFTER_SUMMARY,
15
+ this.default_options.condition = STATUS.PASS_OR_FAIL;
16
+ }
17
+
18
+ #setDefaultInputs() {
19
+ this.default_inputs.title = '';
20
+ this.default_inputs.title_link = '';
21
+ }
22
+
23
+ run() {
24
+ this.#setText();
25
+ this.attach();
26
+ }
27
+
28
+ #setText() {
29
+ const data = this.extension.inputs.data;
30
+ if (!data) {
31
+ return;
32
+ }
33
+
34
+ /**
35
+ * @type {import('../beats/beats.types').IBeatExecutionMetric}
36
+ */
37
+ const execution_metrics = data.execution_metrics[0];
38
+
39
+ if (!execution_metrics) {
40
+ logger.warn('⚠️ No execution metrics found. Skipping.');
41
+ return;
42
+ }
43
+
44
+ const failure_analysis = [];
45
+
46
+ if (execution_metrics.to_investigate) {
47
+ failure_analysis.push(`🔎 To Investigate: ${execution_metrics.to_investigate}`);
48
+ }
49
+ if (execution_metrics.auto_analysed) {
50
+ failure_analysis.push(`🪄 Auto Analysed: ${execution_metrics.auto_analysed}`);
51
+ }
52
+
53
+ this.text = failure_analysis.join('  •  ');
54
+ }
55
+
56
+ }
57
+
58
+ module.exports = { FailureAnalysisExtension };
@@ -13,6 +13,7 @@ const { EXTENSION } = require('../helpers/constants');
13
13
  const { checkCondition } = require('../helpers/helper');
14
14
  const logger = require('../utils/logger');
15
15
  const { ErrorClustersExtension } = require('./error-clusters.extension');
16
+ const { FailureAnalysisExtension } = require('./failure-analysis.extension');
16
17
 
17
18
  async function run(options) {
18
19
  const { target, result, hook } = options;
@@ -59,6 +60,8 @@ function getExtensionRunner(extension, options) {
59
60
  return new CIInfoExtension(options.target, extension, options.result, options.payload, options.root_payload);
60
61
  case EXTENSION.AI_FAILURE_SUMMARY:
61
62
  return new AIFailureSummaryExtension(options.target, extension, options.result, options.payload, options.root_payload);
63
+ case EXTENSION.FAILURE_ANALYSIS:
64
+ return new FailureAnalysisExtension(options.target, extension, options.result, options.payload, options.root_payload);
62
65
  case EXTENSION.SMART_ANALYSIS:
63
66
  return new SmartAnalysisExtension(options.target, extension, options.result, options.payload, options.root_payload);
64
67
  case EXTENSION.ERROR_CLUSTERS:
@@ -65,13 +65,13 @@ class SmartAnalysisExtension extends BaseExtension {
65
65
  for (const item of smart_analysis) {
66
66
  rows.push(item);
67
67
  if (rows.length === 3) {
68
- texts.push(rows.join(''));
68
+ texts.push(rows.join('  •  '));
69
69
  rows.length = 0;
70
70
  }
71
71
  }
72
72
 
73
73
  if (rows.length > 0) {
74
- texts.push(rows.join(''));
74
+ texts.push(rows.join('  •  '));
75
75
  }
76
76
 
77
77
  this.text = this.mergeTexts(texts);
package/src/helpers/ci.js CHANGED
@@ -1,9 +1,21 @@
1
+ const os = require('os');
2
+ const pkg = require('../../package.json');
3
+
1
4
  const ENV = process.env;
2
5
 
3
6
  /**
4
7
  * @returns {import('../extensions/extensions').ICIInfo}
5
8
  */
6
9
  function getCIInformation() {
10
+ const ci_info = getBaseCIInfo();
11
+ const system_info = getSystemInfo();
12
+ return {
13
+ ...ci_info,
14
+ ...system_info
15
+ }
16
+ }
17
+
18
+ function getBaseCIInfo() {
7
19
  if (ENV.GITHUB_ACTIONS) {
8
20
  return getGitHubActionsInformation();
9
21
  }
@@ -16,6 +28,7 @@ function getCIInformation() {
16
28
  if (ENV.SYSTEM_TEAMFOUNDATIONCOLLECTIONURI) {
17
29
  return getAzureDevOpsInformation();
18
30
  }
31
+ return getDefaultInformation();
19
32
  }
20
33
 
21
34
  function getGitHubActionsInformation() {
@@ -82,6 +95,46 @@ function getGitLabInformation() {
82
95
  }
83
96
  }
84
97
 
98
+ function getDefaultInformation() {
99
+ return {
100
+ ci: ENV.TEST_BEATS_CI_NAME,
101
+ git: ENV.TEST_BEATS_CI_GIT,
102
+ repository_url: ENV.TEST_BEATS_CI_REPOSITORY_URL,
103
+ repository_name: ENV.TEST_BEATS_CI_REPOSITORY_NAME,
104
+ repository_ref: ENV.TEST_BEATS_CI_REPOSITORY_REF,
105
+ repository_commit_sha: ENV.TEST_BEATS_CI_REPOSITORY_COMMIT_SHA,
106
+ build_url: ENV.TEST_BEATS_CI_BUILD_URL,
107
+ build_number: ENV.TEST_BEATS_CI_BUILD_NUMBER,
108
+ build_name: ENV.TEST_BEATS_CI_BUILD_NAME,
109
+ build_reason: ENV.TEST_BEATS_CI_BUILD_REASON,
110
+ user: ENV.TEST_BEATS_CI_USER || os.userInfo().username
111
+ }
112
+ }
113
+
114
+ function getSystemInfo() {
115
+ function getRuntimeInfo() {
116
+ if (typeof process !== 'undefined' && process.versions && process.versions.node) {
117
+ return { name: 'node', version: process.versions.node };
118
+ } else if (typeof Deno !== 'undefined') {
119
+ return { name: 'deno', version: Deno.version.deno };
120
+ } else if (typeof Bun !== 'undefined') {
121
+ return { name: 'bun', version: Bun.version };
122
+ } else {
123
+ return { name: 'unknown', version: 'unknown' };
124
+ }
125
+ }
126
+
127
+ const runtime = getRuntimeInfo();
128
+
129
+ return {
130
+ runtime: runtime.name,
131
+ runtime_version: runtime.version,
132
+ os: os.platform(),
133
+ os_version: os.release(),
134
+ testbeats_version: pkg.version
135
+ }
136
+ }
137
+
85
138
  module.exports = {
86
139
  getCIInformation
87
140
  }
@@ -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
+ FAILURE_ANALYSIS: 'failure-analysis',
24
25
  SMART_ANALYSIS: 'smart-analysis',
25
26
  ERROR_CLUSTERS: 'error-clusters',
26
27
  HYPERLINKS: 'hyperlinks',
@@ -39,6 +40,13 @@ const URLS = Object.freeze({
39
40
  QUICK_CHART: 'https://quickchart.io'
40
41
  });
41
42
 
43
+ const PROCESS_STATUS = Object.freeze({
44
+ RUNNING: 'RUNNING',
45
+ COMPLETED: 'COMPLETED',
46
+ FAILED: 'FAILED',
47
+ SKIPPED: 'SKIPPED',
48
+ });
49
+
42
50
  const MIN_NODE_VERSION = 14;
43
51
 
44
52
  module.exports = Object.freeze({
@@ -47,5 +55,6 @@ module.exports = Object.freeze({
47
55
  TARGET,
48
56
  EXTENSION,
49
57
  URLS,
58
+ PROCESS_STATUS,
50
59
  MIN_NODE_VERSION
51
60
  });
package/src/index.d.ts CHANGED
@@ -229,6 +229,7 @@ export interface PublishReport {
229
229
  project?: string;
230
230
  run?: string;
231
231
  show_failure_summary?: boolean;
232
+ show_failure_analysis?: boolean;
232
233
  show_smart_analysis?: boolean;
233
234
  show_error_clusters?: boolean;
234
235
  targets?: Target[];