feat(cli): Refactor request runner and improve folder sorting

~ remove duplicate code by consolidating request collection functionality
~ add proper sorting of folder and request items based on sequence numbers
~ add error request count to run summary output
~ update dev dependencies with proper markings in package-lock.json and removed previously added currently reverted entries
This commit is contained in:
lohit
2025-05-03 23:50:12 +05:30
parent 526fcabffe
commit 520567793a
5 changed files with 110 additions and 203 deletions

62
package-lock.json generated
View File

@@ -54,6 +54,7 @@
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
"integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
@@ -1474,6 +1475,7 @@
"version": "7.26.0",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz",
"integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@ampproject/remapping": "^2.2.0",
@@ -1504,6 +1506,7 @@
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
@@ -1521,6 +1524,7 @@
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true,
"license": "MIT"
},
"node_modules/@babel/generator": {
@@ -1800,6 +1804,7 @@
"version": "7.26.0",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz",
"integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/template": "^7.25.9",
@@ -7784,6 +7789,7 @@
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
"integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/lodash": {
@@ -7806,6 +7812,7 @@
"version": "12.2.3",
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz",
"integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/linkify-it": "*",
@@ -7816,6 +7823,7 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
"integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/ms": {
@@ -11060,6 +11068,7 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
"dev": true,
"license": "MIT"
},
"node_modules/cookie": {
@@ -12670,6 +12679,7 @@
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
"integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -13637,6 +13647,7 @@
"version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
"integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -24196,7 +24207,7 @@
"version": "4.9.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"devOptional": true,
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
@@ -26303,9 +26314,7 @@
"@rollup/plugin-typescript": "^12.1.2",
"@types/jest": "^29.5.14",
"babel-jest": "^29.7.0",
"cheerio": "^1.0.0",
"moment": "^2.29.4",
"playwright": "^1.52.0",
"rollup": "3.29.5",
"rollup-plugin-dts": "^5.0.0",
"rollup-plugin-peer-deps-external": "^2.2.4",
@@ -26815,21 +26824,6 @@
}
}
},
"packages/bruno-common/node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"packages/bruno-common/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -26837,38 +26831,6 @@
"dev": true,
"license": "MIT"
},
"packages/bruno-common/node_modules/playwright": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.52.0.tgz",
"integrity": "sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.52.0"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"packages/bruno-common/node_modules/playwright-core": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.52.0.tgz",
"integrity": "sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"packages/bruno-common/node_modules/typescript": {
"version": "5.8.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",

View File

@@ -12,7 +12,7 @@ const { rpad } = require('../utils/common');
const { bruToJson, getOptions, collectionBruToJson } = require('../utils/bru');
const { dotenvToJson } = require('@usebruno/lang');
const constants = require('../constants');
const { findItemInCollection } = require('../utils/collection');
const { findItemInCollection, getAllRequestsInFolder } = require('../utils/collection');
const command = 'run [filename]';
const desc = 'Run a request';
@@ -22,6 +22,7 @@ const printRunSummary = (results) => {
passedRequests,
failedRequests,
skippedRequests,
errorRequests,
totalAssertions,
passedAssertions,
failedAssertions,
@@ -36,6 +37,9 @@ const printRunSummary = (results) => {
if (failedRequests > 0) {
requestSummary += `, ${chalk.red(`${failedRequests} failed`)}`;
}
if (errorRequests > 0) {
requestSummary += `, ${chalk.red(`${errorRequests} error`)}`;
}
if (skippedRequests > 0) {
requestSummary += `, ${chalk.magenta(`${skippedRequests} skipped`)}`;
}
@@ -62,6 +66,7 @@ const printRunSummary = (results) => {
passedRequests,
failedRequests,
skippedRequests,
errorRequests,
totalAssertions,
passedAssertions,
failedAssertions,
@@ -94,12 +99,10 @@ const createCollectionFromPath = (collectionPath) => {
!filePath.startsWith('node_modules')
) {
let folderItem = { name: file, pathname: filePath, type: 'folder', items: traverse(filePath) }
const folderBruFilePath = path.join(filePath, 'folder.bru');
const folderBruFileExists = fs.existsSync(folderBruFilePath);
if(folderBruFileExists) {
const folderBruContent = fs.readFileSync(folderBruFilePath, 'utf8');
let folderBruJson = collectionBruToJson(folderBruContent);
const folderBruJson = getFolderRoot(filePath);
if (folderBruJson) {
folderItem.root = folderBruJson;
folderItem.seq = folderBruJson.meta.seq;
}
currentDirItems.push(folderItem);
}
@@ -114,15 +117,17 @@ const createCollectionFromPath = (collectionPath) => {
if (!stats.isDirectory() && path.extname(filePath) === '.bru') {
const bruContent = fs.readFileSync(filePath, 'utf8');
const bruJson = bruToJson(bruContent);
const requestItem = bruToJson(bruContent);
currentDirItems.push({
name: file,
pathname: filePath,
...bruJson
...requestItem
});
}
}
return currentDirItems;
const sortedFolderItems = currentDirItems?.filter((iter) => iter.type === 'folder')?.sort((a, b) => a.seq - b.seq);
const sortedRequestItems = currentDirItems?.filter((iter) => iter.type !== 'folder')?.sort((a, b) => a.seq - b.seq);
return sortedFolderItems?.concat(sortedRequestItems);
};
collection.items = traverse(collectionPath);
return collection;
@@ -130,82 +135,6 @@ const createCollectionFromPath = (collectionPath) => {
return getFilesInOrder(collectionPath);
};
const getBruFilesRecursively = (dir, testsOnly) => {
const environmentsPath = 'environments';
const collection = {};
const getFilesInOrder = (dir) => {
let bruJsons = [];
const traverse = (currentPath) => {
const filesInCurrentDir = fs.readdirSync(currentPath);
if (currentPath.includes('node_modules')) {
return;
}
for (const file of filesInCurrentDir) {
const filePath = path.join(currentPath, file);
const stats = fs.statSync(filePath);
// todo: we might need a ignore config inside bruno.json
if (
stats.isDirectory() &&
filePath !== environmentsPath &&
!filePath.startsWith('.git') &&
!filePath.startsWith('node_modules')
) {
traverse(filePath);
}
}
const currentDirBruJsons = [];
for (const file of filesInCurrentDir) {
if (['collection.bru', 'folder.bru'].includes(file)) {
continue;
}
const filePath = path.join(currentPath, file);
const stats = fs.lstatSync(filePath);
if (!stats.isDirectory() && path.extname(filePath) === '.bru') {
const bruContent = fs.readFileSync(filePath, 'utf8');
const bruJson = bruToJson(bruContent);
const requestHasTests = bruJson.request?.tests;
const requestHasActiveAsserts = bruJson.request?.assertions.some((x) => x.enabled) || false;
if (testsOnly) {
if (requestHasTests || requestHasActiveAsserts) {
currentDirBruJsons.push({
bruFilepath: filePath,
bruJson
});
}
} else {
currentDirBruJsons.push({
bruFilepath: filePath,
bruJson
});
}
}
}
// order requests by sequence
currentDirBruJsons.sort((a, b) => {
const aSequence = a.bruJson.seq || 0;
const bSequence = b.bruJson.seq || 0;
return aSequence - bSequence;
});
bruJsons = bruJsons.concat(currentDirBruJsons);
};
traverse(dir);
return bruJsons;
};
return getFilesInOrder(dir);
};
const getCollectionRoot = (dir) => {
const collectionRootPath = path.join(dir, 'collection.bru');
const exists = fs.existsSync(collectionRootPath);
@@ -221,7 +150,7 @@ const getFolderRoot = (dir) => {
const folderRootPath = path.join(dir, 'folder.bru');
const exists = fs.existsSync(folderRootPath);
if (!exists) {
return {};
return null;
}
const content = fs.readFileSync(folderRootPath, 'utf8');
@@ -444,7 +373,6 @@ const handler = async function (argv) {
}
}
if (filename && filename.length) {
const pathExists = await exists(filename);
if (!pathExists) {
@@ -566,54 +494,38 @@ const handler = async function (argv) {
const _isFile = isFile(filename);
let results = [];
let bruJsons = [];
let requestItems = [];
if (_isFile) {
console.log(chalk.yellow('Running Request \n'));
const bruContent = fs.readFileSync(filename, 'utf8');
const bruJson = bruToJson(bruContent);
bruJsons.push({
bruFilepath: filename,
bruJson
});
const requestItem = bruToJson(bruContent);
requestItems.push(requestItem);
}
const _isDirectory = isDirectory(filename);
if (_isDirectory) {
if (!recursive) {
console.log(chalk.yellow('Running Folder \n'));
const files = fs.readdirSync(filename);
const bruFiles = files.filter((file) => !['folder.bru'].includes(file) && file.endsWith('.bru'));
for (const bruFile of bruFiles) {
const bruFilepath = path.join(filename, bruFile);
const bruContent = fs.readFileSync(bruFilepath, 'utf8');
const bruJson = bruToJson(bruContent);
const requestHasTests = bruJson.request?.tests;
const requestHasActiveAsserts = bruJson.request?.assertions.some((x) => x.enabled) || false;
if (testsOnly) {
if (requestHasTests || requestHasActiveAsserts) {
bruJsons.push({
bruFilepath,
bruJson
});
}
} else {
bruJsons.push({
bruFilepath,
bruJson
});
}
}
bruJsons.sort((a, b) => {
const aSequence = a.bruJson.seq || 0;
const bSequence = b.bruJson.seq || 0;
return aSequence - bSequence;
});
} else {
console.log(chalk.yellow('Running Folder Recursively \n'));
}
const resolvedFilepath = path.resolve(filename);
if (resolvedFilepath === collectionPath) {
requestItems = getAllRequestsInFolder(collection?.items, recursive);
} else {
const folderItem = findItemInCollection(collection, resolvedFilepath);
if (folderItem) {
requestItems = getAllRequestsInFolder(folderItem.items, recursive);
}
}
bruJsons = getBruFilesRecursively(filename, testsOnly);
if (testsOnly) {
requestItems = requestItems.filter((iter) => {
const requestHasTests = iter.request?.tests;
const requestHasActiveAsserts = iter.request?.assertions.some((x) => x.enabled) || false;
return requestHasTests || requestHasActiveAsserts;
});
}
}
@@ -625,11 +537,10 @@ const handler = async function (argv) {
if (itemPathname && !itemPathname?.endsWith('.bru')) {
itemPathname = `${itemPathname}.bru`;
}
const bruJson = cloneDeep(findItemInCollection(collection, itemPathname));
if (bruJson) {
const requestItem = cloneDeep(findItemInCollection(collection, itemPathname));
if (requestItem) {
const res = await runSingleRequest(
itemPathname,
bruJson,
requestItem,
collectionPath,
runtimeVariables,
envVars,
@@ -648,14 +559,13 @@ const handler = async function (argv) {
let currentRequestIndex = 0;
let nJumps = 0; // count the number of jumps to avoid infinite loops
while (currentRequestIndex < bruJsons.length) {
const iter = cloneDeep(bruJsons[currentRequestIndex]);
const { bruFilepath, bruJson } = iter;
while (currentRequestIndex < requestItems.length) {
const requestItem = cloneDeep(requestItems[currentRequestIndex]);
const { pathname } = requestItem;
const start = process.hrtime();
const result = await runSingleRequest(
bruFilepath,
bruJson,
requestItem,
collectionPath,
runtimeVariables,
envVars,
@@ -667,7 +577,7 @@ const handler = async function (argv) {
runSingleRequestByPathname
);
const isLastRun = currentRequestIndex === bruJsons.length - 1;
const isLastRun = currentRequestIndex === requestItems.length - 1;
const isValidDelay = !Number.isNaN(delay) && delay > 0;
if(isValidDelay && !isLastRun){
console.log(chalk.yellow(`Waiting for ${delay}ms or ${(delay/1000).toFixed(3)}s before next request.`));
@@ -681,7 +591,7 @@ const handler = async function (argv) {
results.push({
...result,
runtime: process.hrtime(start)[0] + process.hrtime(start)[1] / 1e9,
suitename: bruFilepath.replace('.bru', '')
suitename: pathname.replace('.bru', '')
});
if (reporterSkipAllHeaders) {
@@ -739,7 +649,7 @@ const handler = async function (argv) {
if (nextRequestName === null) {
break;
}
const nextRequestIdx = bruJsons.findIndex((iter) => iter.bruJson.name === nextRequestName);
const nextRequestIdx = requestItems.findIndex((iter) => iter.name === nextRequestName);
if (nextRequestIdx >= 0) {
currentRequestIndex = nextRequestIdx;
} else {

View File

@@ -31,8 +31,7 @@ const onConsoleLog = (type, args) => {
};
const runSingleRequest = async function (
filename,
bruJson,
item,
collectionPath,
runtimeVariables,
envVariables,
@@ -43,14 +42,12 @@ const runSingleRequest = async function (
collection,
runSingleRequestByPathname
) {
const { pathname: itemPathname } = item;
const relativeItemPathname = path.relative(collectionPath, itemPathname);
try {
let request;
let nextRequestName;
let shouldStopRunnerExecution = false;
let item = {
pathname: path.join(collectionPath, filename),
...bruJson
}
request = prepareRequest(item, collection);
request.__bruno__executionMode = 'cli';
@@ -84,7 +81,7 @@ const runSingleRequest = async function (
if (result?.skipRequest) {
return {
test: {
filename: filename
filename: itemPathname
},
request: {
method: request.method,
@@ -98,7 +95,8 @@ const runSingleRequest = async function (
data: null,
responseTime: 0
},
error: 'Request has been skipped from pre-request script',
error: null,
status: 'skipped',
skipped: true,
assertionResults: [],
testResults: [],
@@ -362,10 +360,10 @@ const runSingleRequest = async function (
responseTime = response.headers.get('request-duration');
response.headers.delete('request-duration');
} else {
console.log(chalk.red(stripExtension(filename)) + chalk.dim(` (${err.message})`));
console.log(chalk.red(stripExtension(relativeItemPathname)) + chalk.dim(` (${err.message})`));
return {
test: {
filename: filename
filename: itemPathname
},
request: {
method: request.method,
@@ -374,13 +372,14 @@ const runSingleRequest = async function (
data: request.data
},
response: {
status: null,
status: 'error',
statusText: null,
headers: null,
data: null,
responseTime: 0
},
error: err?.message || err?.errors?.map(e => e?.message)?.at(0) || err?.code || 'Request Failed!',
status: 'error',
assertionResults: [],
testResults: [],
nextRequestName: nextRequestName,
@@ -392,12 +391,12 @@ const runSingleRequest = async function (
response.responseTime = responseTime;
console.log(
chalk.green(stripExtension(filename)) +
chalk.green(stripExtension(relativeItemPathname)) +
chalk.dim(` (${response.status} ${response.statusText}) - ${responseTime} ms`)
);
// run post-response vars
const postResponseVars = get(bruJson, 'request.vars.res');
const postResponseVars = get(item, 'request.vars.res');
if (postResponseVars?.length) {
const varsRuntime = new VarsRuntime({ runtime: scriptingConfig?.runtime });
varsRuntime.runPostResponseVars(
@@ -438,7 +437,7 @@ const runSingleRequest = async function (
// run assertions
let assertionResults = [];
const assertions = get(bruJson, 'request.assertions');
const assertions = get(item, 'request.assertions');
if (assertions) {
const assertRuntime = new AssertRuntime({ runtime: scriptingConfig?.runtime });
assertionResults = assertRuntime.runAssertions(
@@ -500,7 +499,7 @@ const runSingleRequest = async function (
return {
test: {
filename: filename
filename: itemPathname
},
request: {
method: request.method,
@@ -516,16 +515,17 @@ const runSingleRequest = async function (
responseTime
},
error: null,
status: 'pass',
assertionResults,
testResults,
nextRequestName: nextRequestName,
shouldStopRunnerExecution
};
} catch (err) {
console.log(chalk.red(stripExtension(filename)) + chalk.dim(` (${err.message})`));
console.log(chalk.red(stripExtension(relativeItemPathname)) + chalk.dim(` (${err.message})`));
return {
test: {
filename: filename
filename: itemPathname
},
request: {
method: null,
@@ -534,12 +534,13 @@ const runSingleRequest = async function (
data: null
},
response: {
status: null,
status: 'error',
statusText: null,
headers: null,
data: null,
responseTime: 0
},
status: 'error',
error: err.message,
assertionResults: [],
testResults: []

View File

@@ -15,6 +15,17 @@ const collectionBruToJson = (bru) => {
}
};
// add meta if it exists
// this is only for folder bru file
// in the future, all of this will be replaced by standard bru lang
const sequence = _.get(json, 'meta.seq');
if (json?.meta) {
transformedJson.meta = {
name: json.meta.name,
seq: !isNaN(sequence) ? Number(sequence) : 1
};
}
return transformedJson;
} catch (error) {
return Promise.reject(error);

View File

@@ -200,10 +200,33 @@ const getTreePathFromCollectionToItem = (collection, _item) => {
return path;
};
const getAllRequestsInFolder = (folderItems = [], recursive = true) => {
let requests = [];
if (folderItems && folderItems.length) {
folderItems.forEach((item) => {
if (item.type !== 'folder') {
requests.push(item);
} else {
if (recursive) {
requests = requests.concat(getAllRequestsInFolder(item.items, recursive));
}
}
});
}
return requests;
};
const getAllRequestsAtFolderRoot = (folderItems = []) => {
return getAllRequestsInFolder(folderItems, false);
};
module.exports = {
mergeHeaders,
mergeVars,
mergeScripts,
findItemInCollection,
getTreePathFromCollectionToItem
getTreePathFromCollectionToItem,
getAllRequestsInFolder,
getAllRequestsAtFolderRoot
}