fix/902 --bail flag not stopping execution when a test fails (#8103)

* fix/902 --bail flag not stopping execution when a test fails in a CSV file

* addressed review comments

* addressed review comments

* updated the package-lock file

* addressed review comments

* addressed review comments

* fix: add stripExtension utility to suitename assignment in run command
This commit is contained in:
sharan-bruno
2026-05-28 16:39:25 +05:30
committed by GitHub
parent b43a5e6e0a
commit 49088e98c8
6 changed files with 247 additions and 175 deletions

View File

@@ -59,6 +59,7 @@
"axios-ntlm": "^1.4.2",
"chai": "^4.3.7",
"chalk": "^3.0.0",
"cli-table3": "^0.6.5",
"decomment": "^0.9.5",
"form-data": "4.0.4",
"fs-extra": "^10.1.0",

View File

@@ -4,17 +4,17 @@ const path = require('path');
const yaml = require('js-yaml');
const { forOwn, cloneDeep } = require('lodash');
const { getRunnerSummary } = require('@usebruno/common/runner');
const { exists, isFile, isDirectory } = require('../utils/filesystem');
const { exists, isFile, isDirectory, stripExtension } = require('../utils/filesystem');
const { runSingleRequest } = require('../runner/run-single-request');
const { getEnvVars } = require('../utils/bru');
const { parseEnvironmentJson } = require('../utils/environment');
const { isRequestTagsIncluded } = require('@usebruno/common');
const makeJUnitOutput = require('../reporters/junit');
const makeHtmlOutput = require('../reporters/html');
const { rpad } = require('../utils/common');
const { getOptions } = require('../utils/bru');
const { parseDotEnv, parseEnvironment } = require('@usebruno/filestore');
const constants = require('../constants');
const Table = require('cli-table3');
const { findItemInCollection, createCollectionJsonFromPathname, getCallStack, FORMAT_CONFIG } = require('../utils/collection');
const { hasExecutableTestInScript } = require('../utils/request');
const { createSkippedFileResults } = require('../utils/run');
@@ -23,86 +23,64 @@ const { getSystemProxy } = require('@usebruno/requests');
const command = 'run [paths...]';
const desc = 'Run one or more requests/folders';
const formatTestSummary = (label, maxLength, passed, failed, total, errorCount = 0, skippedCount = 0) => {
const parts = [
`${rpad(label, maxLength)} ${chalk.green(`${passed} passed`)}`
];
const formatRequestsCellFromSummary = (summary) => {
const total = summary.totalRequests || 0;
const passed = summary.passedRequests || 0;
const failedOrErrored = (summary.failedRequests || 0) + (summary.errorRequests || 0);
const totalSkipped = summary.skippedRequests || 0;
const skippedByBail = summary.skippedByBail || 0;
const skippedByUser = Math.max(totalSkipped - skippedByBail, 0);
if (failed > 0) parts.push(chalk.red(`${failed} failed`));
if (errorCount > 0) parts.push(chalk.red(`${errorCount} error`));
if (skippedCount > 0) parts.push(chalk.magenta(`${skippedCount} skipped`));
const parts = [];
if (passed > 0) parts.push(chalk.green(`${passed} Passed`));
if (failedOrErrored > 0) parts.push(chalk.red(`${failedOrErrored} Failed`));
if (skippedByUser > 0) parts.push(chalk.magenta(`${skippedByUser} Skipped`));
if (skippedByBail > 0) parts.push(chalk.hex(constants.COLORS.ORANGE)(`${skippedByBail} Skipped (Bail)`));
parts.push(`${total} total`);
return parts.length ? `${total} (${parts.join(', ')})` : `${total}`;
};
return parts.join(', ');
const printGenericTable = (headers, rows, title) => {
const colAligns = headers.map((_, idx) => (idx === 0 ? 'left' : 'center'));
const table = new Table({ head: headers, style: { head: [], border: [] }, colAligns });
rows.forEach((row) => table.push(row));
console.log('\n' + chalk.bold(title));
console.log(table.toString());
};
const printRunSummary = (results) => {
const {
totalRequests,
passedRequests,
failedRequests,
skippedRequests,
errorRequests,
totalAssertions,
passedAssertions,
failedAssertions,
totalTests,
passedTests,
failedTests,
totalPreRequestTests,
passedPreRequestTests,
failedPreRequestTests,
totalPostResponseTests,
passedPostResponseTests,
failedPostResponseTests
} = getRunnerSummary(results);
const summary = getRunnerSummary(results);
const maxLength = 12;
const duration = Math.round(
results.reduce((acc, res) => acc + (res.runDuration || 0), 0) * 1000
);
const requestSummary = formatTestSummary('Requests:', maxLength, passedRequests, failedRequests, totalRequests, errorRequests, skippedRequests);
const testSummary = formatTestSummary('Tests:', maxLength, passedTests, failedTests, totalTests);
const assertSummary = formatTestSummary('Assertions:', maxLength, passedAssertions, failedAssertions, totalAssertions);
const hasFailures
= summary.failedRequests > 0
|| summary.failedAssertions > 0
|| summary.failedTests > 0
|| (summary.errorRequests || 0) > 0;
let preRequestTestSummary = '';
if (totalPreRequestTests > 0) {
preRequestTestSummary = formatTestSummary('Pre-Request Tests:', maxLength, passedPreRequestTests, failedPreRequestTests, totalPreRequestTests);
}
const status = hasFailures
? chalk.red.bold('✗ FAIL')
: chalk.green.bold('✓ PASS');
let postResponseTestSummary = '';
if (totalPostResponseTests > 0) {
postResponseTestSummary = formatTestSummary('Post-Response Tests:', maxLength, passedPostResponseTests, failedPostResponseTests, totalPostResponseTests);
}
const requests = formatRequestsCellFromSummary(summary);
const tests = `${summary.passedTests}/${summary.totalTests}`;
const assertions = `${summary.passedAssertions}/${summary.totalAssertions}`;
console.log('\n' + chalk.bold(requestSummary));
if (preRequestTestSummary) {
console.log(chalk.bold(preRequestTestSummary));
}
if (postResponseTestSummary) {
console.log(chalk.bold(postResponseTestSummary));
}
console.log(chalk.bold(testSummary));
console.log(chalk.bold(assertSummary));
const headers = [chalk.bold('Metric'), chalk.bold('Result')];
const rows = [
['Status', status],
['Requests', requests],
['Tests', tests],
['Assertions', assertions],
['Duration (ms)', duration]
];
return {
totalRequests,
passedRequests,
failedRequests,
skippedRequests,
errorRequests,
totalAssertions,
passedAssertions,
failedAssertions,
totalTests,
passedTests,
failedTests,
totalPreRequestTests,
passedPreRequestTests,
failedPreRequestTests,
totalPostResponseTests,
passedPostResponseTests,
failedPostResponseTests
};
printGenericTable(headers, rows, '📊 Execution Summary');
return summary;
};
const getJsSandboxRuntime = (sandbox) => {
@@ -679,6 +657,7 @@ const handler = async function (argv) {
let currentRequestIndex = 0;
let nJumps = 0; // count the number of jumps to avoid infinite loops
let bailInfo = null; // populated only if --bail triggers
while (currentRequestIndex < requestItems.length) {
const requestItem = cloneDeep(requestItems[currentRequestIndex]);
const { name, pathname } = requestItem;
@@ -712,7 +691,7 @@ const handler = async function (argv) {
results.push({
...result,
runDuration: process.hrtime(start)[0] + process.hrtime(start)[1] / 1e9,
suitename: pathname.replace('.bru', ''),
suitename: stripExtension(pathname),
name,
path: result.test?.filename || path.relative(collectionPath, pathname)
});
@@ -732,6 +711,64 @@ const handler = async function (argv) {
const preRequestTestFailure = result?.preRequestTestResults?.find((iter) => iter.status === 'fail');
const postResponseTestFailure = result?.postResponseTestResults?.find((iter) => iter.status === 'fail');
if (requestFailure || testFailure || assertionFailure || preRequestTestFailure || postResponseTestFailure) {
// Pick the most specific reason for the user-facing message
let bailReason;
if (requestFailure) bailReason = 'request failure';
else if (assertionFailure) bailReason = 'assertion failure';
else if (preRequestTestFailure) bailReason = 'pre-request test failure';
else if (postResponseTestFailure) bailReason = 'post-response test failure';
else bailReason = 'test failure';
const remainingItems = requestItems.slice(currentRequestIndex + 1);
// Synthesize "Skipped (Bail)" placeholder results for the requests that never
// ran due to bail. These let getRunnerSummary count them as skipped, and the
// summary table can distinguish them from user-initiated skips via skipReason.
for (const ri of remainingItems) {
const relativePath = path.relative(collectionPath, ri.pathname);
results.push({
test: {
filename: relativePath
},
request: {
method: null,
url: null,
headers: null,
data: null
},
response: {
status: 'skipped',
statusText: null,
data: null,
responseTime: 0
},
status: 'skipped',
skipped: true,
skipReason: 'bail',
testResults: [],
assertionResults: [],
preRequestTestResults: [],
postResponseTestResults: [],
runDuration: 0,
suitename: stripExtension(ri.pathname),
name: ri.name,
path: relativePath
});
}
bailInfo = {
bailed: true,
bailReason,
bailedAt: name,
skippedByBail: remainingItems.length
};
console.log(
'\n' + chalk.hex(constants.COLORS.ORANGE)(
`Bail: Stopping run, ${bailReason} in "${name}". Remaining ${remainingItems.length} request(s) skipped.`
)
);
break;
}
}

View File

@@ -2,6 +2,9 @@ const { version } = require('../package.json');
const CLI_EPILOGUE = `Documentation: https://docs.usebruno.com (v${version})`;
const CLI_VERSION = version;
const COLORS = {
ORANGE: '#FFA500'
};
// Exit codes
const EXIT_STATUS = {
@@ -38,5 +41,6 @@ const EXIT_STATUS = {
module.exports = {
CLI_EPILOGUE,
CLI_VERSION,
EXIT_STATUS
EXIT_STATUS,
COLORS
};

View File

@@ -7,6 +7,7 @@ export const getRunnerSummary = (results: T_RunnerRequestExecutionResult[]): T_R
let failedRequests = 0;
let errorRequests = 0;
let skippedRequests = 0;
let skippedByBail = 0;
let totalAssertions = 0;
let passedAssertions = 0;
let failedAssertions = 0;
@@ -30,6 +31,7 @@ export const getRunnerSummary = (results: T_RunnerRequestExecutionResult[]): T_R
if (status === 'skipped') {
skippedRequests += 1;
if (result.skipReason === 'bail') skippedByBail += 1;
continue;
}
@@ -94,6 +96,7 @@ export const getRunnerSummary = (results: T_RunnerRequestExecutionResult[]): T_R
failedRequests,
errorRequests,
skippedRequests,
skippedByBail,
totalAssertions,
passedAssertions,
failedAssertions,

View File

@@ -88,6 +88,8 @@ export type T_RunnerRequestExecutionResult = {
request: T_EmptyRequest | T_Request;
response: T_EmptyResponse | T_Response | T_SkippedResponse;
status: null | undefined | string;
skipped?: boolean;
skipReason?: string;
error: null | undefined | string;
assertionResults?: T_AssertionResult[];
testResults?: T_TestResult[];
@@ -110,6 +112,7 @@ export type T_RunSummary = {
failedRequests: number;
errorRequests: number;
skippedRequests: number;
skippedByBail: number;
totalAssertions: number;
passedAssertions: number;
failedAssertions: number;