testbeats 2.0.7 → 2.0.9

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.7",
3
+ "version": "2.0.9",
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",
@@ -25,7 +25,7 @@ class BeatsApi {
25
25
  */
26
26
  getTestRun(run_id) {
27
27
  return request.get({
28
- url: `${this.getBaseUrl()}/api/core/v1/test-runs/key?id=${run_id}`,
28
+ url: `${this.getBaseUrl()}/api/core/v1/test-runs/${run_id}`,
29
29
  headers: {
30
30
  'x-api-key': this.config.api_key
31
31
  }
@@ -46,6 +46,21 @@ class BeatsApi {
46
46
  getBaseUrl() {
47
47
  return process.env.TEST_BEATS_URL || "https://app.testbeats.com";
48
48
  }
49
+
50
+ /**
51
+ *
52
+ * @param {string} run_id
53
+ * @param {number} limit
54
+ * @returns {import('./beats.types').IErrorClustersResponse}
55
+ */
56
+ getErrorClusters(run_id, limit = 3) {
57
+ return request.get({
58
+ url: `${this.getBaseUrl()}/api/core/v1/test-runs/${run_id}/error-clusters?limit=${limit}`,
59
+ headers: {
60
+ 'x-api-key': this.config.api_key
61
+ }
62
+ });
63
+ }
49
64
  }
50
65
 
51
66
  module.exports = { BeatsApi }
@@ -20,15 +20,20 @@ class Beats {
20
20
  }
21
21
 
22
22
  async publish() {
23
+ this.#setApiKey();
24
+ if (!this.config.api_key) {
25
+ logger.warn('😿 No API key provided, skipping publishing results to TestBeats Portal...');
26
+ return;
27
+ }
23
28
  this.#setCIInfo();
24
29
  this.#setProjectName();
25
30
  this.#setRunName();
26
- this.#setApiKey();
27
31
  await this.#publishTestResults();
28
32
  await this.#uploadAttachments();
29
33
  this.#updateTitleLink();
30
34
  await this.#attachFailureSummary();
31
35
  await this.#attachSmartAnalysis();
36
+ await this.#attachErrorClusters();
32
37
  }
33
38
 
34
39
  #setCIInfo() {
@@ -45,13 +50,10 @@ class Beats {
45
50
 
46
51
  #setRunName() {
47
52
  this.config.run = this.config.run || process.env.TEST_BEATS_RUN || (this.ci && this.ci.build_name) || 'demo-run';
53
+ this.result.name = this.config.run;
48
54
  }
49
55
 
50
56
  async #publishTestResults() {
51
- if (!this.config.api_key) {
52
- logger.warn('😿 No API key provided, skipping publishing results to TestBeats Portal...');
53
- return;
54
- }
55
57
  logger.info("🚀 Publishing results to TestBeats Portal...");
56
58
  try {
57
59
  const payload = this.#getPayload();
@@ -89,18 +91,6 @@ class Beats {
89
91
  }
90
92
  }
91
93
 
92
- #getAllFailedTestCases() {
93
- const test_cases = [];
94
- for (const suite of this.result.suites) {
95
- for (const test of suite.cases) {
96
- if (test.status === 'FAIL') {
97
- test_cases.push(test);
98
- }
99
- }
100
- }
101
- return test_cases;
102
- }
103
-
104
94
  #updateTitleLink() {
105
95
  if (!this.test_run_id) {
106
96
  return;
@@ -200,6 +190,34 @@ class Beats {
200
190
  logger.warn(`🙈 ${text} not generated in given time`);
201
191
  }
202
192
 
193
+ async #attachErrorClusters() {
194
+ if (!this.test_run_id) {
195
+ return;
196
+ }
197
+ if (!this.config.targets) {
198
+ return;
199
+ }
200
+ if (this.result.status !== 'FAIL') {
201
+ return;
202
+ }
203
+ if (this.config.show_error_clusters === false) {
204
+ return;
205
+ }
206
+ try {
207
+ logger.info('🧮 Fetching Error Clusters...');
208
+ const res = await this.api.getErrorClusters(this.test_run_id, 3);
209
+ this.config.extensions.push({
210
+ name: 'error-clusters',
211
+ hook: HOOK.AFTER_SUMMARY,
212
+ inputs: {
213
+ data: res.values
214
+ }
215
+ });
216
+ } catch (error) {
217
+ logger.error(`❌ Unable to attach error clusters: ${error.message}`, error);
218
+ }
219
+ }
220
+
203
221
  }
204
222
 
205
223
  module.exports = { Beats }
@@ -17,3 +17,18 @@ export type IBeatExecutionMetric = {
17
17
  test_run_id: string
18
18
  org_id: string
19
19
  }
20
+
21
+ export type IPaginatedAPIResponse<T> = {
22
+ page: number
23
+ limit: number
24
+ total: number
25
+ values: T[]
26
+ }
27
+
28
+ export type IErrorClustersResponse = {} & IPaginatedAPIResponse<IErrorCluster>;
29
+
30
+ export type IErrorCluster = {
31
+ test_failure_id: string
32
+ failure: string
33
+ count: number
34
+ }
@@ -18,6 +18,7 @@ class PublishCommand {
18
18
  */
19
19
  constructor(opts) {
20
20
  this.opts = opts;
21
+ this.errors = [];
21
22
  }
22
23
 
23
24
  async publish() {
@@ -31,7 +32,7 @@ class PublishCommand {
31
32
  this.#validateConfig();
32
33
  this.#processResults();
33
34
  await this.#publishResults();
34
- logger.info('✅ Results published successfully!');
35
+ await this.#publishErrors();
35
36
  }
36
37
 
37
38
  #validateEnvDetails() {
@@ -184,13 +185,24 @@ class PublishCommand {
184
185
  } else if (result_options.type === 'jmeter') {
185
186
  this.results.push(prp.parse(result_options));
186
187
  } else {
187
- this.results.push(trp.parse(result_options));
188
+ const { result, errors } = trp.parseV2(result_options);
189
+ if (result) {
190
+ this.results.push(result);
191
+ }
192
+ if (errors) {
193
+ this.errors = this.errors.concat(errors);
194
+ }
188
195
  }
189
196
  }
190
197
  }
191
198
  }
192
199
 
193
200
  async #publishResults() {
201
+ if (!this.results.length) {
202
+ logger.warn('⚠️ No results to publish');
203
+ return;
204
+ }
205
+
194
206
  for (const config of this.configs) {
195
207
  for (let i = 0; i < this.results.length; i++) {
196
208
  const result = this.results[i];
@@ -207,6 +219,23 @@ class PublishCommand {
207
219
  }
208
220
  }
209
221
  }
222
+ logger.info('✅ Results published successfully!');
223
+ }
224
+
225
+ async #publishErrors() {
226
+ if (!this.errors.length) {
227
+ logger.debug('⚠️ No errors to publish');
228
+ return;
229
+ }
230
+ logger.info('🛑 Publishing errors...');
231
+ for (const config of this.configs) {
232
+ if (config.targets) {
233
+ for (const target of config.targets) {
234
+ await target_manager.handleErrors({ target, errors: this.errors });
235
+ }
236
+ }
237
+ }
238
+ throw new Error(this.errors.join('\n'));
210
239
  }
211
240
 
212
241
  }
@@ -86,6 +86,22 @@ class BaseExtension {
86
86
  }
87
87
  }
88
88
 
89
+ /**
90
+ * @param {string|number} text
91
+ */
92
+ bold(text) {
93
+ switch (this.target.name) {
94
+ case 'teams':
95
+ return `**${text}**`;
96
+ case 'slack':
97
+ return `*${text}*`;
98
+ case 'chat':
99
+ return `<b>${text}</b>`;
100
+ default:
101
+ break;
102
+ }
103
+ }
104
+
89
105
  }
90
106
 
91
107
  module.exports = { BaseExtension }
@@ -3,6 +3,8 @@ const { getCIInformation } = require('../helpers/ci');
3
3
  const { getMetaDataText } = require("../helpers/metadata.helper");
4
4
  const { STATUS, HOOK } = require("../helpers/constants");
5
5
 
6
+ const COMMON_BRANCH_NAMES = ['main', 'master', 'dev', 'develop', 'qa', 'test'];
7
+
6
8
  class CIInfoExtension extends BaseExtension {
7
9
 
8
10
  constructor(target, extension, result, payload, root_payload) {
@@ -17,23 +19,24 @@ class CIInfoExtension extends BaseExtension {
17
19
  }
18
20
 
19
21
  #setDefaultOptions() {
20
- this.default_options.hook = HOOK.AFTER_SUMMARY,
22
+ this.default_options.hook = HOOK.AFTER_SUMMARY;
21
23
  this.default_options.condition = STATUS.PASS_OR_FAIL;
22
24
  }
23
25
 
24
26
  #setDefaultInputs() {
25
27
  this.default_inputs.title = '';
26
28
  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_repository_non_common = true;
30
+ this.default_inputs.show_repository = false;
31
+ this.default_inputs.show_repository_branch = false;
29
32
  this.default_inputs.show_build = true;
30
33
  }
31
34
 
32
35
  async run() {
33
36
  this.ci = getCIInformation();
34
37
 
35
- this.setRepositoryElements();
36
- this.setBuildElements();
38
+ this.#setRepositoryElements();
39
+ this.#setBuildElements();
37
40
 
38
41
  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
42
  const build_text = await getMetaDataText({ elements: this.build_elements, target: this.target, extension: this.extension, result: this.result, default_condition: this.default_options.condition });
@@ -41,28 +44,55 @@ class CIInfoExtension extends BaseExtension {
41
44
  this.attach();
42
45
  }
43
46
 
44
- setRepositoryElements() {
47
+ #setRepositoryElements() {
45
48
  if (!this.ci) {
46
49
  return;
47
50
  }
51
+ if (!this.ci.repository_url || !this.ci.repository_name || !this.ci.repository_ref) {
52
+ return;
53
+ }
48
54
 
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' });
55
+ if (this.extension.inputs.show_repository) {
56
+ this.#setRepositoryElement();
57
+ }
58
+ if (this.extension.inputs.show_repository_branch) {
59
+ if (this.ci.repository_ref.includes('refs/pull')) {
60
+ this.#setPullRequestElement();
61
+ } else {
62
+ this.#setRepositoryBranchElement();
63
+ }
51
64
  }
52
- if (this.extension.inputs.show_repository_branch && this.ci.repository_ref) {
65
+ if (!this.extension.inputs.show_repository && !this.extension.inputs.show_repository_branch && this.extension.inputs.show_repository_non_common) {
53
66
  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' });
67
+ this.#setRepositoryElement();
68
+ this.#setPullRequestElement();
57
69
  } else {
58
- const branch_url = this.ci.repository_url + this.ci.repository_ref.replace('refs/heads/', '/tree/');
59
70
  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' });
71
+ if (!COMMON_BRANCH_NAMES.includes(branch_name.toLowerCase())) {
72
+ this.#setRepositoryElement();
73
+ this.#setRepositoryBranchElement();
74
+ }
61
75
  }
62
76
  }
63
77
  }
64
78
 
65
- setBuildElements() {
79
+ #setRepositoryElement() {
80
+ this.repository_elements.push({ label: 'Repository', key: this.ci.repository_name, value: this.ci.repository_url, type: 'hyperlink' });
81
+ }
82
+
83
+ #setPullRequestElement() {
84
+ const pr_url = this.ci.repository_url + this.ci.repository_ref.replace('refs/pull/', '/pull/');
85
+ const pr_name = this.ci.repository_ref.replace('refs/pull/', '').replace('/merge', '');
86
+ this.repository_elements.push({ label: 'Pull Request', key: pr_name, value: pr_url, type: 'hyperlink' });
87
+ }
88
+
89
+ #setRepositoryBranchElement() {
90
+ const branch_url = this.ci.repository_url + this.ci.repository_ref.replace('refs/heads/', '/tree/');
91
+ const branch_name = this.ci.repository_ref.replace('refs/heads/', '');
92
+ this.repository_elements.push({ label: 'Branch', key: branch_name, value: branch_url, type: 'hyperlink' });
93
+ }
94
+
95
+ #setBuildElements() {
66
96
  if (!this.ci) {
67
97
  return;
68
98
  }
@@ -0,0 +1,46 @@
1
+ const { BaseExtension } = require('./base.extension');
2
+ const { STATUS, HOOK } = require("../helpers/constants");
3
+
4
+ class ErrorClustersExtension 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 = 'Top Errors';
25
+ this.default_inputs.title_link = '';
26
+ }
27
+
28
+ #setText() {
29
+ const data = this.extension.inputs.data;
30
+ if (!data || !data.length) {
31
+ return;
32
+ }
33
+
34
+ const clusters = data;
35
+
36
+ this.extension.inputs.title = `Top ${clusters.length} Errors`;
37
+
38
+ const texts = [];
39
+ for (const cluster of clusters) {
40
+ texts.push(`${this.bold(`(${cluster.count})`)} - ${cluster.failure}`);
41
+ }
42
+ this.text = this.mergeTexts(texts);
43
+ }
44
+ }
45
+
46
+ module.exports = { ErrorClustersExtension }
@@ -12,6 +12,7 @@ const { CustomExtension } = require('./custom.extension');
12
12
  const { EXTENSION } = require('../helpers/constants');
13
13
  const { checkCondition } = require('../helpers/helper');
14
14
  const logger = require('../utils/logger');
15
+ const { ErrorClustersExtension } = require('./error-clusters.extension');
15
16
 
16
17
  async function run(options) {
17
18
  const { target, result, hook } = options;
@@ -60,6 +61,8 @@ function getExtensionRunner(extension, options) {
60
61
  return new AIFailureSummaryExtension(options.target, extension, options.result, options.payload, options.root_payload);
61
62
  case EXTENSION.SMART_ANALYSIS:
62
63
  return new SmartAnalysisExtension(options.target, extension, options.result, options.payload, options.root_payload);
64
+ case EXTENSION.ERROR_CLUSTERS:
65
+ return new ErrorClustersExtension(options.target, extension, options.result, options.payload, options.root_payload);
63
66
  default:
64
67
  return require(extension.name);
65
68
  }
@@ -1,5 +1,6 @@
1
1
  const { BaseExtension } = require('./base.extension');
2
2
  const { STATUS, HOOK } = require("../helpers/constants");
3
+ const logger = require('../utils/logger');
3
4
 
4
5
  class SmartAnalysisExtension extends BaseExtension {
5
6
 
@@ -21,7 +22,7 @@ class SmartAnalysisExtension extends BaseExtension {
21
22
  }
22
23
 
23
24
  #setDefaultInputs() {
24
- this.default_inputs.title = 'Smart Analysis';
25
+ this.default_inputs.title = '';
25
26
  this.default_inputs.title_link = '';
26
27
  }
27
28
 
@@ -37,6 +38,11 @@ class SmartAnalysisExtension extends BaseExtension {
37
38
  */
38
39
  const execution_metrics = data.execution_metrics[0];
39
40
 
41
+ if (!execution_metrics) {
42
+ logger.warn('⚠️ No execution metrics found. Skipping.');
43
+ return;
44
+ }
45
+
40
46
  const smart_analysis = [];
41
47
  if (execution_metrics.newly_failed) {
42
48
  smart_analysis.push(`⭕ Newly Failed: ${execution_metrics.newly_failed}`);
@@ -54,7 +60,21 @@ class SmartAnalysisExtension extends BaseExtension {
54
60
  smart_analysis.push(`🟢 Recovered: ${execution_metrics.recovered}`);
55
61
  }
56
62
 
57
- this.text = smart_analysis.join(' | ');
63
+ const texts = [];
64
+ const rows = [];
65
+ for (const item of smart_analysis) {
66
+ rows.push(item);
67
+ if (rows.length === 3) {
68
+ texts.push(rows.join(' | '));
69
+ rows.length = 0;
70
+ }
71
+ }
72
+
73
+ if (rows.length > 0) {
74
+ texts.push(rows.join(' | '));
75
+ }
76
+
77
+ this.text = this.mergeTexts(texts);
58
78
  }
59
79
 
60
80
  }
@@ -22,6 +22,7 @@ const TARGET = Object.freeze({
22
22
  const EXTENSION = Object.freeze({
23
23
  AI_FAILURE_SUMMARY: 'ai-failure-summary',
24
24
  SMART_ANALYSIS: 'smart-analysis',
25
+ ERROR_CLUSTERS: 'error-clusters',
25
26
  HYPERLINKS: 'hyperlinks',
26
27
  MENTIONS: 'mentions',
27
28
  REPORT_PORTAL_ANALYSIS: 'report-portal-analysis',
package/src/index.d.ts CHANGED
@@ -56,6 +56,7 @@ export interface MentionInputs extends ExtensionInputs {
56
56
  }
57
57
 
58
58
  export interface CIInfoInputs extends ExtensionInputs {
59
+ show_repository_non_common?: boolean;
59
60
  show_repository?: boolean;
60
61
  show_repository_branch?: boolean;
61
62
  show_build?: boolean;
@@ -229,6 +230,7 @@ export interface PublishReport {
229
230
  run?: string;
230
231
  show_failure_summary?: boolean;
231
232
  show_smart_analysis?: boolean;
233
+ show_error_clusters?: boolean;
232
234
  targets?: Target[];
233
235
  extensions?: Extension[];
234
236
  results?: ParseOptions[] | PerformanceParseOptions[] | CustomResultOptions[];
@@ -4,6 +4,7 @@ const extension_manager = require('../extensions');
4
4
  const { HOOK, STATUS } = require('../helpers/constants');
5
5
  const PerformanceTestResult = require('performance-results-parser/src/models/PerformanceTestResult');
6
6
  const { getValidMetrics, getMetricValuesText } = require('../helpers/performance');
7
+ const logger = require('../utils/logger');
7
8
 
8
9
  async function run({ result, target }) {
9
10
  setTargetInputs(target);
@@ -14,6 +15,7 @@ async function run({ result, target }) {
14
15
  } else {
15
16
  await setFunctionalPayload({ result, target, payload, root_payload });
16
17
  }
18
+ logger.info(`🔔 Publishing results to Chat...`);
17
19
  return request.post({
18
20
  url: target.inputs.url,
19
21
  body: root_payload
@@ -144,9 +146,9 @@ async function setPerformancePayload({ result, target, payload, root_payload })
144
146
  }
145
147
 
146
148
  /**
147
- *
149
+ *
148
150
  * @param {object} param0
149
- * @param {PerformanceTestResult} param0.result
151
+ * @param {PerformanceTestResult} param0.result
150
152
  */
151
153
  async function setPerformanceMainBlock({ result, target, payload }) {
152
154
  const title_text_with_emoji = getTitleTextWithEmoji({ result, target });
@@ -169,9 +171,9 @@ async function setPerformanceMainBlock({ result, target, payload }) {
169
171
  }
170
172
 
171
173
  /**
172
- *
174
+ *
173
175
  * @param {object} param0
174
- * @param {PerformanceTestResult} param0.result
176
+ * @param {PerformanceTestResult} param0.result
175
177
  */
176
178
  async function setTransactionBlock({ result, target, payload }) {
177
179
  if (target.inputs.include_suites) {
@@ -236,7 +238,31 @@ const default_inputs = {
236
238
  ]
237
239
  };
238
240
 
241
+ async function handleErrors({ target, errors }) {
242
+ let title = 'Error: Reporting Test Results';
243
+ title = target.inputs.title ? title + ' - ' + target.inputs.title : title;
244
+
245
+ const root_payload = getRootPayload();
246
+ const payload = root_payload.cards[0];
247
+
248
+ payload.sections.push({
249
+ "widgets": [
250
+ {
251
+ "textParagraph": {
252
+ text: `<b>${title}</b><br><br><b>Errors</b>: <br>${errors.join('<br>')}`
253
+ }
254
+ }
255
+ ]
256
+ });
257
+
258
+ return request.post({
259
+ url: target.inputs.url,
260
+ body: root_payload
261
+ });
262
+ }
263
+
239
264
  module.exports = {
240
265
  run,
266
+ handleErrors,
241
267
  default_options
242
268
  }
@@ -34,6 +34,14 @@ async function run(target, result) {
34
34
  }
35
35
  }
36
36
 
37
+ async function handleErrors({ target, errors }) {
38
+ const target_runner = getTargetRunner(target);
39
+ if (target_runner.handleErrors) {
40
+ await target_runner.handleErrors({ target, errors });
41
+ }
42
+ }
43
+
37
44
  module.exports = {
38
- run
45
+ run,
46
+ handleErrors
39
47
  }
@@ -2,12 +2,14 @@ const request = require('phin-retry');
2
2
  const { getPercentage, truncate, getPrettyDuration } = require('../helpers/helper');
3
3
  const extension_manager = require('../extensions');
4
4
  const { HOOK, STATUS } = require('../helpers/constants');
5
+ const logger = require('../utils/logger');
5
6
 
6
7
  const PerformanceTestResult = require('performance-results-parser/src/models/PerformanceTestResult');
7
8
  const { getValidMetrics, getMetricValuesText } = require('../helpers/performance');
8
9
  const TestResult = require('test-results-parser/src/models/TestResult');
9
10
 
10
11
 
12
+
11
13
  const COLORS = {
12
14
  GOOD: '#36A64F',
13
15
  WARNING: '#ECB22E',
@@ -23,6 +25,7 @@ async function run({ result, target }) {
23
25
  await setFunctionalPayload({ result, target, payload });
24
26
  }
25
27
  const message = getRootPayload({ result, target, payload });
28
+ logger.info(`🔔 Publishing results to Slack...`);
26
29
  return request.post({
27
30
  url: target.inputs.url,
28
31
  body: message
@@ -142,10 +145,10 @@ function getFailureDetails(suite) {
142
145
  }
143
146
 
144
147
  /**
145
- *
146
- * @param {object} param0
147
- * @param {PerformanceTestResult | TestResult} param0.result
148
- * @returns
148
+ *
149
+ * @param {object} param0
150
+ * @param {PerformanceTestResult | TestResult} param0.result
151
+ * @returns
149
152
  */
150
153
  function getRootPayload({ result, target, payload }) {
151
154
  let color = COLORS.GOOD;
@@ -182,9 +185,9 @@ async function setPerformancePayload({ result, target, payload }) {
182
185
  }
183
186
 
184
187
  /**
185
- *
186
- * @param {object} param0
187
- * @param {PerformanceTestResult} param0.result
188
+ *
189
+ * @param {object} param0
190
+ * @param {PerformanceTestResult} param0.result
188
191
  */
189
192
  async function setPerformanceMainBlock({ result, target, payload }) {
190
193
  let text = `*${getTitleText(result, target)}*\n`;
@@ -206,9 +209,9 @@ async function setPerformanceMainBlock({ result, target, payload }) {
206
209
  }
207
210
 
208
211
  /**
209
- *
210
- * @param {object} param0
211
- * @param {PerformanceTestResult} param0.result
212
+ *
213
+ * @param {object} param0
214
+ * @param {PerformanceTestResult} param0.result
212
215
  */
213
216
  async function setTransactionBlock({ result, target, payload }) {
214
217
  if (target.inputs.include_suites) {
@@ -261,7 +264,45 @@ const default_inputs = {
261
264
  ]
262
265
  }
263
266
 
267
+ async function handleErrors({ target, errors }) {
268
+ let title = 'Error: Reporting Test Results';
269
+ title = target.inputs.title ? title + ' - ' + target.inputs.title : title;
270
+
271
+ const blocks = [];
272
+
273
+ blocks.push({
274
+ "type": "section",
275
+ "text": {
276
+ "type": "mrkdwn",
277
+ "text": title
278
+ }
279
+ });
280
+ blocks.push({
281
+ "type": "section",
282
+ "text": {
283
+ "type": "mrkdwn",
284
+ "text": errors.join('\n\n')
285
+ }
286
+ });
287
+
288
+ const payload = {
289
+ "attachments": [
290
+ {
291
+ "color": COLORS.DANGER,
292
+ "blocks": blocks,
293
+ "fallback": title,
294
+ }
295
+ ]
296
+ };
297
+
298
+ return request.post({
299
+ url: target.inputs.url,
300
+ body: payload
301
+ });
302
+ }
303
+
264
304
  module.exports = {
265
305
  run,
306
+ handleErrors,
266
307
  default_options
267
308
  }
@@ -3,14 +3,15 @@ const { getPercentage, truncate, getPrettyDuration } = require('../helpers/helpe
3
3
  const { getValidMetrics, getMetricValuesText } = require('../helpers/performance');
4
4
  const extension_manager = require('../extensions');
5
5
  const { HOOK, STATUS } = require('../helpers/constants');
6
+ const logger = require('../utils/logger');
6
7
 
7
8
  const TestResult = require('test-results-parser/src/models/TestResult');
8
9
  const PerformanceTestResult = require('performance-results-parser/src/models/PerformanceTestResult');
9
10
 
10
11
  /**
11
- * @param {object} param0
12
- * @param {PerformanceTestResult | TestResult} param0.result
13
- * @returns
12
+ * @param {object} param0
13
+ * @param {PerformanceTestResult | TestResult} param0.result
14
+ * @returns
14
15
  */
15
16
  async function run({ result, target }) {
16
17
  setTargetInputs(target);
@@ -22,6 +23,7 @@ async function run({ result, target }) {
22
23
  await setFunctionalPayload({ result, target, payload, root_payload });
23
24
  }
24
25
  setRootPayload(root_payload, payload);
26
+ logger.info(`🔔 Publishing results to Teams...`);
25
27
  return request.post({
26
28
  url: target.inputs.url,
27
29
  body: root_payload
@@ -209,8 +211,8 @@ function setRootPayload(root_payload, payload) {
209
211
  }
210
212
 
211
213
  /**
212
- * @param {object} param0
213
- * @param {PerformanceTestResult} param0.result
214
+ * @param {object} param0
215
+ * @param {PerformanceTestResult} param0.result
214
216
  */
215
217
  async function setMainBlockForPerformance({ result, target, payload }) {
216
218
  const total = result.transactions.length;
@@ -245,8 +247,8 @@ async function getFactMetrics({ metrics, target, result }) {
245
247
  }
246
248
 
247
249
  /**
248
- * @param {object} param0
249
- * @param {PerformanceTestResult} param0.result
250
+ * @param {object} param0
251
+ * @param {PerformanceTestResult} param0.result
250
252
  */
251
253
  async function setTransactionBlock({ result, target, payload }) {
252
254
  if (target.inputs.include_suites) {
@@ -298,7 +300,40 @@ const default_inputs = {
298
300
  ]
299
301
  }
300
302
 
303
+ async function handleErrors({ target, errors }) {
304
+ let title = 'Error: Reporting Test Results';
305
+ title = target.inputs.title ? title + ' - ' + target.inputs.title : title;
306
+
307
+ const root_payload = getRootPayload();
308
+ const payload = getMainPayload(target);
309
+
310
+ payload.body.push({
311
+ "type": "TextBlock",
312
+ "text": title,
313
+ "size": "medium",
314
+ "weight": "bolder",
315
+ "wrap": true
316
+ });
317
+
318
+ payload.body.push({
319
+ "type": "TextBlock",
320
+ "text": errors.join('\n'),
321
+ "size": "medium",
322
+ "weight": "bolder",
323
+ "wrap": true
324
+ });
325
+
326
+ setRootPayload(root_payload, payload);
327
+
328
+
329
+ return request.post({
330
+ url: target.inputs.url,
331
+ body: root_payload
332
+ });
333
+ }
334
+
301
335
  module.exports = {
302
336
  run,
337
+ handleErrors,
303
338
  default_options
304
339
  }