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:
@@ -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",
|
||||||
|
|||||||
@@ -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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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 },
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
20
packages/hoppscotch-cli/src/options/test/delay.ts
Normal file
20
packages/hoppscotch-cli/src/options/test/delay.ts
Normal 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",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
@@ -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";
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export type TestCmdOptions = {
|
export type TestCmdOptions = {
|
||||||
env: string;
|
env: string;
|
||||||
|
delay: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type HoppEnvFileExt = "json";
|
export type HoppEnvFileExt = "json";
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
Reference in New Issue
Block a user