diff --git a/packages/fast-check/bench/runners.bench.ts b/packages/fast-check/bench/runners.bench.ts index 8e7b099e9..f626b2710 100644 --- a/packages/fast-check/bench/runners.bench.ts +++ b/packages/fast-check/bench/runners.bench.ts @@ -1,32 +1,57 @@ import { describe, bench } from 'vitest'; +import type { PluginInstance, UniversalPlugin } from '../src/fast-check.js'; import { fc } from './__test-helpers__/Imports.js'; -// Benchmark describe('runner', () => { - bench('assert on sync predicate not returning anything', () => { - // oxlint-disable-next-line no-empty-function - return fc.assert(fc.property(fc.constant(1), (_c) => {})); - }); - bench('assert on sync predicate returning true', () => { - // oxlint-disable-next-line no-empty-function + bench('assert on successful synchronous predicate', () => { return fc.assert(fc.property(fc.constant(1), (_c) => true)); }); - bench('assert on async predicate not returning anything', () => { - // oxlint-disable-next-line typescript/require-await no-empty-function - return fc.assert(fc.property(fc.constant(1), async (_c) => {})); - }); - bench('assert on async predicate returning true', () => { - // oxlint-disable-next-line typescript/require-await no-empty-function + + for (const { plugin, name } of [ + { plugin: emptyPlugin(), name: 'empty' }, + { plugin: fullPlugin(), name: 'full' }, + { plugin: fullAsyncPlugin(), name: 'full async' }, + ]) { + bench(`assert on successful synchronous predicate with 1 ${name} plugin`, () => { + return fc.assert( + fc.property(fc.constant(1), (_c) => true), + { plugins: [plugin] }, + ); + }); + + bench(`assert on successful synchronous predicate with 5 ${name} plugins`, () => { + return fc.assert( + fc.property(fc.constant(1), (_c) => true), + { plugins: [plugin, plugin, plugin, plugin, plugin] }, + ); + }); + } + + bench('assert on successful asynchronous predicate', () => { return fc.assert(fc.property(fc.constant(1), async (_c) => true)); }); - bench('check on sync predicate returning true', () => { - return ( - fc - // oxlint-disable-next-line no-empty-function - .check(fc.property(fc.constant(1), (_c) => true)) - // should not return anything for the `bench` function - // oxlint-disable-next-line no-empty-function - .then(() => {}) - ); - }); }); + +// Helpers + +function emptyPlugin(): UniversalPlugin { + return () => ({}); +} + +function fullPlugin(): UniversalPlugin { + return (): Required> => ({ + decorateGenerate: (nestedGenerate) => (mrng, runId) => nestedGenerate(mrng, runId), + decorateRun: (nestedRun) => (v) => nestedRun(v), + onAllRunsComplete: () => {}, + afterAll: () => {}, + }); +} + +function fullAsyncPlugin(): UniversalPlugin { + return (): Required> => ({ + decorateGenerate: (nestedGenerate) => (mrng, runId) => nestedGenerate(mrng, runId), + decorateRun: (nestedRun) => async (v) => nestedRun(v), + onAllRunsComplete: async () => {}, + afterAll: async () => {}, + }); +} diff --git a/packages/fast-check/src/check/runner/Runner.ts b/packages/fast-check/src/check/runner/Runner.ts index a6da9302a..3ab05ee33 100644 --- a/packages/fast-check/src/check/runner/Runner.ts +++ b/packages/fast-check/src/check/runner/Runner.ts @@ -8,11 +8,11 @@ import { propertyRunner } from './PropertyRunner.js'; import { createRunnerIterator } from './utils/RunnerIteratorBuilder.js'; import { reportRunDetails } from './utils/RunDetailsFormatter.js'; import { - applyPluginDecorators, - extractPluginCompletionCallbacks, + applyPluginGeneratorDecorators, + applyPluginRunDecorators, instantiatePlugins, + runPluginCompletionCallbacks, } from './utils/PluginIntegration.js'; -import { runAllCallbacksAndReturn } from './utils/AllCallbacksThenReturnRunner.js'; /** * Run the property, do not throw contrary to {@link assert} @@ -33,23 +33,23 @@ function check(property: Property, params?: Parameters): Promise - runAllCallbacksAndReturn( + runPluginCompletionCallbacks( + pluginInstances, runnerIterator.runExecution.toRunDetails(qParams.seed, qParams.path, maxSkips, qParams), - completionCallbacks, ), ); } diff --git a/packages/fast-check/src/check/runner/utils/AllCallbacksThenReturnRunner.spec.ts b/packages/fast-check/src/check/runner/utils/AllCallbacksThenReturnRunner.spec.ts index 4c57466aa..c47820635 100644 --- a/packages/fast-check/src/check/runner/utils/AllCallbacksThenReturnRunner.spec.ts +++ b/packages/fast-check/src/check/runner/utils/AllCallbacksThenReturnRunner.spec.ts @@ -37,7 +37,7 @@ describe('runAllCallbacksAndReturn', () => { const value = null; // Act - await runAllCallbacksAndReturn(value, callbacks)?.catch(() => {}); + await runAllCallbacksAndReturn(value, (index) => callbacks[index](), callbacks.length)?.catch(() => {}); // Assert expect(numCalls).toBe(callbacks.length); @@ -70,7 +70,9 @@ describe('runAllCallbacksAndReturn', () => { const value = null; // Act / Assert - expect(() => runAllCallbacksAndReturn(value, callbacks)?.catch(() => {})).not.toThrow(); + expect(() => + runAllCallbacksAndReturn(value, (index) => callbacks[index](), callbacks.length)?.catch(() => {}), + ).not.toThrow(); }, ), ); @@ -80,11 +82,10 @@ describe('runAllCallbacksAndReturn', () => { 'should return the value synchonously if all callbacks are synchronous and successful (callbacks count: $callbacksCount)', async ({ callbacksCount }) => { // Arrange - const callbacks = Array(callbacksCount).fill(() => {}); const value = Symbol(); // Act / Assert - const out = runAllCallbacksAndReturn(value, callbacks); + const out = runAllCallbacksAndReturn(value, () => {}, callbacksCount); // Assert expect(out).toBe(value); diff --git a/packages/fast-check/src/check/runner/utils/AllCallbacksThenReturnRunner.ts b/packages/fast-check/src/check/runner/utils/AllCallbacksThenReturnRunner.ts index 28ba4840a..92b510414 100644 --- a/packages/fast-check/src/check/runner/utils/AllCallbacksThenReturnRunner.ts +++ b/packages/fast-check/src/check/runner/utils/AllCallbacksThenReturnRunner.ts @@ -1,20 +1,20 @@ function runAllCallbacksAndReturnInternal( value: T, - callbacks: ((value: T) => void | Promise)[], + runCallbackAtIndex: (index: number) => void | undefined | Promise, callbackIndex: number, + callbacksCount: number, ): T | Promise { let interceptedOnce = false; let interceptedError: unknown = undefined; - for (let index = callbackIndex; index !== callbacks.length; ++index) { - const followUp = callbacks[index]; + for (let index = callbackIndex; index !== callbacksCount; ++index) { try { - const out = followUp(value); + const out = runCallbackAtIndex(index); if (out !== undefined) { return out.then( - () => runAllCallbacksAndReturnInternal(value, callbacks, index + 1), + () => runAllCallbacksAndReturnInternal(value, runCallbackAtIndex, index + 1, callbacksCount), (error) => { - const next = runAllCallbacksAndReturnInternal(value, callbacks, index + 1); - if (next !== value) { + const next = runAllCallbacksAndReturnInternal(value, runCallbackAtIndex, index + 1, callbacksCount); + if (!Object.is(next, value)) { return Promise.resolve(next).then( () => { throw error; @@ -45,12 +45,13 @@ function runAllCallbacksAndReturnInternal( * Run all callbacks in-order without any overlap. * Returns the originally passed value in case all callbacks were successful and the first encountered error in case of error. * NEVER throw in a synchronous way! - * @param value - The value to be passed to all callbacks and being returned by the function in case of success + * @param value - The value to be passed to all callbacks and being returned by the function in case of success (T should not be a Promise) * @param callbacks - The callbacks to be executed */ export function runAllCallbacksAndReturn( value: T, - callbacks: ((value: T) => void | Promise)[], + runCallbackAtIndex: (index: number) => void | Promise, + callbacksCount: number, ): T | Promise { - return runAllCallbacksAndReturnInternal(value, callbacks, 0); + return runAllCallbacksAndReturnInternal(value, runCallbackAtIndex, 0, callbacksCount); } diff --git a/packages/fast-check/src/check/runner/utils/PluginIntegration.ts b/packages/fast-check/src/check/runner/utils/PluginIntegration.ts index ba1121da0..1821b8dff 100644 --- a/packages/fast-check/src/check/runner/utils/PluginIntegration.ts +++ b/packages/fast-check/src/check/runner/utils/PluginIntegration.ts @@ -1,6 +1,8 @@ import type { Plugin, PluginInstance } from '../../plugin/Plugin.js'; import type { Property } from '../../property/types/Property.js'; import { readInstalledGlobalPlugins } from '../configuration/GlobalPlugins.js'; +import type { RunDetails } from '../reporter/RunDetails.js'; +import { runAllCallbacksAndReturn } from './AllCallbacksThenReturnRunner.js'; export function instantiatePlugins(localPlugins: Plugin[]): PluginInstance[] { const globalPlugins = readInstalledGlobalPlugins(); @@ -15,12 +17,11 @@ export function instantiatePlugins(localPlugins: Plugin[]): PluginInstan return pluginInstances; } -export function applyPluginDecorators( +export function applyPluginGeneratorDecorators( property: Property, pluginInstances: PluginInstance[], -): { generator: Pick, 'generate'>; run: Property['run'] } { +): Pick, 'generate'> { let decoratedGenerate: typeof property.generate | undefined = undefined; - let run: typeof property.run = (v) => property.run(v); for (let index = pluginInstances.length - 1; index >= 0; --index) { const pluginInstance = pluginInstances[index]; if (pluginInstance.decorateGenerate !== undefined) { @@ -29,31 +30,39 @@ export function applyPluginDecorators( } decoratedGenerate = pluginInstance.decorateGenerate(decoratedGenerate); } + } + return decoratedGenerate === undefined ? property : { generate: decoratedGenerate }; +} + +export function applyPluginRunDecorators( + property: Property, + pluginInstances: PluginInstance[], +): Property['run'] { + let run: typeof property.run = (v) => property.run(v); + for (let index = pluginInstances.length - 1; index >= 0; --index) { + const pluginInstance = pluginInstances[index]; if (pluginInstance.decorateRun !== undefined) { run = pluginInstance.decorateRun(run); } } - const generator = decoratedGenerate === undefined ? property : { generate: decoratedGenerate }; - return { generator, run }; + return run; } -export function extractPluginCompletionCallbacks( +export function runPluginCompletionCallbacks( pluginInstances: PluginInstance[], -): NonNullable['onAllRunsComplete']>[] { - const completionCallbacks: NonNullable['onAllRunsComplete']>[] = []; - for (let index = 0; index !== pluginInstances.length; ++index) { - const instance = pluginInstances[index]; - if (instance.onAllRunsComplete !== undefined) { - // oxlint-disable-next-line typescript/no-non-null-assertion - completionCallbacks.push((runDetails) => instance.onAllRunsComplete!(runDetails)); - } - } - for (let index = pluginInstances.length - 1; index >= 0; --index) { - const instance = pluginInstances[index]; - if (instance.afterAll !== undefined) { - // oxlint-disable-next-line typescript/no-non-null-assertion - completionCallbacks.push(() => instance.afterAll!()); - } + runDetails: RunDetails, +): RunDetails | Promise> { + if (pluginInstances.length === 0) { + return runDetails; } - return completionCallbacks; + const halfCount = pluginInstances.length; + const count = halfCount * 2; + return runAllCallbacksAndReturn( + runDetails, + (index) => + index < halfCount + ? pluginInstances[index].onAllRunsComplete?.(runDetails) + : pluginInstances[count - index - 1].afterAll?.(), + count, + ); }