testbeats 2.2.6 → 2.2.8
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 +1 -1
- package/src/beats/beats.attachments.js +1 -1
- package/src/beats/beats.js +2 -2
- package/src/index.d.ts +2 -2
- package/src/platforms/base.platform.js +13 -108
- package/src/platforms/chat.platform.js +4 -1
- package/src/platforms/slack.platform.js +19 -3
- package/src/targets/base.target.js +114 -1
- package/src/targets/chat.js +11 -4
- package/src/targets/custom.target.js +3 -2
- package/src/targets/github.target.js +316 -0
- package/src/targets/index.js +2 -2
- package/src/targets/slack.js +32 -12
- package/src/targets/teams.js +14 -8
- package/src/utils/context.utils.js +5 -0
- package/src/targets/github.js +0 -310
package/package.json
CHANGED
|
@@ -10,7 +10,7 @@ const logger = require('../utils/logger');
|
|
|
10
10
|
const TestAttachment = require('test-results-parser/src/models/TestAttachment');
|
|
11
11
|
|
|
12
12
|
const MAX_ATTACHMENTS_PER_REQUEST = 5;
|
|
13
|
-
const MAX_ATTACHMENTS_PER_RUN =
|
|
13
|
+
const MAX_ATTACHMENTS_PER_RUN = 50;
|
|
14
14
|
const MAX_ATTACHMENT_SIZE = 2 * 1024 * 1024;
|
|
15
15
|
|
|
16
16
|
class BeatsAttachments {
|
package/src/beats/beats.js
CHANGED
|
@@ -186,14 +186,14 @@ class Beats {
|
|
|
186
186
|
if (process.env.TEST_BEATS_DELAY) {
|
|
187
187
|
return parseInt(process.env.TEST_BEATS_DELAY);
|
|
188
188
|
}
|
|
189
|
-
return
|
|
189
|
+
return 5000;
|
|
190
190
|
}
|
|
191
191
|
|
|
192
192
|
async #setTestRun(text, wait_for = 'smart_analysis_status') {
|
|
193
193
|
if (this.test_run && this.test_run[wait_for] === PROCESS_STATUS.COMPLETED) {
|
|
194
194
|
return;
|
|
195
195
|
}
|
|
196
|
-
let retry =
|
|
196
|
+
let retry = 5;
|
|
197
197
|
while (retry >= 0) {
|
|
198
198
|
retry = retry - 1;
|
|
199
199
|
await new Promise(resolve => setTimeout(resolve, this.#getDelay()));
|
package/src/index.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ export interface ITarget {
|
|
|
10
10
|
name: TargetName;
|
|
11
11
|
enable?: string | boolean;
|
|
12
12
|
condition?: Condition;
|
|
13
|
-
inputs?: SlackInputs | TeamsInputs | ChatInputs |
|
|
13
|
+
inputs?: SlackInputs | TeamsInputs | ChatInputs | IGitHubInputs | ICustomTargetInputs | InfluxDBTargetInputs;
|
|
14
14
|
extensions?: IExtension[];
|
|
15
15
|
}
|
|
16
16
|
|
|
@@ -244,7 +244,7 @@ export interface TeamsInputs extends TargetInputs {
|
|
|
244
244
|
|
|
245
245
|
export interface ChatInputs extends TargetInputs { }
|
|
246
246
|
|
|
247
|
-
export interface
|
|
247
|
+
export interface IGitHubInputs extends TargetInputs {
|
|
248
248
|
token?: string;
|
|
249
249
|
owner?: string;
|
|
250
250
|
repo?: string;
|
|
@@ -6,7 +6,10 @@ class BasePlatform {
|
|
|
6
6
|
* @param {string|number} text
|
|
7
7
|
*/
|
|
8
8
|
bold(text) {
|
|
9
|
-
|
|
9
|
+
if (text) {
|
|
10
|
+
return `**${text}**`;
|
|
11
|
+
}
|
|
12
|
+
return text;
|
|
10
13
|
}
|
|
11
14
|
|
|
12
15
|
break() {
|
|
@@ -39,117 +42,19 @@ class BasePlatform {
|
|
|
39
42
|
return text;
|
|
40
43
|
}
|
|
41
44
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
*
|
|
46
|
-
* @param {import('..').ITarget} target
|
|
47
|
-
* @param {import('test-results-parser').ITestSuite} suite
|
|
48
|
-
*/
|
|
49
|
-
getSuiteSummaryText(target, suite) {
|
|
50
|
-
const suite_title = this.getSuiteTitle(suite);
|
|
51
|
-
const suite_results_text = this.#getSuiteResultsText(suite);
|
|
52
|
-
const duration_text = this.#getSuiteDurationText(target, suite);
|
|
53
|
-
|
|
54
|
-
const texts = [
|
|
55
|
-
this.bold(suite_title),
|
|
56
|
-
this.break(),
|
|
57
|
-
this.break(),
|
|
58
|
-
suite_results_text,
|
|
59
|
-
this.break(),
|
|
60
|
-
duration_text,
|
|
61
|
-
];
|
|
62
|
-
|
|
63
|
-
const metadata_text = this.getSuiteMetaDataText(suite);
|
|
64
|
-
|
|
65
|
-
if (metadata_text) {
|
|
66
|
-
texts.push(this.break());
|
|
67
|
-
texts.push(this.break());
|
|
68
|
-
texts.push(metadata_text);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return texts.join('');
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
*
|
|
76
|
-
* @param {import('test-results-parser').ITestSuite} suite
|
|
77
|
-
* @returns {string}
|
|
78
|
-
*/
|
|
79
|
-
getSuiteTitle(suite) {
|
|
80
|
-
const emoji = suite.status === 'PASS' ? '✅' : suite.total === suite.skipped ? '⏭️' : '❌';
|
|
81
|
-
return `${emoji} ${suite.name}`;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
45
|
/**
|
|
85
|
-
*
|
|
86
|
-
* @param {
|
|
87
|
-
* @returns {string}
|
|
88
|
-
*/
|
|
89
|
-
#getSuiteResultsText(suite) {
|
|
90
|
-
const suite_results = this.getSuiteResults(suite);
|
|
91
|
-
return `${this.bold('Results')}: ${suite_results}`;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
*
|
|
96
|
-
* @param {import('test-results-parser').ITestSuite} suite
|
|
97
|
-
* @returns {string}
|
|
98
|
-
*/
|
|
99
|
-
getSuiteResults(suite) {
|
|
100
|
-
return `${suite.passed} / ${suite.total} Passed (${getPercentage(suite.passed, suite.total)}%)`;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
*
|
|
105
|
-
* @param {import('..').ITarget} target
|
|
106
|
-
* @param {import('test-results-parser').ITestSuite} suite
|
|
107
|
-
*/
|
|
108
|
-
#getSuiteDurationText(target, suite) {
|
|
109
|
-
const duration = this.getSuiteDuration(target, suite);
|
|
110
|
-
return `${this.bold('Duration')}: ${duration}`
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
*
|
|
115
|
-
* @param {import('..').ITarget} target
|
|
116
|
-
* @param {import('test-results-parser').ITestSuite} suite
|
|
117
|
-
*/
|
|
118
|
-
getSuiteDuration(target, suite) {
|
|
119
|
-
return getPrettyDuration(suite.duration, target.inputs.duration);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
*
|
|
124
|
-
* @param {import('test-results-parser').ITestSuite} suite
|
|
46
|
+
* @param {string} text
|
|
47
|
+
* @param {string} url
|
|
125
48
|
* @returns {string}
|
|
126
49
|
*/
|
|
127
|
-
|
|
128
|
-
if (
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
// webdriver io
|
|
135
|
-
if (suite.metadata.device && typeof suite.metadata.device === 'string') {
|
|
136
|
-
texts.push(`${suite.metadata.device}`);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
if (suite.metadata.platform && suite.metadata.platform.name && suite.metadata.platform.version) {
|
|
140
|
-
texts.push(`${suite.metadata.platform.name} ${suite.metadata.platform.version}`);
|
|
50
|
+
link(text, url) {
|
|
51
|
+
if (url) {
|
|
52
|
+
if (!text) {
|
|
53
|
+
text = url;
|
|
54
|
+
}
|
|
55
|
+
return `[${text}](${url})`;
|
|
141
56
|
}
|
|
142
|
-
|
|
143
|
-
if (suite.metadata.browser && suite.metadata.browser.name && suite.metadata.browser.version) {
|
|
144
|
-
texts.push(`${suite.metadata.browser.name} ${suite.metadata.browser.version}`);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// playwright
|
|
148
|
-
if (suite.metadata.hostname && typeof suite.metadata.hostname === 'string') {
|
|
149
|
-
texts.push(`${suite.metadata.hostname}`);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
return texts.join(' • ');
|
|
57
|
+
return url;
|
|
153
58
|
}
|
|
154
59
|
|
|
155
60
|
/**
|
|
@@ -6,7 +6,10 @@ class SlackPlatform extends BasePlatform {
|
|
|
6
6
|
* @param {string|number} text
|
|
7
7
|
*/
|
|
8
8
|
bold(text) {
|
|
9
|
-
|
|
9
|
+
if (text) {
|
|
10
|
+
return `*${text}*`;
|
|
11
|
+
}
|
|
12
|
+
return text;
|
|
10
13
|
}
|
|
11
14
|
|
|
12
15
|
/**
|
|
@@ -21,8 +24,21 @@ class SlackPlatform extends BasePlatform {
|
|
|
21
24
|
}
|
|
22
25
|
|
|
23
26
|
code(text) {
|
|
24
|
-
|
|
27
|
+
if (text) {
|
|
28
|
+
return `\`\`\`${text}\`\`\``;
|
|
29
|
+
}
|
|
30
|
+
return text;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
link(text, url) {
|
|
34
|
+
if (url) {
|
|
35
|
+
if (!text) {
|
|
36
|
+
text = url;
|
|
37
|
+
}
|
|
38
|
+
return `<${url}|${text}>`;
|
|
39
|
+
}
|
|
40
|
+
return text;
|
|
25
41
|
}
|
|
26
42
|
}
|
|
27
43
|
|
|
28
|
-
module.exports = { SlackPlatform }
|
|
44
|
+
module.exports = { SlackPlatform }
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const { getPlatform } = require('../platforms');
|
|
2
2
|
const { STATUS } = require('../helpers/constants');
|
|
3
|
+
const { getPercentage, getPrettyDuration } = require('../helpers/helper');
|
|
3
4
|
|
|
4
5
|
class BaseTarget {
|
|
5
6
|
|
|
@@ -34,12 +35,124 @@ class BaseTarget {
|
|
|
34
35
|
* @type {import('../platforms/base.platform').BasePlatform}
|
|
35
36
|
*/
|
|
36
37
|
this.platform = getPlatform(this.name);
|
|
38
|
+
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
async run({ result }) {
|
|
40
42
|
// throw new Error('Not implemented');
|
|
41
43
|
}
|
|
42
44
|
|
|
45
|
+
/**
|
|
46
|
+
*
|
|
47
|
+
* @param {import('..').ITarget} target
|
|
48
|
+
* @param {import('test-results-parser').ITestSuite} suite
|
|
49
|
+
*/
|
|
50
|
+
getSuiteSummaryText(target, suite) {
|
|
51
|
+
const suite_title = this.getSuiteTitle(suite);
|
|
52
|
+
const suite_results_text = this.#getSuiteResultsText(suite);
|
|
53
|
+
const duration_text = this.#getSuiteDurationText(target, suite);
|
|
54
|
+
|
|
55
|
+
const texts = [
|
|
56
|
+
this.platform.bold(suite_title),
|
|
57
|
+
this.platform.break(),
|
|
58
|
+
this.platform.break(),
|
|
59
|
+
suite_results_text,
|
|
60
|
+
this.platform.break(),
|
|
61
|
+
duration_text,
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
const metadata_text = this.getSuiteMetaDataText(suite);
|
|
65
|
+
|
|
66
|
+
if (metadata_text) {
|
|
67
|
+
texts.push(this.platform.break());
|
|
68
|
+
texts.push(this.platform.break());
|
|
69
|
+
texts.push(metadata_text);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return texts.join('');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
*
|
|
77
|
+
* @param {import('test-results-parser').ITestSuite} suite
|
|
78
|
+
* @returns {string}
|
|
79
|
+
*/
|
|
80
|
+
getSuiteTitle(suite) {
|
|
81
|
+
const emoji = suite.status === 'PASS' ? '✅' : suite.total === suite.skipped ? '⏭️' : '❌';
|
|
82
|
+
return `${emoji} ${suite.name}`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
*
|
|
87
|
+
* @param {import('test-results-parser').ITestSuite} suite
|
|
88
|
+
* @returns {string}
|
|
89
|
+
*/
|
|
90
|
+
#getSuiteResultsText(suite) {
|
|
91
|
+
const suite_results = this.getSuiteResults(suite);
|
|
92
|
+
return `${this.platform.bold('Results')}: ${suite_results}`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
*
|
|
97
|
+
* @param {import('test-results-parser').ITestSuite} suite
|
|
98
|
+
* @returns {string}
|
|
99
|
+
*/
|
|
100
|
+
getSuiteResults(suite) {
|
|
101
|
+
return `${suite.passed} / ${suite.total} Passed (${getPercentage(suite.passed, suite.total)}%)`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
*
|
|
106
|
+
* @param {import('..').ITarget} target
|
|
107
|
+
* @param {import('test-results-parser').ITestSuite} suite
|
|
108
|
+
*/
|
|
109
|
+
#getSuiteDurationText(target, suite) {
|
|
110
|
+
const duration = this.getSuiteDuration(target, suite);
|
|
111
|
+
return `${this.platform.bold('Duration')}: ${duration}`
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
*
|
|
116
|
+
* @param {import('..').ITarget} target
|
|
117
|
+
* @param {import('test-results-parser').ITestSuite} suite
|
|
118
|
+
*/
|
|
119
|
+
getSuiteDuration(target, suite) {
|
|
120
|
+
return getPrettyDuration(suite.duration, target.inputs.duration);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
*
|
|
125
|
+
* @param {import('test-results-parser').ITestSuite} suite
|
|
126
|
+
* @returns {string}
|
|
127
|
+
*/
|
|
128
|
+
getSuiteMetaDataText(suite) {
|
|
129
|
+
if (!suite || !suite.metadata) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const texts = [];
|
|
134
|
+
|
|
135
|
+
// webdriver io
|
|
136
|
+
if (suite.metadata.device && typeof suite.metadata.device === 'string') {
|
|
137
|
+
texts.push(`${suite.metadata.device}`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (suite.metadata.platform && suite.metadata.platform.name && suite.metadata.platform.version) {
|
|
141
|
+
texts.push(`${suite.metadata.platform.name} ${suite.metadata.platform.version}`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (suite.metadata.browser && suite.metadata.browser.name && suite.metadata.browser.version) {
|
|
145
|
+
texts.push(`${suite.metadata.browser.name} ${suite.metadata.browser.version}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// playwright
|
|
149
|
+
if (suite.metadata.hostname && typeof suite.metadata.hostname === 'string') {
|
|
150
|
+
texts.push(`${suite.metadata.hostname}`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return texts.join(' • ');
|
|
154
|
+
}
|
|
155
|
+
|
|
43
156
|
}
|
|
44
157
|
|
|
45
|
-
|
|
158
|
+
module.exports = { BaseTarget };
|
package/src/targets/chat.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
const request = require('phin-retry');
|
|
2
2
|
const { getTitleText, getResultText, truncate, getPrettyDuration } = require('../helpers/helper');
|
|
3
3
|
const extension_manager = require('../extensions');
|
|
4
|
-
const { HOOK, STATUS
|
|
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
7
|
const logger = require('../utils/logger');
|
|
8
|
-
const {
|
|
8
|
+
const { BaseTarget } = require('./base.target');
|
|
9
9
|
|
|
10
10
|
async function run({ result, target }) {
|
|
11
11
|
setTargetInputs(target);
|
|
@@ -101,8 +101,9 @@ function setSuiteBlock({ result, target, payload }) {
|
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
function getSuiteSummary({ target, suite }) {
|
|
104
|
-
const
|
|
105
|
-
const
|
|
104
|
+
const tg = new ChatTarget({ target });
|
|
105
|
+
// const platform = getPlatform(TARGET.CHAT);
|
|
106
|
+
const text = tg.getSuiteSummaryText(target, suite);
|
|
106
107
|
return text;
|
|
107
108
|
}
|
|
108
109
|
|
|
@@ -260,6 +261,12 @@ async function handleErrors({ target, errors }) {
|
|
|
260
261
|
});
|
|
261
262
|
}
|
|
262
263
|
|
|
264
|
+
class ChatTarget extends BaseTarget {
|
|
265
|
+
constructor({ target }) {
|
|
266
|
+
super({ target });
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
263
270
|
module.exports = {
|
|
264
271
|
run,
|
|
265
272
|
handleErrors,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const { BaseTarget } = require('./base.target');
|
|
2
2
|
const path = require('path');
|
|
3
|
+
const ctx = require('../utils/context.utils');
|
|
3
4
|
|
|
4
5
|
const DEFAULT_INPUTS = {};
|
|
5
6
|
|
|
@@ -18,9 +19,9 @@ class CustomTarget extends BaseTarget {
|
|
|
18
19
|
if (typeof this.inputs.load === 'string') {
|
|
19
20
|
const cwd = process.cwd();
|
|
20
21
|
const target_runner = require(path.join(cwd, this.inputs.load));
|
|
21
|
-
await target_runner.run({ target: this.target, result });
|
|
22
|
+
await target_runner.run({ target: this.target, result, ctx });
|
|
22
23
|
} else if (typeof this.inputs.load === 'function') {
|
|
23
|
-
await this.inputs.load({ target: this.target, result });
|
|
24
|
+
await this.inputs.load({ target: this.target, result, ctx });
|
|
24
25
|
} else {
|
|
25
26
|
throw `Invalid 'load' input in custom target - ${this.inputs.load}`;
|
|
26
27
|
}
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
const request = require('phin-retry');
|
|
2
|
+
const { getPercentage, truncate, getPrettyDuration } = require('../helpers/helper');
|
|
3
|
+
const extension_manager = require('../extensions');
|
|
4
|
+
const { HOOK, STATUS } = require('../helpers/constants');
|
|
5
|
+
const logger = require('../utils/logger');
|
|
6
|
+
|
|
7
|
+
const PerformanceTestResult = require('performance-results-parser/src/models/PerformanceTestResult');
|
|
8
|
+
const { getValidMetrics, getMetricValuesText } = require('../helpers/performance');
|
|
9
|
+
const { BaseTarget } = require('./base.target');
|
|
10
|
+
|
|
11
|
+
const STATUSES = {
|
|
12
|
+
GOOD: '✅',
|
|
13
|
+
WARNING: '⚠️',
|
|
14
|
+
DANGER: '❌'
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const DEFAULT_INPUTS = {
|
|
18
|
+
token: undefined,
|
|
19
|
+
comment_title: undefined,
|
|
20
|
+
update_comment: false,
|
|
21
|
+
owner: undefined,
|
|
22
|
+
repo: undefined,
|
|
23
|
+
pull_number: undefined,
|
|
24
|
+
title: undefined,
|
|
25
|
+
title_suffix: undefined,
|
|
26
|
+
title_link: undefined,
|
|
27
|
+
include_suites: true,
|
|
28
|
+
include_failure_details: false,
|
|
29
|
+
only_failures: false,
|
|
30
|
+
max_suites: 10,
|
|
31
|
+
duration: 'long',
|
|
32
|
+
publish: 'test-summary'
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const default_options = {
|
|
36
|
+
condition: STATUS.PASS_OR_FAIL
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
class GitHubTarget extends BaseTarget {
|
|
40
|
+
constructor({ target }) {
|
|
41
|
+
super({ target });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async run({ result, target }) {
|
|
45
|
+
this.result = result;
|
|
46
|
+
this.setTargetInputs(target);
|
|
47
|
+
const payload = this.getMainPayload();
|
|
48
|
+
if (result instanceof PerformanceTestResult) {
|
|
49
|
+
await this.setPerformancePayload({ result, target, payload });
|
|
50
|
+
} else {
|
|
51
|
+
await this.setFunctionalPayload({ result, target, payload });
|
|
52
|
+
}
|
|
53
|
+
const message = this.getMarkdownMessage({ result, target, payload });
|
|
54
|
+
logger.info(`🔔 Publishing results to GitHub PR...`);
|
|
55
|
+
return await this.publishToGitHub({ target, message });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async setFunctionalPayload({ result, target, payload }) {
|
|
59
|
+
await extension_manager.run({ result, target, payload, hook: HOOK.START });
|
|
60
|
+
this.setMainContent({ result, target, payload });
|
|
61
|
+
await extension_manager.run({ result, target, payload, hook: HOOK.AFTER_SUMMARY });
|
|
62
|
+
this.setSuiteContent({ result, target, payload });
|
|
63
|
+
await extension_manager.run({ result, target, payload, hook: HOOK.END });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
setTargetInputs(target) {
|
|
67
|
+
target.inputs = Object.assign({}, DEFAULT_INPUTS, target.inputs);
|
|
68
|
+
if (target.inputs.publish === 'test-summary-slim') {
|
|
69
|
+
target.inputs.include_suites = false;
|
|
70
|
+
}
|
|
71
|
+
if (target.inputs.publish === 'failure-details') {
|
|
72
|
+
target.inputs.include_failure_details = true;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
getMainPayload() {
|
|
77
|
+
return {
|
|
78
|
+
content: []
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
setMainContent({ result, target, payload }) {
|
|
83
|
+
const titleText = this.getTitleText(result, target);
|
|
84
|
+
const resultText = this.getResultText(result);
|
|
85
|
+
const durationText = getPrettyDuration(result.duration, target.inputs.duration);
|
|
86
|
+
|
|
87
|
+
let content = `## ${titleText}\n\n`;
|
|
88
|
+
content += `**Results**: ${resultText}\n`;
|
|
89
|
+
content += `**Duration**: ${durationText}\n\n`;
|
|
90
|
+
|
|
91
|
+
payload.content.push(content);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
getTitleText(result, target) {
|
|
95
|
+
let text = target.inputs.title ? target.inputs.title : result.name;
|
|
96
|
+
if (target.inputs.title_suffix) {
|
|
97
|
+
text = `${text} ${target.inputs.title_suffix}`;
|
|
98
|
+
}
|
|
99
|
+
if (target.inputs.title_link) {
|
|
100
|
+
text = `[${text}](${target.inputs.title_link})`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const status = result.status !== 'PASS' ? STATUSES.DANGER : STATUSES.GOOD;
|
|
104
|
+
return `${status} ${text}`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
getResultText(result) {
|
|
108
|
+
const percentage = getPercentage(result.passed, result.total);
|
|
109
|
+
return `${result.passed} / ${result.total} Passed (${percentage}%)`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
setSuiteContent({ result, target, payload }) {
|
|
113
|
+
let suite_count = 0;
|
|
114
|
+
if (target.inputs.include_suites) {
|
|
115
|
+
for (let i = 0; i < result.suites.length && suite_count < target.inputs.max_suites; i++) {
|
|
116
|
+
const suite = result.suites[i];
|
|
117
|
+
if (target.inputs.only_failures && suite.status !== 'FAIL') {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// if suites length eq to 1 then main content will include suite summary
|
|
122
|
+
if (result.suites.length > 1) {
|
|
123
|
+
payload.content.push(this.getSuiteSummary({ target, suite }));
|
|
124
|
+
suite_count += 1;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (target.inputs.include_failure_details) {
|
|
128
|
+
// Only attach failure details if there were failures
|
|
129
|
+
if (suite.failed > 0) {
|
|
130
|
+
payload.content.push(this.getFailureDetails(suite));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
getSuiteSummary({ target, suite }) {
|
|
138
|
+
const text = this.getSuiteSummaryText(target, suite);
|
|
139
|
+
return `### ${suite.name}\n${text}\n\n`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
getFailureDetails(suite) {
|
|
143
|
+
let content = `<details>\n<summary>❌ Failed Tests</summary>\n\n`;
|
|
144
|
+
const cases = suite.cases;
|
|
145
|
+
for (let i = 0; i < cases.length; i++) {
|
|
146
|
+
const test_case = cases[i];
|
|
147
|
+
if (test_case.status === 'FAIL') {
|
|
148
|
+
content += `**Test**: ${test_case.name}\n`;
|
|
149
|
+
content += `**Error**: \n\`\`\`\n${truncate(test_case.failure ?? 'N/A', 500)}\n\`\`\`\n\n`;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
content += `</details>\n\n`;
|
|
153
|
+
return content;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
getMarkdownMessage({ result, target, payload }) {
|
|
157
|
+
return payload.content.join('');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async publishToGitHub({ target, message }) {
|
|
161
|
+
const { url, repo, owner, pull_number } = this.extractGitHubInfo(target);
|
|
162
|
+
const token = target.inputs.token || process.env.GITHUB_TOKEN;
|
|
163
|
+
|
|
164
|
+
if (!token) {
|
|
165
|
+
throw new Error('GitHub token is required. Set GITHUB_TOKEN environment variable or provide token in target inputs.');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (!pull_number) {
|
|
169
|
+
throw new Error('Pull request number not found. This target only works in GitHub Actions triggered by pull requests.');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const comment_body = target.inputs.comment_title ?
|
|
173
|
+
`${target.inputs.comment_title}\n\n${message}` :
|
|
174
|
+
message;
|
|
175
|
+
|
|
176
|
+
const headers = {
|
|
177
|
+
'Authorization': `token ${token}`,
|
|
178
|
+
'Accept': 'application/vnd.github.v3+json',
|
|
179
|
+
'User-Agent': 'testbeats'
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
if (target.inputs.update_comment) {
|
|
183
|
+
// Try to find existing comment and update it
|
|
184
|
+
const existingComment = await findExistingComment({ owner, repo, pull_number, token, comment_title: target.inputs.comment_title });
|
|
185
|
+
if (existingComment) {
|
|
186
|
+
return request.patch({
|
|
187
|
+
url: `${url}/repos/${owner}/${repo}/issues/comments/${existingComment.id}`,
|
|
188
|
+
headers,
|
|
189
|
+
body: { body: comment_body }
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Create new comment
|
|
195
|
+
return request.post({
|
|
196
|
+
url: `${url}/repos/${owner}/${repo}/issues/${pull_number}/comments`,
|
|
197
|
+
headers,
|
|
198
|
+
body: { body: comment_body }
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async findExistingComment({ owner, repo, pull_number, token, comment_title }) {
|
|
203
|
+
if (!comment_title) return null;
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
const url = `https://api.github.com/repos/${owner}/${repo}/issues/${pull_number}/comments`;
|
|
207
|
+
const headers = {
|
|
208
|
+
'Authorization': `token ${token}`,
|
|
209
|
+
'Accept': 'application/vnd.github.v3+json',
|
|
210
|
+
'User-Agent': 'testbeats'
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const response = await request.get({ url, headers });
|
|
214
|
+
const comments = JSON.parse(response.body);
|
|
215
|
+
|
|
216
|
+
return comments.find(comment => comment.body.includes(comment_title));
|
|
217
|
+
} catch (error) {
|
|
218
|
+
logger.warn('Failed to find existing comment:', error.message);
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
extractGitHubInfo(target) {
|
|
224
|
+
let url = target.inputs.url;
|
|
225
|
+
let owner = target.inputs.owner;
|
|
226
|
+
let repo = target.inputs.repo;
|
|
227
|
+
let pull_number = target.inputs.pull_number;
|
|
228
|
+
|
|
229
|
+
if (!owner || !repo) {
|
|
230
|
+
const repository = process.env.GITHUB_REPOSITORY;
|
|
231
|
+
const ref = process.env.GITHUB_REF;
|
|
232
|
+
[owner, repo] = repository.split('/');
|
|
233
|
+
if (ref && ref.includes('refs/pull/')) {
|
|
234
|
+
pull_number = ref.replace('refs/pull/', '').replace('/merge', '');
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (!url) {
|
|
239
|
+
url = `https://api.github.com`;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
url,
|
|
244
|
+
owner,
|
|
245
|
+
repo,
|
|
246
|
+
pull_number
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async setPerformancePayload({ result, target, payload }) {
|
|
251
|
+
await extension_manager.run({ result, target, payload, hook: HOOK.START });
|
|
252
|
+
await this.setPerformanceMainContent({ result, target, payload });
|
|
253
|
+
await extension_manager.run({ result, target, payload, hook: HOOK.AFTER_SUMMARY });
|
|
254
|
+
await this.setTransactionContent({ result, target, payload });
|
|
255
|
+
await extension_manager.run({ result, target, payload, hook: HOOK.END });
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async setPerformanceMainContent({ result, target, payload }) {
|
|
259
|
+
const titleText = this.getTitleText(result, target);
|
|
260
|
+
let content = `## ${titleText}\n\n`;
|
|
261
|
+
|
|
262
|
+
const metrics = getValidMetrics(result.metrics);
|
|
263
|
+
if (metrics.length > 0) {
|
|
264
|
+
content += `**Performance Metrics**:\n`;
|
|
265
|
+
content += getMetricValuesText(metrics);
|
|
266
|
+
content += '\n\n';
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
content += `**Duration**: ${getPrettyDuration(result.duration, target.inputs.duration)}\n\n`;
|
|
270
|
+
payload.content.push(content);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async setTransactionContent({ result, target, payload }) {
|
|
274
|
+
let transaction_count = 0;
|
|
275
|
+
if (target.inputs.include_suites) {
|
|
276
|
+
for (let i = 0; i < result.transactions.length && transaction_count < target.inputs.max_suites; i++) {
|
|
277
|
+
const transaction = result.transactions[i];
|
|
278
|
+
if (target.inputs.only_failures && transaction.status !== 'FAIL') {
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
payload.content.push(getTransactionSummary({ target, transaction }));
|
|
283
|
+
transaction_count += 1;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
getTransactionSummary({ target, transaction }) {
|
|
289
|
+
let content = `### ${transaction.name}\n`;
|
|
290
|
+
content += `**Status**: ${transaction.status === 'PASS' ? STATUSES.GOOD : STATUSES.DANGER}\n`;
|
|
291
|
+
content += `**Duration**: ${getPrettyDuration(transaction.duration, target.inputs.duration)}\n`;
|
|
292
|
+
|
|
293
|
+
if (transaction.metrics && transaction.metrics.length > 0) {
|
|
294
|
+
const metrics = getValidMetrics(transaction.metrics);
|
|
295
|
+
if (metrics.length > 0) {
|
|
296
|
+
content += `**Metrics**: ${getMetricValuesText(metrics)}\n`;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
content += '\n';
|
|
301
|
+
return content;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async handleErrors({ target, errors }) {
|
|
305
|
+
logger.error('GitHub target errors:', errors);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
module.exports = {
|
|
310
|
+
// name: 'GitHub',
|
|
311
|
+
// run,
|
|
312
|
+
// handleErrors,
|
|
313
|
+
// default_inputs,
|
|
314
|
+
// default_options,
|
|
315
|
+
GitHubTarget
|
|
316
|
+
};
|
package/src/targets/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const teams = require('./teams');
|
|
2
2
|
const slack = require('./slack');
|
|
3
3
|
const chat = require('./chat');
|
|
4
|
-
const
|
|
4
|
+
const { GitHubTarget} = require('./github.target');
|
|
5
5
|
const { CustomTarget } = require('./custom.target');
|
|
6
6
|
const { DelayTarget } = require('./delay.target');
|
|
7
7
|
const { HttpTarget } = require('./http.target');
|
|
@@ -18,7 +18,7 @@ function getTargetRunner(target) {
|
|
|
18
18
|
case TARGET.CHAT:
|
|
19
19
|
return chat;
|
|
20
20
|
case TARGET.GITHUB:
|
|
21
|
-
return
|
|
21
|
+
return new GitHubTarget({ target });
|
|
22
22
|
case TARGET.CUSTOM:
|
|
23
23
|
return new CustomTarget({ target });
|
|
24
24
|
case TARGET.DELAY:
|
package/src/targets/slack.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
const request = require('phin-retry');
|
|
2
2
|
const { getPercentage, truncate, getPrettyDuration } = require('../helpers/helper');
|
|
3
3
|
const extension_manager = require('../extensions');
|
|
4
|
-
const { HOOK, STATUS
|
|
4
|
+
const { HOOK, STATUS } = require('../helpers/constants');
|
|
5
5
|
const logger = require('../utils/logger');
|
|
6
|
+
const ctx = require('../utils/context.utils');
|
|
6
7
|
|
|
7
8
|
const PerformanceTestResult = require('performance-results-parser/src/models/PerformanceTestResult');
|
|
8
9
|
const { getValidMetrics, getMetricValuesText } = require('../helpers/performance');
|
|
9
10
|
const TestResult = require('test-results-parser/src/models/TestResult');
|
|
10
|
-
const {
|
|
11
|
+
const { BaseTarget } = require('./base.target');
|
|
12
|
+
|
|
11
13
|
|
|
12
14
|
const SLACK_BASE_URL = 'https://slack.com';
|
|
13
15
|
|
|
@@ -33,7 +35,7 @@ async function run({ result, target }) {
|
|
|
33
35
|
}
|
|
34
36
|
const message = getRootPayload({ result, target, payload });
|
|
35
37
|
logger.info(`🔔 Publishing results to Slack...`);
|
|
36
|
-
return publish({
|
|
38
|
+
return publish({ target, message });
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
async function setFunctionalPayload({ result, target, payload }) {
|
|
@@ -73,7 +75,7 @@ function setMainBlock({ result, target, payload }) {
|
|
|
73
75
|
});
|
|
74
76
|
}
|
|
75
77
|
|
|
76
|
-
function getTitleText(result, target, {allowTitleLink = true} = {}) {
|
|
78
|
+
function getTitleText(result, target, { allowTitleLink = true } = {}) {
|
|
77
79
|
let text = target.inputs.title ? target.inputs.title : result.name;
|
|
78
80
|
if (target.inputs.title_suffix) {
|
|
79
81
|
text = `${text} ${target.inputs.title_suffix}`;
|
|
@@ -121,8 +123,8 @@ function setSuiteBlock({ result, target, payload }) {
|
|
|
121
123
|
}
|
|
122
124
|
|
|
123
125
|
function getSuiteSummary({ target, suite }) {
|
|
124
|
-
const
|
|
125
|
-
const text =
|
|
126
|
+
const tg = new SlackTarget({ target });
|
|
127
|
+
const text = tg.getSuiteSummaryText(target, suite);
|
|
126
128
|
return {
|
|
127
129
|
"type": "section",
|
|
128
130
|
"text": {
|
|
@@ -172,7 +174,7 @@ function getRootPayload({ result, target, payload }) {
|
|
|
172
174
|
}
|
|
173
175
|
}
|
|
174
176
|
|
|
175
|
-
const fallback_text = `${getTitleText(result, target, {allowTitleLink: false})}\nResults: ${getResultText(result)}`;
|
|
177
|
+
const fallback_text = `${getTitleText(result, target, { allowTitleLink: false })}\nResults: ${getResultText(result)}`;
|
|
176
178
|
|
|
177
179
|
if (target.inputs.message_format === 'blocks') {
|
|
178
180
|
return {
|
|
@@ -324,18 +326,29 @@ async function handleErrors({ target, errors }) {
|
|
|
324
326
|
});
|
|
325
327
|
}
|
|
326
328
|
|
|
327
|
-
async function publish({
|
|
328
|
-
const { url, token, channels } = inputs;
|
|
329
|
+
async function publish({ target, message }) {
|
|
330
|
+
const { url, token, channels } = target.inputs;
|
|
329
331
|
if (token) {
|
|
330
|
-
for (
|
|
331
|
-
message.channel =
|
|
332
|
-
|
|
332
|
+
for (const channel of channels) {
|
|
333
|
+
message.channel = channel;
|
|
334
|
+
const response = await request.post({
|
|
333
335
|
url: url ? url : `${SLACK_BASE_URL}/api/chat.postMessage`,
|
|
334
336
|
headers: {
|
|
337
|
+
'Content-Type': 'application/json',
|
|
335
338
|
'Authorization': `Bearer ${token}`
|
|
336
339
|
},
|
|
337
340
|
body: message
|
|
338
341
|
});
|
|
342
|
+
ctx.stores.push({
|
|
343
|
+
target,
|
|
344
|
+
response,
|
|
345
|
+
});
|
|
346
|
+
if (response && response.ok) {
|
|
347
|
+
logger.info(`✔ Published to Slack channel - ${channel}`);
|
|
348
|
+
} else {
|
|
349
|
+
logger.error(`✖ Failed to publish to Slack channel - ${channel}`);
|
|
350
|
+
logger.error(response);
|
|
351
|
+
}
|
|
339
352
|
}
|
|
340
353
|
|
|
341
354
|
} else {
|
|
@@ -346,6 +359,13 @@ async function publish({ inputs, message}) {
|
|
|
346
359
|
}
|
|
347
360
|
}
|
|
348
361
|
|
|
362
|
+
|
|
363
|
+
class SlackTarget extends BaseTarget {
|
|
364
|
+
constructor({ target }) {
|
|
365
|
+
super({ target });
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
349
369
|
module.exports = {
|
|
350
370
|
run,
|
|
351
371
|
handleErrors,
|
package/src/targets/teams.js
CHANGED
|
@@ -2,12 +2,12 @@ const request = require('phin-retry');
|
|
|
2
2
|
const { getPercentage, truncate, getPrettyDuration } = require('../helpers/helper');
|
|
3
3
|
const { getValidMetrics, getMetricValuesText } = require('../helpers/performance');
|
|
4
4
|
const extension_manager = require('../extensions');
|
|
5
|
-
const { HOOK, STATUS
|
|
5
|
+
const { HOOK, STATUS } = require('../helpers/constants');
|
|
6
6
|
const logger = require('../utils/logger');
|
|
7
7
|
|
|
8
8
|
const TestResult = require('test-results-parser/src/models/TestResult');
|
|
9
9
|
const PerformanceTestResult = require('performance-results-parser/src/models/PerformanceTestResult');
|
|
10
|
-
const {
|
|
10
|
+
const { BaseTarget } = require('./base.target');
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* @param {object} param0
|
|
@@ -145,11 +145,11 @@ function setSuiteBlock({ result, target, payload }) {
|
|
|
145
145
|
}
|
|
146
146
|
|
|
147
147
|
function getSuiteSummary({ suite, target }) {
|
|
148
|
-
|
|
149
|
-
const platform = getPlatform(TARGET.TEAMS);
|
|
150
|
-
const suite_title =
|
|
151
|
-
const suite_results =
|
|
152
|
-
const duration =
|
|
148
|
+
const tg = new TeamsTarget({ target });
|
|
149
|
+
// const platform = getPlatform(TARGET.TEAMS);
|
|
150
|
+
const suite_title = tg.getSuiteTitle(suite);
|
|
151
|
+
const suite_results = tg.getSuiteResults(suite);
|
|
152
|
+
const duration = tg.getSuiteDuration(target, suite);
|
|
153
153
|
|
|
154
154
|
const blocks = [
|
|
155
155
|
{
|
|
@@ -174,7 +174,7 @@ function getSuiteSummary({ suite, target }) {
|
|
|
174
174
|
}
|
|
175
175
|
];
|
|
176
176
|
|
|
177
|
-
const suite_metadata_text =
|
|
177
|
+
const suite_metadata_text = tg.getSuiteMetaDataText(suite);
|
|
178
178
|
if (suite_metadata_text) {
|
|
179
179
|
blocks.push({
|
|
180
180
|
"type": "TextBlock",
|
|
@@ -348,6 +348,12 @@ async function handleErrors({ target, errors }) {
|
|
|
348
348
|
});
|
|
349
349
|
}
|
|
350
350
|
|
|
351
|
+
class TeamsTarget extends BaseTarget {
|
|
352
|
+
constructor({ target }) {
|
|
353
|
+
super({ target });
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
351
357
|
module.exports = {
|
|
352
358
|
run,
|
|
353
359
|
handleErrors,
|
package/src/targets/github.js
DELETED
|
@@ -1,310 +0,0 @@
|
|
|
1
|
-
const request = require('phin-retry');
|
|
2
|
-
const { getPercentage, truncate, getPrettyDuration } = require('../helpers/helper');
|
|
3
|
-
const extension_manager = require('../extensions');
|
|
4
|
-
const { HOOK, STATUS, TARGET } = require('../helpers/constants');
|
|
5
|
-
const logger = require('../utils/logger');
|
|
6
|
-
|
|
7
|
-
const PerformanceTestResult = require('performance-results-parser/src/models/PerformanceTestResult');
|
|
8
|
-
const { getValidMetrics, getMetricValuesText } = require('../helpers/performance');
|
|
9
|
-
const TestResult = require('test-results-parser/src/models/TestResult');
|
|
10
|
-
const { getPlatform } = require('../platforms');
|
|
11
|
-
|
|
12
|
-
const STATUSES = {
|
|
13
|
-
GOOD: '✅',
|
|
14
|
-
WARNING: '⚠️',
|
|
15
|
-
DANGER: '❌'
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
async function run({ result, target }) {
|
|
19
|
-
setTargetInputs(target);
|
|
20
|
-
const payload = getMainPayload();
|
|
21
|
-
if (result instanceof PerformanceTestResult) {
|
|
22
|
-
await setPerformancePayload({ result, target, payload });
|
|
23
|
-
} else {
|
|
24
|
-
await setFunctionalPayload({ result, target, payload });
|
|
25
|
-
}
|
|
26
|
-
const message = getMarkdownMessage({ result, target, payload });
|
|
27
|
-
logger.info(`🔔 Publishing results to GitHub PR...`);
|
|
28
|
-
return await publishToGitHub({ target, message });
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
async function setFunctionalPayload({ result, target, payload }) {
|
|
32
|
-
await extension_manager.run({ result, target, payload, hook: HOOK.START });
|
|
33
|
-
setMainContent({ result, target, payload });
|
|
34
|
-
await extension_manager.run({ result, target, payload, hook: HOOK.AFTER_SUMMARY });
|
|
35
|
-
setSuiteContent({ result, target, payload });
|
|
36
|
-
await extension_manager.run({ result, target, payload, hook: HOOK.END });
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function setTargetInputs(target) {
|
|
40
|
-
target.inputs = Object.assign({}, default_inputs, target.inputs);
|
|
41
|
-
if (target.inputs.publish === 'test-summary-slim') {
|
|
42
|
-
target.inputs.include_suites = false;
|
|
43
|
-
}
|
|
44
|
-
if (target.inputs.publish === 'failure-details') {
|
|
45
|
-
target.inputs.include_failure_details = true;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function getMainPayload() {
|
|
50
|
-
return {
|
|
51
|
-
content: []
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function setMainContent({ result, target, payload }) {
|
|
56
|
-
const titleText = getTitleText(result, target);
|
|
57
|
-
const resultText = getResultText(result);
|
|
58
|
-
const durationText = getPrettyDuration(result.duration, target.inputs.duration);
|
|
59
|
-
|
|
60
|
-
let content = `## ${titleText}\n\n`;
|
|
61
|
-
content += `**Results**: ${resultText}\n`;
|
|
62
|
-
content += `**Duration**: ${durationText}\n\n`;
|
|
63
|
-
|
|
64
|
-
payload.content.push(content);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function getTitleText(result, target) {
|
|
68
|
-
let text = target.inputs.title ? target.inputs.title : result.name;
|
|
69
|
-
if (target.inputs.title_suffix) {
|
|
70
|
-
text = `${text} ${target.inputs.title_suffix}`;
|
|
71
|
-
}
|
|
72
|
-
if (target.inputs.title_link) {
|
|
73
|
-
text = `[${text}](${target.inputs.title_link})`;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const status = result.status !== 'PASS' ? STATUSES.DANGER : STATUSES.GOOD;
|
|
77
|
-
return `${status} ${text}`;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function getResultText(result) {
|
|
81
|
-
const percentage = getPercentage(result.passed, result.total);
|
|
82
|
-
return `${result.passed} / ${result.total} Passed (${percentage}%)`;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function setSuiteContent({ result, target, payload }) {
|
|
86
|
-
let suite_count = 0;
|
|
87
|
-
if (target.inputs.include_suites) {
|
|
88
|
-
for (let i = 0; i < result.suites.length && suite_count < target.inputs.max_suites; i++) {
|
|
89
|
-
const suite = result.suites[i];
|
|
90
|
-
if (target.inputs.only_failures && suite.status !== 'FAIL') {
|
|
91
|
-
continue;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// if suites length eq to 1 then main content will include suite summary
|
|
95
|
-
if (result.suites.length > 1) {
|
|
96
|
-
payload.content.push(getSuiteSummary({ target, suite }));
|
|
97
|
-
suite_count += 1;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if (target.inputs.include_failure_details) {
|
|
101
|
-
// Only attach failure details if there were failures
|
|
102
|
-
if (suite.failed > 0) {
|
|
103
|
-
payload.content.push(getFailureDetails(suite));
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function getSuiteSummary({ target, suite }) {
|
|
111
|
-
const platform = getPlatform(TARGET.GITHUB);
|
|
112
|
-
const text = platform.getSuiteSummaryText(target, suite);
|
|
113
|
-
return `### ${suite.name}\n${text}\n\n`;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function getFailureDetails(suite) {
|
|
117
|
-
let content = `<details>\n<summary>❌ Failed Tests</summary>\n\n`;
|
|
118
|
-
const cases = suite.cases;
|
|
119
|
-
for (let i = 0; i < cases.length; i++) {
|
|
120
|
-
const test_case = cases[i];
|
|
121
|
-
if (test_case.status === 'FAIL') {
|
|
122
|
-
content += `**Test**: ${test_case.name}\n`;
|
|
123
|
-
content += `**Error**: \n\`\`\`\n${truncate(test_case.failure ?? 'N/A', 500)}\n\`\`\`\n\n`;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
content += `</details>\n\n`;
|
|
127
|
-
return content;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function getMarkdownMessage({ result, target, payload }) {
|
|
131
|
-
return payload.content.join('');
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
async function publishToGitHub({ target, message }) {
|
|
135
|
-
const { url, repo, owner, pull_number } = extractGitHubInfo(target);
|
|
136
|
-
const token = target.inputs.token || process.env.GITHUB_TOKEN;
|
|
137
|
-
|
|
138
|
-
if (!token) {
|
|
139
|
-
throw new Error('GitHub token is required. Set GITHUB_TOKEN environment variable or provide token in target inputs.');
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
if (!pull_number) {
|
|
143
|
-
throw new Error('Pull request number not found. This target only works in GitHub Actions triggered by pull requests.');
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
const comment_body = target.inputs.comment_title ?
|
|
147
|
-
`${target.inputs.comment_title}\n\n${message}` :
|
|
148
|
-
message;
|
|
149
|
-
|
|
150
|
-
const headers = {
|
|
151
|
-
'Authorization': `token ${token}`,
|
|
152
|
-
'Accept': 'application/vnd.github.v3+json',
|
|
153
|
-
'User-Agent': 'testbeats'
|
|
154
|
-
};
|
|
155
|
-
|
|
156
|
-
if (target.inputs.update_comment) {
|
|
157
|
-
// Try to find existing comment and update it
|
|
158
|
-
const existingComment = await findExistingComment({ owner, repo, pull_number, token, comment_title: target.inputs.comment_title });
|
|
159
|
-
if (existingComment) {
|
|
160
|
-
return request.patch({
|
|
161
|
-
url: `${url}/repos/${owner}/${repo}/issues/comments/${existingComment.id}`,
|
|
162
|
-
headers,
|
|
163
|
-
body: { body: comment_body }
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// Create new comment
|
|
169
|
-
return request.post({
|
|
170
|
-
url: `${url}/repos/${owner}/${repo}/issues/${pull_number}/comments`,
|
|
171
|
-
headers,
|
|
172
|
-
body: { body: comment_body }
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
async function findExistingComment({ owner, repo, pull_number, token, comment_title }) {
|
|
177
|
-
if (!comment_title) return null;
|
|
178
|
-
|
|
179
|
-
try {
|
|
180
|
-
const url = `https://api.github.com/repos/${owner}/${repo}/issues/${pull_number}/comments`;
|
|
181
|
-
const headers = {
|
|
182
|
-
'Authorization': `token ${token}`,
|
|
183
|
-
'Accept': 'application/vnd.github.v3+json',
|
|
184
|
-
'User-Agent': 'testbeats'
|
|
185
|
-
};
|
|
186
|
-
|
|
187
|
-
const response = await request.get({ url, headers });
|
|
188
|
-
const comments = JSON.parse(response.body);
|
|
189
|
-
|
|
190
|
-
return comments.find(comment => comment.body.includes(comment_title));
|
|
191
|
-
} catch (error) {
|
|
192
|
-
logger.warn('Failed to find existing comment:', error.message);
|
|
193
|
-
return null;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
function extractGitHubInfo(target) {
|
|
198
|
-
let url = target.inputs.url;
|
|
199
|
-
let owner = target.inputs.owner;
|
|
200
|
-
let repo = target.inputs.repo;
|
|
201
|
-
let pull_number = target.inputs.pull_number;
|
|
202
|
-
|
|
203
|
-
if (!owner || !repo) {
|
|
204
|
-
const repository = process.env.GITHUB_REPOSITORY;
|
|
205
|
-
const ref = process.env.GITHUB_REF;
|
|
206
|
-
[owner, repo] = repository.split('/');
|
|
207
|
-
if (ref && ref.includes('refs/pull/')) {
|
|
208
|
-
pull_number = ref.replace('refs/pull/', '').replace('/merge', '');
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
if (!url) {
|
|
213
|
-
url = `https://api.github.com`;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
return {
|
|
217
|
-
url,
|
|
218
|
-
owner,
|
|
219
|
-
repo,
|
|
220
|
-
pull_number
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
async function setPerformancePayload({ result, target, payload }) {
|
|
225
|
-
await extension_manager.run({ result, target, payload, hook: HOOK.START });
|
|
226
|
-
await setPerformanceMainContent({ result, target, payload });
|
|
227
|
-
await extension_manager.run({ result, target, payload, hook: HOOK.AFTER_SUMMARY });
|
|
228
|
-
await setTransactionContent({ result, target, payload });
|
|
229
|
-
await extension_manager.run({ result, target, payload, hook: HOOK.END });
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
async function setPerformanceMainContent({ result, target, payload }) {
|
|
233
|
-
const titleText = getTitleText(result, target);
|
|
234
|
-
let content = `## ${titleText}\n\n`;
|
|
235
|
-
|
|
236
|
-
const metrics = getValidMetrics(result.metrics);
|
|
237
|
-
if (metrics.length > 0) {
|
|
238
|
-
content += `**Performance Metrics**:\n`;
|
|
239
|
-
content += getMetricValuesText(metrics);
|
|
240
|
-
content += '\n\n';
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
content += `**Duration**: ${getPrettyDuration(result.duration, target.inputs.duration)}\n\n`;
|
|
244
|
-
payload.content.push(content);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
async function setTransactionContent({ result, target, payload }) {
|
|
248
|
-
let transaction_count = 0;
|
|
249
|
-
if (target.inputs.include_suites) {
|
|
250
|
-
for (let i = 0; i < result.transactions.length && transaction_count < target.inputs.max_suites; i++) {
|
|
251
|
-
const transaction = result.transactions[i];
|
|
252
|
-
if (target.inputs.only_failures && transaction.status !== 'FAIL') {
|
|
253
|
-
continue;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
payload.content.push(getTransactionSummary({ target, transaction }));
|
|
257
|
-
transaction_count += 1;
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
function getTransactionSummary({ target, transaction }) {
|
|
263
|
-
let content = `### ${transaction.name}\n`;
|
|
264
|
-
content += `**Status**: ${transaction.status === 'PASS' ? STATUSES.GOOD : STATUSES.DANGER}\n`;
|
|
265
|
-
content += `**Duration**: ${getPrettyDuration(transaction.duration, target.inputs.duration)}\n`;
|
|
266
|
-
|
|
267
|
-
if (transaction.metrics && transaction.metrics.length > 0) {
|
|
268
|
-
const metrics = getValidMetrics(transaction.metrics);
|
|
269
|
-
if (metrics.length > 0) {
|
|
270
|
-
content += `**Metrics**: ${getMetricValuesText(metrics)}\n`;
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
content += '\n';
|
|
275
|
-
return content;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
async function handleErrors({ target, errors }) {
|
|
279
|
-
logger.error('GitHub target errors:', errors);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
const default_inputs = {
|
|
283
|
-
token: undefined,
|
|
284
|
-
comment_title: undefined,
|
|
285
|
-
update_comment: false,
|
|
286
|
-
owner: undefined,
|
|
287
|
-
repo: undefined,
|
|
288
|
-
pull_number: undefined,
|
|
289
|
-
title: undefined,
|
|
290
|
-
title_suffix: undefined,
|
|
291
|
-
title_link: undefined,
|
|
292
|
-
include_suites: true,
|
|
293
|
-
include_failure_details: false,
|
|
294
|
-
only_failures: false,
|
|
295
|
-
max_suites: 10,
|
|
296
|
-
duration: 'long',
|
|
297
|
-
publish: 'test-summary'
|
|
298
|
-
};
|
|
299
|
-
|
|
300
|
-
const default_options = {
|
|
301
|
-
condition: STATUS.PASS_OR_FAIL
|
|
302
|
-
};
|
|
303
|
-
|
|
304
|
-
module.exports = {
|
|
305
|
-
name: 'GitHub',
|
|
306
|
-
run,
|
|
307
|
-
handleErrors,
|
|
308
|
-
default_inputs,
|
|
309
|
-
default_options
|
|
310
|
-
};
|