@fast-check/vitest
Version:
Property based testing for Vitest based on fast-check
193 lines (192 loc) • 7.72 kB
JavaScript
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 };