From 7a62852147b9db2246d45379a69e4cbca4d75439 Mon Sep 17 00:00:00 2001 From: Anudeep Date: Sat, 4 Feb 2023 13:21:32 +0530 Subject: [PATCH] feat: add influx target --- package-lock.json | 17 ++++- package.json | 9 ++- src/extensions/percy-analysis.js | 17 +---- src/helpers/constants.js | 3 +- src/index.d.ts | 12 ++- src/targets/index.js | 3 + src/targets/influx.js | 122 +++++++++++++++++++++++++++++++ test/mocks/index.js | 3 +- test/mocks/influx.mock.js | 39 ++++++++++ test/targets.influx.spec.js | 79 ++++++++++++++++++++ 10 files changed, 278 insertions(+), 26 deletions(-) create mode 100644 src/targets/influx.js create mode 100644 test/mocks/influx.mock.js create mode 100644 test/targets.influx.spec.js diff --git a/package-lock.json b/package-lock.json index f42d5d6..be83d22 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "test-results-reporter", - "version": "1.0.15", + "version": "1.0.16", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -863,6 +863,15 @@ "wrappy": "1" } }, + "influxdb-v1": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/influxdb-v1/-/influxdb-v1-1.0.4.tgz", + "integrity": "sha512-p5uWPGtyI7ylkBBTGoBvpGWLxvHYfHyYLU1i6EOCh6yultryh2kk48m+8vTwpbY3toT/VOaFnj9DajIcBZKDDw==", + "requires": { + "phin-retry": "^1.0.3", + "sade": "^1.8.1" + } + }, "inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -1421,9 +1430,9 @@ "dev": true }, "performance-results-parser": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/performance-results-parser/-/performance-results-parser-0.0.4.tgz", - "integrity": "sha512-ph77xm+SsetceC6Yq7nZB+vmLA/pKpZShkolnxNUshK8kpoKL9tXNfUyfJ2LGHBSO7I8NErlqElchDB8mTEb2w==", + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/performance-results-parser/-/performance-results-parser-0.0.5.tgz", + "integrity": "sha512-HfaWRo5OCh0TDUG0DtqpwCmepxqAr0dRVzRH3GldcbJSdRZMqPmbS49fk6Yttn0FbQGpPIL+K1wVviBzSKjOig==", "requires": { "csvjson": "^5.1.0", "globrex": "^0.1.2", diff --git a/package.json b/package.json index f44a72f..8cfc4c5 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "test-results-reporter", - "version": "1.0.15", - "description": "Publish test results to Microsoft Teams, Google Chat and Slack", + "version": "1.0.16", + "description": "Publish test results to Microsoft Teams, Google Chat, Slack and InfluxDB", "main": "src/index.js", "types": "./src/index.d.ts", "bin": { @@ -26,6 +26,8 @@ "microsoft teams", "teams", "slack", + "influx", + "influxdb", "junit", "mocha", "cucumber", @@ -45,7 +47,8 @@ "dependencies": { "async-retry": "^1.3.3", "dotenv": "^14.3.2", - "performance-results-parser": "0.0.4", + "influxdb-v1": "^1.0.4", + "performance-results-parser": "0.0.5", "phin-retry": "^1.0.3", "pretty-ms": "^7.0.1", "rosters": "0.0.1", diff --git a/src/extensions/percy-analysis.js b/src/extensions/percy-analysis.js index 748edae..9306008 100644 --- a/src/extensions/percy-analysis.js +++ b/src/extensions/percy-analysis.js @@ -68,9 +68,6 @@ async function setBuildByLastRun(extension) { */ function getLastFinishedBuild(extension) { const { inputs } = extension; - - // Added a default value for retries in case the input is missing or is not a number - const retries = inputs.retries || 3; const minTimeout = 5000; return retry(async () => { @@ -78,22 +75,17 @@ function getLastFinishedBuild(extension) { try { response = await getLastBuild(inputs); } catch (error) { - // Used the Error class to throw errors instead of using template literals, - // which provides more information about the error, - // including a stack trace, and allows us to distinguish between different types of errors. throw new Error(`Error occurred while fetching the last build: ${error}`); } - // Added Checks to make sure that the response data is valid and is as expected. if (!response.data || !response.data[0] || !response.data[0].attributes) { throw new Error(`Invalid response data: ${JSON.stringify(response)}`); } const state = response.data[0].attributes.state; - // Included percy's "failed" build status for this fix: #113 if (state !== "finished" && state !== "failed") { throw new Error(`build is still '${state}'`); } return response; - }, { retries, minTimeout }); + }, { retries: inputs.retries, minTimeout }); } /** @@ -164,20 +156,13 @@ function getAnalysisSummary(outputs, bold_start = '**', bold_end = '**') { if (approved) { results.push(`${bold_start}✔ AP - ${approved}${bold_end}`); } else { - // Implemented a defensive check to handle scenarios where the variable "approved" may be null, - // by setting it to zero to avoid displaying null values. results.push(`✔ AP - ${approved || 0}`); } if (un_reviewed) { results.push(`${bold_start}🔎 UR - ${un_reviewed}${bold_end}`); } else { - // As a solution for issue #113, we have implemented a check for Percy's build status, - // if it is "failed", we set the value of "un_reviewed" to 0, - // this way we handle the scenario where "un_reviewed" would be null and display a default value of zero instead. results.push(`🔎 UR - ${un_reviewed || 0}`); } - // Implemented a defensive check for when "removed_snapshots" is null or undefined, - // to avoid displaying null or undefined values and provide a default value of zero instead. if (removed_snapshots && removed_snapshots.length) { results.push(`${bold_start}🗑 RM - ${removed_snapshots.length}${bold_end}`); } else { diff --git a/src/helpers/constants.js b/src/helpers/constants.js index b07837a..b642434 100644 --- a/src/helpers/constants.js +++ b/src/helpers/constants.js @@ -15,7 +15,8 @@ const TARGET = Object.freeze({ TEAMS: 'teams', CHAT: 'chat', CUSTOM: 'custom', - DELAY: 'delay' + DELAY: 'delay', + INFLUX: 'influx', }); const EXTENSION = Object.freeze({ diff --git a/src/index.d.ts b/src/index.d.ts index bbf11d6..f7b0504 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -167,6 +167,16 @@ export interface TeamsInputs extends TargetInputs { export interface ChatInputs extends TargetInputs { } +export interface InfluxDBTargetInputs { + url: string; + db: string; + username?: string; + password?: string; + measurement_perf_run?: string; + measurement_perf_transaction?: string; + tags?: object; +} + export interface CustomTargetFunctionContext { target: Target; result: TestResult; @@ -181,7 +191,7 @@ export interface CustomTargetInputs { export interface Target { name: TargetName; condition: Condition; - inputs: SlackInputs | TeamsInputs | ChatInputs | CustomTargetInputs; + inputs: SlackInputs | TeamsInputs | ChatInputs | CustomTargetInputs | InfluxDBTargetInputs; extensions?: Extension[]; } diff --git a/src/targets/index.js b/src/targets/index.js index e4a6045..f344d88 100644 --- a/src/targets/index.js +++ b/src/targets/index.js @@ -3,6 +3,7 @@ const slack = require('./slack'); const chat = require('./chat'); const custom = require('./custom'); const delay = require('./delay'); +const influx = require('./influx'); const { TARGET } = require('../helpers/constants'); const { checkCondition } = require('../helpers/helper'); @@ -18,6 +19,8 @@ function getTargetRunner(target) { return custom; case TARGET.DELAY: return delay; + case TARGET.INFLUX: + return influx; default: return require(target.name); } diff --git a/src/targets/influx.js b/src/targets/influx.js new file mode 100644 index 0000000..6e150b0 --- /dev/null +++ b/src/targets/influx.js @@ -0,0 +1,122 @@ +const influx_v1 = require('influxdb-v1'); +const Metric = require('performance-results-parser/src/models/Metric'); +const PerformanceTestResult = require('performance-results-parser/src/models/PerformanceTestResult'); +const Transaction = require('performance-results-parser/src/models/Transaction'); + + +const { STATUS } = require('../helpers/constants'); + +/** + * + * @param {object} param0 + * @param {PerformanceTestResult | TestResult} param0.result + */ +async function run({ result, target }) { + target.inputs = Object.assign({}, default_inputs, target.inputs); + const metrics = getMetrics({ result, target }); + await influx_v1.write( + { + url: target.inputs.url, + db: target.inputs.db, + username: target.inputs.username, + password: target.inputs.password, + }, + metrics + ); +} + +function getMetrics({ result, target }) { + const influx_metrics = []; + if (result instanceof PerformanceTestResult) { + const tags = Object.assign({}, target.inputs.tags); + tags.Name = result.name; + tags.Status = result.status; + + const fields = {}; + fields.status = result.status === 'PASS' ? 0 : 1; + fields.transactions = result.transactions.length; + fields.transactions_passed = result.transactions.filter(_transaction => _transaction.status === "PASS").length; + fields.transactions_failed = result.transactions.filter(_transaction => _transaction.status === "FAIL").length; + + for (const metric of result.metrics) { + setPerfMetrics(metric, fields); + } + + influx_metrics.push({ + measurement: target.inputs.measurement_perf_run, + tags, + fields + }); + + for (const transaction of result.transactions) { + influx_metrics.push(getTransactionInfluxMetric(transaction, target)); + } + + } + return influx_metrics; +} + +/** + * + * @param {Metric} metric + */ +function setPerfMetrics(metric, fields) { + let name = metric.name; + name = name.toLowerCase(); + name = name.replace(' ', '_'); + if (metric.type === "COUNTER" || metric.type === "RATE") { + fields[`${name}_sum`] = metric.sum; + fields[`${name}_rate`] = metric.rate; + } else if (metric.type === "TREND") { + fields[`${name}_avg`] = metric.avg; + fields[`${name}_med`] = metric.med; + fields[`${name}_max`] = metric.max; + fields[`${name}_min`] = metric.min; + fields[`${name}_p90`] = metric.p90; + fields[`${name}_p95`] = metric.p95; + fields[`${name}_p99`] = metric.p99; + } +} + +/** + * + * @param {Transaction} transaction + */ +function getTransactionInfluxMetric(transaction, target) { + const tags = Object.assign({}, target.inputs.tags); + tags.Name = transaction.name; + tags.Status = transaction.status; + + const fields = {}; + fields.status = transaction.status === 'PASS' ? 0 : 1; + + for (const metric of transaction.metrics) { + setPerfMetrics(metric, fields); + } + + return { + measurement: target.inputs.measurement_perf_transaction, + tags, + fields + } +} + + +const default_inputs = { + url: '', + db: '', + username: '', + password: '', + measurement_perf_run: 'PerfRun', + measurement_perf_transaction: 'PerfTransaction', + tags: {} +} + +const default_options = { + condition: STATUS.PASS_OR_FAIL +} + +module.exports = { + run, + default_options +} \ No newline at end of file diff --git a/test/mocks/index.js b/test/mocks/index.js index 895cb23..93600fd 100644 --- a/test/mocks/index.js +++ b/test/mocks/index.js @@ -3,4 +3,5 @@ require('./rp.mock'); require('./slack.mock'); require('./teams.mock'); require('./chat.mock'); -require('./percy.mock'); \ No newline at end of file +require('./percy.mock'); +require('./influx.mock'); \ No newline at end of file diff --git a/test/mocks/influx.mock.js b/test/mocks/influx.mock.js new file mode 100644 index 0000000..b3e563f --- /dev/null +++ b/test/mocks/influx.mock.js @@ -0,0 +1,39 @@ +const { addInteractionHandler } = require('pactum').handler; + +addInteractionHandler('save perf results', () => { + return { + request: { + method: 'POST', + path: '/write', + headers: { + "authorization": "Basic dXNlcjpwYXNz" + }, + queryParams: { + "db": "TestResults" + }, + body: "PerfRun,Name=TOTAL,Status=PASS status=0,transactions=2,transactions_passed=2,transactions_failed=0,samples_sum=39,samples_rate=0.55535,duration_avg=4660,duration_med=3318,duration_max=15513,duration_min=1135,duration_p90=11354,duration_p95=11446,duration_p99=15513,errors_sum=0,errors_rate=0,data_sent_sum=0,data_sent_rate=38.87,data_received_sum=0,data_received_rate=5166.44\nPerfTransaction,Name=S01_T01_Application_Launch,Status=PASS status=0,samples_sum=10,samples_rate=0.14422,duration_avg=3086,duration_med=2832,duration_max=3797,duration_min=2119,duration_p90=3795,duration_p95=3795,duration_p99=3797,errors_sum=0,errors_rate=0.001,data_sent_sum=0,data_sent_rate=5.36,data_received_sum=0,data_received_rate=2662.79\nPerfTransaction,Name=S01_T02_Application_Login,Status=PASS status=0,samples_sum=9,samples_rate=0.1461,duration_avg=4355,duration_med=3273,duration_max=10786,duration_min=3042,duration_p90=4416,duration_p95=10786,duration_p99=10786,errors_sum=0,errors_rate=0,data_sent_sum=0,data_sent_rate=12.94,data_received_sum=0,data_received_rate=2754.9" + }, + response: { + status: 200 + } + } +}); + +addInteractionHandler('save perf results with custom tags', () => { + return { + request: { + method: 'POST', + path: '/write', + headers: { + "authorization": "Basic dXNlcjpwYXNz" + }, + queryParams: { + "db": "TestResults" + }, + body: "PerfRun,Team=QA,App=PactumJS,Name=TOTAL,Status=PASS status=0,transactions=2,transactions_passed=2,transactions_failed=0,samples_sum=39,samples_rate=0.55535,duration_avg=4660,duration_med=3318,duration_max=15513,duration_min=1135,duration_p90=11354,duration_p95=11446,duration_p99=15513,errors_sum=0,errors_rate=0,data_sent_sum=0,data_sent_rate=38.87,data_received_sum=0,data_received_rate=5166.44\nPerfTransaction,Team=QA,App=PactumJS,Name=S01_T01_Application_Launch,Status=PASS status=0,samples_sum=10,samples_rate=0.14422,duration_avg=3086,duration_med=2832,duration_max=3797,duration_min=2119,duration_p90=3795,duration_p95=3795,duration_p99=3797,errors_sum=0,errors_rate=0.001,data_sent_sum=0,data_sent_rate=5.36,data_received_sum=0,data_received_rate=2662.79\nPerfTransaction,Team=QA,App=PactumJS,Name=S01_T02_Application_Login,Status=PASS status=0,samples_sum=9,samples_rate=0.1461,duration_avg=4355,duration_med=3273,duration_max=10786,duration_min=3042,duration_p90=4416,duration_p95=10786,duration_p99=10786,errors_sum=0,errors_rate=0,data_sent_sum=0,data_sent_rate=12.94,data_received_sum=0,data_received_rate=2754.9" + }, + response: { + status: 200 + } + } +}); \ No newline at end of file diff --git a/test/targets.influx.spec.js b/test/targets.influx.spec.js new file mode 100644 index 0000000..0be6f36 --- /dev/null +++ b/test/targets.influx.spec.js @@ -0,0 +1,79 @@ +const { mock } = require('pactum'); +const assert = require('assert'); +const { publish } = require('../src'); + +describe('targets - influx - performance', () => { + + it('should save results', async () => { + const id = mock.addInteraction('save perf results'); + await publish({ + config: { + "reports": [ + { + "targets": [ + { + "name": "influx", + "inputs": { + "url": "http://localhost:9393", + "db": "TestResults", + "username": "user", + "password": "pass" + } + } + ], + "results": [ + { + "type": "jmeter", + "files": [ + "test/data/jmeter/sample.csv" + ] + } + ] + } + ] + } + }); + assert.equal(mock.getInteraction(id).exercised, true); + }); + + it('should save results with custom tags', async () => { + const id = mock.addInteraction('save perf results with custom tags'); + await publish({ + config: { + "reports": [ + { + "targets": [ + { + "name": "influx", + "inputs": { + "url": "http://localhost:9393", + "db": "TestResults", + "username": "user", + "password": "pass", + "tags": { + "Team": "QA", + "App": "PactumJS" + } + } + } + ], + "results": [ + { + "type": "jmeter", + "files": [ + "test/data/jmeter/sample.csv" + ] + } + ] + } + ] + } + }); + assert.equal(mock.getInteraction(id).exercised, true); + }); + + afterEach(() => { + mock.clearInteractions(); + }); + +}); \ No newline at end of file