Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: failure analysis #251

Merged
merged 1 commit into from
Oct 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"cSpell.words": [
"analysed",
"mstest",
"nunit",
"pactum",
Expand Down
51 changes: 33 additions & 18 deletions src/beats/beats.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const { getCIInformation } = require('../helpers/ci');
const logger = require('../utils/logger');
const { BeatsApi } = require('./beats.api');
const { HOOK } = require('../helpers/constants');
const { HOOK, PROCESS_STATUS } = require('../helpers/constants');
const TestResult = require('test-results-parser/src/models/TestResult');
const { BeatsAttachments } = require('./beats.attachments');

Expand Down Expand Up @@ -31,9 +31,7 @@ class Beats {
await this.#publishTestResults();
await this.#uploadAttachments();
this.#updateTitleLink();
await this.#attachFailureSummary();
await this.#attachSmartAnalysis();
await this.#attachErrorClusters();
await this.#attachExtensions();
}

#setCIInfo() {
Expand Down Expand Up @@ -104,13 +102,20 @@ class Beats {
}
}

async #attachFailureSummary() {
async #attachExtensions() {
if (!this.test_run_id) {
return;
}
if (!this.config.targets) {
return;
}
await this.#attachFailureSummary();
await this.#attachFailureAnalysis();
await this.#attachSmartAnalysis();
await this.#attachErrorClusters();
}

async #attachFailureSummary() {
if (this.result.status !== 'FAIL') {
return;
}
Expand All @@ -132,13 +137,29 @@ class Beats {
}
}

async #attachSmartAnalysis() {
if (!this.test_run_id) {
async #attachFailureAnalysis() {
if (this.result.status !== 'FAIL') {
return;
}
if (!this.config.targets) {
if (this.config.show_failure_analysis === false) {
return;
}
try {
logger.info('🪄 Fetching Failure Analysis...');
await this.#setTestRun('Failure Analysis Status', 'failure_analysis_status');
this.config.extensions.push({
name: 'failure-analysis',
hook: HOOK.AFTER_SUMMARY,
inputs: {
data: this.test_run
}
});
} catch (error) {
logger.error(`❌ Unable to attach failure analysis: ${error.message}`, error);
}
}

async #attachSmartAnalysis() {
if (this.config.show_smart_analysis === false) {
return;
}
Expand All @@ -165,7 +186,7 @@ class Beats {
}

async #setTestRun(text, wait_for = 'smart_analysis_status') {
if (this.test_run && this.test_run[wait_for] === 'COMPLETED') {
if (this.test_run && this.test_run[wait_for] === PROCESS_STATUS.COMPLETED) {
return;
}
let retry = 3;
Expand All @@ -175,13 +196,13 @@ class Beats {
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':
case PROCESS_STATUS.COMPLETED:
logger.debug(`☑️ ${text} generated successfully`);
return;
case 'FAILED':
case PROCESS_STATUS.FAILED:
logger.error(`❌ Failed to generate ${text}`);
return;
case 'SKIPPED':
case PROCESS_STATUS.SKIPPED:
logger.warn(`❗ Skipped generating ${text}`);
return;
}
Expand All @@ -191,12 +212,6 @@ class Beats {
}

async #attachErrorClusters() {
if (!this.test_run_id) {
return;
}
if (!this.config.targets) {
return;
}
if (this.result.status !== 'FAIL') {
return;
}
Expand Down
6 changes: 6 additions & 0 deletions src/beats/beats.types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ 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
Expand Down
58 changes: 58 additions & 0 deletions src/extensions/failure-analysis.extension.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
const { BaseExtension } = require('./base.extension');
const { STATUS, HOOK } = require("../helpers/constants");

class FailureAnalysisExtension extends BaseExtension {

constructor(target, extension, result, payload, root_payload) {
super(target, extension, result, payload, root_payload);
this.#setDefaultOptions();
this.#setDefaultInputs();
this.updateExtensionInputs();
}

#setDefaultOptions() {
this.default_options.hook = HOOK.AFTER_SUMMARY,
this.default_options.condition = STATUS.PASS_OR_FAIL;
}

#setDefaultInputs() {
this.default_inputs.title = '';
this.default_inputs.title_link = '';
}

run() {
this.#setText();
this.attach();
}

#setText() {
const data = this.extension.inputs.data;
if (!data) {
return;
}

/**
* @type {import('../beats/beats.types').IBeatExecutionMetric}
*/
const execution_metrics = data.execution_metrics[0];

if (!execution_metrics) {
logger.warn('⚠️ No execution metrics found. Skipping.');
return;
}

const failure_analysis = [];

if (execution_metrics.to_investigate) {
failure_analysis.push(`🔎 To Investigate: ${execution_metrics.to_investigate}`);
}
if (execution_metrics.auto_analysed) {
failure_analysis.push(`🪄 Auto Analysed: ${execution_metrics.auto_analysed}`);
}

this.text = failure_analysis.join('  •  ');
}

}

module.exports = { FailureAnalysisExtension };
3 changes: 3 additions & 0 deletions src/extensions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const { EXTENSION } = require('../helpers/constants');
const { checkCondition } = require('../helpers/helper');
const logger = require('../utils/logger');
const { ErrorClustersExtension } = require('./error-clusters.extension');
const { FailureAnalysisExtension } = require('./failure-analysis.extension');

async function run(options) {
const { target, result, hook } = options;
Expand Down Expand Up @@ -59,6 +60,8 @@ function getExtensionRunner(extension, options) {
return new CIInfoExtension(options.target, extension, options.result, options.payload, options.root_payload);
case EXTENSION.AI_FAILURE_SUMMARY:
return new AIFailureSummaryExtension(options.target, extension, options.result, options.payload, options.root_payload);
case EXTENSION.FAILURE_ANALYSIS:
return new FailureAnalysisExtension(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);
case EXTENSION.ERROR_CLUSTERS:
Expand Down
4 changes: 2 additions & 2 deletions src/extensions/smart-analysis.extension.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
9 changes: 9 additions & 0 deletions src/helpers/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const TARGET = Object.freeze({

const EXTENSION = Object.freeze({
AI_FAILURE_SUMMARY: 'ai-failure-summary',
FAILURE_ANALYSIS: 'failure-analysis',
SMART_ANALYSIS: 'smart-analysis',
ERROR_CLUSTERS: 'error-clusters',
HYPERLINKS: 'hyperlinks',
Expand All @@ -39,6 +40,13 @@ const URLS = Object.freeze({
QUICK_CHART: 'https://quickchart.io'
});

const PROCESS_STATUS = Object.freeze({
RUNNING: 'RUNNING',
COMPLETED: 'COMPLETED',
FAILED: 'FAILED',
SKIPPED: 'SKIPPED',
});

const MIN_NODE_VERSION = 14;

module.exports = Object.freeze({
Expand All @@ -47,5 +55,6 @@ module.exports = Object.freeze({
TARGET,
EXTENSION,
URLS,
PROCESS_STATUS,
MIN_NODE_VERSION
});
1 change: 1 addition & 0 deletions src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ export interface PublishReport {
project?: string;
run?: string;
show_failure_summary?: boolean;
show_failure_analysis?: boolean;
show_smart_analysis?: boolean;
show_error_clusters?: boolean;
targets?: Target[];
Expand Down
34 changes: 34 additions & 0 deletions test/beats.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -230,4 +230,38 @@ describe('TestBeats', () => {
assert.equal(mock.getInteraction(id4).exercised, true);
});

it('should send results with failure analysis to beats', async () => {
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');
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);
assert.equal(mock.getInteraction(id4).exercised, true);
});

});
37 changes: 37 additions & 0 deletions test/mocks/beats.mock.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ addInteractionHandler('get test results from beats', () => {
body: {
id: 'test-run-id',
"failure_summary_status": "COMPLETED",
"failure_analysis_status": "COMPLETED",
"smart_analysis_status": "COMPLETED",
"execution_metrics": [
{
Expand Down Expand Up @@ -69,6 +70,42 @@ addInteractionHandler('get test results with smart analysis from beats', () => {
}
});

addInteractionHandler('get test results with failure analysis from beats', () => {
return {
strict: false,
request: {
method: 'GET',
path: '/api/core/v1/test-runs/test-run-id'
},
response: {
status: 200,
body: {
id: 'test-run-id',
"failure_summary_status": "COMPLETED",
"failure_analysis_status": "COMPLETED",
"smart_analysis_status": "SKIPPED",
"execution_metrics": [
{
"failure_summary": "",
"newly_failed": 1,
"always_failing": 1,
"recovered": 1,
"added": 0,
"removed": 0,
"flaky": 1,
"product_bugs": 1,
"environment_issues": 1,
"automation_bugs": 1,
"not_a_defects": 1,
"to_investigate": 1,
"auto_analysed": 1
}
]
}
}
}
});

addInteractionHandler('get error clusters from beats', () => {
return {
strict: false,
Expand Down
Loading
Loading