diff --git a/package-lock.json b/package-lock.json index 76e8ca2..5add1f9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "testbeats", - "version": "2.1.4", + "version": "2.1.5", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "testbeats", - "version": "2.1.4", + "version": "2.1.5", "license": "ISC", "dependencies": { "async-retry": "^1.3.3", diff --git a/package.json b/package.json index 6d90a52..e566e38 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "testbeats", - "version": "2.1.4", + "version": "2.1.5", "description": "Publish test results to Microsoft Teams, Google Chat, Slack and InfluxDB", "main": "src/index.js", "types": "./src/index.d.ts", diff --git a/src/beats/beats.api.js b/src/beats/beats.api.js index 6338b56..aa1f281 100644 --- a/src/beats/beats.api.js +++ b/src/beats/beats.api.js @@ -61,6 +61,20 @@ class BeatsApi { } }); } + + /** + * + * @param {string} run_id + * @returns {import('./beats.types').IFailureAnalysisMetric[]} + */ + getFailureAnalysis(run_id) { + return request.get({ + url: `${this.getBaseUrl()}/api/core/v1/test-runs/${run_id}/failure-analysis`, + headers: { + 'x-api-key': this.config.api_key + } + }); + } } module.exports = { BeatsApi } \ No newline at end of file diff --git a/src/beats/beats.js b/src/beats/beats.js index 1b97545..150c55d 100644 --- a/src/beats/beats.js +++ b/src/beats/beats.js @@ -147,11 +147,12 @@ class Beats { try { logger.info('šŸŖ„ Fetching Failure Analysis...'); await this.#setTestRun('Failure Analysis Status', 'failure_analysis_status'); + const metrics = await this.api.getFailureAnalysis(this.test_run_id); this.config.extensions.push({ name: 'failure-analysis', hook: HOOK.AFTER_SUMMARY, inputs: { - data: this.test_run + data: metrics } }); } catch (error) { diff --git a/src/beats/beats.types.d.ts b/src/beats/beats.types.d.ts index ea6ab37..7cbf0e4 100644 --- a/src/beats/beats.types.d.ts +++ b/src/beats/beats.types.d.ts @@ -9,12 +9,6 @@ export type IBeatExecutionMetric = { added: number removed: number flaky: number - product_bugs: number - environment_issues: number - automation_bugs: number - not_a_defects: number - to_investigate: number - auto_analysed: number failure_summary: any failure_summary_provider: any failure_summary_model: any @@ -38,3 +32,9 @@ export type IErrorCluster = { failure: string count: number } + +export type IFailureAnalysisMetric = { + id: string + name: string + count: number +} diff --git a/src/commands/publish.command.js b/src/commands/publish.command.js index 54feac5..8c07297 100644 --- a/src/commands/publish.command.js +++ b/src/commands/publish.command.js @@ -14,7 +14,7 @@ const { MIN_NODE_VERSION } = require('../helpers/constants'); class PublishCommand { /** - * @param {import('../index').PublishOptions} opts + * @param {import('../index').CommandLineOptions} opts */ constructor(opts) { this.opts = opts; @@ -28,6 +28,7 @@ class PublishCommand { this.#buildConfig(); this.#validateOptions(); this.#setConfigFromFile(); + this.#mergeConfigOptions(); this.#processConfig(); this.#validateConfig(); this.#processResults(); @@ -76,6 +77,14 @@ class PublishCommand { } } + #mergeConfigOptions() { + if (this.opts.config && typeof this.opts.config === 'object') { + this.opts.config.project = this.opts.project || this.opts.config.project; + this.opts.config.run = this.opts.run || this.opts.config.run; + this.opts.config.api_key = this.opts['api-key'] || this.opts.config.api_key; + } + } + #processConfig() { const processed_config = processData(this.opts.config); /**@type {import('../index').PublishConfig[]} */ diff --git a/src/extensions/failure-analysis.extension.js b/src/extensions/failure-analysis.extension.js index 0ae6c86..d3adb82 100644 --- a/src/extensions/failure-analysis.extension.js +++ b/src/extensions/failure-analysis.extension.js @@ -26,31 +26,32 @@ class FailureAnalysisExtension extends BaseExtension { } #setText() { - const data = this.extension.inputs.data; - if (!data) { - return; - } - /** - * @type {import('../beats/beats.types').IBeatExecutionMetric} + * @type {import('../beats/beats.types').IFailureAnalysisMetric[]} */ - const execution_metrics = data.execution_metrics[0]; - - if (!execution_metrics) { - logger.warn('āš ļø No execution metrics found. Skipping.'); + const metrics = this.extension.inputs.data; + if (!metrics || metrics.length === 0) { + logger.warn('āš ļø No failure analysis metrics found. Skipping.'); return; } + const to_investigate = metrics.find(metric => metric.name === 'To Investigate'); + const auto_analysed = metrics.find(metric => metric.name === 'Auto Analysed'); + const failure_analysis = []; - if (execution_metrics.to_investigate) { - failure_analysis.push(`šŸ”Ž To Investigate: ${execution_metrics.to_investigate}`); + if (to_investigate && to_investigate.count > 0) { + failure_analysis.push(`šŸ”Ž To Investigate: ${to_investigate.count}`); + } + if (auto_analysed && auto_analysed.count > 0) { + failure_analysis.push(`šŸŖ„ Auto Analysed: ${auto_analysed.count}`); } - if (execution_metrics.auto_analysed) { - failure_analysis.push(`šŸŖ„ Auto Analysed: ${execution_metrics.auto_analysed}`); + + if (failure_analysis.length === 0) { + return; } - this.text = failure_analysis.join(' ā€„ā€¢ā€„ '); + this.text = failure_analysis.join(' ā€„ā€„ '); } } diff --git a/src/extensions/smart-analysis.extension.js b/src/extensions/smart-analysis.extension.js index b355fab..97b1171 100644 --- a/src/extensions/smart-analysis.extension.js +++ b/src/extensions/smart-analysis.extension.js @@ -65,13 +65,13 @@ class SmartAnalysisExtension extends BaseExtension { for (const item of smart_analysis) { rows.push(item); if (rows.length === 3) { - texts.push(rows.join(' ā€„ā€¢ā€„ ')); + texts.push(rows.join(' ā€„ā€„ ')); rows.length = 0; } } if (rows.length > 0) { - texts.push(rows.join(' ā€„ā€¢ā€„ ')); + texts.push(rows.join(' ā€„ā€„ ')); } this.text = this.mergeTexts(texts); diff --git a/src/index.d.ts b/src/index.d.ts index ab6be91..438ce55 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -251,7 +251,7 @@ export interface PublishOptions { } export interface CommandLineOptions { - config?: string; + config?: string | PublishConfig; project?: string; run?: string; 'api-key'?: string; diff --git a/test/beats.spec.js b/test/beats.spec.js index 4341ff7..0812381 100644 --- a/test/beats.spec.js +++ b/test/beats.spec.js @@ -234,7 +234,8 @@ describe('TestBeats', () => { const id1 = mock.addInteraction('post test results to beats'); const id2 = mock.addInteraction('get test results with failure analysis from beats'); const id3 = mock.addInteraction('get empty error clusters from beats'); - const id4 = mock.addInteraction('post test-summary with beats to teams with ai failure summary and smart analysis and failure analysis'); + const id4 = mock.addInteraction('get failure analysis from beats'); + const id5 = mock.addInteraction('post test-summary with beats to teams with ai failure summary and smart analysis and failure analysis'); await publish({ config: { api_key: 'api-key', @@ -262,6 +263,7 @@ describe('TestBeats', () => { assert.equal(mock.getInteraction(id2).exercised, true); assert.equal(mock.getInteraction(id3).exercised, true); assert.equal(mock.getInteraction(id4).exercised, true); + assert.equal(mock.getInteraction(id5).exercised, true); }); }); \ No newline at end of file diff --git a/test/cli.spec.js b/test/cli.spec.js index 2c2f29c..00bb34a 100644 --- a/test/cli.spec.js +++ b/test/cli.spec.js @@ -52,4 +52,16 @@ describe('CLI', () => { }); }); + it('publish results with config file and cli options', (done) => { + mock.addInteraction('post test results to beats'); + mock.addInteraction('get test results from beats'); + mock.addInteraction('post test-summary with beats to teams'); + exec('node src/cli.js publish --api-key api-key --project project-name --run build-name --config test/data/configs/teams.config.json', (error, stdout, stderr) => { + console.log(stdout); + assert.match(stdout, /šŸš€ Publishing results to TestBeats Portal/); + assert.match(stdout, /āœ… Results published successfully!/); + done(); + }); + }); + });; \ No newline at end of file diff --git a/test/data/configs/teams.config.json b/test/data/configs/teams.config.json new file mode 100644 index 0000000..ae3376d --- /dev/null +++ b/test/data/configs/teams.config.json @@ -0,0 +1,18 @@ +{ + "targets": [ + { + "name": "teams", + "inputs": { + "url": "http://localhost:9393/message" + } + } + ], + "results": [ + { + "type": "testng", + "files": [ + "test/data/testng/single-suite.xml" + ] + } + ] +} \ No newline at end of file diff --git a/test/mocks/beats.mock.js b/test/mocks/beats.mock.js index 64313c7..795afe4 100644 --- a/test/mocks/beats.mock.js +++ b/test/mocks/beats.mock.js @@ -1,5 +1,4 @@ const { addInteractionHandler } = require('pactum').handler; -const { like, includes } = require('pactum-matchers'); addInteractionHandler('post test results to beats', () => { return { @@ -155,6 +154,29 @@ addInteractionHandler('get empty error clusters from beats', () => { } }); +addInteractionHandler('get failure analysis from beats', () => { + return { + strict: false, + request: { + method: 'GET', + path: '/api/core/v1/test-runs/test-run-id/failure-analysis' + }, + response: { + status: 200, + body: [ + { + name: 'To Investigate', + count: 1 + }, + { + name: 'Auto Analysed', + count: 1 + } + ] + } + } +}); + addInteractionHandler('upload attachments', () => { return { strict: false, diff --git a/test/mocks/teams.mock.js b/test/mocks/teams.mock.js index 029eee4..c0d0423 100644 --- a/test/mocks/teams.mock.js +++ b/test/mocks/teams.mock.js @@ -1644,7 +1644,7 @@ addInteractionHandler('post test-summary with beats to teams with ai failure sum }, { "type": "TextBlock", - "text": "ā­• Newly Failed: 1 ā€„ā€¢ā€„ šŸ”“ Always Failing: 1 ā€„ā€¢ā€„ šŸŸ” Flaky: 1\n\nšŸŸ¢ Recovered: 1", + "text": "ā­• Newly Failed: 1 ā€„ā€„ šŸ”“ Always Failing: 1 ā€„ā€„ šŸŸ” Flaky: 1\n\nšŸŸ¢ Recovered: 1", "wrap": true, "separator": true, } @@ -1688,13 +1688,13 @@ addInteractionHandler('post test-summary with beats to teams with ai failure sum }, { "type": "TextBlock", - "text": "šŸ”Ž To Investigate: 1 ā€„ā€¢ā€„ šŸŖ„ Auto Analysed: 1", + "text": "šŸ”Ž To Investigate: 1 ā€„ā€„ šŸŖ„ Auto Analysed: 1", "wrap": true, "separator": true, }, { "type": "TextBlock", - "text": "ā­• Newly Failed: 1 ā€„ā€¢ā€„ šŸ”“ Always Failing: 1 ā€„ā€¢ā€„ šŸŸ” Flaky: 1\n\nšŸŸ¢ Recovered: 1", + "text": "ā­• Newly Failed: 1 ā€„ā€„ šŸ”“ Always Failing: 1 ā€„ā€„ šŸŸ” Flaky: 1\n\nšŸŸ¢ Recovered: 1", "wrap": true, "separator": true, }