testbeats 2.0.0 → 2.0.1

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.0.0",
3
+ "version": "2.0.1",
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",
@@ -44,7 +44,7 @@
44
44
  "bugs": {
45
45
  "url": "https://github.com/test-results-reporter/testbeats/issues"
46
46
  },
47
- "homepage": "https://test-results-reporter.github.io",
47
+ "homepage": "https://testbeats.com",
48
48
  "dependencies": {
49
49
  "async-retry": "^1.3.3",
50
50
  "dotenv": "^14.3.2",
@@ -1,6 +1,7 @@
1
1
  const request = require('phin-retry');
2
2
  const TestResult = require('test-results-parser/src/models/TestResult');
3
3
  const { getCIInformation } = require('../helpers/ci');
4
+ const { HOOK } = require('../helpers/constants');
4
5
 
5
6
  function get_base_url() {
6
7
  return process.env.TEST_BEATS_URL || "https://app.testbeats.com";
@@ -11,19 +12,42 @@ function get_base_url() {
11
12
  * @param {TestResult} result
12
13
  */
13
14
  async function run(config, result) {
14
- if (config.project && config.run && config.api_key) {
15
+ init(config);
16
+ if (isValid(config)) {
15
17
  const run_id = await publishTestResults(config, result);
16
18
  if (run_id) {
17
19
  attachTestBeatsReportHyperLink(config, run_id);
20
+ await attachTestBeatsFailureSummary(config, result, run_id);
18
21
  }
22
+ } else {
23
+ console.warn('Missing testbeats config parameters');
19
24
  }
20
25
  }
21
26
 
22
27
  /**
23
- * @param {import('../index').PublishReport} config
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
24
47
  * @param {TestResult} result
25
48
  */
26
49
  async function publishTestResults(config, result) {
50
+ console.log("Publishing results to TestBeats");
27
51
  try {
28
52
  const payload = {
29
53
  project: config.project,
@@ -34,7 +58,7 @@ async function publishTestResults(config, result) {
34
58
  if (ci) {
35
59
  payload.ci_details = [ci];
36
60
  }
37
-
61
+
38
62
  const response = await request.post({
39
63
  url: `${get_base_url()}/api/core/v1/test-runs`,
40
64
  headers: {
@@ -63,9 +87,85 @@ function attachTestBeatsReportHyperLink(config, run_id) {
63
87
  }
64
88
 
65
89
  /**
66
- *
67
- * @param {string} run_id
68
- * @returns
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
69
169
  */
70
170
  function get_test_beats_report_link(run_id) {
71
171
  return `${get_base_url()}/reports/${run_id}`;
package/src/cli.js CHANGED
@@ -2,12 +2,12 @@
2
2
  require('dotenv').config();
3
3
 
4
4
  const sade = require('sade');
5
-
6
- const prog = sade('test-results-reporter');
5
+
6
+ const prog = sade('testbeats');
7
7
  const publish = require('./commands/publish');
8
-
8
+
9
9
  prog
10
- .version('0.0.7')
10
+ .version('2.0.1')
11
11
  .option('-c, --config', 'Provide path to custom config', 'config.json');
12
12
 
13
13
  prog.command('publish')
@@ -7,26 +7,39 @@ const beats = require('../beats');
7
7
  const target_manager = require('../targets');
8
8
 
9
9
  /**
10
- * @param {import('../index').PublishOptions} opts
10
+ * @param {import('../index').PublishOptions} opts
11
11
  */
12
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
+ }
13
19
  if (typeof opts.config === 'string') {
14
20
  const cwd = process.cwd();
15
- opts.config = require(path.join(cwd, opts.config));
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
+ }
16
27
  }
17
28
  const config = processData(opts.config);
18
29
  if (config.reports) {
19
30
  for (const report of config.reports) {
31
+ validateConfig(report);
20
32
  await processReport(report);
21
33
  }
22
34
  } else {
35
+ validateConfig(config);
23
36
  await processReport(config);
24
37
  }
25
38
  }
26
39
 
27
40
  /**
28
- *
29
- * @param {import('../index').PublishReport} report
41
+ *
42
+ * @param {import('../index').PublishReport} report
30
43
  */
31
44
  async function processReport(report) {
32
45
  const parsed_results = [];
@@ -43,8 +56,98 @@ async function processReport(report) {
43
56
  for (let i = 0; i < parsed_results.length; i++) {
44
57
  const result = parsed_results[i];
45
58
  await beats.run(report, result);
46
- for (const target of report.targets) {
47
- await target_manager.run(target, 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
+ }
48
151
  }
49
152
  }
50
153
  }
@@ -0,0 +1,72 @@
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
+ }
@@ -7,6 +7,7 @@ const percy_analysis = require('./percy-analysis');
7
7
  const custom = require('./custom');
8
8
  const metadata = require('./metadata');
9
9
  const ci_info = require('./ci-info');
10
+ const ai_failure_summary = require('./ai-failure-summary');
10
11
  const { EXTENSION } = require('../helpers/constants');
11
12
  const { checkCondition } = require('../helpers/helper');
12
13
 
@@ -53,6 +54,8 @@ function getExtensionRunner(extension) {
53
54
  return metadata;
54
55
  case EXTENSION.CI_INFO:
55
56
  return ci_info;
57
+ case EXTENSION.AI_FAILURE_SUMMARY:
58
+ return ai_failure_summary;
56
59
  default:
57
60
  return require(extension.name);
58
61
  }
@@ -20,6 +20,7 @@ const TARGET = Object.freeze({
20
20
  });
21
21
 
22
22
  const EXTENSION = Object.freeze({
23
+ AI_FAILURE_SUMMARY: 'ai-failure-summary',
23
24
  HYPERLINKS: 'hyperlinks',
24
25
  MENTIONS: 'mentions',
25
26
  REPORT_PORTAL_ANALYSIS: 'report-portal-analysis',
package/src/index.d.ts CHANGED
@@ -4,7 +4,7 @@ import { Schedule, User } from 'rosters';
4
4
  import { ParseOptions } from 'test-results-parser';
5
5
  import TestResult from 'test-results-parser/src/models/TestResult';
6
6
 
7
- export type ExtensionName = 'report-portal-analysis' | 'hyperlinks' | 'mentions' | 'report-portal-history' | 'quick-chart-test-summary' | 'metadata' | 'ci-info' | 'custom';
7
+ export type ExtensionName = 'report-portal-analysis' | 'hyperlinks' | 'mentions' | 'report-portal-history' | 'quick-chart-test-summary' | 'metadata' | 'ci-info' | 'custom' | 'ai-failure-summary';
8
8
  export type Hook = 'start' | 'end' | 'after-summary';
9
9
  export type TargetName = 'slack' | 'teams' | 'chat' | 'custom' | 'delay';
10
10
  export type PublishReportType = 'test-summary' | 'test-summary-slim' | 'failure-details';
@@ -61,11 +61,15 @@ export interface CIInfoInputs extends ExtensionInputs {
61
61
  data?: Metadata[];
62
62
  }
63
63
 
64
+ export interface AIFailureSummaryInputs extends ExtensionInputs {
65
+ failure_summary: string;
66
+ }
67
+
64
68
  export interface Extension {
65
69
  name: ExtensionName;
66
70
  condition?: Condition;
67
71
  hook?: Hook;
68
- inputs?: ReportPortalAnalysisInputs | ReportPortalHistoryInputs | HyperlinkInputs | MentionInputs | QuickChartTestSummaryInputs | PercyAnalysisInputs | CustomExtensionInputs | MetadataInputs | CIInfoInputs;
72
+ inputs?: ReportPortalAnalysisInputs | ReportPortalHistoryInputs | HyperlinkInputs | MentionInputs | QuickChartTestSummaryInputs | PercyAnalysisInputs | CustomExtensionInputs | MetadataInputs | CIInfoInputs | AIFailureSummaryInputs;
69
73
  }
70
74
 
71
75
  export interface PercyAnalysisInputs extends ExtensionInputs {
@@ -208,8 +212,8 @@ export interface CustomTargetInputs {
208
212
 
209
213
  export interface Target {
210
214
  name: TargetName;
211
- condition: Condition;
212
- inputs: SlackInputs | TeamsInputs | ChatInputs | CustomTargetInputs | InfluxDBTargetInputs;
215
+ condition?: Condition;
216
+ inputs?: SlackInputs | TeamsInputs | ChatInputs | CustomTargetInputs | InfluxDBTargetInputs;
213
217
  extensions?: Extension[];
214
218
  }
215
219
 
@@ -222,6 +226,7 @@ export interface PublishReport {
222
226
  api_key?: string;
223
227
  project?: string;
224
228
  run?: string;
229
+ show_failure_summary?: boolean;
225
230
  targets?: Target[];
226
231
  results?: ParseOptions[] | PerformanceParseOptions[] | CustomResultOptions[];
227
232
  }
@@ -232,7 +237,6 @@ export interface PublishConfig {
232
237
  run?: string;
233
238
  targets?: Target[];
234
239
  results?: ParseOptions[] | PerformanceParseOptions[] | CustomResultOptions[];
235
- reports?: PublishReport[];
236
240
  }
237
241
 
238
242
  export interface PublishOptions {