import { HoppRESTRequest } from "@hoppscotch/data"; import { execTestScript, TestDescriptor } from "@hoppscotch/js-sandbox"; import { hrtime } from "process"; import { flow, pipe } from "fp-ts/function"; import * as RA from "fp-ts/ReadonlyArray"; import * as A from "fp-ts/Array"; import * as TE from "fp-ts/TaskEither"; import * as T from "fp-ts/Task"; import { RequestRunnerResponse, TestReport, TestScriptParams, } from "../interfaces/response"; import { error, HoppCLIError } from "../types/errors"; import { HoppEnvs } from "../types/request"; import { ExpectResult, TestMetrics, TestRunnerRes } from "../types/response"; import { getDurationInSeconds } from "./getters"; /** * Executes test script and runs testDescriptorParser to generate test-report using * expected-results, test-status & test-descriptor. * @param testScriptData Parameters related to test-script function. * @returns If executes successfully, we get TestRunnerRes(updated ENVs, test-reports, duration). * Else, HoppCLIError with appropriate code & data. */ export const testRunner = ( testScriptData: TestScriptParams ): TE.TaskEither => pipe( /** * Executing test-script. */ TE.Do, TE.bind("start", () => TE.of(hrtime())), TE.bind("test_response", () => pipe( TE.of(testScriptData), TE.chain(({ testScript, response, envs }) => execTestScript(testScript, envs, response) ) ) ), /** * Recursively parsing test-results using test-descriptor-parser * to generate test-reports. */ TE.chainTaskK(({ test_response: { tests, envs }, start }) => pipe( tests, A.map(testDescriptorParser), T.sequenceArray, T.map( flow( RA.flatten, RA.toArray, (testsReport) => { envs, testsReport, duration: pipe(start, hrtime, getDurationInSeconds), } ) ) ) ), TE.mapLeft((e) => error({ code: "TEST_SCRIPT_ERROR", data: e, }) ) ); /** * Recursive function to parse test-descriptor from nested-children and * generate tests-report. * @param testDescriptor Object with details of test-descriptor. * @returns Flattened array of TestReport parsed from TestDescriptor. */ export const testDescriptorParser = ( testDescriptor: TestDescriptor ): T.Task => pipe( /** * Generate single TestReport from given testDescriptor. */ testDescriptor, ({ expectResults, descriptor }) => A.isNonEmpty(expectResults) ? pipe( expectResults, A.reduce({ failed: 0, passed: 0 }, (prev, { status }) => /** * Incrementing number of passed test-cases if status is "pass", * else, incrementing number of failed test-cases. */ status === "pass" ? { failed: prev.failed, passed: prev.passed + 1 } : { failed: prev.failed + 1, passed: prev.passed } ), ({ failed, passed }) => { failed, passed, descriptor, expectResults, }, Array.of ) : [], T.of, /** * Recursive call to testDescriptorParser on testDescriptor's children. * The result is concated with previous testReport. */ T.chain((testReport) => pipe( testDescriptor.children, A.map(testDescriptorParser), T.sequenceArray, T.map(flow(RA.flatten, RA.toArray, A.concat(testReport))) ) ) ); /** * Extracts parameter object from request-runner's response, request and envs * for test-runner. * @param reqRunnerRes Provides response data. * @param request Provides test-script data. * @param envs Current ENVs state with-in collections-runner. * @returns Object to be passed as parameter for test-runner */ export const getTestScriptParams = ( reqRunnerRes: RequestRunnerResponse, request: HoppRESTRequest, envs: HoppEnvs ) => { const testScriptParams: TestScriptParams = { testScript: request.testScript, response: { body: reqRunnerRes.body, status: reqRunnerRes.status, headers: reqRunnerRes.headers, }, envs: envs, }; return testScriptParams; }; /** * Combines quantitative details (test-cases passed/failed) of each test-report * to generate TestMetrics object with total test-cases & total test-suites. * @param testsReport Contains details of each test-report (failed/passed test-cases). * @param testDuration Time taken (in seconds) to execute the test-script. * @param errors List of HoppCLIErrors to check for TEST_SCRIPT_ERROR code. * @returns Object containing details of total test-cases passed/failed and * total test-suites passed/failed. */ export const getTestMetrics = ( testsReport: TestReport[], testDuration: number, errors: HoppCLIError[] ): TestMetrics => testsReport.reduce( ({ testSuites, tests, duration, scripts }, testReport) => ({ tests: { failed: tests.failed + testReport.failed, passed: tests.passed + testReport.passed, }, testSuites: { failed: testSuites.failed + (testReport.failed > 0 ? 1 : 0), passed: testSuites.passed + (testReport.failed === 0 ? 1 : 0), }, scripts: scripts, duration: duration, }), { tests: { failed: 0, passed: 0 }, testSuites: { failed: 0, passed: 0 }, duration: testDuration, scripts: errors.some(({ code }) => code === "TEST_SCRIPT_ERROR") ? { failed: 1, passed: 0 } : { failed: 0, passed: 1 }, } ); /** * Filters tests-report containing atleast one or more failed test-cases. * @param testsReport Provides "failed" test-cases data. * @returns Tests report with one or more test-cases failed. */ export const getFailedTestsReport = (testsReport: TestReport[]) => pipe( testsReport, A.filter(({ failed }) => failed > 0) ); /** * Filters expected-results containing which has status as "fail" or "error". * @param expectResults Provides "status" data for each expected result. * @returns Expected results with "fail" or "error" status. */ export const getFailedExpectedResults = (expectResults: ExpectResult[]) => pipe( expectResults, A.filter(({ status }) => status !== "pass") ); /** * Checks if any of the tests-report have failed test-cases. * @param testsReport Provides "failed" test-cases data. * @returns True, if one or more failed test-cases found. * False, if all test-cases passed. */ export const hasFailedTestCases = (testsReport: TestReport[]) => pipe( testsReport, A.every(({ failed }) => failed === 0) );