From fd4d981b6a05f453d7afc8ee9cc01ab58db645f1 Mon Sep 17 00:00:00 2001 From: Nicolas DUBIEN Date: Wed, 10 Jun 2020 08:30:49 +0200 Subject: [PATCH 1/5] Add the ability to provide a custom reporter --- src/check/runner/Runner.ts | 20 +++++++--- src/check/runner/Sampler.ts | 4 +- .../runner/configuration/GlobalParameters.ts | 2 +- src/check/runner/configuration/Parameters.ts | 8 ++++ .../configuration/QualifiedParameters.ts | 40 +++++++++++++++++++ src/check/runner/reporter/RunDetails.ts | 8 ++++ src/check/runner/reporter/RunExecution.ts | 11 ++++- src/check/runner/utils/RunDetailsFormatter.ts | 14 ++++++- test/type/index.test-d.ts | 28 ++++++++++++- 9 files changed, 122 insertions(+), 13 deletions(-) diff --git a/src/check/runner/Runner.ts b/src/check/runner/Runner.ts index 483bf409577..8ebb2552112 100644 --- a/src/check/runner/Runner.ts +++ b/src/check/runner/Runner.ts @@ -13,7 +13,7 @@ import { RunnerIterator } from './RunnerIterator'; import { SourceValuesIterator } from './SourceValuesIterator'; import { toss } from './Tosser'; import { pathWalk } from './utils/PathWalker'; -import { throwIfFailed } from './utils/RunDetailsFormatter'; +import { reportRunDetails } from './utils/RunDetailsFormatter'; import { IAsyncProperty } from '../property/AsyncProperty'; import { IProperty } from '../property/Property'; @@ -99,7 +99,14 @@ function check(rawProperty: IRawProperty, params?: Parameters) { throw new Error('Invalid property encountered, please use a valid property'); if (rawProperty.run == null) throw new Error('Invalid property encountered, please use a valid property not an arbitrary'); - const qParams = QualifiedParameters.read({ ...readConfigureGlobal(), ...params }); + const qParams: QualifiedParameters = QualifiedParameters.read({ + ...readConfigureGlobal(), + ...params, + }); + if (qParams.reporter !== null && qParams.asyncReporter !== null) + throw new Error('Invalid parameters encountered, reporter and asyncReporter cannot be specified together'); + if (qParams.asyncReporter !== null && rawProperty.isAsync()) + throw new Error('Invalid parameters encountered, only asyncProperty can be used when asyncReporter specified'); const property = decorateProperty(rawProperty, qParams); const generator = toss(property, qParams.seed, qParams.randomType, qParams.examples); @@ -109,13 +116,14 @@ function check(rawProperty: IRawProperty, params?: Parameters) { const sourceValues = new SourceValuesIterator(initialValues, maxInitialIterations, maxSkips); return property.isAsync() ? asyncRunIt(property, sourceValues, qParams.verbose, qParams.markInterruptAsFailure).then((e) => - e.toRunDetails(qParams.seed, qParams.path, qParams.numRuns, maxSkips) + e.toRunDetails(qParams.seed, qParams.path, qParams.numRuns, maxSkips, qParams) ) : runIt(property, sourceValues, qParams.verbose, qParams.markInterruptAsFailure).toRunDetails( qParams.seed, qParams.path, qParams.numRuns, - maxSkips + maxSkips, + qParams ); } @@ -144,8 +152,8 @@ function assert(property: IProperty, params?: Parameters): void; function assert(property: IRawProperty, params?: Parameters): Promise | void; function assert(property: IRawProperty, params?: Parameters) { const out = check(property, params); - if (property.isAsync()) return (out as Promise>).then(throwIfFailed); - else throwIfFailed(out as RunDetails); + if (property.isAsync()) return (out as Promise>).then(reportRunDetails); + else reportRunDetails(out as RunDetails); } export { check, assert }; diff --git a/src/check/runner/Sampler.ts b/src/check/runner/Sampler.ts index 0bcbde1ce1b..39c3bc9a075 100644 --- a/src/check/runner/Sampler.ts +++ b/src/check/runner/Sampler.ts @@ -31,7 +31,7 @@ function streamSample( typeof params === 'number' ? { ...readConfigureGlobal(), numRuns: params } : { ...readConfigureGlobal(), ...params }; - const qParams: QualifiedParameters = QualifiedParameters.read(extendedParams); + const qParams: QualifiedParameters = QualifiedParameters.read(extendedParams); const tossedValues: Stream<() => Shrinkable> = stream( toss(toProperty(generator, qParams), qParams.seed, qParams.randomType, qParams.examples) ); @@ -94,7 +94,7 @@ function statistics( typeof params === 'number' ? { ...readConfigureGlobal(), numRuns: params } : { ...readConfigureGlobal(), ...params }; - const qParams = QualifiedParameters.read(extendedParams); + const qParams: QualifiedParameters = QualifiedParameters.read(extendedParams); const recorded: { [key: string]: number } = {}; for (const g of streamSample(generator, params)) { const out = classify(g); diff --git a/src/check/runner/configuration/GlobalParameters.ts b/src/check/runner/configuration/GlobalParameters.ts index 14aab1165e0..dec89b998ec 100644 --- a/src/check/runner/configuration/GlobalParameters.ts +++ b/src/check/runner/configuration/GlobalParameters.ts @@ -3,7 +3,7 @@ import { Parameters } from './Parameters'; const globalParametersSymbol = Symbol.for('fast-check/GlobalParameters'); -export type GlobalParameters = Pick>; +export type GlobalParameters = Pick, Exclude, 'path' | 'examples'>>; /** * Define global parameters that will be used by all the runners diff --git a/src/check/runner/configuration/Parameters.ts b/src/check/runner/configuration/Parameters.ts index 0f0d9fdeff0..72e63709b02 100644 --- a/src/check/runner/configuration/Parameters.ts +++ b/src/check/runner/configuration/Parameters.ts @@ -1,6 +1,7 @@ import { RandomGenerator } from 'pure-rand'; import { RandomType } from './RandomType'; import { VerbosityLevel } from './VerbosityLevel'; +import { RunDetails } from '../reporter/RunDetails'; /** * Customization of the parameters used to run the properties @@ -112,4 +113,11 @@ export interface Parameters { * it replays only the minimal counterexample. */ endOnFailure?: boolean; + + // TODO Default typings when T=void should be unknown + // Add note explaining that only one of them can be specified + // Add note explaining that specifying asyncReporter require users to use asyncProperty everywhere + // TODO ensure reporter is not asynchronous in typings + reporter?: (runDetails: RunDetails) => void; + asyncReporter?: (runDetails: RunDetails) => Promise; } diff --git a/src/check/runner/configuration/QualifiedParameters.ts b/src/check/runner/configuration/QualifiedParameters.ts index 9a35e24971d..4545be5583d 100644 --- a/src/check/runner/configuration/QualifiedParameters.ts +++ b/src/check/runner/configuration/QualifiedParameters.ts @@ -1,6 +1,7 @@ import prand, { RandomGenerator } from 'pure-rand'; import { Parameters } from './Parameters'; import { VerbosityLevel } from './VerbosityLevel'; +import { RunDetails } from '../reporter/RunDetails'; /** * @hidden @@ -24,6 +25,8 @@ export class QualifiedParameters { skipAllAfterTimeLimit: number | null; interruptAfterTimeLimit: number | null; markInterruptAsFailure: boolean; + reporter: ((runDetails: RunDetails) => void) | null; + asyncReporter: ((runDetails: RunDetails) => Promise) | null; constructor(op?: Parameters) { const p = op || {}; @@ -44,6 +47,43 @@ export class QualifiedParameters { this.unbiased = QualifiedParameters.readBoolean(p, 'unbiased'); this.examples = QualifiedParameters.readOrDefault(p, 'examples', []); this.endOnFailure = QualifiedParameters.readBoolean(p, 'endOnFailure'); + this.reporter = QualifiedParameters.readOrDefault(p, 'reporter', null); + this.asyncReporter = QualifiedParameters.readOrDefault(p, 'asyncReporter', null); + } + + toParameters(): Parameters { + const orUndefined = (value: V | null) => (value !== null ? value : undefined); + const parameters = { + seed: this.seed, + randomType: this.randomType, + numRuns: this.numRuns, + maxSkipsPerRun: this.maxSkipsPerRun, + timeout: orUndefined(this.timeout), + skipAllAfterTimeLimit: orUndefined(this.skipAllAfterTimeLimit), + interruptAfterTimeLimit: orUndefined(this.interruptAfterTimeLimit), + markInterruptAsFailure: this.markInterruptAsFailure, + path: this.path, + logger: this.logger, + unbiased: this.unbiased, + verbose: this.verbose, + examples: this.examples, + endOnFailure: this.endOnFailure, + reporter: orUndefined(this.reporter), + asyncReporter: orUndefined(this.asyncReporter), + }; + + // As we do not want to miss any of the parameters, + // we want the compilation to fail in case we missed one when building `parameters` + // in the code above. `failIfMissing` is ensuring that for us. + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const _failIfMissing: keyof Parameters extends keyof typeof parameters + ? true + : 'Some properties of Parameters have not been specified' = true; + + return parameters; } private static readSeed = (p: Parameters): number => { diff --git a/src/check/runner/reporter/RunDetails.ts b/src/check/runner/reporter/RunDetails.ts index ca00ab4552e..4c085fa1f30 100644 --- a/src/check/runner/reporter/RunDetails.ts +++ b/src/check/runner/reporter/RunDetails.ts @@ -1,5 +1,6 @@ import { VerbosityLevel } from '../configuration/VerbosityLevel'; import { ExecutionTree } from './ExecutionTree'; +import { Parameters } from '../configuration/Parameters'; /** * Post-run details produced by {@link check} @@ -129,4 +130,11 @@ interface RunDetailsWithDoc { * Verbosity level required by the user */ verbose: VerbosityLevel; + /** + * Configuration of the run + * + * It includes both local parameters set on `fc.assert` or `fc.check` + * and global ones specified using `fc.configureGlobal` + */ + runConfiguration: Parameters; } diff --git a/src/check/runner/reporter/RunExecution.ts b/src/check/runner/reporter/RunExecution.ts index 92778f4e6a5..1b5096e4379 100644 --- a/src/check/runner/reporter/RunExecution.ts +++ b/src/check/runner/reporter/RunExecution.ts @@ -2,6 +2,7 @@ import { VerbosityLevel } from '../configuration/VerbosityLevel'; import { ExecutionStatus } from './ExecutionStatus'; import { ExecutionTree } from './ExecutionTree'; import { RunDetails } from './RunDetails'; +import { QualifiedParameters } from '../configuration/QualifiedParameters'; /** * @hidden @@ -91,7 +92,13 @@ export class RunExecution { return [...offsetItems.slice(0, offsetItems.length - 1), `${middle}`, ...remainingItems.slice(1)].join(':'); }; - toRunDetails(seed: number, basePath: string, numRuns: number, maxSkips: number): RunDetails { + toRunDetails( + seed: number, + basePath: string, + numRuns: number, + maxSkips: number, + qParams: QualifiedParameters + ): RunDetails { if (!this.isSuccess()) { // encountered a property failure return { @@ -114,6 +121,7 @@ export class RunExecution { failures: this.extractFailures(), executionSummary: this.rootExecutionTrees, verbose: this.verbosity, + runConfiguration: qParams.toParameters(), }; } @@ -144,6 +152,7 @@ export class RunExecution { failures: [], executionSummary: this.rootExecutionTrees, verbose: this.verbosity, + runConfiguration: qParams.toParameters(), }; } } diff --git a/src/check/runner/utils/RunDetailsFormatter.ts b/src/check/runner/utils/RunDetailsFormatter.ts index 6acfdb4f441..5595c5ef137 100644 --- a/src/check/runner/utils/RunDetailsFormatter.ts +++ b/src/check/runner/utils/RunDetailsFormatter.ts @@ -153,4 +153,16 @@ function throwIfFailed(out: RunDetails): void { throw new Error(defaultReportMessage(out)); } -export { defaultReportMessage, throwIfFailed }; +/** + * @hidden + * In case this code has to be executed synchronously the caller + * has to make sure that no asyncReporter has been defined + * otherwise it might trigger an unchecked promise + */ +function reportRunDetails(out: RunDetails): Promise | void { + if (out.runConfiguration.asyncReporter) return out.runConfiguration.asyncReporter(out); + else if (out.runConfiguration.reporter) return out.runConfiguration.reporter(out); + else return throwIfFailed(out); +} + +export { defaultReportMessage, reportRunDetails }; diff --git a/test/type/index.test-d.ts b/test/type/index.test-d.ts index 208124b30c4..4f2092fe020 100644 --- a/test/type/index.test-d.ts +++ b/test/type/index.test-d.ts @@ -5,6 +5,28 @@ import * as fc from 'fast-check'; expectType(fc.assert(fc.property(fc.nat(), () => {}))); expectType>(fc.assert(fc.asyncProperty(fc.nat(), async () => {}))); +// assert (beforeEach, afterEach) +expectError(fc.assert(fc.property(fc.nat(), () => {}).beforeEach(async () => {}))); +expectError(fc.assert(fc.property(fc.nat(), () => {}).afterEach(async () => {}))); + +// assert (reporter) +expectType( + fc.assert( + fc.property(fc.nat(), fc.string(), () => {}), + { + reporter: (out: fc.RunDetails<[number, string]>) => {}, + } + ) +); +expectError( + fc.assert( + fc.property(fc.nat(), () => {}), + { + reporter: (out: fc.RunDetails<[string, string]>) => {}, + } + ) +); + // property expectType(fc.property(fc.nat(), (a) => {}) as fc.IProperty<[number]>); expectType(fc.property(fc.nat(), fc.string(), (a, b) => {}) as fc.IProperty<[number, string]>); @@ -17,8 +39,6 @@ expectType( ) ); expectError(fc.property(fc.nat(), fc.string(), (a: number, b: number) => {})); -expectError(fc.assert(fc.property(fc.nat(), () => {}).beforeEach(async () => {}))); -expectError(fc.assert(fc.property(fc.nat(), () => {}).afterEach(async () => {}))); // asyncProperty expectType(fc.asyncProperty(fc.nat(), async (a) => {}) as fc.IAsyncProperty<[number]>); @@ -110,3 +130,7 @@ expectType>(fc.dedup(fc.nat(), 5)); // TODO Typings shoul // func arbitrary expectType number>>(fc.func(fc.nat())); expectError(fc.func(1)); + +// configureGlobal +expectType(fc.configureGlobal({ reporter: (out: fc.RunDetails) => {} })); +expectError(fc.configureGlobal({ reporter: (out: fc.RunDetails<[number]>) => {} })); From 9b60831a6a38b166fa719b53d4c0035539667f27 Mon Sep 17 00:00:00 2001 From: Nicolas DUBIEN Date: Thu, 11 Jun 2020 22:31:29 +0200 Subject: [PATCH 2/5] [fix] Merge error --- .../configuration/QualifiedParameters.ts | 35 +------------------ 1 file changed, 1 insertion(+), 34 deletions(-) diff --git a/src/check/runner/configuration/QualifiedParameters.ts b/src/check/runner/configuration/QualifiedParameters.ts index c1b29282383..40ae76467d8 100644 --- a/src/check/runner/configuration/QualifiedParameters.ts +++ b/src/check/runner/configuration/QualifiedParameters.ts @@ -53,7 +53,7 @@ export class QualifiedParameters { toParameters(): Parameters { const orUndefined = (value: V | null) => (value !== null ? value : undefined); - const parameters = { + return { seed: this.seed, randomType: this.randomType, numRuns: this.numRuns, @@ -71,39 +71,6 @@ export class QualifiedParameters { reporter: orUndefined(this.reporter), asyncReporter: orUndefined(this.asyncReporter), }; - - // As we do not want to miss any of the parameters, - // we want the compilation to fail in case we missed one when building `parameters` - // in the code above. `failIfMissing` is ensuring that for us. - - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const _failIfMissing: keyof Parameters extends keyof typeof parameters - ? true - : 'Some properties of Parameters have not been specified' = true; - - return parameters; - } - - toParameters(): Parameters { - const orUndefined = (value: V | null) => (value !== null ? value : undefined); - return { - seed: this.seed, - randomType: this.randomType, - numRuns: this.numRuns, - maxSkipsPerRun: this.maxSkipsPerRun, - timeout: orUndefined(this.timeout), - skipAllAfterTimeLimit: orUndefined(this.skipAllAfterTimeLimit), - interruptAfterTimeLimit: orUndefined(this.interruptAfterTimeLimit), - markInterruptAsFailure: this.markInterruptAsFailure, - path: this.path, - logger: this.logger, - unbiased: this.unbiased, - verbose: this.verbose, - examples: this.examples, - endOnFailure: this.endOnFailure, - }; } private static readSeed = (p: Parameters): number => { From 31bb69797f36b8e6aae1b96b1da32216ea6fc47c Mon Sep 17 00:00:00 2001 From: Nicolas DUBIEN Date: Thu, 11 Jun 2020 22:44:44 +0200 Subject: [PATCH 3/5] Add missing JSDoc and Markdown doc --- documentation/1-Guides/Runners.md | 11 +++++++++ src/check/runner/configuration/Parameters.ts | 24 ++++++++++++++++---- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/documentation/1-Guides/Runners.md b/documentation/1-Guides/Runners.md index b1f911e77e5..c5870905dd5 100644 --- a/documentation/1-Guides/Runners.md +++ b/documentation/1-Guides/Runners.md @@ -110,6 +110,17 @@ export interface Parameters { interruptAfterTimeLimit?: number; // optional, interrupt test execution after a given time limit // in milliseconds (relies on Date.now): disabled by default markInterruptAsFailure?: boolean; // optional, mark interrupted runs as failure: disabled by default + reporter?: (runDetails: RunDetails) => void; // optional, custom reporter replacing the default one + // reporter is responsible to throw in case of failure, as an example default one throws + // whenever `runDetails.failed` is true but it is up to you + // it cannot be used in conjonction with asyncReporter + // it will be used by assert for both synchronous and asynchronous properties + asyncReporter?: (runDetails: RunDetails) => Promise; // optional, custom reporter replacing the default one + // reporter is responsible to throw in case of failure, as an example default one throws + // whenever `runDetails.failed` is true but it is up to you + // it cannot be used in conjonction with reporter + // it cannot be set on synchronous properties + // it will be used by assert for asynchronous properties } ``` diff --git a/src/check/runner/configuration/Parameters.ts b/src/check/runner/configuration/Parameters.ts index 72e63709b02..0c9722b3f86 100644 --- a/src/check/runner/configuration/Parameters.ts +++ b/src/check/runner/configuration/Parameters.ts @@ -113,11 +113,25 @@ export interface Parameters { * it replays only the minimal counterexample. */ endOnFailure?: boolean; - - // TODO Default typings when T=void should be unknown - // Add note explaining that only one of them can be specified - // Add note explaining that specifying asyncReporter require users to use asyncProperty everywhere - // TODO ensure reporter is not asynchronous in typings + /** + * Replace the default reporter handling errors by a custom one + * + * Reporter is responsible to throw in case of failure: default one throws whenever `runDetails.failed` is true. + * But you may want to change this behaviour in yours. + * + * Only used when calling `fc.assert` + * Cannot be defined in conjonction with `asyncReporter` + */ reporter?: (runDetails: RunDetails) => void; + /** + * Replace the default reporter handling errors by a custom one + * + * Reporter is responsible to throw in case of failure: default one throws whenever `runDetails.failed` is true. + * But you may want to change this behaviour in yours. + * + * Only used when calling `fc.assert` + * Cannot be defined in conjonction with `reporter` + * Not compatible with synchronous properties: runner will throw + */ asyncReporter?: (runDetails: RunDetails) => Promise; } From 6153e3e90381412da80abc09d55a371eb6c64349 Mon Sep 17 00:00:00 2001 From: Nicolas DUBIEN Date: Thu, 11 Jun 2020 23:00:51 +0200 Subject: [PATCH 4/5] Update tips --- documentation/1-Guides/Tips.md | 46 +++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/documentation/1-Guides/Tips.md b/documentation/1-Guides/Tips.md index b04aabc42c3..cf05f2a68d9 100644 --- a/documentation/1-Guides/Tips.md +++ b/documentation/1-Guides/Tips.md @@ -605,27 +605,33 @@ If you opt for `markInterruptAsFailure: true`, you can still limit the time take ## Customize the reported error -By default, fast-check automatically failures when you use `fc.assert` to run your properties. +By default, `fc.assert` automatically handles and formats the failures that occur when running your properties. -Nonetheless, in some cases you might be interested to customize, extend or even change what is supposed or not to be a failure. In order to plug your own report strategy you may do something like this: +Nonetheless, in some cases you might be interested into customizing, extending or even changing what should be a failure or how it should be formated. +In order to customize it, you can define your own reporting strategy by passing a custom reporter to `fc.assert`: ```javascript -const myCustomAssert = (property, parameters) => { - // `check` will execute the property for you - // its output contains all the data you need for your report - const out = fc.check(property, parameters); - - // Let's say we want to re-create the same `assert` - // as the one offered by fast-check - if (out.failed) { - // `defaultReportMessage` is an utility that make you able to have the exact - // same report as the one that would have been generated by `assert` - throw new Error(fc.defaultReportMessage(out)); +fc.assert( + // You can either use it with `fc.property` + // or `fc.asyncProperty` + fc.property(...), + { + reporter(out) { + // Let's say we want to re-create the default reporter of `assert` + if (out.failed) { + // `defaultReportMessage` is an utility that make you able to have the exact + // same report as the one that would have been generated by `assert` + throw new Error(fc.defaultReportMessage(out)); + } + } } -} +) ``` -Please note that, while the snippet above will work correctly on synchronous properties, it does not fully fulfilled what `assert` does: it cannot deal with asynchronous properties. If you want to fully re-implement `assert` the code should be updated as follow: +In case your reporter is relying on asynchronous code, you can specify it by setting `asyncReporter` instead of `reporter`. +Contrary to `reporter` that will be used for both synchronous and asynchronous properties, `asyncReporter` is forbidden for synchronous properties and makes them throw. + +In the past, before `reporter` or `asyncReporter`, writing your own `fc.assert` including your own reporter would have been written as follow: ```javascript const throwIfFailed = (out) => { @@ -635,9 +641,13 @@ const throwIfFailed = (out) => { } const myCustomAssert = (property, parameters) => { const out = fc.check(property, parameters); - return property.isAsync()) - ? out.then(throwIfFailed) - : throwIfFailed(out); + + if (property.isAsync()) { + return out.then(runDetails => { + throwIfFailed(runDetails) + }); + } + throwIfFailed(out); } ``` From 7e4ace507999e9c3362b0839d32aa455443be9e2 Mon Sep 17 00:00:00 2001 From: Nicolas DUBIEN Date: Thu, 11 Jun 2020 23:29:47 +0200 Subject: [PATCH 5/5] CodeSandbox trick --- documentation/1-Guides/Tips.md | 51 ++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/documentation/1-Guides/Tips.md b/documentation/1-Guides/Tips.md index cf05f2a68d9..9f13b38b182 100644 --- a/documentation/1-Guides/Tips.md +++ b/documentation/1-Guides/Tips.md @@ -17,6 +17,7 @@ Simple tips to unlock all the power of fast-check with only few changes. - [Setup global settings](#setup-global-settings) - [Avoid tests to reach the timeout of your test runner](#avoid-tests-to-reach-the-timeout-of-your-test-runner) - [Customize the reported error](#customize-the-reported-error) +- [Create a CodeSandbox link on error](#create-a-codesandbox-link-on-error) - [Migrate from jsverify to fast-check](#migrate-from-jsverify-to-fast-check) - [Supported targets from node to deno](#supported-targets-from-node-to-deno) @@ -651,6 +652,56 @@ const myCustomAssert = (property, parameters) => { } ``` +## Create a CodeSandbox link on error + +_If you have not read about ways to customize the reporter used by `fc.assert` please refer to the section above._ + +In some situations, it can be useful to directly publish a minimal reproduction of an issue in order to be able to play with it. +Custom reporters can be used to provide such capabilities. + +For instance, you can automatically generate CodeSandbox environments in case of failed property with the snippet below: + +```javascript +import { getParameters } from 'codesandbox/lib/api/define'; + +const buildCodeSandboxReporter = (createFiles) => { + return function reporter(runDetails) { + if (!runDetails.failed) { + return; + } + const counterexample = runDetails.counterexample; + const originalErrorMessage = fc.defaultReportMessage(runDetails); + if (counterexample === undefined) { + throw new Error(originalErrorMessage); + } + const files = { + ...createFiles(counterexample), + 'counterexample.js': { + content: `export const counterexample = ${fc.stringify(counterexample)}` + }, + 'report.txt': { + content: originalErrorMessage + } + } + const url = `https://codesandbox.io/api/v1/sandboxes/define?parameters=${getParameters({ files })}`; + throw new Error(`${originalErrorMessage}\n\nPlay with the failure here: ${url}`); + } +} + +fc.assert( + fc.property(...), + { + reporter: buildCodeSandboxReporter(counterexample => ({ + 'index.js': { + content: 'console.log("Code to reproduce the issue")' + } + })) + } +) +``` + +The official documentation explaining how to build CodeSandbox environments from an url is available here: https://codesandbox.io/docs/importing#get-request + ## Migrate from jsverify to fast-check The npm package [jsverify-to-fast-check](https://www.npmjs.com/package/jsverify-to-fast-check) comes with a set of tools to help users to migrate from jsverify to fast-check smoothly.