feat: added delay flag in @hoppscotch/cli and related tests (#2527)

Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
This commit is contained in:
Deepanshu Dhruw
2022-08-04 19:19:14 +05:30
committed by GitHub
parent 0c31d9201f
commit 73fdfbd2c8
13 changed files with 162 additions and 18 deletions

View File

@@ -1,6 +1,6 @@
{ {
"name": "@hoppscotch/cli", "name": "@hoppscotch/cli",
"version": "0.2.1", "version": "0.3.0",
"description": "A CLI to run Hoppscotch test scripts in CI environments.", "description": "A CLI to run Hoppscotch test scripts in CI environments.",
"homepage": "https://hoppscotch.io", "homepage": "https://hoppscotch.io",
"main": "dist/index.js", "main": "dist/index.js",

View File

@@ -92,12 +92,41 @@ describe("Test 'hopp test <file> --env <file>' command:", () => {
expect(out).toBe<HoppErrorCode>("FILE_NOT_FOUND"); expect(out).toBe<HoppErrorCode>("FILE_NOT_FOUND");
}); });
// test("No errors occured (exit code 0).", async () => { test("No errors occured (exit code 0).", async () => {
// const TESTS_PATH = getTestJsonFilePath("env-flag-tests.json"); const TESTS_PATH = getTestJsonFilePath("env-flag-tests.json");
// const ENV_PATH = getTestJsonFilePath("env-flag-envs.json"); const ENV_PATH = getTestJsonFilePath("env-flag-envs.json");
// const cmd = `node ./bin/hopp test ${TESTS_PATH} --env ${ENV_PATH}`; const cmd = `node ./bin/hopp test ${TESTS_PATH} --env ${ENV_PATH}`;
// const { error } = await execAsync(cmd); const { error } = await execAsync(cmd);
// expect(error).toBeNull(); expect(error).toBeNull();
// }); });
});
describe("Test 'hopp test <file> --delay <delay_in_ms>' command:", () => {
const VALID_TEST_CMD = `node ./bin/hopp test ${getTestJsonFilePath(
"passes.json"
)}`;
test("No value passed to delay flag.", async () => {
const cmd = `${VALID_TEST_CMD} --delay`;
const { stdout } = await execAsync(cmd);
const out = getErrorCode(stdout);
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
});
test("Invalid value passed to delay flag.", async () => {
const cmd = `${VALID_TEST_CMD} --delay 'NaN'`;
const { stdout } = await execAsync(cmd);
const out = getErrorCode(stdout);
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
});
test("Valid value passed to delay flag.", async () => {
const cmd = `${VALID_TEST_CMD} --delay 1`;
const { error } = await execAsync(cmd);
expect(error).toBeNull();
});
}); });

View File

@@ -0,0 +1,30 @@
import { hrtime } from "process";
import { getDurationInSeconds } from "../../../utils/getters";
import { delayPromiseFunction } from "../../../utils/request";
describe("describePromiseFunction", () => {
let promiseFunc = (): Promise<number> => new Promise((resolve) => resolve(2));
beforeEach(() => {
promiseFunc = (): Promise<number> => new Promise((resolve) => resolve(2));
});
it("Should resolve the promise<number> after 2 seconds.", async () => {
const start = hrtime();
const res = await delayPromiseFunction(promiseFunc, 2000);
const end = hrtime(start);
const duration = getDurationInSeconds(end);
expect(Math.floor(duration)).toEqual(2);
expect(typeof res).toBe("number");
});
it("Should resolve the promise<number> after 4 seconds.", async () => {
const start = hrtime();
const res = await delayPromiseFunction(promiseFunc, 4000);
const end = hrtime(start);
const duration = getDurationInSeconds(end);
expect(Math.floor(duration)).toEqual(4);
expect(typeof res).toBe("number");
});
});

View File

@@ -58,7 +58,12 @@ describe("processRequest", () => {
(axios as unknown as jest.Mock).mockResolvedValue(DEFAULT_RESPONSE); (axios as unknown as jest.Mock).mockResolvedValue(DEFAULT_RESPONSE);
return expect( return expect(
processRequest(SAMPLE_REQUEST, DEFAULT_ENVS, "fake/collection/path")() processRequest({
request: SAMPLE_REQUEST,
envs: DEFAULT_ENVS,
path: "fake/collection/path",
delay: 0,
})()
).resolves.toMatchObject({ ).resolves.toMatchObject({
report: { report: {
result: true, result: true,
@@ -79,7 +84,12 @@ describe("processRequest", () => {
(axios as unknown as jest.Mock).mockResolvedValue(DEFAULT_RESPONSE); (axios as unknown as jest.Mock).mockResolvedValue(DEFAULT_RESPONSE);
return expect( return expect(
processRequest(SAMPLE_REQUEST, DEFAULT_ENVS, "fake/collection/path")() processRequest({
request: SAMPLE_REQUEST,
envs: DEFAULT_ENVS,
path: "fake/collection/path",
delay: 0,
})()
).resolves.toMatchObject({ ).resolves.toMatchObject({
envs: { envs: {
selected: [{ key: "ENDPOINT", value: "https://example.com" }], selected: [{ key: "ENDPOINT", value: "https://example.com" }],
@@ -96,7 +106,12 @@ describe("processRequest", () => {
(axios as unknown as jest.Mock).mockResolvedValue(DEFAULT_RESPONSE); (axios as unknown as jest.Mock).mockResolvedValue(DEFAULT_RESPONSE);
return expect( return expect(
processRequest(SAMPLE_REQUEST, DEFAULT_ENVS, "fake/request/path")() processRequest({
request: SAMPLE_REQUEST,
envs: DEFAULT_ENVS,
path: "fake/request/path",
delay: 0,
})()
).resolves.toMatchObject({ ).resolves.toMatchObject({
report: { result: false }, report: { result: false },
}); });

View File

@@ -9,12 +9,14 @@ import { handleError } from "../handlers/error";
import { parseCollectionData } from "../utils/mutators"; import { parseCollectionData } from "../utils/mutators";
import { parseEnvsData } from "../options/test/env"; import { parseEnvsData } from "../options/test/env";
import { TestCmdOptions } from "../types/commands"; import { TestCmdOptions } from "../types/commands";
import { parseDelayOption } from "../options/test/delay";
export const test = (path: string, options: TestCmdOptions) => async () => { export const test = (path: string, options: TestCmdOptions) => async () => {
await pipe( await pipe(
TE.Do, TE.Do,
TE.bind("envs", () => parseEnvsData(options.env)), TE.bind("envs", () => parseEnvsData(options.env)),
TE.bind("collections", () => parseCollectionData(path)), TE.bind("collections", () => parseCollectionData(path)),
TE.bind("delay", () => parseDelayOption(options.delay)),
TE.chainTaskK(collectionsRunner), TE.chainTaskK(collectionsRunner),
TE.chainW(flow(collectionsRunnerResult, collectionsRunnerExit, TE.of)), TE.chainW(flow(collectionsRunnerResult, collectionsRunnerExit, TE.of)),
TE.mapLeft((e) => { TE.mapLeft((e) => {

View File

@@ -50,6 +50,10 @@ program
"path to a hoppscotch collection.json file for CI testing" "path to a hoppscotch collection.json file for CI testing"
) )
.option("-e, --env <file_path>", "path to an environment variables json file") .option("-e, --env <file_path>", "path to an environment variables json file")
.option(
"-d, --delay <delay_in_ms>",
"delay in milliseconds(ms) between consecutive requests within a collection"
)
.allowExcessArguments(false) .allowExcessArguments(false)
.allowUnknownOption(false) .allowUnknownOption(false)
.description("running hoppscotch collection.json file") .description("running hoppscotch collection.json file")

View File

@@ -0,0 +1,20 @@
import * as TE from "fp-ts/TaskEither";
import * as S from "fp-ts/string";
import { pipe } from "fp-ts/function";
import { error, HoppCLIError } from "../../types/errors";
export const parseDelayOption = (
delay: unknown
): TE.TaskEither<HoppCLIError, number> =>
!S.isString(delay)
? TE.right(0)
: pipe(
delay,
Number,
TE.fromPredicate(Number.isSafeInteger, () =>
error({
code: "INVALID_ARGUMENT",
data: "Expected '-d, --delay' value to be number",
})
)
);

View File

@@ -4,6 +4,7 @@ import { HoppEnvs } from "./request";
export type CollectionRunnerParam = { export type CollectionRunnerParam = {
collections: HoppCollection<HoppRESTRequest>[]; collections: HoppCollection<HoppRESTRequest>[];
envs: HoppEnvs; envs: HoppEnvs;
delay?: number;
}; };
export type HoppCollectionFileExt = "json"; export type HoppCollectionFileExt = "json";

View File

@@ -1,5 +1,6 @@
export type TestCmdOptions = { export type TestCmdOptions = {
env: string; env: string;
delay: number;
}; };
export type HoppEnvFileExt = "json"; export type HoppEnvFileExt = "json";

View File

@@ -26,3 +26,10 @@ export type RequestReport = {
result: boolean; result: boolean;
duration: { test: number; request: number; preRequest: number }; duration: { test: number; request: number; preRequest: number };
}; };
export type ProcessRequestParams = {
request: HoppRESTRequest;
envs: HoppEnvs;
path: string;
delay: number;
};

View File

@@ -5,7 +5,12 @@ import { bold } from "chalk";
import { log } from "console"; import { log } from "console";
import round from "lodash/round"; import round from "lodash/round";
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data"; import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data";
import { HoppEnvs, CollectionStack, RequestReport } from "../types/request"; import {
HoppEnvs,
CollectionStack,
RequestReport,
ProcessRequestParams,
} from "../types/request";
import { import {
getRequestMetrics, getRequestMetrics,
preProcessRequest, preProcessRequest,
@@ -41,6 +46,7 @@ export const collectionsRunner =
(param: CollectionRunnerParam): T.Task<RequestReport[]> => (param: CollectionRunnerParam): T.Task<RequestReport[]> =>
async () => { async () => {
const envs: HoppEnvs = param.envs; const envs: HoppEnvs = param.envs;
const delay = param.delay ?? 0;
const requestsReport: RequestReport[] = []; const requestsReport: RequestReport[] = [];
const collectionStack: CollectionStack[] = getCollectionStack( const collectionStack: CollectionStack[] = getCollectionStack(
param.collections param.collections
@@ -54,12 +60,18 @@ export const collectionsRunner =
for (const request of collection.requests) { for (const request of collection.requests) {
const _request = preProcessRequest(request); const _request = preProcessRequest(request);
const requestPath = `${path}/${_request.name}`; const requestPath = `${path}/${_request.name}`;
const processRequestParams: ProcessRequestParams = {
path: requestPath,
request: _request,
envs,
delay,
};
// Request processing initiated message. // Request processing initiated message.
log(WARN(`\nRunning: ${bold(requestPath)}`)); log(WARN(`\nRunning: ${bold(requestPath)}`));
// Processing current request. // Processing current request.
const result = await processRequest(_request, envs, requestPath)(); const result = await processRequest(processRequestParams)();
// Updating global & selected envs with new envs from processed-request output. // Updating global & selected envs with new envs from processed-request output.
const { global, selected } = result.envs; const { global, selected } = result.envs;

View File

@@ -129,3 +129,8 @@ export const getDurationInSeconds = (
const durationInSeconds = (end[0] * 1e9 + end[1]) / 1e9; const durationInSeconds = (end[0] * 1e9 + end[1]) / 1e9;
return round(durationInSeconds, precision); return round(durationInSeconds, precision);
}; };
export const roundDuration = (
duration: number,
precision: number = DEFAULT_DURATION_PRECISION
) => round(duration, precision);

View File

@@ -12,7 +12,11 @@ import { testRunner, getTestScriptParams, hasFailedTestCases } from "./test";
import { RequestConfig, EffectiveHoppRESTRequest } from "../interfaces/request"; import { RequestConfig, EffectiveHoppRESTRequest } from "../interfaces/request";
import { RequestRunnerResponse } from "../interfaces/response"; import { RequestRunnerResponse } from "../interfaces/response";
import { preRequestScriptRunner } from "./pre-request"; import { preRequestScriptRunner } from "./pre-request";
import { HoppEnvs, RequestReport } from "../types/request"; import {
HoppEnvs,
ProcessRequestParams,
RequestReport,
} from "../types/request";
import { import {
printPreRequestRunner, printPreRequestRunner,
printRequestRunner, printRequestRunner,
@@ -189,11 +193,11 @@ const getRequest = {
*/ */
export const processRequest = export const processRequest =
( (
request: HoppRESTRequest, params: ProcessRequestParams
envs: HoppEnvs,
path: string
): T.Task<{ envs: HoppEnvs; report: RequestReport }> => ): T.Task<{ envs: HoppEnvs; report: RequestReport }> =>
async () => { async () => {
const { envs, path, request, delay } = params;
// Initialising updatedEnvs with given parameter envs, will eventually get updated. // Initialising updatedEnvs with given parameter envs, will eventually get updated.
const result = { const result = {
envs: <HoppEnvs>envs, envs: <HoppEnvs>envs,
@@ -247,7 +251,9 @@ export const processRequest =
duration: 0, duration: 0,
}; };
// Executing request-runner. // Executing request-runner.
const requestRunnerRes = await requestRunner(requestConfig)(); const requestRunnerRes = await delayPromiseFunction<
E.Either<HoppCLIError, RequestRunnerResponse>
>(requestRunner(requestConfig), delay);
if (E.isLeft(requestRunnerRes)) { if (E.isLeft(requestRunnerRes)) {
// Updating report for errors & current result // Updating report for errors & current result
report.errors.push(requestRunnerRes.left); report.errors.push(requestRunnerRes.left);
@@ -358,3 +364,15 @@ export const getRequestMetrics = (
hasReqErrors ? { failed: 1, passed: 0 } : { failed: 0, passed: 1 }, hasReqErrors ? { failed: 1, passed: 0 } : { failed: 0, passed: 1 },
(requests) => <RequestMetrics>{ requests, duration } (requests) => <RequestMetrics>{ requests, duration }
); );
/**
* A function to execute promises with specific delay in milliseconds.
* @param func Function with promise with return type T.
* @param delay TIme in milliseconds to delay function.
* @returns Promise of type same as func.
*/
export const delayPromiseFunction = <T>(
func: () => Promise<T>,
delay: number
): Promise<T> =>
new Promise((resolve) => setTimeout(() => resolve(func()), delay));