mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-07 22:18:33 +00:00
feat: improve stack traces for script and test failures (#7181)
* chore: update package-lock.json to include yaml dependency * feat: add script-aware stack traces and source context for script/test failures * chore: sync package-lock.json with yaml dependency in bruno-js * fix: handle null check in getErrorTypeName and align JSDoc style * refactor: derive script path from request.pathname and use SCRIPT_TYPES constant * fix: avoid showing source context for collection/folder script errors * feat: map collection/folder script errors to source file and line * fix: update error formatting and avoid undefined message * fix: resolve script block location in collection/folder yml files * refactor: use script wrapper utils and rename wrapper offsets * refactor: move script wrapper to utils, add wrapScriptInClosure fn
This commit is contained in:
@@ -42,6 +42,7 @@ const prepareRequest = async (item = {}, collection = {}) => {
|
||||
url: request.url,
|
||||
headers: headers,
|
||||
name: item.name,
|
||||
pathname: item.pathname,
|
||||
tags: item.tags || [],
|
||||
pathParams: request.params?.filter((param) => param.type === 'path'),
|
||||
settings: item.settings,
|
||||
@@ -398,6 +399,7 @@ const prepareRequest = async (item = {}, collection = {}) => {
|
||||
|
||||
if (request.tests) {
|
||||
axiosRequest.tests = request.tests;
|
||||
axiosRequest.testsMetadata = request.testsMetadata;
|
||||
}
|
||||
|
||||
axiosRequest.vars = request.vars;
|
||||
|
||||
@@ -6,7 +6,7 @@ const { forOwn, isUndefined, isNull, each, extend, get, compact } = require('lod
|
||||
const prepareRequest = require('./prepare-request');
|
||||
const interpolateVars = require('./interpolate-vars');
|
||||
const { interpolateString, interpolateObject } = require('./interpolate-string');
|
||||
const { ScriptRuntime, TestRuntime, VarsRuntime, AssertRuntime } = require('@usebruno/js');
|
||||
const { ScriptRuntime, TestRuntime, VarsRuntime, AssertRuntime, formatErrorWithContext, SCRIPT_TYPES } = require('@usebruno/js');
|
||||
const { stripExtension } = require('../utils/filesystem');
|
||||
const { getOptions } = require('../utils/bru');
|
||||
const https = require('https');
|
||||
@@ -102,7 +102,7 @@ const runSingleRequest = async function (
|
||||
const { pathname: itemPathname } = item;
|
||||
const relativeItemPathname = path.relative(collectionPath, itemPathname);
|
||||
|
||||
const logResults = (results, title) => {
|
||||
const logResults = (results, title, scriptType = null, request = null) => {
|
||||
if (results?.length) {
|
||||
if (title) {
|
||||
console.log(chalk.dim(title));
|
||||
@@ -113,7 +113,18 @@ const runSingleRequest = async function (
|
||||
console.log(chalk.green(` ✓ `) + chalk.dim(message));
|
||||
} else {
|
||||
console.log(chalk.red(` ✕ `) + chalk.red(message));
|
||||
if (r.error) {
|
||||
if (r.stack && scriptType) {
|
||||
const errorObj = {
|
||||
message: r.error || message,
|
||||
stack: r.stack,
|
||||
name: r.errorName || 'Error'
|
||||
};
|
||||
const metadata = scriptType === SCRIPT_TYPES.PRE_REQUEST ? request?.script?.reqMetadata
|
||||
: scriptType === SCRIPT_TYPES.POST_RESPONSE ? request?.script?.resMetadata
|
||||
: scriptType === SCRIPT_TYPES.TEST ? request?.testsMetadata
|
||||
: null;
|
||||
console.log('\n' + formatErrorWithContext(errorObj, relativeItemPathname, scriptType, 5, metadata) + '\n');
|
||||
} else if (r.error) {
|
||||
console.log(chalk.red(` ${r.error}`));
|
||||
}
|
||||
}
|
||||
@@ -208,7 +219,7 @@ const runSingleRequest = async function (
|
||||
if (requestScriptFile?.length) {
|
||||
const scriptRuntime = new ScriptRuntime({ runtime: scriptingConfig?.runtime });
|
||||
try {
|
||||
const result = await scriptRuntime.runRequestScript(decomment(requestScriptFile),
|
||||
const result = await scriptRuntime.runRequestScript(decomment(requestScriptFile, { space: true }),
|
||||
request,
|
||||
envVariables,
|
||||
runtimeVariables,
|
||||
@@ -263,8 +274,8 @@ const runSingleRequest = async function (
|
||||
preRequestTestResults = result?.results || [];
|
||||
} catch (error) {
|
||||
// Pre-request errors are treated as request errors (we return early with status: 'error'), not as failures. Unlike post-response and test script errors, we do not add a synthetic fail and continue.
|
||||
console.error('Pre-request script execution error:', error);
|
||||
console.log(chalk.red(stripExtension(relativeItemPathname)) + chalk.dim(` (${error.message})`));
|
||||
console.error(chalk.red(`[${relativeItemPathname}] Pre-request script error:`));
|
||||
console.log('\n' + formatErrorWithContext(error, relativeItemPathname, SCRIPT_TYPES.PRE_REQUEST, 5, request.script?.reqMetadata) + '\n');
|
||||
|
||||
// Extract partial results from the error (tests that passed before the error)
|
||||
preRequestTestResults = error?.partialResults?.results || [];
|
||||
@@ -279,7 +290,7 @@ const runSingleRequest = async function (
|
||||
shouldStopRunnerExecution = true;
|
||||
}
|
||||
|
||||
logResults(preRequestTestResults, 'Pre-Request Tests');
|
||||
logResults(preRequestTestResults, 'Pre-Request Tests', SCRIPT_TYPES.PRE_REQUEST, request);
|
||||
|
||||
// Pre-request script error: execution didn't complete (request never sent). Return early so we don't run the HTTP request, post-response script, assertions, or tests.
|
||||
return {
|
||||
@@ -764,7 +775,7 @@ const runSingleRequest = async function (
|
||||
);
|
||||
|
||||
// Log pre-request test results
|
||||
logResults(preRequestTestResults, 'Pre-Request Tests');
|
||||
logResults(preRequestTestResults, 'Pre-Request Tests', SCRIPT_TYPES.PRE_REQUEST, request);
|
||||
|
||||
// run post-response vars
|
||||
const postResponseVars = get(item, 'request.vars.res');
|
||||
@@ -787,7 +798,7 @@ const runSingleRequest = async function (
|
||||
const scriptRuntime = new ScriptRuntime({ runtime: scriptingConfig?.runtime });
|
||||
try {
|
||||
const result = await scriptRuntime.runResponseScript(
|
||||
decomment(responseScriptFile),
|
||||
decomment(responseScriptFile, { space: true }),
|
||||
request,
|
||||
response,
|
||||
envVariables,
|
||||
@@ -814,9 +825,10 @@ const runSingleRequest = async function (
|
||||
}
|
||||
|
||||
postResponseTestResults = result?.results || [];
|
||||
logResults(postResponseTestResults, 'Post-Response Tests');
|
||||
logResults(postResponseTestResults, 'Post-Response Tests', SCRIPT_TYPES.POST_RESPONSE, request);
|
||||
} catch (error) {
|
||||
console.error('Post-response script execution error:', error);
|
||||
console.error(chalk.red(`[${relativeItemPathname}] Post-response script error:`));
|
||||
console.log('\n' + formatErrorWithContext(error, relativeItemPathname, SCRIPT_TYPES.POST_RESPONSE, 5, request.script?.resMetadata) + '\n');
|
||||
|
||||
const partialResults = error?.partialResults?.results || [];
|
||||
postResponseTestResults = [
|
||||
@@ -837,7 +849,7 @@ const runSingleRequest = async function (
|
||||
shouldStopRunnerExecution = true;
|
||||
}
|
||||
|
||||
logResults(postResponseTestResults, 'Post-Response Tests');
|
||||
logResults(postResponseTestResults, 'Post-Response Tests', SCRIPT_TYPES.POST_RESPONSE, request);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -862,7 +874,7 @@ const runSingleRequest = async function (
|
||||
const testRuntime = new TestRuntime({ runtime: scriptingConfig?.runtime });
|
||||
try {
|
||||
const result = await testRuntime.runTests(
|
||||
decomment(testFile),
|
||||
decomment(testFile, { space: true }),
|
||||
request,
|
||||
response,
|
||||
envVariables,
|
||||
@@ -890,9 +902,10 @@ const runSingleRequest = async function (
|
||||
}
|
||||
}
|
||||
|
||||
logResults(testResults, 'Tests');
|
||||
logResults(testResults, 'Tests', SCRIPT_TYPES.TEST, request);
|
||||
} catch (error) {
|
||||
console.error('Test script execution error:', error);
|
||||
console.error(chalk.red(`[${relativeItemPathname}] Test script error:`));
|
||||
console.log('\n' + formatErrorWithContext(error, relativeItemPathname, SCRIPT_TYPES.TEST, 5, request.testsMetadata) + '\n');
|
||||
|
||||
const partialResults = error?.partialResults?.results || [];
|
||||
testResults = [
|
||||
@@ -913,7 +926,7 @@ const runSingleRequest = async function (
|
||||
shouldStopRunnerExecution = true;
|
||||
}
|
||||
|
||||
logResults(testResults, 'Tests');
|
||||
logResults(testResults, 'Tests', SCRIPT_TYPES.TEST, request);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const { get, each, find, compact } = require('lodash');
|
||||
const { get, each, find } = require('lodash');
|
||||
const os = require('os');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
@@ -227,31 +227,101 @@ ${script}
|
||||
})();`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Wraps each script segment in an async IIFE, joins them with double newlines,
|
||||
* and records the line range of the "request" segment for stack-trace mapping.
|
||||
*
|
||||
* Merged scripts = collection + folders + request; the runtime runs one combined
|
||||
* script, so we need requestStartLine/requestEndLine to map a VM line number
|
||||
* back to the request's script in the .bru file.
|
||||
*
|
||||
* @param {string[]} scripts - Script segments in order (e.g. collection, folders, request).
|
||||
* @param {number} requestIndex - Index in scripts of the request-level segment.
|
||||
* @returns {{ code: string, metadata: { requestStartLine: number, requestEndLine: number } | null }}
|
||||
*/
|
||||
const wrapAndJoinScripts = (scripts, requestIndex, segmentSources = null) => {
|
||||
const wrapped = scripts.map((s) => wrapScriptInClosure(s));
|
||||
const code = wrapped.filter(Boolean).join('\n\n');
|
||||
|
||||
let offset = 0;
|
||||
let metadata = null;
|
||||
const segments = [];
|
||||
|
||||
for (let i = 0; i < scripts.length; i++) {
|
||||
if (!wrapped[i]) continue;
|
||||
const lineCount = wrapped[i].split('\n').length;
|
||||
const startLine = offset + 1;
|
||||
const endLine = offset + lineCount;
|
||||
|
||||
if (i === requestIndex) {
|
||||
metadata = { requestStartLine: startLine, requestEndLine: endLine };
|
||||
}
|
||||
|
||||
if (segmentSources?.[i]) {
|
||||
segments.push({ startLine, endLine, ...segmentSources[i] });
|
||||
}
|
||||
|
||||
offset += lineCount + 1;
|
||||
}
|
||||
|
||||
// Request-level script was empty, but collection/folder scripts produced code.
|
||||
// Use a zero line range to prevent stack traces from mapping to the request file.
|
||||
if (!metadata && code) {
|
||||
metadata = { requestStartLine: 0, requestEndLine: 0 };
|
||||
}
|
||||
|
||||
if (metadata && segments.length > 0) {
|
||||
metadata.segments = segments;
|
||||
}
|
||||
|
||||
return { code, metadata };
|
||||
};
|
||||
|
||||
const mergeScripts = (collection, request, requestTreePath, scriptFlow) => {
|
||||
const collectionRoot = collection?.draft?.root || collection?.root || {};
|
||||
let collectionPreReqScript = get(collectionRoot, 'request.script.req', '');
|
||||
let collectionPostResScript = get(collectionRoot, 'request.script.res', '');
|
||||
let collectionTests = get(collectionRoot, 'request.tests', '');
|
||||
|
||||
// Build source file info for error trace mapping
|
||||
const format = collection.format || 'bru';
|
||||
const config = FORMAT_CONFIG[format];
|
||||
const collectionSource = {
|
||||
filePath: path.join(collection.pathname, config.collectionFile),
|
||||
displayPath: config.collectionFile
|
||||
};
|
||||
|
||||
let combinedPreReqScript = [];
|
||||
let combinedPreReqSources = [];
|
||||
let combinedPostResScript = [];
|
||||
let combinedPostResSources = [];
|
||||
let combinedTests = [];
|
||||
let combinedTestsSources = [];
|
||||
|
||||
for (let i of requestTreePath) {
|
||||
if (i.type === 'folder') {
|
||||
const folderRoot = i?.draft || i?.root;
|
||||
const folderSource = {
|
||||
filePath: path.join(i.pathname, config.folderFile),
|
||||
displayPath: path.relative(collection.pathname, path.join(i.pathname, config.folderFile))
|
||||
};
|
||||
|
||||
let preReqScript = get(folderRoot, 'request.script.req', '');
|
||||
if (preReqScript && preReqScript.trim() !== '') {
|
||||
combinedPreReqScript.push(preReqScript);
|
||||
combinedPreReqSources.push(folderSource);
|
||||
}
|
||||
|
||||
let postResScript = get(folderRoot, 'request.script.res', '');
|
||||
if (postResScript && postResScript.trim() !== '') {
|
||||
combinedPostResScript.push(postResScript);
|
||||
combinedPostResSources.push(folderSource);
|
||||
}
|
||||
|
||||
let tests = get(folderRoot, 'request.tests', '');
|
||||
if (tests && tests?.trim?.() !== '') {
|
||||
combinedTests.push(tests);
|
||||
combinedTestsSources.push(folderSource);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -265,7 +335,10 @@ const mergeScripts = (collection, request, requestTreePath, scriptFlow) => {
|
||||
...combinedPreReqScript,
|
||||
request?.script?.req || ''
|
||||
];
|
||||
request.script.req = compact(preReqScripts.map(wrapScriptInClosure)).join(os.EOL + os.EOL);
|
||||
const preReqSources = [collectionSource, ...combinedPreReqSources, null];
|
||||
const preReq = wrapAndJoinScripts(preReqScripts, preReqScripts.length - 1, preReqSources);
|
||||
request.script.req = preReq.code;
|
||||
request.script.reqMetadata = preReq.metadata;
|
||||
|
||||
// Handle post-response scripts based on scriptFlow
|
||||
if (scriptFlow === 'sequential') {
|
||||
@@ -274,7 +347,10 @@ const mergeScripts = (collection, request, requestTreePath, scriptFlow) => {
|
||||
...combinedPostResScript,
|
||||
request?.script?.res || ''
|
||||
];
|
||||
request.script.res = compact(postResScripts.map(wrapScriptInClosure)).join(os.EOL + os.EOL);
|
||||
const postResSources = [collectionSource, ...combinedPostResSources, null];
|
||||
const postRes = wrapAndJoinScripts(postResScripts, postResScripts.length - 1, postResSources);
|
||||
request.script.res = postRes.code;
|
||||
request.script.resMetadata = postRes.metadata;
|
||||
} else {
|
||||
// Reverse order for non-sequential flow
|
||||
const postResScripts = [
|
||||
@@ -282,7 +358,10 @@ const mergeScripts = (collection, request, requestTreePath, scriptFlow) => {
|
||||
...[...combinedPostResScript].reverse(),
|
||||
collectionPostResScript
|
||||
];
|
||||
request.script.res = compact(postResScripts.map(wrapScriptInClosure)).join(os.EOL + os.EOL);
|
||||
const postResSources = [null, ...[...combinedPostResSources].reverse(), collectionSource];
|
||||
const postRes = wrapAndJoinScripts(postResScripts, 0, postResSources);
|
||||
request.script.res = postRes.code;
|
||||
request.script.resMetadata = postRes.metadata;
|
||||
}
|
||||
|
||||
// Handle tests based on scriptFlow
|
||||
@@ -292,7 +371,10 @@ const mergeScripts = (collection, request, requestTreePath, scriptFlow) => {
|
||||
...combinedTests,
|
||||
request?.tests || ''
|
||||
];
|
||||
request.tests = compact(testScripts.map(wrapScriptInClosure)).join(os.EOL + os.EOL);
|
||||
const testSources = [collectionSource, ...combinedTestsSources, null];
|
||||
const tests = wrapAndJoinScripts(testScripts, testScripts.length - 1, testSources);
|
||||
request.tests = tests.code;
|
||||
request.testsMetadata = tests.metadata;
|
||||
} else {
|
||||
// Reverse order for non-sequential flow
|
||||
const testScripts = [
|
||||
@@ -300,7 +382,10 @@ const mergeScripts = (collection, request, requestTreePath, scriptFlow) => {
|
||||
...[...combinedTests].reverse(),
|
||||
collectionTests
|
||||
];
|
||||
request.tests = compact(testScripts.map(wrapScriptInClosure)).join(os.EOL + os.EOL);
|
||||
const testSources = [null, ...[...combinedTestsSources].reverse(), collectionSource];
|
||||
const tests = wrapAndJoinScripts(testScripts, 0, testSources);
|
||||
request.tests = tests.code;
|
||||
request.testsMetadata = tests.metadata;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user