UNPKG

@nestia/e2e

Version:

E2E test utilify functions

275 lines (244 loc) 8.03 kB
import fs from "fs"; import NodePath from "path"; /** * Dynamic Executor running prefixed functions. * * `DynamicExecutor` runs every (or some filtered) prefixed functions in a * specific directory. * * For reference, it's useful for test program development of a backend server. * Just write test functions under a directory, and just specify it. * Furthermore, if you compose e2e test programs to utilize the `@nestia/sdk` * generated API functions, you can take advantage of {@link DynamicBenchmarker} * at the same time. * * When you want to see some utilization cases, see the below example links. * * @author Jeongho Nam - https://github.com/samchon * @example * https://github.com/samchon/nestia-start/blob/master/test/index.ts * * @example * https://github.com/samchon/backend/blob/master/test/index.ts */ export namespace DynamicExecutor { /** * Function type of a prefixed. * * @template Arguments Type of parameters * @template Ret Type of return value */ export interface Closure<Arguments extends any[], Ret = any> { (...args: Arguments): Promise<Ret>; } /** Options for dynamic executor. */ export interface IProps<Parameters extends any[], Ret = any> { /** * Prefix of function name. * * Every prefixed function will be executed. * * In other words, if a function name doesn't start with the prefix, then it * would never be executed. */ prefix: string; /** Location of the test functions. */ location: string; /** * Get parameters of a function. * * @param name Function name * @returns Parameters */ parameters: (name: string) => Parameters; /** * On complete function. * * Listener of completion of a test function. * * @param exec Execution result of a test function */ onComplete?: (exec: IExecution) => void; /** * Filter function whether to run or not. * * @param name Function name * @returns Whether to run or not */ filter?: (name: string) => boolean; /** * Wrapper of test function. * * If you specify this `wrapper` property, every dynamic functions loaded * and called by this `DynamicExecutor` would be wrapped by the `wrapper` * function. * * @param name Function name * @param closure Function to be executed * @param parameters Parameters, result of options.parameters function. * @returns Wrapper function */ wrapper?: ( name: string, closure: Closure<Parameters, Ret>, parameters: Parameters, ) => Promise<any>; /** * Number of simultaneous requests. * * The number of requests to be executed simultaneously. * * If you configure a value greater than one, the dynamic executor will * process the functions concurrently with the given capacity value. * * @default 1 */ simultaneous?: number; /** * Extension of dynamic functions. * * @default js */ extension?: string; } /** Report, result of dynamic execution. */ export interface IReport { /** Location path of dynamic functions. */ location: string; /** Execution results of dynamic functions. */ executions: IExecution[]; /** Total elapsed time. */ time: number; } /** Execution of a test function. */ export interface IExecution { /** Name of function. */ name: string; /** Location path of the function. */ location: string; /** Returned value from the function. */ value: unknown; /** Error when occurred. */ error: Error | null; /** Elapsed time. */ started_at: string; /** Completion time. */ completed_at: string; } /** * Prepare dynamic executor in strict mode. * * In strict mode, if any error occurs, the program will be terminated * directly. Otherwise, {@link validate} mode does not terminate when error * occurs, but just archive the error log. * * @param props Properties of dynamic execution * @returns Report of dynamic test functions execution */ export const assert = <Arguments extends any[]>( props: IProps<Arguments>, ): Promise<IReport> => main(true)(props); /** * Prepare dynamic executor in loose mode. * * In loose mode, the program would not be terminated even when error occurs. * Instead, the error would be archived and returns as a list. Otherwise, * {@link assert} mode terminates the program directly when error occurs. * * @param props Properties of dynamic executor * @returns Report of dynamic test functions execution */ export const validate = <Arguments extends any[]>( props: IProps<Arguments>, ): Promise<IReport> => main(false)(props); const main = (assert: boolean) => async <Arguments extends any[]>( props: IProps<Arguments>, ): Promise<IReport> => { const report: IReport = { location: props.location, time: Date.now(), executions: [], }; const executor = execute(props)(report)(assert); const processes: Array<() => Promise<void>> = await iterate({ extension: props.extension ?? "js", location: props.location, executor, }); await Promise.all( new Array(props.simultaneous ?? 1).fill(0).map(async () => { while (processes.length !== 0) { const task = processes.shift(); await task?.(); } }), ); report.time = Date.now() - report.time; return report; }; const iterate = async <Arguments extends any[]>(props: { location: string; extension: string; executor: (path: string, modulo: Module<Arguments>) => Promise<void>; }): Promise<Array<() => Promise<void>>> => { const container: Array<() => Promise<void>> = []; const visitor = async (path: string): Promise<void> => { const directory: string[] = await fs.promises.readdir(path); for (const file of directory) { const location: string = NodePath.resolve(`${path}/${file}`); const stats: fs.Stats = await fs.promises.lstat(location); if (stats.isDirectory() === true) { await visitor(location); continue; } else if (file.substr(-3) !== `.${props.extension}`) continue; const modulo: Module<Arguments> = await import(location); container.push(() => props.executor(location, modulo)); } }; await visitor(props.location); return container; }; const execute = <Arguments extends any[]>(props: IProps<Arguments>) => (report: IReport) => (assert: boolean) => async (location: string, modulo: Module<Arguments>): Promise<void> => { for (const [key, closure] of Object.entries(modulo)) { if ( key.substring(0, props.prefix.length) !== props.prefix || typeof closure !== "function" || (props.filter && props.filter(key) === false) ) continue; const func = () => { if (props.wrapper !== undefined) return props.wrapper(key, closure, props.parameters(key)); else return closure(...props.parameters(key)); }; const result: IExecution = { name: key, location, value: undefined, error: null, started_at: new Date().toISOString(), completed_at: new Date().toISOString(), }; report.executions.push(result); try { result.value = await func(); result.completed_at = new Date().toISOString(); } catch (exp) { result.error = exp as Error; if (assert === true) throw exp; } finally { result.completed_at = new Date().toISOString(); if (props.onComplete) props.onComplete(result); } } }; interface Module<Arguments extends any[]> { [key: string]: Closure<Arguments>; } }