Skip to content

Commit

Permalink
feat: smart analysis extension (#202)
Browse files Browse the repository at this point in the history
* chore: removed dependency on semver

* feat: smart analysis extension

* feat: added beats config in cli
  • Loading branch information
ASaiAnudeep authored Jun 30, 2024
1 parent d65da76 commit 1d05f47
Show file tree
Hide file tree
Showing 19 changed files with 419 additions and 144 deletions.
9 changes: 5 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -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": {
Expand Down
88 changes: 56 additions & 32 deletions src/beats/beats.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class Beats {
this.result = result;
this.api = new BeatsApi(config);
this.test_run_id = '';
this.test_run = null;
}

async publish() {
Expand All @@ -27,6 +28,7 @@ class Beats {
await this.#uploadAttachments();
this.#updateTitleLink();
await this.#attachFailureSummary();
await this.#attachSmartAnalysis();
}

#setCIInfo() {
Expand Down Expand Up @@ -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`);
}

}
Expand Down
18 changes: 18 additions & 0 deletions src/beats/beats.types.d.ts
Original file line number Diff line number Diff line change
@@ -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
}
3 changes: 3 additions & 0 deletions src/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
28 changes: 20 additions & 8 deletions src/commands/publish.command.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
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');
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 {

Expand All @@ -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();
Expand All @@ -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();
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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 {
Expand Down
43 changes: 43 additions & 0 deletions src/extensions/ai-failure-summary.extension.js
Original file line number Diff line number Diff line change
@@ -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 }
Loading

0 comments on commit 1d05f47

Please sign in to comment.