Skip to content

Commit 6a38c42

Browse files
authored
Merge pull request #77 from test-results-reporter/69-performance-jmeter-aggregate-csv
feat: support performance results in teams
2 parents 9c23552 + 4234c10 commit 6a38c42

12 files changed

+1016
-942
lines changed

package-lock.json

+62-915
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "test-results-reporter",
3-
"version": "1.0.8",
3+
"version": "1.0.9",
44
"description": "Publish test results to Microsoft Teams, Google Chat and Slack",
55
"main": "src/index.js",
66
"types": "./src/index.d.ts",
@@ -11,8 +11,7 @@
1111
"/src"
1212
],
1313
"scripts": {
14-
"test": "mocha test",
15-
"coverage": "nyc --reporter=lcov --reporter=text npm run test",
14+
"test": "c8 mocha test",
1615
"build": "pkg --out-path dist ."
1716
},
1817
"repository": {
@@ -45,15 +44,16 @@
4544
"dependencies": {
4645
"async-retry": "^1.3.3",
4746
"dotenv": "^14.3.0",
47+
"performance-results-parser": "0.0.3",
4848
"phin-retry": "^1.0.3",
4949
"pretty-ms": "^7.0.0",
5050
"rosters": "0.0.1",
5151
"sade": "^1.7.4",
5252
"test-results-parser": "^0.1.0"
5353
},
5454
"devDependencies": {
55+
"c8": "^7.12.0",
5556
"mocha": "^10.0.0",
56-
"nyc": "^15.1.0",
5757
"pactum": "^3.1.10",
5858
"pkg": "^5.8.0"
5959
}

src/commands/publish.js

+15-6
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,30 @@
11
const path = require('path');
2-
const { parse } = require('test-results-parser');
2+
const trp = require('test-results-parser');
3+
const prp = require('performance-results-parser');
4+
35
const { processData } = require('../helpers/helper');
46
const target_manager = require('../targets');
57

8+
/**
9+
* @param {import('../index').PublishOptions} opts
10+
*/
611
async function run(opts) {
712
if (typeof opts.config === 'string') {
813
const cwd = process.cwd();
914
opts.config = require(path.join(cwd, opts.config));
1015
}
1116
const config = processData(opts.config);
1217
for (const report of config.reports) {
13-
const results = [];
14-
for (const result of report.results) {
15-
results.push(parse(result));
18+
const parsed_results = [];
19+
for (const result_options of report.results) {
20+
if (result_options.type === 'jmeter') {
21+
parsed_results.push(prp.parse(result_options));
22+
} else {
23+
parsed_results.push(trp.parse(result_options));
24+
}
1625
}
17-
for (let i = 0; i < results.length; i++) {
18-
const result = results[i];
26+
for (let i = 0; i < parsed_results.length; i++) {
27+
const result = parsed_results[i];
1928
for (const target of report.targets) {
2029
await target_manager.run(target, result);
2130
}

src/helpers/helper.js

+6
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ const pretty_ms = require('pretty-ms');
22

33
const DATA_REF_PATTERN = /(\{[^\}]+\})/g;
44
const ALLOWED_CONDITIONS = new Set(['pass', 'fail', 'passorfail']);
5+
const GENERIC_CONDITIONS = new Set(['always', 'never']);
56

67
function getPercentage(x, y) {
78
if (y > 0) {
@@ -30,6 +31,9 @@ function processText(raw) {
3031
return raw;
3132
}
3233

34+
/**
35+
* @returns {import('../index').PublishConfig }
36+
*/
3337
function processData(data) {
3438
if (typeof data === 'string') {
3539
return processText(data);
@@ -79,6 +83,8 @@ async function checkCondition({ condition, result, target, extension }) {
7983
const lower_condition = condition.toLowerCase();
8084
if (ALLOWED_CONDITIONS.has(lower_condition)) {
8185
return lower_condition.includes(result.status.toLowerCase());
86+
} else if (GENERIC_CONDITIONS.has(lower_condition)) {
87+
return lower_condition === 'always';
8288
} else {
8389
return eval(condition);
8490
}

src/helpers/performance.js

+140
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
const Metric = require("performance-results-parser/src/models/Metric");
2+
const { getPrettyDuration, checkCondition } = require("./helper");
3+
4+
/**
5+
* @param {object} param0
6+
* @param {Metric[]} param0.metrics
7+
* @param {object} param0.result
8+
* @param {object} param0.target
9+
*/
10+
async function getValidMetrics({ metrics, result, target }) {
11+
if (target.inputs.metrics && target.inputs.metrics.length > 0) {
12+
const valid_metrics = [];
13+
for (let i = 0; i < metrics.length; i++) {
14+
const metric = metrics[i];
15+
for (let j = 0; j < target.inputs.metrics.length; j++) {
16+
const metric_config = target.inputs.metrics[j];
17+
if (metric.name === metric_config.name) {
18+
const include = await checkCondition({ condition: metric_config.condition || 'always', result, target });
19+
if (include) valid_metrics.push(metric);
20+
}
21+
}
22+
}
23+
return valid_metrics;
24+
}
25+
return metrics;
26+
}
27+
28+
/**
29+
* @param {Metric} metric
30+
* @param {string[]} fields
31+
*/
32+
function getCounterMetricFieldValue(metric, fields) {
33+
let value = '';
34+
if (fields.includes('sum')) {
35+
const sum_failure = metric.failures.find(_failure => _failure.field === 'sum');
36+
if (sum_failure) {
37+
const emoji = getEmoji(sum_failure.difference);
38+
value = `${emoji} ${metric['sum']} (${getDifferenceSymbol(sum_failure.difference)}${sum_failure.difference}) `
39+
} else {
40+
value = `${metric['sum']} `;
41+
}
42+
}
43+
if (fields.includes('rate')) {
44+
let metric_unit = metric.unit.startsWith('/') ? metric.unit : ` ${metric.unit}`;
45+
const rate_failure = metric.failures.find(_failure => _failure.field === 'rate');
46+
if (rate_failure) {
47+
const emoji = getEmoji(rate_failure.difference);
48+
value += `${emoji} ${metric['rate']}${metric_unit} (${getDifferenceSymbol(rate_failure.difference)}${rate_failure.difference})`
49+
} else {
50+
value += `${metric['rate']}${metric_unit}`;
51+
}
52+
}
53+
return value;
54+
}
55+
56+
/**
57+
* @param {Metric} metric
58+
*/
59+
function getTrendMetricFieldValue(metric, field) {
60+
const failure = metric.failures.find(_failure => _failure.field === field);
61+
if (failure) {
62+
const emoji = getEmoji(failure.difference);
63+
return `${emoji} ${field}=${getPrettyDuration(metric[field])} (${getDifferenceSymbol(failure.difference)}${getPrettyDuration(failure.difference)})`
64+
}
65+
return `${field}=${getPrettyDuration(metric[field])}`;
66+
}
67+
68+
/**
69+
* @param {Metric} metric
70+
*/
71+
function getRateMetricFieldValue(metric) {
72+
const failure = metric.failures.find(_failure => _failure.field === 'rate');
73+
if (failure) {
74+
const emoji = getEmoji(failure.difference);
75+
return `${emoji} ${metric['rate']} ${metric.unit} (${getDifferenceSymbol(failure.difference)}${failure.difference})`;
76+
}
77+
return `${metric['rate']} ${metric.unit}`;
78+
}
79+
80+
function getEmoji(value) {
81+
return value > 0 ? '🔺' : '🔻';
82+
}
83+
84+
function getDifferenceSymbol(value) {
85+
return value > 0 ? `+` : '';
86+
}
87+
88+
/**
89+
*
90+
* @param {object} param0
91+
* @param {Metric} param0.metric
92+
*/
93+
function getDisplayFields({ metric, target }) {
94+
let fields = [];
95+
if (target.inputs.metrics) {
96+
const metric_config = target.inputs.metrics.find(_metric => _metric.name === metric.name);
97+
if (metric_config) {
98+
fields = metric_config.fields;
99+
}
100+
}
101+
if (fields && fields.length > 0) {
102+
return fields;
103+
} else {
104+
switch (metric.type) {
105+
case 'COUNTER':
106+
return ['sum', 'rate'];
107+
case 'RATE':
108+
return ['rate'];
109+
case 'TREND':
110+
return ['avg', 'min', 'med', 'max', 'p90', 'p95', 'p99'];
111+
default:
112+
return ['sum', 'min', 'max'];
113+
}
114+
}
115+
}
116+
117+
/**
118+
* @param {object} param0
119+
* @param {Metric} param0.metric
120+
*/
121+
function getMetricValuesText({ metric, target }) {
122+
const fields = getDisplayFields({ metric, target });
123+
const values = [];
124+
if (metric.type === 'COUNTER') {
125+
values.push(getCounterMetricFieldValue(metric, fields));
126+
} else if (metric.type === 'TREND') {
127+
for (let i = 0; i < fields.length; i++) {
128+
const field_metric = fields[i];
129+
values.push(getTrendMetricFieldValue(metric, field_metric));
130+
}
131+
} else if (metric.type === 'RATE') {
132+
values.push(getRateMetricFieldValue(metric));
133+
}
134+
return values.join(' | ');
135+
}
136+
137+
module.exports = {
138+
getValidMetrics,
139+
getMetricValuesText
140+
}

src/index.d.ts

+12-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { User, Schedule } from 'rosters';
22
import TestResult from 'test-results-parser/src/models/TestResult';
3+
import { PerformanceParseOptions } from 'performance-results-parser';
34

45
export type ExtensionName = 'report-portal-analysis' | 'hyperlinks' | 'mentions' | 'report-portal-history' | 'quick-chart-test-summary' | 'custom';
56
export type Hook = 'start' | 'end';
@@ -12,7 +13,7 @@ export interface ConditionFunctionContext {
1213
result: TestResult;
1314
}
1415
export type ConditionFunction = (ctx: ConditionFunctionContext) => boolean | Promise<boolean>;
15-
export type Condition = 'pass' | 'fail' | 'passOrFail' | ConditionFunction;
16+
export type Condition = 'pass' | 'fail' | 'passOrFail' | 'always' | 'never' | ConditionFunction;
1617

1718
/**
1819
* Extensions
@@ -123,6 +124,12 @@ export interface HyperlinksExtension extends Extension {
123124
* Targets
124125
*/
125126

127+
export interface MetricConfig {
128+
name: string;
129+
condition: Condition;
130+
fields: string[];
131+
}
132+
126133
export interface TargetInputs {
127134
url: string;
128135
title?: string;
@@ -131,15 +138,16 @@ export interface TargetInputs {
131138
duration?: string;
132139
publish?: PublishReportType;
133140
only_failures?: boolean;
141+
metrics?: MetricConfig[];
134142
}
135143

136-
export interface SlackInputs extends TargetInputs {}
144+
export interface SlackInputs extends TargetInputs { }
137145

138146
export interface TeamsInputs extends TargetInputs {
139147
width?: string;
140148
}
141149

142-
export interface ChatInputs extends TargetInputs {}
150+
export interface ChatInputs extends TargetInputs { }
143151

144152
export interface CustomTargetFunctionContext {
145153
target: Target;
@@ -166,7 +174,7 @@ export interface PublishResult {
166174

167175
export interface PublishReport {
168176
targets: Target[];
169-
results: PublishResult[];
177+
results: PublishResult[] | PerformanceParseOptions[];
170178
}
171179

172180
export interface PublishConfig {

src/targets/chat.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ function setSuiteBlock({ result, target, payload }) {
7272
let texts = [];
7373
for (let i = 0; i < result.suites.length; i++) {
7474
const suite = result.suites[i];
75-
if (target.inputs.only_failure_suites && suite.status !== 'FAIL') {
75+
if (target.inputs.only_failures && suite.status !== 'FAIL') {
7676
continue;
7777
}
7878
// if suites length eq to 1 then main block will include suite summary
@@ -125,7 +125,7 @@ const default_options = {
125125
const default_inputs = {
126126
publish: 'test-summary',
127127
include_suites: true,
128-
only_failure_suites: false,
128+
only_failures: false,
129129
include_failure_details: false,
130130
duration: 'colonNotation'
131131
};

src/targets/slack.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ function setSuiteBlock({ result, target, payload }) {
7373
if (target.inputs.include_suites) {
7474
for (let i = 0; i < result.suites.length; i++) {
7575
const suite = result.suites[i];
76-
if (target.inputs.only_failure_suites && suite.status !== 'FAIL') {
76+
if (target.inputs.only_failures && suite.status !== 'FAIL') {
7777
continue;
7878
}
7979
// if suites length eq to 1 then main block will include suite summary
@@ -153,7 +153,7 @@ const default_options = {
153153
const default_inputs = {
154154
publish: 'test-summary',
155155
include_suites: true,
156-
only_failure_suites: false,
156+
only_failures: false,
157157
include_failure_details: false,
158158
duration: 'colonNotation'
159159
}

0 commit comments

Comments
 (0)