testbeats 2.0.0

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.
@@ -0,0 +1,208 @@
1
+ const influx = require('influxdb-lite');
2
+ const Metric = require('performance-results-parser/src/models/Metric');
3
+ const PerformanceTestResult = require('performance-results-parser/src/models/PerformanceTestResult');
4
+ const Transaction = require('performance-results-parser/src/models/Transaction');
5
+ const TestCase = require('test-results-parser/src/models/TestCase');
6
+ const TestResult = require('test-results-parser/src/models/TestResult');
7
+ const TestSuite = require('test-results-parser/src/models/TestSuite');
8
+
9
+
10
+ const { STATUS } = require('../helpers/constants');
11
+
12
+ /**
13
+ *
14
+ * @param {object} param0
15
+ * @param {PerformanceTestResult | TestResult} param0.result
16
+ * @param {import('..').Target} param0.target
17
+ */
18
+ async function run({ result, target }) {
19
+ target.inputs = Object.assign({}, default_inputs, target.inputs);
20
+ const metrics = getMetrics({ result, target });
21
+ await influx.write(
22
+ {
23
+ url: target.inputs.url,
24
+ version: target.inputs.version,
25
+ db: target.inputs.db,
26
+ username: target.inputs.username,
27
+ password: target.inputs.password,
28
+ org: target.inputs.org,
29
+ bucket: target.inputs.bucket,
30
+ token: target.inputs.token,
31
+ },
32
+ metrics
33
+ );
34
+ }
35
+
36
+ /**
37
+ *
38
+ * @param {object} param0
39
+ * @param {PerformanceTestResult | TestResult} param0.result
40
+ * @param {import('..').Target} param0.target
41
+ */
42
+ function getMetrics({ result, target }) {
43
+ const influx_metrics = [];
44
+ if (result instanceof PerformanceTestResult) {
45
+ influx_metrics.push(getPerfRunInfluxMetric({ result, target }));
46
+
47
+ for (const transaction of result.transactions) {
48
+ influx_metrics.push(getTransactionInfluxMetric(transaction, target));
49
+ }
50
+ } else if (result instanceof TestResult) {
51
+ influx_metrics.push(getTestInfluxMetric({ result, target }, target.inputs.measurement_test_run));
52
+ for (const suite of result.suites) {
53
+ influx_metrics.push(getTestInfluxMetric({ result: suite, target }, target.inputs.measurement_test_suite));
54
+ for (const test_case of suite.cases) {
55
+ influx_metrics.push(getTestCaseInfluxMetric({ result: test_case, target }));
56
+ }
57
+ }
58
+ }
59
+ return influx_metrics;
60
+ }
61
+
62
+ /**
63
+ * @param {object} param0
64
+ * @param {PerformanceTestResult} param0.result
65
+ * @param {import('..').Target} param0.target
66
+ * @returns
67
+ */
68
+ function getPerfRunInfluxMetric({ result, target }) {
69
+ const tags = Object.assign({}, target.inputs.tags);
70
+ tags.Name = result.name;
71
+ tags.Status = result.status;
72
+
73
+ const fields = Object.assign({}, target.inputs.fields);
74
+ fields.status = result.status === 'PASS' ? 0 : 1;
75
+ fields.transactions = result.transactions.length;
76
+ fields.transactions_passed = result.transactions.filter(_transaction => _transaction.status === "PASS").length;
77
+ fields.transactions_failed = result.transactions.filter(_transaction => _transaction.status === "FAIL").length;
78
+
79
+ for (const metric of result.metrics) {
80
+ setPerfInfluxMetricFields(metric, fields);
81
+ }
82
+
83
+ return {
84
+ measurement: target.inputs.measurement_perf_run,
85
+ tags,
86
+ fields
87
+ }
88
+ }
89
+
90
+ /**
91
+ *
92
+ * @param {Metric} metric
93
+ */
94
+ function setPerfInfluxMetricFields(metric, fields) {
95
+ let name = metric.name;
96
+ name = name.toLowerCase();
97
+ name = name.replace(' ', '_');
98
+ if (metric.type === "COUNTER" || metric.type === "RATE") {
99
+ fields[`${name}_sum`] = metric.sum;
100
+ fields[`${name}_rate`] = metric.rate;
101
+ } else if (metric.type === "TREND") {
102
+ fields[`${name}_avg`] = metric.avg;
103
+ fields[`${name}_med`] = metric.med;
104
+ fields[`${name}_max`] = metric.max;
105
+ fields[`${name}_min`] = metric.min;
106
+ fields[`${name}_p90`] = metric.p90;
107
+ fields[`${name}_p95`] = metric.p95;
108
+ fields[`${name}_p99`] = metric.p99;
109
+ }
110
+ }
111
+
112
+ /**
113
+ *
114
+ * @param {Transaction} transaction
115
+ */
116
+ function getTransactionInfluxMetric(transaction, target) {
117
+ const tags = Object.assign({}, target.inputs.tags);
118
+ tags.Name = transaction.name;
119
+ tags.Status = transaction.status;
120
+
121
+ const fields = Object.assign({}, target.inputs.fields);
122
+ fields.status = transaction.status === 'PASS' ? 0 : 1;
123
+
124
+ for (const metric of transaction.metrics) {
125
+ setPerfInfluxMetricFields(metric, fields);
126
+ }
127
+
128
+ return {
129
+ measurement: target.inputs.measurement_perf_transaction,
130
+ tags,
131
+ fields
132
+ }
133
+ }
134
+
135
+ /**
136
+ * @param {object} param0
137
+ * @param {TestResult | TestSuite} param0.result
138
+ * @param {import('..').Target} param0.target
139
+ * @returns
140
+ */
141
+ function getTestInfluxMetric({ result, target }, measurement) {
142
+ const tags = Object.assign({}, target.inputs.tags);
143
+ tags.Name = result.name;
144
+ tags.Status = result.status;
145
+
146
+ const fields = Object.assign({}, target.inputs.fields);
147
+ fields.status = result.status === 'PASS' ? 0 : 1;
148
+ fields.total = result.total;
149
+ fields.passed = result.passed;
150
+ fields.failed = result.failed;
151
+ fields.duration = result.duration;
152
+
153
+ return {
154
+ measurement,
155
+ tags,
156
+ fields
157
+ }
158
+ }
159
+
160
+ /**
161
+ * @param {object} param0
162
+ * @param {TestCase} param0.result
163
+ * @param {import('..').Target} param0.target
164
+ * @returns
165
+ */
166
+ function getTestCaseInfluxMetric({ result, target }) {
167
+ const tags = Object.assign({}, target.inputs.tags);
168
+ tags.Name = result.name;
169
+ tags.Status = result.status;
170
+
171
+ const fields = Object.assign({}, target.inputs.fields);
172
+ fields.status = result.status === 'PASS' ? 0 : 1;
173
+ fields.duration = result.duration;
174
+
175
+ return {
176
+ measurement: target.inputs.measurement_test_case,
177
+ tags,
178
+ fields
179
+ }
180
+ }
181
+
182
+
183
+ const default_inputs = {
184
+ url: '',
185
+ version: 'v1',
186
+ db: '',
187
+ username: '',
188
+ password: '',
189
+ org: '',
190
+ bucket: '',
191
+ token: '',
192
+ measurement_perf_run: 'PerfRun',
193
+ measurement_perf_transaction: 'PerfTransaction',
194
+ measurement_test_run: 'TestRun',
195
+ measurement_test_suite: 'TestSuite',
196
+ measurement_test_case: 'TestCase',
197
+ tags: {},
198
+ fields: {}
199
+ }
200
+
201
+ const default_options = {
202
+ condition: STATUS.PASS_OR_FAIL
203
+ }
204
+
205
+ module.exports = {
206
+ run,
207
+ default_options
208
+ }
@@ -0,0 +1,266 @@
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
+
6
+ const PerformanceTestResult = require('performance-results-parser/src/models/PerformanceTestResult');
7
+ const { getValidMetrics, getMetricValuesText } = require('../helpers/performance');
8
+ const TestResult = require('test-results-parser/src/models/TestResult');
9
+
10
+
11
+ const COLORS = {
12
+ GOOD: '#36A64F',
13
+ WARNING: '#ECB22E',
14
+ DANGER: '#DC143C'
15
+ }
16
+
17
+ async function run({ result, target }) {
18
+ setTargetInputs(target);
19
+ const payload = getMainPayload();
20
+ if (result instanceof PerformanceTestResult) {
21
+ await setPerformancePayload({ result, target, payload });
22
+ } else {
23
+ await setFunctionalPayload({ result, target, payload });
24
+ }
25
+ const message = getRootPayload({ result, payload });
26
+ return request.post({
27
+ url: target.inputs.url,
28
+ body: message
29
+ });
30
+ }
31
+
32
+ async function setFunctionalPayload({ result, target, payload }) {
33
+ await extension_manager.run({ result, target, payload, hook: HOOK.START });
34
+ setMainBlock({ result, target, payload });
35
+ await extension_manager.run({ result, target, payload, hook: HOOK.AFTER_SUMMARY });
36
+ setSuiteBlock({ result, target, payload });
37
+ await extension_manager.run({ result, target, payload, hook: HOOK.END });
38
+ }
39
+
40
+ function setTargetInputs(target) {
41
+ target.inputs = Object.assign({}, default_inputs, target.inputs);
42
+ if (target.inputs.publish === 'test-summary-slim') {
43
+ target.inputs.include_suites = false;
44
+ }
45
+ if (target.inputs.publish === 'failure-details') {
46
+ target.inputs.include_failure_details = true;
47
+ }
48
+ }
49
+
50
+ function getMainPayload() {
51
+ return {
52
+ "blocks": []
53
+ };
54
+ }
55
+
56
+ function setMainBlock({ result, target, payload }) {
57
+ let text = `*${getTitleText(result, target)}*\n`;
58
+ text += `\n*Results*: ${getResultText(result)}`;
59
+ text += `\n*Duration*: ${getPrettyDuration(result.duration, target.inputs.duration)}`;
60
+ payload.blocks.push({
61
+ "type": "section",
62
+ "text": {
63
+ "type": "mrkdwn",
64
+ "text": text
65
+ }
66
+ });
67
+ }
68
+
69
+ function getTitleText(result, target) {
70
+ let text = target.inputs.title ? target.inputs.title : result.name;
71
+ if (target.inputs.title_suffix) {
72
+ text = `${text} ${target.inputs.title_suffix}`;
73
+ }
74
+ if (target.inputs.title_link) {
75
+ text = `<${target.inputs.title_link}|${text}>`;
76
+ }
77
+ return 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 setSuiteBlock({ result, target, payload }) {
86
+ let suite_attachments_length = 0;
87
+ if (target.inputs.include_suites) {
88
+ for (let i = 0; i < result.suites.length && suite_attachments_length < target.inputs.max_suites; i++) {
89
+ const suite = result.suites[i];
90
+ if (target.inputs.only_failures && suite.status !== 'FAIL') {
91
+ continue;
92
+ }
93
+ // if suites length eq to 1 then main block will include suite summary
94
+ if (result.suites.length > 1) {
95
+ payload.blocks.push(getSuiteSummary({ target, suite }));
96
+ suite_attachments_length += 1;
97
+ }
98
+ if (target.inputs.include_failure_details) {
99
+ // Only attach failure details block if there were failures
100
+ if (suite.failed > 0) {
101
+ payload.blocks.push(getFailureDetails(suite));
102
+ }
103
+ }
104
+ }
105
+ }
106
+ }
107
+
108
+ function getSuiteSummary({ target, suite }) {
109
+ let text = `*${getSuiteTitle(suite)}*\n`;
110
+ text += `\n*Results*: ${getResultText(suite)}`;
111
+ text += `\n*Duration*: ${getPrettyDuration(suite.duration, target.inputs.duration)}`;
112
+ return {
113
+ "type": "section",
114
+ "text": {
115
+ "type": "mrkdwn",
116
+ "text": text
117
+ }
118
+ };
119
+ }
120
+
121
+ function getSuiteTitle(suite) {
122
+ const emoji = suite.status === 'PASS' ? '✅' : suite.total === suite.skipped ? '⏭️' : '❌';
123
+ return `${emoji} ${suite.name}`;
124
+ }
125
+
126
+ function getFailureDetails(suite) {
127
+ let text = '';
128
+ const cases = suite.cases;
129
+ for (let i = 0; i < cases.length; i++) {
130
+ const test_case = cases[i];
131
+ if (test_case.status === 'FAIL') {
132
+ text += `*Test*: ${test_case.name}\n*Error*: ${truncate(test_case.failure, 150)}\n\n`;
133
+ }
134
+ }
135
+ return {
136
+ "type": "section",
137
+ "text": {
138
+ "type": "mrkdwn",
139
+ "text": text
140
+ }
141
+ }
142
+ }
143
+
144
+ /**
145
+ *
146
+ * @param {object} param0
147
+ * @param {PerformanceTestResult | TestResult} param0.result
148
+ * @returns
149
+ */
150
+ function getRootPayload({ result, payload }) {
151
+ let color = COLORS.GOOD;
152
+ if (result.status !== 'PASS') {
153
+ let somePassed = true;
154
+ if (result instanceof PerformanceTestResult) {
155
+ somePassed = result.transactions.some(suite => suite.status === 'PASS');
156
+ } else {
157
+ somePassed = result.suites.some(suite => suite.status === 'PASS');
158
+ }
159
+ if (somePassed) {
160
+ color = COLORS.WARNING;
161
+ } else {
162
+ color = COLORS.DANGER;
163
+ }
164
+ }
165
+ return {
166
+ "attachments": [
167
+ {
168
+ "color": color,
169
+ "blocks": payload.blocks
170
+ }
171
+ ]
172
+ };
173
+ }
174
+
175
+ async function setPerformancePayload({ result, target, payload }) {
176
+ await extension_manager.run({ result, target, payload, hook: HOOK.START });
177
+ await setPerformanceMainBlock({ result, target, payload });
178
+ await extension_manager.run({ result, target, payload, hook: HOOK.AFTER_SUMMARY });
179
+ await setTransactionBlock({ result, target, payload });
180
+ await extension_manager.run({ result, target, payload, hook: HOOK.END });
181
+ }
182
+
183
+ /**
184
+ *
185
+ * @param {object} param0
186
+ * @param {PerformanceTestResult} param0.result
187
+ */
188
+ async function setPerformanceMainBlock({ result, target, payload }) {
189
+ let text = `*${getTitleText(result, target)}*\n`;
190
+ result.total = result.transactions.length;
191
+ result.passed = result.transactions.filter(_transaction => _transaction.status === 'PASS').length;
192
+ text += `\n*Results*: ${getResultText(result)}`;
193
+ const valid_metrics = await getValidMetrics({ metrics: result.metrics, target, result });
194
+ for (let i = 0; i < valid_metrics.length; i++) {
195
+ const metric = valid_metrics[i];
196
+ text += `\n*${metric.name}*: ${getMetricValuesText({ metric, target, result })}`;
197
+ }
198
+ payload.blocks.push({
199
+ "type": "section",
200
+ "text": {
201
+ "type": "mrkdwn",
202
+ "text": text
203
+ }
204
+ });
205
+ }
206
+
207
+ /**
208
+ *
209
+ * @param {object} param0
210
+ * @param {PerformanceTestResult} param0.result
211
+ */
212
+ async function setTransactionBlock({ result, target, payload }) {
213
+ if (target.inputs.include_suites) {
214
+ for (let i = 0; i < result.transactions.length; i++) {
215
+ const transaction = result.transactions[i];
216
+ if (target.inputs.only_failures && transaction.status !== 'FAIL') {
217
+ continue;
218
+ }
219
+ // if transactions length eq to 1 then main block will include transaction summary
220
+ if (result.transactions.length > 1) {
221
+ let text = `*${getTitleText(transaction, target)}*\n`;
222
+ const valid_metrics = await getValidMetrics({ metrics: transaction.metrics, target, result });
223
+ for (let i = 0; i < valid_metrics.length; i++) {
224
+ const metric = valid_metrics[i];
225
+ text += `\n*${metric.name}*: ${getMetricValuesText({ metric, target, result })}`;
226
+ }
227
+ payload.blocks.push({
228
+ "type": "section",
229
+ "text": {
230
+ "type": "mrkdwn",
231
+ "text": text
232
+ }
233
+ });
234
+ }
235
+ }
236
+ }
237
+ }
238
+
239
+
240
+ const default_options = {
241
+ condition: STATUS.PASS_OR_FAIL
242
+ }
243
+
244
+ const default_inputs = {
245
+ publish: 'test-summary',
246
+ include_suites: true,
247
+ max_suites: 10,
248
+ only_failures: false,
249
+ include_failure_details: false,
250
+ duration: '',
251
+ metrics: [
252
+ {
253
+ "name": "Samples",
254
+ },
255
+ {
256
+ "name": "Duration",
257
+ "condition": "always",
258
+ "fields": ["avg", "p95"]
259
+ }
260
+ ]
261
+ }
262
+
263
+ module.exports = {
264
+ run,
265
+ default_options
266
+ }