diff --git a/.changeset/vast-mangos-end.md b/.changeset/vast-mangos-end.md new file mode 100644 index 00000000..e1ca8898 --- /dev/null +++ b/.changeset/vast-mangos-end.md @@ -0,0 +1,5 @@ +--- +"fast-check": minor +--- + +✨ Add ability to configure plugins globally diff --git a/packages/fast-check/src/check/runner/Runner.ts b/packages/fast-check/src/check/runner/Runner.ts index e8115e02..b704fbf1 100644 --- a/packages/fast-check/src/check/runner/Runner.ts +++ b/packages/fast-check/src/check/runner/Runner.ts @@ -18,6 +18,7 @@ import type { IAsyncProperty } from '../property/AsyncProperty.js'; import type { IProperty } from '../property/Property.js'; import type { Value } from '../arbitrary/definition/Value.js'; import type { Plugin, PluginInstance } from '../plugin/Plugin.js'; +import { readInstalledGlobalPlugins } from './configuration/GlobalPlugins.js'; /** @internal */ function runIt( @@ -65,6 +66,26 @@ async function asyncPropertyExecution(property: IRawProperty, v: Ts) { return out; } +function applyPlugins( + plugins: Plugin[], + pluginOffset: number, + crossPluginContext: { [K in any]?: unknown }, + run: IRawProperty['run'], + pluginAfterAllCallbacks: Required>['afterAll'][], +) { + let nextRun = run; + for (let index = 0; index !== plugins.length; ++index) { + const pluginInstance = plugins[index](pluginOffset, crossPluginContext); + if (pluginInstance.decorateRun !== undefined) { + nextRun = pluginInstance.decorateRun(nextRun); + } + if (pluginInstance.afterAll !== undefined) { + pluginAfterAllCallbacks.push(pluginInstance.afterAll.bind(pluginInstance)); + } + } + return nextRun; +} + /** * Run the property, do not throw contrary to {@link assert} * @@ -125,21 +146,15 @@ function check(rawProperty: IRawProperty, params?: Parameters): unkn throw new Error('Invalid parameters encountered, only asyncProperty can be used when asyncReporter specified'); const property = decorateProperty(rawProperty, qParams); - const pluginSharedSessionContext: { [K in any]?: unknown } = {}; - const plugins: Plugin[] = qParams.plugins; - const pluginAfterAllCallbacks: Required>['afterAll'][] = []; let run: typeof property.run = property.isAsync() ? async (v) => asyncPropertyExecution(property, v) : (v) => propertyExecution(property, v); - for (let index = 0; index !== plugins.length; ++index) { - const pluginInstance = plugins[index](index, pluginSharedSessionContext); - if (pluginInstance.decorateRun !== undefined) { - run = pluginInstance.decorateRun(run); - } - if (pluginInstance.afterAll !== undefined) { - pluginAfterAllCallbacks.push(pluginInstance.afterAll.bind(pluginInstance)); - } - } + const pluginAfterAllCallbacks: Required>['afterAll'][] = []; + + const crossPluginContext: { [K in any]?: unknown } = {}; + const globalPlugins = readInstalledGlobalPlugins(); + run = applyPlugins(globalPlugins, 0, crossPluginContext, run, pluginAfterAllCallbacks); + run = applyPlugins(qParams.plugins, globalPlugins.length, crossPluginContext, run, pluginAfterAllCallbacks); const maxInitialIterations = qParams.path.length === 0 || qParams.path.indexOf(':') === -1 ? qParams.numRuns : -1; const maxSkips = qParams.numRuns * qParams.maxSkipsPerRun; diff --git a/packages/fast-check/src/check/runner/configuration/GlobalPlugins.ts b/packages/fast-check/src/check/runner/configuration/GlobalPlugins.ts new file mode 100644 index 00000000..c488f899 --- /dev/null +++ b/packages/fast-check/src/check/runner/configuration/GlobalPlugins.ts @@ -0,0 +1,28 @@ +import type { Plugin } from '../../plugin/Plugin.js'; + +const globalPlugins: Plugin[] = []; + +/** + * Install a plugin to be used by all the runners + * Installed plugins come before the ones passed via the `plugins` option of the run. + * + * @example + * ```typescript + * fc.installGlobalPlugin(myPlugin()); + * //... + * fc.assert(myProp, { plugins: [myOtherPlugin()] }) + * // equivalent to { plugins: [myPlugin(), myOtherPlugin()] } + * ``` + * + * @param plugin - Plugin to be installed globally + * + * @remarks Since 4.10.0 + * @public + */ +export function installGlobalPlugin(plugin: Plugin): void { + globalPlugins.push(plugin); +} + +export function readInstalledGlobalPlugins(): Plugin[] { + return globalPlugins; +} diff --git a/packages/fast-check/src/fast-check-default.ts b/packages/fast-check/src/fast-check-default.ts index 93a77d74..0cda5d19 100644 --- a/packages/fast-check/src/fast-check-default.ts +++ b/packages/fast-check/src/fast-check-default.ts @@ -209,6 +209,7 @@ import { noBias } from './arbitrary/noBias.js'; import { limitShrink } from './arbitrary/limitShrink.js'; import type { RandomGenerator } from './random/generator/RandomGenerator.js'; import type { Plugin, PluginInstance } from './check/plugin/Plugin.js'; +import { installGlobalPlugin } from './check/runner/configuration/GlobalPlugins.js'; // Explicit cast into string to avoid to have __type: "process.env.__PACKAGE_TYPE__" /** @@ -452,6 +453,7 @@ export { configureGlobal, readConfigureGlobal, resetConfigureGlobal, + installGlobalPlugin, ExecutionStatus, Random, Stream, diff --git a/website/docs/advanced/plugins.md b/website/docs/advanced/plugins.md index 076d1728..6b90ab7b 100644 --- a/website/docs/advanced/plugins.md +++ b/website/docs/advanced/plugins.md @@ -25,3 +25,13 @@ await fc.assert(fc.asyncProperty(...arbs, predicate), { plugins: [pluginA(...paramsForPluginA), pluginB(...paramsForPluginB)], }); ``` + +## Installing plugins globally + +Plugins needed by all your properties can be installed once, typically in a [setup file](/docs/configuration/global-settings/#integration-with-test-frameworks), with `installGlobalPlugin`. + +```ts +fc.installGlobalPlugin(pluginA()); +``` + +Installed plugins run before the ones passed to the runner, so the snippet above followed by `fc.assert(myProp, { plugins: [pluginB()] })` is equivalent to `plugins: [pluginA(), pluginB()]`. diff --git a/website/docs/configuration/global-settings.md b/website/docs/configuration/global-settings.md index c375a900..063c3e5b 100644 --- a/website/docs/configuration/global-settings.md +++ b/website/docs/configuration/global-settings.md @@ -58,6 +58,10 @@ fc.configureGlobal({ ...fc.readConfigureGlobal(), ...myNewOptions }); You can also fully reset all the global options by calling `resetConfigureGlobal`. ::: +:::info Plugins +[Plugins](/docs/advanced/plugins/) cannot be shared via `configureGlobal`, they have their own installer: `fc.installGlobalPlugin(myPlugin())`. +::: + Resources: [API reference](/docs/api/functions/configureGlobal). Available since 1.18.0.