UNPKG

@fast-check/vitest

Version:

Property based testing for Vitest based on fast-check

193 lines (192 loc) 7.72 kB
import * as fc from "fast-check"; import { assert, asyncProperty, gen, readConfigureGlobal, record } from "fast-check"; import { TestRunner, test as test$1 } from "vitest"; //#region src/internals/TestWithPropRunnerBuilder.ts function wrapProp(prop) { return (...args) => Promise.resolve(prop(...args)); } /** * Collect the suite chain from the given suite up to the file-level suite. * Returns the chain with the innermost suite first. */ function getSuiteChain(suite) { const chain = []; let current = suite; while (true) { chain.push(current); if ("filepath" in current) break; current = current.suite ?? current.file; } return chain; } /** Collect beforeEach hooks in top-down order (parent suites first) as vitest does */ function collectBeforeEachHooks(suite) { const chain = getSuiteChain(suite); const hooks = []; for (let i = chain.length - 1; i >= 0; i--) { const h = TestRunner.getSuiteHooks(chain[i]); hooks.push(...h.beforeEach); } return hooks; } /** Collect afterEach hooks in bottom-up order (current suite first) as vitest does */ function collectAfterEachHooks(suite) { const chain = getSuiteChain(suite); const hooks = []; for (let i = 0; i < chain.length; i++) { const h = TestRunner.getSuiteHooks(chain[i]); for (let j = h.afterEach.length - 1; j >= 0; j--) hooks.push(h.afterEach[j]); } return hooks; } function buildTestWithPropRunner(testFn, label, arbitraries, prop, params, timeout, fc) { const customParams = { ...params }; if (customParams.seed === void 0) { const seedFromGlobals = readConfigureGlobal().seed; if (seedFromGlobals !== void 0) customParams.seed = seedFromGlobals; else customParams.seed = Date.now() ^ Math.random() * 4294967296; } if (customParams.interruptAfterTimeLimit === void 0) customParams.interruptAfterTimeLimit = fc.readConfigureGlobal().interruptAfterTimeLimit; const promiseProp = wrapProp(prop); const propertyInstance = fc.asyncProperty(...arbitraries, promiseProp); testFn(`${label} (with seed=${customParams.seed})`, async () => { const test = TestRunner.getCurrentTest(); if (test === void 0) throw new Error("Could not find the running test context. Make sure your property-based test is defined inside a vitest test() or it() block. Running outside a standard vitest test callback (e.g. in a worker thread) is not supported."); const suite = test.suite ?? test.file; if (suite === void 0) throw new Error("Could not find a parent suite or file for the current test. Make sure your test is defined inside a describe() block or a test file, not as a standalone detached test."); const beforeEachHooks = collectBeforeEachHooks(suite); const afterEachHooks = collectAfterEachHooks(suite); const pendingCleanups = []; if (beforeEachHooks.length > 0 || afterEachHooks.length > 0) { let isFirstRun = true; propertyInstance.beforeEach(async (previousHook) => { await previousHook(); if (isFirstRun) { isFirstRun = false; return; } for (const hook of afterEachHooks) await hook(test.context, suite); for (let i = pendingCleanups.length - 1; i >= 0; i--) await pendingCleanups[i](); pendingCleanups.length = 0; for (const hook of beforeEachHooks) { const result = await hook(test.context, suite); if (typeof result === "function") pendingCleanups.push(result); } }); } try { await fc.assert(propertyInstance, customParams); } finally { for (let i = pendingCleanups.length - 1; i >= 0; i--) await pendingCleanups[i](); } }, timeout); } //#endregion //#region src/internals/TestBuilder.ts function adaptParametersForRecord(parameters, originalParamaters) { const parametersV3OrV4 = parameters; return { ...parameters, errorWithCause: parametersV3OrV4.errorWithCause !== void 0 ? parametersV3OrV4.errorWithCause : true, examples: parameters.examples !== void 0 ? parameters.examples.map((example) => example[0]) : void 0, reporter: originalParamaters.reporter, asyncReporter: originalParamaters.asyncReporter }; } function adaptExecutionTreeForRecord(executionSummary) { return executionSummary.map((summary) => ({ ...summary, value: summary.value[0], children: adaptExecutionTreeForRecord(summary.children) })); } function adaptRunDetailsForRecord(runDetails, originalParamaters) { return { ...runDetails, counterexample: runDetails.counterexample !== null ? runDetails.counterexample[0] : null, failures: runDetails.failures.map((failure) => failure[0]), executionSummary: adaptExecutionTreeForRecord(runDetails.executionSummary), runConfiguration: adaptParametersForRecord(runDetails.runConfiguration, originalParamaters) }; } function buildTestProp(testFn, fc) { return (arbitraries, params) => { if (Array.isArray(arbitraries)) return (testName, prop, timeout) => buildTestWithPropRunner(testFn, testName, arbitraries, prop, params, timeout, fc); return (testName, prop, timeout) => { const recordArb = record(arbitraries); const recordParams = params !== void 0 ? { ...params, examples: params.examples !== void 0 ? params.examples.map((example) => [example]) : void 0, reporter: params.reporter !== void 0 ? (runDetails) => params.reporter(adaptRunDetailsForRecord(runDetails, params)) : void 0, asyncReporter: params.asyncReporter !== void 0 ? (runDetails) => params.asyncReporter(adaptRunDetailsForRecord(runDetails, params)) : void 0 } : void 0; buildTestWithPropRunner(testFn, testName, [recordArb], (value) => prop(value), recordParams, timeout, fc); }; }; } /** * Build the enriched version of {it,test}, the one with added `.prop` */ function buildTest(testFn, testFnExtended, fc, ancestors = /* @__PURE__ */ new Set()) { let atLeastOneExtra = false; const extraKeys = {}; for (const unsafeKey of Object.getOwnPropertyNames(testFnExtended)) { const key = unsafeKey; if (!ancestors.has(key) && typeof testFnExtended[key] === "function") { atLeastOneExtra = true; extraKeys[key] = key !== "each" ? buildTest(testFn[key], testFnExtended[key], fc, new Set([...ancestors, key])) : testFn[key].bind(testFn); } } if (!atLeastOneExtra) return testFnExtended; const enrichedTestFnExtended = (...args) => testFnExtended(...args); if ("each" in testFnExtended) extraKeys["prop"] = buildTestProp(testFn, fc); return Object.assign(enrichedTestFnExtended, extraKeys); } //#endregion //#region src/internals/TestAlongGenerator.ts function isSig1OrSig2(args) { return typeof args[1] === "function" || args[1] === void 0 && args[2] === void 0; } function taskCollectorBuilder(...args) { const [name, fn, options] = isSig1OrSig2(args) ? args : [ args[0], args[2], args[1] ]; const taskName = typeof name === "function" ? name.name : name; TestRunner.getCurrentSuite().task(taskName, { ...this, ...typeof options === "number" ? { timeout: options } : options, handler: fn !== void 0 ? async (context) => { let calledOnce = false; const config = readConfigureGlobal(); try { const parameters = { numRuns: config.numRuns ?? 1, endOnFailure: config.endOnFailure ?? true, includeErrorInReport: false, errorWithCause: true }; await assert(asyncProperty(gen(), (g) => { const refinedG = Object.assign((arb, ...args) => { calledOnce = true; return g(arb, ...args); }, { values: () => g.values() }); return fn({ ...context, g: refinedG }); }), parameters); } catch (error) { if (calledOnce) throw error; throw error.cause; } } : void 0 }); } //#endregion //#region src/vitest-fast-check.ts const test = buildTest(test$1, TestRunner.createTaskCollector(taskCollectorBuilder), fc); const it = test; //#endregion export { fc, it, test };