diff --git a/package-lock.json b/package-lock.json index 40b8bae..648a97f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "testbeats", - "version": "2.0.4", + "version": "2.0.5", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "testbeats", - "version": "2.0.4", + "version": "2.0.5", "license": "ISC", "dependencies": { "async-retry": "^1.3.3", @@ -18,7 +18,6 @@ "pretty-ms": "^7.0.1", "rosters": "0.0.1", "sade": "^1.8.1", - "semver": "^7.6.2", "test-results-parser": "latest" }, "bin": { @@ -2223,6 +2222,7 @@ "version": "7.6.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, "bin": { "semver": "bin/semver.js" }, @@ -4019,7 +4019,8 @@ "semver": { "version": "7.6.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==" + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true }, "serialize-javascript": { "version": "6.0.0", diff --git a/package.json b/package.json index a810b97..03bea1b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "testbeats", - "version": "2.0.4", + "version": "2.0.5", "description": "Publish test results to Microsoft Teams, Google Chat, Slack and InfluxDB", "main": "src/index.js", "types": "./src/index.d.ts", @@ -55,7 +55,6 @@ "pretty-ms": "^7.0.1", "rosters": "0.0.1", "sade": "^1.8.1", - "semver": "^7.6.2", "test-results-parser": "latest" }, "devDependencies": { diff --git a/src/beats/beats.js b/src/beats/beats.js index 6e852e9..a89e27e 100644 --- a/src/beats/beats.js +++ b/src/beats/beats.js @@ -16,6 +16,7 @@ class Beats { this.result = result; this.api = new BeatsApi(config); this.test_run_id = ''; + this.test_run = null; } async publish() { @@ -27,6 +28,7 @@ class Beats { await this.#uploadAttachments(); this.#updateTitleLink(); await this.#attachFailureSummary(); + await this.#attachSmartAnalysis(); } #setCIInfo() { @@ -125,55 +127,77 @@ class Beats { if (this.config.show_failure_summary === false) { return; } - const text = await this.#getFailureSummary(); - if (!text) { + try { + logger.info('✨ Fetching AI Failure Summary...'); + await this.#setTestRun(' AI Failure Summary', 'failure_summary_status'); + this.config.extensions.push({ + name: 'ai-failure-summary', + hook: HOOK.AFTER_SUMMARY, + inputs: { + data: this.test_run + } + }); + } catch (error) { + logger.error(`❌ Unable to attach failure summary: ${error.message}`, error); + } + } + + async #attachSmartAnalysis() { + if (!this.test_run_id) { return; } - const extension = this.#getAIFailureSummaryExtension(text); - for (const target of this.config.targets) { - target.extensions = target.extensions || []; - target.extensions.push(extension); + if (!this.config.targets) { + return; + } + if (this.config.show_smart_analysis === false) { + return; + } + try { + logger.info('🤓 Fetching Smart Analysis...'); + await this.#setTestRun('Smart Analysis', 'smart_analysis_status'); + this.config.extensions.push({ + name: 'smart-analysis', + hook: HOOK.AFTER_SUMMARY, + inputs: { + data: this.test_run + } + }); + } catch (error) { + logger.error(`❌ Unable to attach smart analysis: ${error.message}`, error); + } + } + + #getDelay() { + if (process.env.TEST_BEATS_DELAY) { + return parseInt(process.env.TEST_BEATS_DELAY); } + return 3000; } - async #getFailureSummary() { - logger.info('✨ Fetching AI Failure Summary...'); + async #setTestRun(text, wait_for = 'smart_analysis_status') { + if (this.test_run && this.test_run[wait_for] === 'COMPLETED') { + return; + } let retry = 3; while (retry >= 0) { retry = retry - 1; await new Promise(resolve => setTimeout(resolve, this.#getDelay())); - const test_run = await this.api.getTestRun(this.test_run_id); - const status = test_run && test_run.failure_summary_status; + this.test_run = await this.api.getTestRun(this.test_run_id); + const status = this.test_run && this.test_run[wait_for]; switch (status) { case 'COMPLETED': - return test_run.execution_metrics[0].failure_summary; + logger.debug(`☑️ ${text} generated successfully`); + return; case 'FAILED': - logger.error(`❌ Failed to generate AI Failure Summary`); + logger.error(`❌ Failed to generate ${text}`); return; case 'SKIPPED': - logger.warn(`❗ Skipped generating AI Failure Summary`); + logger.warn(`❗ Skipped generating ${text}`); return; } - logger.info(`🔄 AI Failure Summary not generated, retrying...`); - } - logger.warn(`🙈 AI Failure Summary not generated in given time`); - } - - #getDelay() { - if (process.env.TEST_BEATS_DELAY) { - return parseInt(process.env.TEST_BEATS_DELAY); + logger.info(`🔄 ${text} not generated, retrying...`); } - return 3000; - } - - #getAIFailureSummaryExtension(text) { - return { - name: 'ai-failure-summary', - hook: HOOK.AFTER_SUMMARY, - inputs: { - failure_summary: text - } - }; + logger.warn(`🙈 ${text} not generated in given time`); } } diff --git a/src/beats/beats.types.d.ts b/src/beats/beats.types.d.ts new file mode 100644 index 0000000..55b24a0 --- /dev/null +++ b/src/beats/beats.types.d.ts @@ -0,0 +1,18 @@ +export type IBeatExecutionMetric = { + id: string + created_at: string + updated_at: string + newly_failed: number + always_failing: number + recovered: number + added: number + removed: number + flaky: number + failure_summary: any + failure_summary_provider: any + failure_summary_model: any + status: string + status_message: any + test_run_id: string + org_id: string +} diff --git a/src/cli.js b/src/cli.js index 0ddae1f..12cf9a9 100755 --- a/src/cli.js +++ b/src/cli.js @@ -11,6 +11,9 @@ prog .version('2.0.4') .option('-c, --config', 'path to config file') .option('-l, --logLevel', 'Log Level', "INFO") + .option('--api-key', 'api key') + .option('--project', 'project name') + .option('--run', 'run name') .option('--slack', 'slack webhook url') .option('--teams', 'teams webhook url') .option('--chat', 'chat webhook url') diff --git a/src/commands/publish.command.js b/src/commands/publish.command.js index 16e45f2..03c06af 100644 --- a/src/commands/publish.command.js +++ b/src/commands/publish.command.js @@ -1,6 +1,7 @@ const path = require('path'); const trp = require('test-results-parser'); const prp = require('performance-results-parser'); +const os = require('os'); const beats = require('../beats'); const { ConfigBuilder } = require('../utils/config.builder'); @@ -8,7 +9,7 @@ const target_manager = require('../targets'); const logger = require('../utils/logger'); const { processData } = require('../helpers/helper'); const pkg = require('../../package.json'); -const {checkEnvDetails} = require('../helpers/helper'); +const { MIN_NODE_VERSION } = require('../helpers/constants'); class PublishCommand { @@ -22,10 +23,7 @@ class PublishCommand { async publish() { logger.info(`🥁 TestBeats v${pkg.version}`); - const envDetails = checkEnvDetails(); - // Check OS and NodeJS version - logger.info(`💻 ${envDetails}`); - + this.#validateEnvDetails(); this.#buildConfig(); this.#validateOptions(); this.#setConfigFromFile(); @@ -36,6 +34,20 @@ class PublishCommand { logger.info('✅ Results published successfully!'); } + #validateEnvDetails() { + try { + const current_major_version = parseInt(process.version.split('.')[0].replace('v', '')); + if (current_major_version >= MIN_NODE_VERSION) { + logger.info(`💻 NodeJS: ${process.version}, OS: ${os.platform()}, Version: ${os.release()}, Arch: ${os.machine()}`); + return; + } + } catch (error) { + logger.warn(`⚠️ Unable to verify NodeJS version: ${error.message}`); + return; + } + throw new Error(`❌ Supported NodeJS version is >= v${MIN_NODE_VERSION}. Current version is ${process.version}`) + } + #buildConfig() { const config_builder = new ConfigBuilder(this.opts); config_builder.build(); @@ -77,7 +89,7 @@ class PublishCommand { } #validateConfig() { - logger.info("🛠️ Validating configuration...") + logger.info("🚓 Validating configuration...") for (const config of this.configs) { this.#validateResults(config); this.#validateTargets(config); @@ -182,12 +194,12 @@ class PublishCommand { for (const config of this.configs) { for (let i = 0; i < this.results.length; i++) { const result = this.results[i]; - const global_extensions = config.extensions || []; + config.extensions = config.extensions || []; await beats.run(config, result); if (config.targets) { for (const target of config.targets) { target.extensions = target.extensions || []; - target.extensions = global_extensions.concat(target.extensions); + target.extensions = config.extensions.concat(target.extensions); await target_manager.run(target, result); } } else { diff --git a/src/extensions/ai-failure-summary.extension.js b/src/extensions/ai-failure-summary.extension.js new file mode 100644 index 0000000..fc39568 --- /dev/null +++ b/src/extensions/ai-failure-summary.extension.js @@ -0,0 +1,43 @@ +const { BaseExtension } = require('./base.extension'); +const { STATUS, HOOK } = require("../helpers/constants"); + + +class AIFailureSummaryExtension extends BaseExtension { + + constructor(target, extension, result, payload, root_payload) { + super(target, extension, result, payload, root_payload); + this.#setDefaultOptions(); + this.#setDefaultInputs(); + this.updateExtensionInputs(); + } + + run() { + this.#setText(); + this.attach(); + } + + #setDefaultOptions() { + this.default_options.hook = HOOK.AFTER_SUMMARY, + this.default_options.condition = STATUS.PASS_OR_FAIL; + } + + #setDefaultInputs() { + this.default_inputs.title = 'AI Failure Summary ✨'; + this.default_inputs.title_link = ''; + } + + #setText() { + const data = this.extension.inputs.data; + if (!data) { + return; + } + + /** + * @type {import('../beats/beats.types').IBeatExecutionMetric} + */ + const execution_metrics = data.execution_metrics[0]; + this.text = execution_metrics.failure_summary; + } +} + +module.exports = { AIFailureSummaryExtension } \ No newline at end of file diff --git a/src/extensions/ai-failure-summary.js b/src/extensions/ai-failure-summary.js deleted file mode 100644 index 7226750..0000000 --- a/src/extensions/ai-failure-summary.js +++ /dev/null @@ -1,72 +0,0 @@ -const { STATUS, HOOK } = require("../helpers/constants"); -const { addChatExtension, addSlackExtension, addTeamsExtension } = require('../helpers/extension.helper'); - -/** - * @param {object} param0 - * @param {import('..').Target} param0.target - * @param {import('..').MetadataExtension} param0.extension - */ -async function run({ target, extension, result, payload, root_payload }) { - extension.inputs = Object.assign({}, default_inputs, extension.inputs); - if (target.name === 'teams') { - extension.inputs = Object.assign({}, default_inputs_teams, extension.inputs); - await attachForTeams({ target, extension, payload, result }); - } else if (target.name === 'slack') { - extension.inputs = Object.assign({}, default_inputs_slack, extension.inputs); - await attachForSlack({ target, extension, payload, result }); - } else if (target.name === 'chat') { - extension.inputs = Object.assign({}, default_inputs_chat, extension.inputs); - await attachForChat({ target, extension, payload, result }); - } -} - -/** - * @param {object} param0 - * @param {import('..').MetadataExtension} param0.extension - */ -async function attachForTeams({ target, extension, payload, result }) { - const text = extension.inputs.failure_summary - if (text) { - addTeamsExtension({ payload, extension, text }); - } -} - -async function attachForSlack({ target, extension, payload, result }) { - const text = extension.inputs.failure_summary - if (text) { - addSlackExtension({ payload, extension, text }); - } -} - -async function attachForChat({ target, extension, payload, result }) { - const text = extension.inputs.failure_summary - if (text) { - addChatExtension({ payload, extension, text }); - } -} - -const default_options = { - hook: HOOK.AFTER_SUMMARY, - condition: STATUS.FAIL, -} - -const default_inputs = { - title: 'AI Failure Summary ✨' -} - -const default_inputs_teams = { - separator: true -} - -const default_inputs_slack = { - separator: false -} - -const default_inputs_chat = { - separator: true -} - -module.exports = { - run, - default_options -} \ No newline at end of file diff --git a/src/extensions/base.extension.js b/src/extensions/base.extension.js new file mode 100644 index 0000000..17b8adb --- /dev/null +++ b/src/extensions/base.extension.js @@ -0,0 +1,74 @@ +const logger = require('../utils/logger'); +const { addChatExtension, addSlackExtension, addTeamsExtension } = require('../helpers/extension.helper'); + +class BaseExtension { + + /** + * + * @param {import('..').Target} target + * @param {import('..').Extension} extension + * @param {import('..').TestResult} result + * @param {any} payload + * @param {any} root_payload + */ + constructor(target, extension, result, payload, root_payload) { + this.target = target; + this.extension = extension; + this.result = result; + this.payload = payload; + this.root_payload = root_payload; + + this.text = ''; + + /** + * @type {import('..').ExtensionInputs} + */ + this.default_inputs = {}; + + /** + * @type {import('..').IExtensionDefaultOptions} + */ + this.default_options = {}; + } + + updateExtensionInputs() { + this.extension.inputs = Object.assign({}, this.default_inputs, this.extension.inputs); + switch (this.target.name) { + case 'teams': + this.extension.inputs = Object.assign({}, { separator: true }, this.extension.inputs); + break; + case 'slack': + this.extension.inputs = Object.assign({}, { separator: false }, this.extension.inputs); + break; + case 'chat': + this.extension.inputs = Object.assign({}, { separator: true }, this.extension.inputs); + break; + default: + break; + } + } + + attach() { + if (!this.text) { + logger.warn(`⚠️ Extension '${this.extension.name}' has no text. Skipping.`); + return; + } + + switch (this.target.name) { + case 'teams': + addTeamsExtension({ payload: this.payload, extension: this.extension, text: this.text }); + break; + case 'slack': + addSlackExtension({ payload: this.payload, extension: this.extension, text: this.text }); + break; + case 'chat': + addChatExtension({ payload: this.payload, extension: this.extension, text: this.text }); + break; + default: + break; + } + } + +} + +module.exports = { BaseExtension } \ No newline at end of file diff --git a/src/extensions/index.js b/src/extensions/index.js index a4e2aea..f272346 100644 --- a/src/extensions/index.js +++ b/src/extensions/index.js @@ -7,7 +7,8 @@ const percy_analysis = require('./percy-analysis'); const custom = require('./custom'); const metadata = require('./metadata'); const ci_info = require('./ci-info'); -const ai_failure_summary = require('./ai-failure-summary'); +const { AIFailureSummaryExtension } = require('./ai-failure-summary.extension'); +const { SmartAnalysisExtension } = require('./smart-analysis.extension'); const { EXTENSION } = require('../helpers/constants'); const { checkCondition } = require('../helpers/helper'); const logger = require('../utils/logger'); @@ -17,7 +18,7 @@ async function run(options) { const extensions = target.extensions || []; for (let i = 0; i < extensions.length; i++) { const extension = extensions[i]; - const extension_runner = getExtensionRunner(extension); + const extension_runner = getExtensionRunner(extension, options); const extension_options = Object.assign({}, extension_runner.default_options, extension); if (extension_options.hook === hook) { if (await checkCondition({ condition: extension_options.condition, result, target, extension })) { @@ -35,7 +36,7 @@ async function run(options) { } } -function getExtensionRunner(extension) { +function getExtensionRunner(extension, options) { switch (extension.name) { case EXTENSION.HYPERLINKS: return hyperlinks; @@ -56,7 +57,9 @@ function getExtensionRunner(extension) { case EXTENSION.CI_INFO: return ci_info; case EXTENSION.AI_FAILURE_SUMMARY: - return ai_failure_summary; + return new AIFailureSummaryExtension(options.target, extension, options.result, options.payload, options.root_payload); + case EXTENSION.SMART_ANALYSIS: + return new SmartAnalysisExtension(options.target, extension, options.result, options.payload, options.root_payload); default: return require(extension.name); } diff --git a/src/extensions/mentions.js b/src/extensions/mentions.js index 0c34fae..fc5f6ac 100644 --- a/src/extensions/mentions.js +++ b/src/extensions/mentions.js @@ -87,7 +87,7 @@ function setPayloadWithMSTeamsEntities(payload) { } const default_options = { - hook: HOOK.END, + hook: HOOK.AFTER_SUMMARY, condition: STATUS.FAIL } diff --git a/src/extensions/smart-analysis.extension.js b/src/extensions/smart-analysis.extension.js new file mode 100644 index 0000000..ff85df1 --- /dev/null +++ b/src/extensions/smart-analysis.extension.js @@ -0,0 +1,59 @@ +const { BaseExtension } = require('./base.extension'); +const { STATUS, HOOK } = require("../helpers/constants"); + +class SmartAnalysisExtension extends BaseExtension { + + constructor(target, extension, result, payload, root_payload) { + super(target, extension, result, payload, root_payload); + this.#setDefaultOptions(); + this.#setDefaultInputs(); + this.updateExtensionInputs(); + } + + run() { + this.#setText(); + this.attach(); + } + + #setDefaultOptions() { + this.default_options.hook = HOOK.AFTER_SUMMARY, + this.default_options.condition = STATUS.PASS_OR_FAIL; + } + + #setDefaultInputs() { + this.default_inputs.title = 'Smart Analysis'; + this.default_inputs.title_link = ''; + } + + #setText() { + const data = this.extension.inputs.data; + + if (!data) { + return; + } + + /** + * @type {import('../beats/beats.types').IBeatExecutionMetric} + */ + const execution_metrics = data.execution_metrics[0]; + + const smart_analysis = []; + if (execution_metrics.always_failing) { + smart_analysis.push(`🚫 AF: ${execution_metrics.always_failing}`); + } + if (execution_metrics.newly_failed) { + smart_analysis.push(`⭕ NF: ${execution_metrics.newly_failed}`); + } + if (execution_metrics.flaky) { + smart_analysis.push(`❄️ FL: ${execution_metrics.flaky}`); + } + if (execution_metrics.recovered) { + smart_analysis.push(`🟢 RC: ${execution_metrics.recovered}`); + } + + this.text = smart_analysis.join(' | '); + } + +} + +module.exports = { SmartAnalysisExtension }; \ No newline at end of file diff --git a/src/helpers/constants.js b/src/helpers/constants.js index f2f1a5f..5a94632 100644 --- a/src/helpers/constants.js +++ b/src/helpers/constants.js @@ -21,6 +21,7 @@ const TARGET = Object.freeze({ const EXTENSION = Object.freeze({ AI_FAILURE_SUMMARY: 'ai-failure-summary', + SMART_ANALYSIS: 'smart-analysis', HYPERLINKS: 'hyperlinks', MENTIONS: 'mentions', REPORT_PORTAL_ANALYSIS: 'report-portal-analysis', @@ -37,7 +38,7 @@ const URLS = Object.freeze({ QUICK_CHART: 'https://quickchart.io' }); -const MIN_NODE_VERSION = '14.0.0' +const MIN_NODE_VERSION = 14; module.exports = Object.freeze({ STATUS, diff --git a/src/helpers/helper.js b/src/helpers/helper.js index 7de1e6d..41ec01b 100644 --- a/src/helpers/helper.js +++ b/src/helpers/helper.js @@ -1,7 +1,4 @@ const pretty_ms = require('pretty-ms'); -const os = require('os'); -const semver = require('semver'); -const {MIN_NODE_VERSION } = require('./constants'); const DATA_REF_PATTERN = /(\{[^\}]+\})/g; const ALLOWED_CONDITIONS = new Set(['pass', 'fail', 'passorfail']); @@ -34,7 +31,7 @@ function processText(raw) { return raw; } -/** +/** * @returns {import('../index').PublishConfig } */ function processData(data) { @@ -75,21 +72,9 @@ function getResultText({ result }) { } /** - * Checks Environment/System details - * OS Version, NodeJS Version - */ -function checkEnvDetails() { - if (!semver.gte(process.version, MIN_NODE_VERSION)) { - throw new Error(`❌ Supported NodeJS version is >= v${MIN_NODE_VERSION}. Current version is ${process.version}`) - } - - return `Environment Details - NodeJS ${process.version}, OS: ${os.platform()}, Version: ${os.release()}, arch: ${os.machine()}` -} - -/** - * + * * @param {object} param0 - * @param {string | Function} param0.condition + * @param {string | Function} param0.condition */ async function checkCondition({ condition, result, target, extension }) { if (typeof condition === 'function') { @@ -114,5 +99,4 @@ module.exports = { getTitleText, getResultText, checkCondition, - checkEnvDetails, } \ No newline at end of file diff --git a/src/index.d.ts b/src/index.d.ts index 0f6210c..d2457da 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -25,6 +25,7 @@ export interface ExtensionInputs { title?: string; title_link?: string; separator?: boolean; + data?: any; } export interface ReportPortalAnalysisInputs extends ExtensionInputs { @@ -227,6 +228,7 @@ export interface PublishReport { project?: string; run?: string; show_failure_summary?: boolean; + show_smart_analysis?: boolean; targets?: Target[]; extensions?: Extension[]; results?: ParseOptions[] | PerformanceParseOptions[] | CustomResultOptions[]; @@ -265,5 +267,10 @@ export interface CommandLineOptions { mstest?: string; } +export type IExtensionDefaultOptions = { + hook: Hook + condition: Condition +} + export function publish(options: PublishOptions): Promise export function defineConfig(config: PublishConfig): PublishConfig \ No newline at end of file diff --git a/test/beats.spec.js b/test/beats.spec.js index c7d7f89..4f89de3 100644 --- a/test/beats.spec.js +++ b/test/beats.spec.js @@ -14,7 +14,8 @@ describe('TestBeats', () => { it('should send results to beats', async () => { const id1 = mock.addInteraction('post test results to beats'); - const id2 = mock.addInteraction('post test-summary with beats to teams'); + const id2 = mock.addInteraction('get test results from beats'); + const id3 = mock.addInteraction('post test-summary with beats to teams'); await publish({ config: { api_key: 'api-key', @@ -40,6 +41,7 @@ describe('TestBeats', () => { }); assert.equal(mock.getInteraction(id1).exercised, true); assert.equal(mock.getInteraction(id2).exercised, true); + assert.equal(mock.getInteraction(id3).exercised, true); }); it('should send results with failures to beats', async () => { @@ -128,4 +130,36 @@ describe('TestBeats', () => { assert.equal(mock.getInteraction(id1).exercised, true); }); + it('should send results with smart analysis to beats', async () => { + const id1 = mock.addInteraction('post test results to beats'); + const id2 = mock.addInteraction('get test results with smart analysis from beats'); + const id3 = mock.addInteraction('post test-summary with beats to teams with ai failure summary and smart analysis'); + await publish({ + config: { + api_key: 'api-key', + project: 'project-name', + run: 'build-name', + targets: [ + { + name: 'teams', + inputs: { + url: 'http://localhost:9393/message' + } + } + ], + results: [ + { + type: 'testng', + files: [ + 'test/data/testng/single-suite-failures.xml' + ] + } + ] + } + }); + assert.equal(mock.getInteraction(id1).exercised, true); + assert.equal(mock.getInteraction(id2).exercised, true); + assert.equal(mock.getInteraction(id3).exercised, true); + }); + }); \ No newline at end of file diff --git a/test/cli.spec.js b/test/cli.spec.js index 0135ca8..2c2f29c 100644 --- a/test/cli.spec.js +++ b/test/cli.spec.js @@ -42,6 +42,7 @@ describe('CLI', () => { it('publish results to beats', (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 --teams http://localhost:9393/message --testng test/data/testng/single-suite.xml', (error, stdout, stderr) => { console.log(stdout); diff --git a/test/mocks/beats.mock.js b/test/mocks/beats.mock.js index 659b9d8..b15d0b1 100644 --- a/test/mocks/beats.mock.js +++ b/test/mocks/beats.mock.js @@ -32,6 +32,7 @@ addInteractionHandler('get test results from beats', () => { body: { id: 'test-run-id', "failure_summary_status": "COMPLETED", + "smart_analysis_status": "COMPLETED", "execution_metrics": [ { "failure_summary": "test failure summary" @@ -42,6 +43,38 @@ addInteractionHandler('get test results from beats', () => { } }); +addInteractionHandler('get test results with smart analysis from beats', () => { + return { + strict: false, + request: { + method: 'GET', + path: '/api/core/v1/test-runs/key', + queryParams: { + "id": "test-run-id" + } + }, + response: { + status: 200, + body: { + id: 'test-run-id', + "failure_summary_status": "COMPLETED", + "smart_analysis_status": "SKIPPED", + "execution_metrics": [ + { + "failure_summary": "", + "newly_failed": 1, + "always_failing": 1, + "recovered": 1, + "added": 0, + "removed": 0, + "flaky": 1, + } + ] + } + } + } +}); + addInteractionHandler('upload attachments', () => { return { strict: false, diff --git a/test/mocks/teams.mock.js b/test/mocks/teams.mock.js index 1a353cb..38724c8 100644 --- a/test/mocks/teams.mock.js +++ b/test/mocks/teams.mock.js @@ -1576,6 +1576,57 @@ addInteractionHandler('post test-summary with beats to teams with ai failure sum } }); +addInteractionHandler('post test-summary with beats to teams with ai failure summary and smart analysis', () => { + return { + request: { + method: 'POST', + path: '/message', + body: { + "type": "message", + "attachments": [ + { + "contentType": "application/vnd.microsoft.card.adaptive", + "content": { + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "type": "AdaptiveCard", + "version": "1.0", + "body": [ + { + "type": "TextBlock", + "text": "[❌ Default suite](http://localhost:9393/reports/test-run-id)", + "size": "medium", + "weight": "bolder", + "wrap": true + }, + { + "@DATA:TEMPLATE@": "TEAMS_ROOT_RESULTS_SINGLE_SUITE_FAILURES", + }, + { + "type": "TextBlock", + "text": "Smart Analysis", + "isSubtle": true, + "weight": "bolder", + "separator": true, + "wrap": true + }, + { + "type": "TextBlock", + "text": "🚫 AF: 1 | ⭕ NF: 1 | ❄️ FL: 1 | 🟢 RC: 1", + "wrap": true + } + ], + "actions": [] + } + } + ] + } + }, + response: { + status: 200 + } + } +}); + addInteractionHandler('post test-summary to teams with strict as false', () => { return { strict: false,