import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data"; import chalk from "chalk"; import { log } from "console"; import * as A from "fp-ts/Array"; import { pipe } from "fp-ts/function"; import { round } from "lodash-es"; import { CollectionRunnerParam } from "../types/collections"; import { CollectionQueue, HoppEnvs, ProcessRequestParams, RequestReport, } from "../types/request"; import { PreRequestMetrics, RequestMetrics, TestMetrics, } from "../types/response"; import { DEFAULT_DURATION_PRECISION } from "./constants"; import { printErrorsReport, printFailedTestsReport, printPreRequestMetrics, printRequestsMetrics, printTestsMetrics, } from "./display"; import { exceptionColors } from "./getters"; import { getPreRequestMetrics } from "./pre-request"; import { buildJUnitReport, generateJUnitReportExport } from "./reporters/junit"; import { getRequestMetrics, preProcessRequest, processRequest, } from "./request"; import { getTestMetrics } from "./test"; import { filterValidScripts } from "./scripting"; const { WARN, FAIL, INFO } = exceptionColors; /** * Processes each requests within collections to prints details of subsequent requests, * tests and to display complete errors-report, failed-tests-report and test-metrics. * @param param Data of hopp-collection with hopp-requests, envs to be processed. * @returns List of report for each processed request. */ export const collectionsRunner = async ( param: CollectionRunnerParam ): Promise => { const { collections, envs, delay, iterationCount, iterationData, legacySandbox, } = param; const resolvedDelay = delay ?? 0; const requestsReport: RequestReport[] = []; const collectionQueue = getCollectionQueue(collections); // If iteration count is not supplied, it should be based on the size of iteration data if in scope const resolvedIterationCount = iterationCount ?? iterationData?.length ?? 1; const originalSelectedEnvs = [...envs.selected]; for (let count = 0; count < resolvedIterationCount; count++) { if (resolvedIterationCount > 1) { log(INFO(`\nIteration: ${count + 1}/${resolvedIterationCount}`)); } // Reset `envs` to the original value at the start of each iteration envs.selected = [...originalSelectedEnvs]; if (iterationData) { // Ensure last item is picked if the iteration count exceeds size of the iteration data const iterationDataItem = iterationData[Math.min(count, iterationData.length - 1)]; // Ensure iteration data takes priority over supplied environment variables envs.selected = envs.selected .filter( (envPair) => !iterationDataItem.some((dataPair) => dataPair.key === envPair.key) ) .concat(iterationDataItem); } for (const { collection, path } of collectionQueue) { await processCollection( collection, path, envs, resolvedDelay, requestsReport, legacySandbox ); } } return requestsReport; }; const processCollection = async ( collection: HoppCollection, path: string, envs: HoppEnvs, delay: number, requestsReport: RequestReport[], legacySandbox?: boolean, ancestorPreRequestScripts: string[] = [], ancestorTestScripts: string[] = [] ) => { // Accumulate scripts from root -> current collection for inheritance // filterValidScripts strips empty, whitespace-only, and module-prefix-only scripts const inheritedPreRequestScripts = filterValidScripts([ ...ancestorPreRequestScripts, collection.preRequestScript, ]); const inheritedTestScripts = filterValidScripts([ ...ancestorTestScripts, collection.testScript, ]); // Process each request in the collection for (const request of collection.requests) { const _request = preProcessRequest(request as HoppRESTRequest, collection); const requestPath = `${path}/${_request.name}`; const collectionVariables = collection.variables.filter( (variable) => variable.key && variable.key.trim() !== "" ); const processRequestParams: ProcessRequestParams = { path: requestPath, request: _request, envs, delay, legacySandbox, collectionVariables, inheritedPreRequestScripts, inheritedTestScripts, }; // Request processing initiated message. log(WARN(`\nRunning: ${chalk.bold(requestPath)}`)); // Processing current request. const result = await processRequest(processRequestParams)(); // Updating global & selected envs with new envs from processed-request output. const { global, selected } = result.envs; envs.global = global; envs.selected = selected; // Storing current request's report. const requestReport = result.report; requestsReport.push(requestReport); } // Process each folder in the collection for (const folder of collection.folders) { const updatedFolder: HoppCollection = { ...folder }; if (updatedFolder.auth.authType === "inherit") { updatedFolder.auth = collection.auth; } if (collection.headers.length) { // Filter out header entries present in the parent collection under the same name // This ensures the folder headers take precedence over the collection headers const filteredHeaders = collection.headers.filter( (collectionHeaderEntries) => { return !updatedFolder.headers.some( (folderHeaderEntries) => folderHeaderEntries.key === collectionHeaderEntries.key ); } ); updatedFolder.headers.push(...filteredHeaders); } // Inherit collection variables into folder, with folder variables taking precedence if (collection.variables.length) { // Filter out collection variables with same key as folder variables const filteredVariables = collection.variables.filter( (collectionVariableEntries) => { return !updatedFolder.variables.some( (folderVariableEntries) => folderVariableEntries.key === collectionVariableEntries.key ); } ); updatedFolder.variables.push(...filteredVariables); } await processCollection( updatedFolder, `${path}/${updatedFolder.name}`, envs, delay, requestsReport, legacySandbox, inheritedPreRequestScripts, inheritedTestScripts ); } }; /** * Transforms collections to generate collection-stack which describes each collection's * path within collection & the collection itself. * @param collections Hopp-collection objects to be mapped to collection-stack type. * @returns Mapped collections to collection-stack. */ const getCollectionQueue = (collections: HoppCollection[]): CollectionQueue[] => pipe( collections, A.map( (collection) => { collection, path: collection.name } ) ); /** * Prints collection-runner-report using test-metrics, request-metrics and * pre-request-metrics data in pretty-format. * @param requestsReport Provides data for each request-report which includes * path of each request within collection-json file, failed-tests-report, errors, * total execution duration for requests, pre-request-scripts, test-scripts. * @returns True, if collection runner executed without any errors or failed test-cases. * False, if errors occurred or test-cases failed. */ export const collectionsRunnerResult = ( requestsReport: RequestReport[], reporterJUnitExportPath?: string ): boolean => { const overallTestMetrics = { tests: { failed: 0, passed: 0 }, testSuites: { failed: 0, passed: 0 }, duration: 0, scripts: { failed: 0, passed: 0 }, }; const overallRequestMetrics = { requests: { failed: 0, passed: 0 }, duration: 0, }; const overallPreRequestMetrics = { scripts: { failed: 0, passed: 0 }, duration: 0, }; let finalResult = true; let totalErroredTestCases = 0; let totalFailedTestCases = 0; // Printing requests-report details of failed-tests and errors for (const requestReport of requestsReport) { const { path, tests, errors, result, duration } = requestReport; const requestDuration = duration.request; const testsDuration = duration.test; const preRequestDuration = duration.preRequest; finalResult = finalResult && result; printFailedTestsReport(path, tests); printErrorsReport(path, errors); if (reporterJUnitExportPath) { const { failedRequestTestCases, erroredRequestTestCases } = buildJUnitReport({ path, tests, errors, duration: duration.test, }); totalFailedTestCases += failedRequestTestCases; totalErroredTestCases += erroredRequestTestCases; } /** * Extracting current request report's test-metrics and updating * overall test-metrics. */ const testMetrics = getTestMetrics(tests, testsDuration, errors); overallTestMetrics.duration += testMetrics.duration; overallTestMetrics.testSuites.failed += testMetrics.testSuites.failed; overallTestMetrics.testSuites.passed += testMetrics.testSuites.passed; overallTestMetrics.tests.failed += testMetrics.tests.failed; overallTestMetrics.tests.passed += testMetrics.tests.passed; overallTestMetrics.scripts.failed += testMetrics.scripts.failed; overallTestMetrics.scripts.passed += testMetrics.scripts.passed; /** * Extracting current request report's request-metrics and updating * overall request-metrics. */ const requestMetrics = getRequestMetrics(errors, requestDuration); overallRequestMetrics.duration += requestMetrics.duration; overallRequestMetrics.requests.failed += requestMetrics.requests.failed; overallRequestMetrics.requests.passed += requestMetrics.requests.passed; /** * Extracting current request report's pre-request-metrics and updating * overall pre-request-metrics. */ const preRequestMetrics = getPreRequestMetrics(errors, preRequestDuration); overallPreRequestMetrics.duration += preRequestMetrics.duration; overallPreRequestMetrics.scripts.failed += preRequestMetrics.scripts.failed; overallPreRequestMetrics.scripts.passed += preRequestMetrics.scripts.passed; } const testMetricsDuration = overallTestMetrics.duration; const requestMetricsDuration = overallRequestMetrics.duration; // Rounding-off overall test-metrics duration upto DEFAULT_DURATION_PRECISION. overallTestMetrics.duration = round( testMetricsDuration, DEFAULT_DURATION_PRECISION ); // Rounding-off overall request-metrics duration upto DEFAULT_DURATION_PRECISION. overallRequestMetrics.duration = round( requestMetricsDuration, DEFAULT_DURATION_PRECISION ); printTestsMetrics(overallTestMetrics); printRequestsMetrics(overallRequestMetrics); printPreRequestMetrics(overallPreRequestMetrics); if (reporterJUnitExportPath) { const totalTestCases = overallTestMetrics.tests.failed + overallTestMetrics.tests.passed; generateJUnitReportExport({ totalTestCases, totalFailedTestCases, totalErroredTestCases, testDuration: overallTestMetrics.duration, reporterJUnitExportPath, }); } return finalResult; }; /** * Exiting hopp cli process with appropriate exit code depending on * collections-runner result. * If result is true, we exit the cli process with code 0. * Else, exit with code 1. * @param result Boolean defining the collections-runner result. */ export const collectionsRunnerExit = (result: boolean): never => { if (!result) { const EXIT_MSG = FAIL(`\nExited with code 1`); process.stderr.write(EXIT_MSG); process.exit(1); } process.exit(0); };