test-runner.ts (hoppscotch-2.2.1) | : | test-runner.ts (hoppscotch-3.0.0) | ||
---|---|---|---|---|
import { isLeft } from "fp-ts/lib/Either" | import * as O from "fp-ts/Option" | |||
import { pipe } from "fp-ts/lib/function" | import * as E from "fp-ts/Either" | |||
import { TaskEither, tryCatch, chain, right, left } from "fp-ts/lib/TaskEither" | import * as TE from "fp-ts/TaskEither" | |||
import { pipe } from "fp-ts/function" | ||||
import * as qjs from "quickjs-emscripten" | import * as qjs from "quickjs-emscripten" | |||
import { marshalObjectToVM } from "./utils" | import { Environment, parseTemplateStringE } from "@hoppscotch/data" | |||
import cloneDeep from "lodash/cloneDeep" | ||||
import { getEnv, marshalObjectToVM, setEnv } from "./utils" | ||||
/** | /** | |||
* The response object structure exposed to the test script | * The response object structure exposed to the test script | |||
*/ | */ | |||
export type TestResponse = { | export type TestResponse = { | |||
/** Status Code of the response */ | /** Status Code of the response */ | |||
status: number | status: number | |||
/** List of headers returned */ | /** List of headers returned */ | |||
headers: { key: string; value: string }[] | headers: { key: string; value: string }[] | |||
/** | /** | |||
skipping to change at line 48 | skipping to change at line 51 | |||
*/ | */ | |||
expectResults: ExpectResult[] | expectResults: ExpectResult[] | |||
/** | /** | |||
* Children test blocks (test blocks inside the test block) | * Children test blocks (test blocks inside the test block) | |||
*/ | */ | |||
children: TestDescriptor[] | children: TestDescriptor[] | |||
} | } | |||
/** | /** | |||
* Defines the result of a test script execution | ||||
*/ | ||||
export type TestResult = { | ||||
tests: TestDescriptor[] | ||||
envs: { | ||||
global: Environment["variables"] | ||||
selected: Environment["variables"] | ||||
} | ||||
} | ||||
/** | ||||
* Creates an Expectation object for use inside the sandbox | * Creates an Expectation object for use inside the sandbox | |||
* @param vm The QuickJS sandbox VM instance | * @param vm The QuickJS sandbox VM instance | |||
* @param expectVal The expecting value of the expectation | * @param expectVal The expecting value of the expectation | |||
* @param negated Whether the expectation is negated (negative) | * @param negated Whether the expectation is negated (negative) | |||
* @param currTestStack The current state of the test execution stack | * @param currTestStack The current state of the test execution stack | |||
* @returns Handle to the expectation object in VM | * @returns Handle to the expectation object in VM | |||
*/ | */ | |||
function createExpectation( | function createExpectation( | |||
vm: qjs.QuickJSVm, | vm: qjs.QuickJSVm, | |||
expectVal: any, | expectVal: any, | |||
skipping to change at line 301 | skipping to change at line 315 | |||
currTestStack[currTestStack.length - 1].expectResults.push({ | currTestStack[currTestStack.length - 1].expectResults.push({ | |||
status: "error", | status: "error", | |||
message: `Argument for toHaveLength should be a number`, | message: `Argument for toHaveLength should be a number`, | |||
}) | }) | |||
} | } | |||
return { value: vm.undefined } | return { value: vm.undefined } | |||
} | } | |||
) | ) | |||
const toIncludeHandle = vm.newFunction("toInclude", (needleHandle) => { | ||||
const expectedVal = vm.dump(needleHandle) | ||||
if (!(Array.isArray(expectVal) || typeof expectVal === "string")) { | ||||
currTestStack[currTestStack.length - 1].expectResults.push({ | ||||
status: "error", | ||||
message: `Expected toInclude to be called for an array or string`, | ||||
}) | ||||
return { value: vm.undefined } | ||||
} | ||||
if (expectedVal === null) { | ||||
currTestStack[currTestStack.length - 1].expectResults.push({ | ||||
status: "error", | ||||
message: `Argument for toInclude should not be null`, | ||||
}) | ||||
return { value: vm.undefined } | ||||
} | ||||
if (expectedVal === undefined) { | ||||
currTestStack[currTestStack.length - 1].expectResults.push({ | ||||
status: "error", | ||||
message: `Argument for toInclude should not be undefined`, | ||||
}) | ||||
return { value: vm.undefined } | ||||
} | ||||
let assertion = expectVal.includes(expectedVal) | ||||
if (negated) assertion = !assertion | ||||
const expectValPretty = JSON.stringify(expectVal) | ||||
const expectedValPretty = JSON.stringify(expectedVal) | ||||
if (assertion) { | ||||
currTestStack[currTestStack.length - 1].expectResults.push({ | ||||
status: "pass", | ||||
message: `Expected ${expectValPretty} to${ | ||||
negated ? " not" : "" | ||||
} include ${expectedValPretty}`, | ||||
}) | ||||
} else { | ||||
currTestStack[currTestStack.length - 1].expectResults.push({ | ||||
status: "fail", | ||||
message: `Expected ${expectValPretty} to${ | ||||
negated ? " not" : "" | ||||
} include ${expectedValPretty}`, | ||||
}) | ||||
} | ||||
return { value: vm.undefined } | ||||
}) | ||||
vm.setProp(resultHandle, "toBe", toBeFnHandle) | vm.setProp(resultHandle, "toBe", toBeFnHandle) | |||
vm.setProp(resultHandle, "toBeLevel2xx", toBeLevel2xxHandle) | vm.setProp(resultHandle, "toBeLevel2xx", toBeLevel2xxHandle) | |||
vm.setProp(resultHandle, "toBeLevel3xx", toBeLevel3xxHandle) | vm.setProp(resultHandle, "toBeLevel3xx", toBeLevel3xxHandle) | |||
vm.setProp(resultHandle, "toBeLevel4xx", toBeLevel4xxHandle) | vm.setProp(resultHandle, "toBeLevel4xx", toBeLevel4xxHandle) | |||
vm.setProp(resultHandle, "toBeLevel5xx", toBeLevel5xxHandle) | vm.setProp(resultHandle, "toBeLevel5xx", toBeLevel5xxHandle) | |||
vm.setProp(resultHandle, "toBeType", toBeTypeHandle) | vm.setProp(resultHandle, "toBeType", toBeTypeHandle) | |||
vm.setProp(resultHandle, "toHaveLength", toHaveLengthHandle) | vm.setProp(resultHandle, "toHaveLength", toHaveLengthHandle) | |||
vm.setProp(resultHandle, "toInclude", toIncludeHandle) | ||||
vm.defineProp(resultHandle, "not", { | vm.defineProp(resultHandle, "not", { | |||
get: () => { | get: () => { | |||
return createExpectation(vm, expectVal, !negated, currTestStack) | return createExpectation(vm, expectVal, !negated, currTestStack) | |||
}, | }, | |||
}) | }) | |||
toBeFnHandle.dispose() | toBeFnHandle.dispose() | |||
toBeLevel2xxHandle.dispose() | toBeLevel2xxHandle.dispose() | |||
toBeLevel3xxHandle.dispose() | toBeLevel3xxHandle.dispose() | |||
toBeLevel4xxHandle.dispose() | toBeLevel4xxHandle.dispose() | |||
toBeLevel5xxHandle.dispose() | toBeLevel5xxHandle.dispose() | |||
toBeTypeHandle.dispose() | toBeTypeHandle.dispose() | |||
toHaveLengthHandle.dispose() | toHaveLengthHandle.dispose() | |||
toIncludeHandle.dispose() | ||||
return resultHandle | return resultHandle | |||
} | } | |||
export const execTestScript = ( | export const execTestScript = ( | |||
testScript: string, | testScript: string, | |||
envs: TestResult["envs"], | ||||
response: TestResponse | response: TestResponse | |||
): TaskEither<string, TestDescriptor[]> => | ): TE.TaskEither<string, TestResult> => | |||
pipe( | pipe( | |||
tryCatch( | TE.tryCatch( | |||
async () => await qjs.getQuickJS(), | async () => await qjs.getQuickJS(), | |||
(reason) => `QuickJS initialization failed: ${reason}` | (reason) => `QuickJS initialization failed: ${reason}` | |||
), | ), | |||
chain( | TE.chain( | |||
// TODO: Make this more functional ? | // TODO: Make this more functional ? | |||
(QuickJS) => { | (QuickJS) => { | |||
let currentEnvs = cloneDeep(envs) | ||||
const vm = QuickJS.createVm() | const vm = QuickJS.createVm() | |||
const pwHandle = vm.newObject() | const pwHandle = vm.newObject() | |||
const testRunStack: TestDescriptor[] = [ | const testRunStack: TestDescriptor[] = [ | |||
{ descriptor: "root", expectResults: [], children: [] }, | { descriptor: "root", expectResults: [], children: [] }, | |||
] | ] | |||
const testFuncHandle = vm.newFunction( | const testFuncHandle = vm.newFunction( | |||
"test", | "test", | |||
skipping to change at line 377 | skipping to change at line 451 | |||
const expectFnHandle = vm.newFunction("expect", (expectValueHandle) => { | const expectFnHandle = vm.newFunction("expect", (expectValueHandle) => { | |||
const expectVal = vm.dump(expectValueHandle) | const expectVal = vm.dump(expectValueHandle) | |||
return { | return { | |||
value: createExpectation(vm, expectVal, false, testRunStack), | value: createExpectation(vm, expectVal, false, testRunStack), | |||
} | } | |||
}) | }) | |||
// Marshal response object | // Marshal response object | |||
const responseObjHandle = marshalObjectToVM(vm, response) | const responseObjHandle = marshalObjectToVM(vm, response) | |||
if (isLeft(responseObjHandle)) | if (E.isLeft(responseObjHandle)) | |||
return left(`Response marshalling failed: ${responseObjHandle.left}`) | return TE.left( | |||
`Response marshalling failed: ${responseObjHandle.left}` | ||||
) | ||||
vm.setProp(pwHandle, "response", responseObjHandle.right) | vm.setProp(pwHandle, "response", responseObjHandle.right) | |||
responseObjHandle.right.dispose() | responseObjHandle.right.dispose() | |||
vm.setProp(pwHandle, "expect", expectFnHandle) | vm.setProp(pwHandle, "expect", expectFnHandle) | |||
expectFnHandle.dispose() | expectFnHandle.dispose() | |||
vm.setProp(pwHandle, "test", testFuncHandle) | vm.setProp(pwHandle, "test", testFuncHandle) | |||
testFuncHandle.dispose() | testFuncHandle.dispose() | |||
// Environment management APIs | ||||
// TODO: Unified Implementation | ||||
const envHandle = vm.newObject() | ||||
const envGetHandle = vm.newFunction("get", (keyHandle) => { | ||||
const key: unknown = vm.dump(keyHandle) | ||||
if (typeof key !== "string") { | ||||
return { | ||||
error: vm.newString("Expected key to be a string"), | ||||
} | ||||
} | ||||
const result = pipe( | ||||
getEnv(key, currentEnvs), | ||||
O.match( | ||||
() => vm.undefined, | ||||
({ value }) => vm.newString(value) | ||||
) | ||||
) | ||||
return { | ||||
value: result, | ||||
} | ||||
}) | ||||
const envGetResolveHandle = vm.newFunction( | ||||
"getResolve", | ||||
(keyHandle) => { | ||||
const key: unknown = vm.dump(keyHandle) | ||||
if (typeof key !== "string") { | ||||
return { | ||||
error: vm.newString("Expected key to be a string"), | ||||
} | ||||
} | ||||
const result = pipe( | ||||
getEnv(key, currentEnvs), | ||||
E.fromOption(() => "INVALID_KEY" as const), | ||||
E.map(({ value }) => | ||||
pipe( | ||||
parseTemplateStringE(value, [ | ||||
...envs.selected, | ||||
...envs.global, | ||||
]), | ||||
// If the recursive resolution failed, return the unresolved v | ||||
alue | ||||
E.getOrElse(() => value) | ||||
) | ||||
), | ||||
// Create a new VM String | ||||
// NOTE: Do not shorten this to map(vm.newString) apparently it br | ||||
eaks it | ||||
E.map((x) => vm.newString(x)), | ||||
E.getOrElse(() => vm.undefined) | ||||
) | ||||
return { | ||||
value: result, | ||||
} | ||||
} | ||||
) | ||||
const envSetHandle = vm.newFunction("set", (keyHandle, valueHandle) => { | ||||
const key: unknown = vm.dump(keyHandle) | ||||
const value: unknown = vm.dump(valueHandle) | ||||
if (typeof key !== "string") { | ||||
return { | ||||
error: vm.newString("Expected key to be a string"), | ||||
} | ||||
} | ||||
if (typeof value !== "string") { | ||||
return { | ||||
error: vm.newString("Expected value to be a string"), | ||||
} | ||||
} | ||||
currentEnvs = setEnv(key, value, currentEnvs) | ||||
return { | ||||
value: vm.undefined, | ||||
} | ||||
}) | ||||
const envResolveHandle = vm.newFunction("resolve", (valueHandle) => { | ||||
const value: unknown = vm.dump(valueHandle) | ||||
if (typeof value !== "string") { | ||||
return { | ||||
error: vm.newString("Expected value to be a string"), | ||||
} | ||||
} | ||||
const result = pipe( | ||||
parseTemplateStringE(value, [ | ||||
...currentEnvs.selected, | ||||
...currentEnvs.global, | ||||
]), | ||||
E.getOrElse(() => value) | ||||
) | ||||
return { | ||||
value: vm.newString(result), | ||||
} | ||||
}) | ||||
vm.setProp(envHandle, "resolve", envResolveHandle) | ||||
envResolveHandle.dispose() | ||||
vm.setProp(envHandle, "set", envSetHandle) | ||||
envSetHandle.dispose() | ||||
vm.setProp(envHandle, "getResolve", envGetResolveHandle) | ||||
envGetResolveHandle.dispose() | ||||
vm.setProp(envHandle, "get", envGetHandle) | ||||
envGetHandle.dispose() | ||||
vm.setProp(pwHandle, "env", envHandle) | ||||
envHandle.dispose() | ||||
vm.setProp(vm.global, "pw", pwHandle) | vm.setProp(vm.global, "pw", pwHandle) | |||
pwHandle.dispose() | pwHandle.dispose() | |||
const evalRes = vm.evalCode(testScript) | const evalRes = vm.evalCode(testScript) | |||
if (evalRes.error) { | if (evalRes.error) { | |||
const errorData = vm.dump(evalRes.error) | const errorData = vm.dump(evalRes.error) | |||
evalRes.error.dispose() | evalRes.error.dispose() | |||
return left(`Script evaluation failed: ${errorData}`) | return TE.left(`Script evaluation failed: ${errorData}`) | |||
} | } | |||
vm.dispose() | vm.dispose() | |||
return right(testRunStack) | return TE.right({ | |||
tests: testRunStack, | ||||
envs: currentEnvs, | ||||
}) | ||||
} | } | |||
) | ) | |||
) | ) | |||
End of changes. 15 change blocks. | ||||
11 lines changed or deleted | 217 lines changed or added |