testbeats
Version:
Publish test results to Microsoft Teams, Google Chat, Slack and InfluxDB
374 lines (338 loc) • 10.2 kB
JavaScript
const request = require('phin-retry');
const { getPercentage, truncate, getPrettyDuration } = require('../helpers/helper');
const extension_manager = require('../extensions');
const { HOOK, STATUS } = require('../helpers/constants');
const logger = require('../utils/logger');
const ctx = require('../utils/context.utils');
const PerformanceTestResult = require('performance-results-parser/src/models/PerformanceTestResult');
const { getValidMetrics, getMetricValuesText } = require('../helpers/performance');
const TestResult = require('test-results-parser/src/models/TestResult');
const { BaseTarget } = require('./base.target');
const SLACK_BASE_URL = 'https://slack.com';
const STATUSES = {
GOOD: ':white_check_mark:',
WARNING: ':warning:',
DANGER: ':x:'
}
const COLORS = {
GOOD: '#36A64F',
WARNING: '#ECB22E',
DANGER: '#DC143C'
}
async function run({ result, target }) {
setTargetInputs(target);
const payload = getMainPayload();
if (result instanceof PerformanceTestResult) {
await setPerformancePayload({ result, target, payload });
} else {
await setFunctionalPayload({ result, target, payload });
}
const message = getRootPayload({ result, target, payload });
logger.info(`🔔 Publishing results to Slack...`);
return publish({ target, message });
}
async function setFunctionalPayload({ result, target, payload }) {
await extension_manager.run({ result, target, payload, hook: HOOK.START });
setMainBlock({ result, target, payload });
await extension_manager.run({ result, target, payload, hook: HOOK.AFTER_SUMMARY });
setSuiteBlock({ result, target, payload });
await extension_manager.run({ result, target, payload, hook: HOOK.END });
}
function setTargetInputs(target) {
target.inputs = Object.assign({}, default_inputs, target.inputs);
if (target.inputs.publish === 'test-summary-slim') {
target.inputs.include_suites = false;
}
if (target.inputs.publish === 'failure-details') {
target.inputs.include_failure_details = true;
}
}
function getMainPayload() {
return {
"blocks": []
};
}
function setMainBlock({ result, target, payload }) {
let text = `*${getTitleText(result, target)}*\n`;
text += `\n*Results*: ${getResultText(result)}`;
text += `\n*Duration*: ${getPrettyDuration(result.duration, target.inputs.duration)}`;
payload.blocks.push({
"type": "section",
"text": {
"type": "mrkdwn",
"text": text
}
});
}
function getTitleText(result, target, { allowTitleLink = true } = {}) {
let text = target.inputs.title ? target.inputs.title : result.name;
if (target.inputs.title_suffix) {
text = `${text} ${target.inputs.title_suffix}`;
}
if (allowTitleLink && target.inputs.title_link) {
text = `<${target.inputs.title_link}|${text}>`;
}
if (target.inputs.message_format === 'blocks') {
if (result.status !== 'PASS') {
return `${STATUSES.DANGER} ${text}`;
} else {
return `${STATUSES.GOOD} ${text}`;
}
} else {
return text;
}
}
function getResultText(result) {
const percentage = getPercentage(result.passed, result.total);
return `${result.passed} / ${result.total} Passed (${percentage}%)`;
}
function setSuiteBlock({ result, target, payload }) {
let suite_attachments_length = 0;
if (target.inputs.include_suites) {
for (let i = 0; i < result.suites.length && suite_attachments_length < target.inputs.max_suites; i++) {
const suite = result.suites[i];
if (target.inputs.only_failures && suite.status !== 'FAIL') {
continue;
}
// if suites length eq to 1 then main block will include suite summary
if (result.suites.length > 1) {
payload.blocks.push(getSuiteSummary({ target, suite }));
suite_attachments_length += 1;
}
if (target.inputs.include_failure_details) {
// Only attach failure details block if there were failures
if (suite.failed > 0) {
payload.blocks.push(getFailureDetails(suite));
}
}
}
}
}
function getSuiteSummary({ target, suite }) {
const tg = new SlackTarget({ target });
const text = tg.getSuiteSummaryText(target, suite);
return {
"type": "section",
"text": {
"type": "mrkdwn",
"text": text
}
};
}
function getFailureDetails(suite) {
let text = '';
const cases = suite.cases;
for (let i = 0; i < cases.length; i++) {
const test_case = cases[i];
if (test_case.status === 'FAIL') {
text += `*Test*: ${test_case.name}\n*Error*: ${truncate(test_case.failure ?? 'N/A', 150)}\n\n`;
}
}
return {
"type": "section",
"text": {
"type": "mrkdwn",
"text": text
}
}
}
/**
*
* @param {object} param0
* @param {PerformanceTestResult | TestResult} param0.result
* @returns
*/
function getRootPayload({ result, target, payload }) {
let color = COLORS.GOOD;
if (result.status !== 'PASS') {
let somePassed = true;
if (result instanceof PerformanceTestResult) {
somePassed = result.transactions.some(suite => suite.status === 'PASS');
} else {
somePassed = result.suites.some(suite => suite.status === 'PASS');
}
if (somePassed) {
color = COLORS.WARNING;
} else {
color = COLORS.DANGER;
}
}
const fallback_text = `${getTitleText(result, target, { allowTitleLink: false })}\nResults: ${getResultText(result)}`;
if (target.inputs.message_format === 'blocks') {
return {
"text": fallback_text,
"blocks": payload.blocks
}
} else {
return {
"attachments": [
{
"color": color,
"blocks": payload.blocks,
"fallback": fallback_text,
}
]
}
}
}
async function setPerformancePayload({ result, target, payload }) {
await extension_manager.run({ result, target, payload, hook: HOOK.START });
await setPerformanceMainBlock({ result, target, payload });
await extension_manager.run({ result, target, payload, hook: HOOK.AFTER_SUMMARY });
await setTransactionBlock({ result, target, payload });
await extension_manager.run({ result, target, payload, hook: HOOK.END });
}
/**
*
* @param {object} param0
* @param {PerformanceTestResult} param0.result
*/
async function setPerformanceMainBlock({ result, target, payload }) {
let text = `*${getTitleText(result, target)}*\n`;
result.total = result.transactions.length;
result.passed = result.transactions.filter(_transaction => _transaction.status === 'PASS').length;
text += `\n*Results*: ${getResultText(result)}`;
const valid_metrics = await getValidMetrics({ metrics: result.metrics, target, result });
for (let i = 0; i < valid_metrics.length; i++) {
const metric = valid_metrics[i];
text += `\n*${metric.name}*: ${getMetricValuesText({ metric, target, result })}`;
}
payload.blocks.push({
"type": "section",
"text": {
"type": "mrkdwn",
"text": text
}
});
}
/**
*
* @param {object} param0
* @param {PerformanceTestResult} param0.result
*/
async function setTransactionBlock({ result, target, payload }) {
if (target.inputs.include_suites) {
for (let i = 0; i < result.transactions.length; i++) {
const transaction = result.transactions[i];
if (target.inputs.only_failures && transaction.status !== 'FAIL') {
continue;
}
// if transactions length eq to 1 then main block will include transaction summary
if (result.transactions.length > 1) {
let text = `*${getTitleText(transaction, target)}*\n`;
const valid_metrics = await getValidMetrics({ metrics: transaction.metrics, target, result });
for (let i = 0; i < valid_metrics.length; i++) {
const metric = valid_metrics[i];
text += `\n*${metric.name}*: ${getMetricValuesText({ metric, target, result })}`;
}
payload.blocks.push({
"type": "section",
"text": {
"type": "mrkdwn",
"text": text
}
});
}
}
}
}
const default_options = {
condition: STATUS.PASS_OR_FAIL
}
const default_inputs = {
publish: 'test-summary',
include_suites: true,
max_suites: 10,
only_failures: true,
include_failure_details: false,
duration: '',
metrics: [
{
"name": "Samples",
},
{
"name": "Duration",
"condition": "always",
"fields": ["avg", "p95"]
}
]
}
async function handleErrors({ target, errors }) {
let title = 'Error: Reporting Test Results';
title = target.inputs.title ? title + ' - ' + target.inputs.title : title;
const blocks = [];
blocks.push({
"type": "section",
"text": {
"type": "mrkdwn",
"text": title
}
});
blocks.push({
"type": "section",
"text": {
"type": "mrkdwn",
"text": errors.join('\n\n')
}
});
let payload = {
"attachments": [
{
"color": COLORS.DANGER,
"blocks": blocks,
"fallback": title,
}
]
};
if (target.inputs.message_format === 'blocks') {
payload = {
"text": title, // fallback text
blocks
}
}
return request.post({
url: target.inputs.url,
body: payload
});
}
async function publish({ target, message }) {
const { url, token, channels } = target.inputs;
if (token) {
for (const channel of channels) {
message.channel = channel;
const response = await request.post({
url: url ? url : `${SLACK_BASE_URL}/api/chat.postMessage`,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: message
});
ctx.stores.push({
target,
response,
});
if (response && response.ok) {
logger.info(`✔ Published to Slack channel - ${channel}`);
} else {
logger.error(`✖ Failed to publish to Slack channel - ${channel}`);
logger.error(response);
}
}
} else {
return request.post({
url,
body: message
});
}
}
class SlackTarget extends BaseTarget {
constructor({ target }) {
super({ target });
}
}
module.exports = {
run,
handleErrors,
default_options
}