Repository: dubzzz/fast-check
Pull request #1683 (2021-04-05 05:22:02)

⬆️ Bump webpack from 5.28.0 to 5.30.0 in /test/esm/webpack-with-require
Bumps [webpack](https://github.com/webpack/webpack) from 5.28.0 to 5.30.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/webpack/webpack/releases">webpack's releases</a>.</em></p>
<blockquote>
<h2>v5.30.0</h2>
<h1>Features</h1>
<ul>
<li>add GC to memory cache
<ul>
<li>opt-in via <code>cache.maxGenerations</code> when <code>cache.type: &quot;memory&quot;</code></li>
<li>default for <code>cache.type: &quot;filesystem&quot;</code> and <code>mode: &quot;development&quot;</code></li>
<li>configure via <code>cache.maxMemoryGenerations</code> when <code>cache.type: &quot;filesystem&quot;</code></li>
<li>Generations = Rebuilds</li>
</ul>
</li>
<li>add GC for untouched filesystem cache files</li>
<li>allow to configurate GC for the filesystem cache via <code>cache.maxAge</code></li>
<li>allow to disable memory cache when using the filesystem cache with <code>cache.maxMemoryGenerations: 0</code></li>
<li>Caches will be cleared on Compiler close resp Cache shutdown (after persisting for the filesystem cache)</li>
</ul>
<h1>Bugfixes</h1>
<ul>
<li>add a few workarounds for v8 bug that causes memory leaks in optimized code (only partially fixes it)</li>
<li>after serializing filesystem no longer keeps cache items in memory, instead it will read them from disk again when accessed</li>
</ul>
<p>GC = Garbage Collection</p>
<h2>v5.29.0</h2>
<h1>Bugfixes</h1>
<ul>
<li>fix some edge cases for <code>splitChunks.maxSize</code> which cause too large chunks to be created</li>
<li>add <code>stats.groupModulesByType</code> to the schema</li>
</ul>
<h1>Developer Experience</h1>
<ul>
<li>add resolving trace for error during resolving build dependencies</li>
<li>expose Stats-related types</li>
<li>exports AsyncDependenciesBlock and <code>Module/Const/NullDependency</code> on the API</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/webpack/webpack/commit/f3dce69e9df5f778fb4850662b0f16f9fd9951d2"><code>f3dce69</code></a> 5.30.0</li>
<li><a href="https://github.com/webpack/webpack/commit/3b8d26df1e314bab4b0b481675995dd7a2c765c8"><code>3b8d26d</code></a> Merge pull request <a href="https://github-redirect.dependabot.com/webpack/webpack/issues/12990">#12990</a> from webpack/bugfix/memory-leak-in-ic</li>
<li><a href="https://github.com/webpack/webpack/commit/0522deb7618636b37acae64f5e3b581c7bbb8fca"><code>0522deb</code></a> 5.29.0</li>
<li><a href="https://github.com/webpack/webpack/commit/b9519dcf013c717d598da06f535cee72eb93bf18"><code>b9519dc</code></a> Merge pull request <a href="https://github-redirect.dependabot.com/webpack/webpack/issues/12987">#12987</a> from barak007/export-missing-entities</li>
<li><a href="https://github.com/webpack/webpack/commit/e42f6d71d5c0dab397adf4ede658bfd3822610c2"><code>e42f6d7</code></a> Merge pull request <a href="https://github-redirect.dependabot.com/webpack/webpack/issues/12963">#12963</a> from smelukov/add-missing-groupModulesByType</li>
<li><a href="https://github.com/webpack/webpack/commit/c84329f73cb52968495e2e8bd0e029a289ea0b3d"><code>c84329f</code></a> add GC to the memory cache and make it configurable</li>
<li><a href="https://github.com/webpack/webpack/commit/8074127b985118975d098981804617873b6953d8"><code>8074127</code></a> avoid leaking memory after the compiler has been closed</li>
<li><a href="https://github.com/webpack/webpack/commit/fb384fead70165b786f832a8f9e882741e9f6b57"><code>fb384fe</code></a> Merge pull request <a href="https://github-redirect.dependabot.com/webpack/webpack/issues/13033">#13033</a> from webpack/bugfix/split-chunks-max-size</li>
<li><a href="https://github.com/webpack/webpack/commit/d39e9483476f70bc28809e482a392f22235b458c"><code>d39e948</code></a> update cli snapshot</li>
<li><a href="https://github.com/webpack/webpack/commit/3d90836f29d0382660ba828e6a3d02e6364b4727"><code>3d90836</code></a> fix some edge cases in deterministicGrouping and add more tests</li>
<li>Additional commits viewable in <a href="https://github.com/webpack/webpack/compare/v5.28.0...v5.30.0">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=webpack&package-manager=npm_and_yarn&previous-version=5.28.0&new-version=5.30.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually


</details>

codesandbox[bot]: This pull request is automatically built and testable in [CodeSandbox](https://codesandbox.io).

  To see build info of the built libraries, click [here](https://ci.codesandbox.io/status/dubzzz/fast-check/pr/1683/builds/116342) or the icon next to each commit SHA.

Latest deployment of this branch, based on commit 73383dfbee37572ec6320d7230da784938e5a116:

|Sandbox| Source |
|--|--|
|[Vanilla](https://codesandbox.io/s/vanilla-hvt39)| Configuration |
|[fast-check-examples](https://codesandbox.io/s/fast-check-examples-d2zxd)| Configuration |

codecov[bot]: # [Codecov](https://codecov.io/gh/dubzzz/fast-check/pull/1683?src=pr&el=h1) Report
> Merging [#1683](https://codecov.io/gh/dubzzz/fast-check/pull/1683?src=pr&el=desc) (73383df) into [main](https://codecov.io/gh/dubzzz/fast-check/commit/99e9b3b1c918a627d92138fc7e00f190ea99fecb?el=desc) (99e9b3b) will **not change** coverage.
> The diff coverage is `n/a`.

[![Impacted file tree graph](https://codecov.io/gh/dubzzz/fast-check/pull/1683/graphs/tree.svg?width=650&height=150&src=pr&token=fRi2fJN730)](https://codecov.io/gh/dubzzz/fast-check/pull/1683?src=pr&el=tree)

```diff
@@           Coverage Diff           @@
##             main    #1683   +/-   ##
=======================================
  Coverage   97.04%   97.04%           
=======================================
  Files          98       98           
  Lines        3484     3484           
  Branches      680      680           
=======================================
  Hits         3381     3381           
  Misses         99       99           
  Partials        4        4           
```

| Flag | Coverage Δ | |
|---|---|---|
| unit-tests | `97.04% <ø> (ø)` | |
| unit-tests-10.x-Linux | `97.04% <ø> (ø)` | |
| unit-tests-12.x-Linux | `?` | |
| unit-tests-14.x-Linux | `?` | |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags#carryforward-flags-in-the-pull-request-comment) to find out more.


------

[Continue to review full report at Codecov](https://codecov.io/gh/dubzzz/fast-check/pull/1683?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/dubzzz/fast-check/pull/1683?src=pr&el=footer). Last update [99e9b3b...73383df](https://codecov.io/gh/dubzzz/fast-check/pull/1683?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

dubzzz: @dependabot rebase

Patch:
diff --git a/.github/workflows/build-status.yml b/.github/workflows/build-status.yml
index 953aa605..da5b465e 100644
--- a/.github/workflows/build-status.yml
+++ b/.github/workflows/build-status.yml
@@ -350,7 +350,7 @@ jobs:
           name: docs
           path: docs/
       - name: Publish to gh-pages
-        uses: JamesIves/github-pages-deploy-action@4.1.0
+        uses: JamesIves/github-pages-deploy-action@4.1.1
         with:
           branch: gh-pages
           folder: docs
diff --git a/README.md b/README.md
index fce47dc1..ea9fd203 100644
--- a/README.md
+++ b/README.md
@@ -142,7 +142,7 @@ m.stringify({bar: ['a', null, 'b']}, {arrayFormat: 'bracket'}) //=> "bar[]=a&bar
 m.parse('bar[]=a&bar&bar[]=b', {arrayFormat: 'bracket'})       //=> {bar: [null, 'b']}
 ```
 
-**[MORE: Issues detected thanks of fast-check](https://github.com/dubzzz/fast-check/blob/main/documentation/IssuesDiscovered.md)**
+**[MORE: Issues detected thanks to fast-check](https://github.com/dubzzz/fast-check/blob/main/documentation/IssuesDiscovered.md)**
 
 ## Credits
 
diff --git a/src/check/arbitrary/TupleArbitrary.ts b/src/check/arbitrary/TupleArbitrary.ts
index eef6d3e6..674d5f10 100644
--- a/src/check/arbitrary/TupleArbitrary.ts
+++ b/src/check/arbitrary/TupleArbitrary.ts
@@ -1,16 +1,18 @@
 import { Random } from '../../random/generator/Random';
 import { Stream } from '../../stream/Stream';
-import { cloneMethod } from '../symbols';
+import { cloneIfNeeded, cloneMethod, WithCloneMethod } from '../symbols';
 import { Arbitrary } from './definition/Arbitrary';
-import { Shrinkable } from './definition/Shrinkable';
+import { convertFromNext, convertToNext } from './definition/Converters';
+import { NextArbitrary } from './definition/NextArbitrary';
+import { NextValue } from './definition/NextValue';
 
 /** @internal */
-type ArbsArray<Ts extends unknown[]> = { [K in keyof Ts]: Arbitrary<Ts[K]> };
+type ArbsArray<Ts extends unknown[]> = { [K in keyof Ts]: NextArbitrary<Ts[K]> };
 /** @internal */
-type ShrinksArray<Ts extends unknown[]> = { [K in keyof Ts]: Shrinkable<Ts[K]> };
+type ValuesArray<Ts extends unknown[]> = { [K in keyof Ts]: NextValue<Ts[K]> };
 
 /** @internal */
-export class GenericTupleArbitrary<Ts extends unknown[]> extends Arbitrary<Ts> {
+export class GenericTupleArbitrary<Ts extends unknown[]> extends NextArbitrary<Ts> {
   constructor(readonly arbs: ArbsArray<Ts>) {
     super();
     for (let idx = 0; idx !== arbs.length; ++idx) {
@@ -19,53 +21,69 @@ export class GenericTupleArbitrary<Ts extends unknown[]> extends Arbitrary<Ts> {
         throw new Error(`Invalid parameter encountered at index ${idx}: expecting an Arbitrary`);
     }
   }
-  private static makeItCloneable<Ts>(vs: Ts[], shrinkables: Shrinkable<Ts>[]) {
-    (vs as any)[cloneMethod] = () => {
-      const cloned = [];
-      for (let idx = 0; idx !== shrinkables.length; ++idx) {
-        cloned.push(shrinkables[idx].value); // push potentially cloned values
-      }
-      GenericTupleArbitrary.makeItCloneable(cloned, shrinkables);
-      return cloned;
-    };
-    return vs;
+  private static makeItCloneable<TValue>(vs: TValue[], values: NextValue<TValue>[]): WithCloneMethod<TValue[]> {
+    return Object.defineProperty(vs, cloneMethod, {
+      value: () => {
+        const cloned = [];
+        for (let idx = 0; idx !== values.length; ++idx) {
+          cloned.push(values[idx].value); // push potentially cloned values
+        }
+        GenericTupleArbitrary.makeItCloneable(cloned, values);
+        return cloned;
+      },
+    });
   }
-  private static wrapper<Ts extends unknown[]>(shrinkables: ShrinksArray<Ts>): Shrinkable<Ts> {
+  private static wrapper<Ts extends unknown[]>(values: ValuesArray<Ts>): NextValue<Ts> {
     let cloneable = false;
-    const vs = ([] as unknown) as Ts;
-    for (let idx = 0; idx !== shrinkables.length; ++idx) {
-      const s = shrinkables[idx];
-      cloneable = cloneable || s.hasToBeCloned;
-      vs.push(s.value);
+    const vs = ([] as unknown) as Ts & unknown[];
+    const ctxs: unknown[] = [];
+    for (let idx = 0; idx !== values.length; ++idx) {
+      const v = values[idx];
+      cloneable = cloneable || v.hasToBeCloned;
+      vs.push(v.value);
+      ctxs.push(v.context);
     }
     if (cloneable) {
-      GenericTupleArbitrary.makeItCloneable(vs, shrinkables);
+      GenericTupleArbitrary.makeItCloneable(vs, values);
     }
-    return new Shrinkable(vs, () => GenericTupleArbitrary.shrinkImpl(shrinkables).map(GenericTupleArbitrary.wrapper));
+    return new NextValue(vs, ctxs);
+  }
+  generate(mrng: Random, biasFactor: number | undefined): NextValue<Ts> {
+    return GenericTupleArbitrary.wrapper<Ts>(this.arbs.map((a) => a.generate(mrng, biasFactor)) as ValuesArray<Ts>);
   }
-  generate(mrng: Random): Shrinkable<Ts> {
-    return GenericTupleArbitrary.wrapper<Ts>(this.arbs.map((a) => a.generate(mrng)) as ShrinksArray<Ts>);
+  canGenerate(value: unknown): value is Ts {
+    if (!Array.isArray(value) || value.length !== this.arbs.length) {
+      return false;
+    }
+    for (let index = 0; index !== this.arbs.length; ++index) {
+      if (!this.arbs[index].canGenerate(value[index])) {
+        return false;
+      }
+    }
+    return true;
   }
-  private static shrinkImpl<Ts extends unknown[]>(value: ShrinksArray<Ts>): Stream<ShrinksArray<Ts>> {
+  shrink(value: Ts, context?: unknown): Stream<NextValue<Ts>> {
     // shrinking one by one is the not the most comprehensive
     // but allows a reasonable number of entries in the shrink
-    let s = Stream.nil<ShrinksArray<Ts>>();
-    for (let idx = 0; idx !== value.length; ++idx) {
-      s = s.join(
-        value[idx].shrink().map(
-          (v) =>
-            value
-              .slice(0, idx)
-              .concat([v])
-              .concat(value.slice(idx + 1)) as ShrinksArray<Ts>
-        )
-      );
+    let s = Stream.nil<NextValue<Ts>>();
+    const safeContext: unknown[] = Array.isArray(context) ? context : [];
+    for (let idx = 0; idx !== this.arbs.length; ++idx) {
+      const shrinksForIndex: Stream<NextValue<Ts>> = this.arbs[idx]
+        .shrink(value[idx], safeContext[idx])
+        .map((v) => {
+          const nextValues: NextValue<unknown>[] = value.map(
+            (v, idx) => new NextValue(cloneIfNeeded(v), safeContext[idx])
+          );
+          return nextValues
+            .slice(0, idx)
+            .concat([v])
+            .concat(nextValues.slice(idx + 1));
+        })
+        .map((values) => GenericTupleArbitrary.wrapper(values) as NextValue<Ts>);
+      s = s.join(shrinksForIndex);
     }
     return s;
   }
-  withBias(freq: number): Arbitrary<Ts> {
-    return new GenericTupleArbitrary<Ts>(this.arbs.map((a) => a.withBias(freq)) as ArbsArray<Ts>);
-  }
 }
 
 /**
@@ -78,7 +96,8 @@ export class GenericTupleArbitrary<Ts extends unknown[]> extends Arbitrary<Ts> {
  * @public
  */
 function genericTuple<Ts extends unknown[]>(arbs: { [K in keyof Ts]: Arbitrary<Ts[K]> }): Arbitrary<Ts> {
-  return new GenericTupleArbitrary<Ts>(arbs);
+  const nextArbs = arbs.map((arb) => convertToNext(arb)) as { [K in keyof Ts]: NextArbitrary<Ts[K]> };
+  return convertFromNext(new GenericTupleArbitrary<Ts>(nextArbs));
 }
 
 /**
@@ -90,7 +109,8 @@ function genericTuple<Ts extends unknown[]>(arbs: { [K in keyof Ts]: Arbitrary<T
  * @public
  */
 function tuple<Ts extends unknown[]>(...arbs: { [K in keyof Ts]: Arbitrary<Ts[K]> }): Arbitrary<Ts> {
-  return new GenericTupleArbitrary<Ts>(arbs);
+  const nextArbs = arbs.map((arb) => convertToNext(arb)) as { [K in keyof Ts]: NextArbitrary<Ts[K]> };
+  return convertFromNext(new GenericTupleArbitrary<Ts>(nextArbs));
 }
 
 export { genericTuple, tuple };


Test patch:
diff --git a/test/esm/webpack-with-require/package.json b/test/esm/webpack-with-require/package.json
index 514abe38..20e48a67 100644
--- a/test/esm/webpack-with-require/package.json
+++ b/test/esm/webpack-with-require/package.json
@@ -11,7 +11,7 @@
   },
   "devDependencies": {
     "querystring-es3": "^0.2.1",
-    "webpack": "^5.28.0",
+    "webpack": "^5.30.0",
     "webpack-cli": "^4.6.0"
   },
   "license": "MIT",
diff --git a/test/esm/webpack-with-require/yarn.lock b/test/esm/webpack-with-require/yarn.lock
index 412f3cc5..44c21d91 100644
--- a/test/esm/webpack-with-require/yarn.lock
+++ b/test/esm/webpack-with-require/yarn.lock
@@ -807,10 +807,10 @@ webpack-sources@^2.1.1:
     source-list-map "^2.0.1"
     source-map "^0.6.1"
 
-webpack@^5.28.0:
-  version "5.28.0"
-  resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.28.0.tgz#0de8bcd706186b26da09d4d1e8cbd3e4025a7c2f"
-  integrity sha512-1xllYVmA4dIvRjHzwELgW4KjIU1fW4PEuEnjsylz7k7H5HgPOctIq7W1jrt3sKH9yG5d72//XWzsHhfoWvsQVg==
+webpack@^5.30.0:
+  version "5.30.0"
+  resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.30.0.tgz#07d87c182a060e0c2491062f3dc0edc85a29d884"
+  integrity sha512-Zr9NIri5yzpfmaMea2lSMV1UygbW0zQsSlGLMgKUm63ACXg6alhd1u4v5UBSBjzYKXJN6BNMGVM7w165e7NxYA==
   dependencies:
     "@types/eslint-scope" "^3.7.0"
     "@types/estree" "^0.0.46"
diff --git a/test/unit/check/arbitrary/TupleArbitrary.generic.spec.ts b/test/unit/check/arbitrary/TupleArbitrary.generic.spec.ts
deleted file mode 100644
index 4e7b051a..00000000
--- a/test/unit/check/arbitrary/TupleArbitrary.generic.spec.ts
+++ /dev/null
@@ -1,124 +0,0 @@
-import * as fc from '../../../../lib/fast-check';
-import * as prand from 'pure-rand';
-import { dummy } from './TupleArbitrary.properties';
-
-import { Arbitrary } from '../../../../src/check/arbitrary/definition/Arbitrary';
-import { context } from '../../../../src/check/arbitrary/ContextArbitrary';
-import { integer, nat } from '../../../../src/check/arbitrary/IntegerArbitrary';
-import { genericTuple } from '../../../../src/check/arbitrary/TupleArbitrary';
-import { Shrinkable } from '../../../../src/check/arbitrary/definition/Shrinkable';
-import { hasCloneMethod, cloneMethod } from '../../../../src/check/symbols';
-import { stream } from '../../../../src/stream/Stream';
-import { Random } from '../../../../src/random/generator/Random';
-
-import * as genericHelper from './generic/GenericArbitraryHelper';
-import * as stubRng from '../../stubs/generators';
-
-describe('TupleArbitrary', () => {
-  describe('genericTuple', () => {
-    genericHelper.isValidArbitrary((mins: number[]) => genericTuple(mins.map((m) => integer(m, m + 10))), {
-      seedGenerator: fc.array(fc.nat(1000)),
-      isStrictlySmallerValue: (g1: number[], g2: number[]) => g1.findIndex((v, idx) => v < g2[idx]) !== -1,
-      isValidValue: (g: number[], mins: number[]) => {
-        // right size
-        if (g.length !== mins.length) return false;
-        // values in the right range
-        for (let idx = 0; idx !== g.length; ++idx) {
-          if (g[idx] < mins[idx]) return false;
-          if (g[idx] > mins[idx] + 10) return false;
-        }
-        return true;
-      },
-    });
-    it('Should throw on null arbitrary', () =>
-      expect(() => genericTuple([dummy(1), dummy(2), (null as any) as Arbitrary<string>])).toThrowError());
-    it('Should throw on invalid arbitrary', () =>
-      expect(() => genericTuple([dummy(1), dummy(2), {} as Arbitrary<any>])).toThrowError());
-    it('Should produce cloneable tuple if one cloneable children', () =>
-      fc.assert(
-        fc.property(fc.nat(50), fc.nat(50), (before, after) => {
-          const arbsBefore = [...Array(before)].map(() => integer(0, 0));
-          const arbsAfter = [...Array(after)].map(() => integer(0, 0));
-          const arbs: Arbitrary<unknown>[] = [...arbsBefore, context(), ...arbsAfter];
-          const mrng = stubRng.mutable.counter(0);
-          const g = genericTuple(arbs).generate(mrng).value;
-          return hasCloneMethod(g);
-        })
-      ));
-    it('Should not produce cloneable tuple if no cloneable children', () =>
-      fc.assert(
-        fc.property(fc.nat(100), (num) => {
-          const arbs = [...Array(num)].map(() => integer(0, 0));
-          const mrng = stubRng.mutable.counter(0);
-          const g = genericTuple(arbs).generate(mrng).value;
-          return !hasCloneMethod(g);
-        })
-      ));
-    it('Should not clone on generate', () => {
-      let numCallsToClone = 0;
-      const withClonedAndCounter = new (class extends Arbitrary<any> {
-        generate() {
-          const v = {
-            [cloneMethod]: () => {
-              ++numCallsToClone;
-              return v;
-            },
-          };
-          return new Shrinkable(v);
-        }
-      })();
-      const arbs = [withClonedAndCounter];
-      const mrng = stubRng.mutable.counter(0);
-      genericTuple(arbs).generate(mrng);
-      expect(numCallsToClone).toEqual(0);
-    });
-  });
-  it('Should use the cloneable instance for a single run only', () => {
-    class CloneableInstance {
-      static SharedId = 0;
-      readonly id: number;
-      constructor() {
-        this.id = ++CloneableInstance.SharedId;
-      }
-      [cloneMethod] = () => new CloneableInstance();
-    }
-    const cloneableArbitrary = new (class extends Arbitrary<CloneableInstance> {
-      generate() {
-        function* g() {
-          yield new Shrinkable(new CloneableInstance());
-          yield new Shrinkable(new CloneableInstance());
-        }
-        return new Shrinkable(new CloneableInstance(), () => stream(g()));
-      }
-    })();
-    const arbs = genericTuple([nat(16), cloneableArbitrary, nat(16)] as Arbitrary<any>[]);
-    const extractId = (shrinkable: Shrinkable<[number, CloneableInstance, number]>) => shrinkable.value_[1].id;
-    fc.assert(
-      fc.property(fc.integer().noShrink(), fc.infiniteStream(fc.nat()), (seed, shrinkPath) => {
-        // Reset SharedId in order to have reproducible runs
-        CloneableInstance.SharedId = 0;
-
-        // Generate the first shrinkable
-        const it = shrinkPath[Symbol.iterator]();
-        const mrng = new Random(prand.xorshift128plus(seed));
-        let shrinkable: Shrinkable<[number, CloneableInstance, number]> | null = arbs.generate(mrng) as any;
-
-        // Traverse the shrink tree in order to detect already seen ids
-        const alreadySeenIds: { [id: number]: boolean } = {
-          [extractId(shrinkable!)]: true,
-        };
-        while (shrinkable !== null) {
-          shrinkable = shrinkable
-            .shrink()
-            .map((nextShrinkable) => {
-              const id = extractId(nextShrinkable);
-              if (alreadySeenIds[id]) throw new Error(`Already encountered id ${id}`);
-              alreadySeenIds[id] = true;
-              return nextShrinkable;
-            })
-            .getNthOrLast(it.next().value);
-        }
-      })
-    );
-  });
-});
diff --git a/test/unit/check/arbitrary/TupleArbitrary.itest.spec.ts b/test/unit/check/arbitrary/TupleArbitrary.itest.spec.ts
new file mode 100644
index 00000000..418754dd
--- /dev/null
+++ b/test/unit/check/arbitrary/TupleArbitrary.itest.spec.ts
@@ -0,0 +1,231 @@
+import { tuple as tupleOld } from '../../../../src/check/arbitrary/TupleArbitrary';
+
+import { convertFromNext, convertToNext } from '../../../../src/check/arbitrary/definition/Converters';
+import { Arbitrary } from '../../../../src/check/arbitrary/definition/Arbitrary';
+import { NextArbitrary } from '../../../../src/check/arbitrary/definition/NextArbitrary';
+import { NextValue } from '../../../../src/check/arbitrary/definition/NextValue';
+import { Random } from '../../../../src/random/generator/Random';
+import { Stream } from '../../../../src/stream/Stream';
+
+import * as stubRng from '../../stubs/generators';
+import { buildNextShrinkTree, renderTree, walkTree } from './generic/ShrinkTree';
+import {
+  assertGenerateProducesCorrectValues,
+  assertGenerateProducesSameValueGivenSameSeed,
+  assertGenerateProducesValuesFlaggedAsCanGenerate,
+  assertShrinkProducesCorrectValues,
+  assertShrinkProducesSameValueWithoutInitialContext,
+  assertShrinkProducesStrictlySmallerValue,
+  assertShrinkProducesValuesFlaggedAsCanGenerate,
+} from './generic/NextArbitraryAssertions';
+import { FakeIntegerArbitrary } from './generic/NextArbitraryHelpers';
+import { cloneMethod } from '../../../../src/check/symbols';
+
+const mrngNoCall = stubRng.mutable.nocall();
+function tuple<Ts extends unknown[]>(...arbs: { [K in keyof Ts]: NextArbitrary<Ts[K]> }): NextArbitrary<Ts> {
+  const oldArbs = arbs.map((arb) => convertFromNext(arb)) as { [K in keyof Ts]: Arbitrary<Ts[K]> };
+  return convertToNext(tupleOld<Ts>(...oldArbs));
+}
+
+describe('tuple', () => {
+  const tupleBuilder = () => tuple(new FakeIntegerArbitrary(), new FakeIntegerArbitrary(), new FakeIntegerArbitrary());
+  it('should generate the same values given the same seed', () => {
+    assertGenerateProducesSameValueGivenSameSeed(tupleBuilder);
+  });
+
+  it('should only generate correct values', () => {
+    assertGenerateProducesCorrectValues(tupleBuilder, (value) => Array.isArray(value) && value.length === 3);
+  });
+
+  it('should recognize values that would have been generated using it during generate', () => {
+    assertGenerateProducesValuesFlaggedAsCanGenerate(tupleBuilder);
+  });
+
+  it('should shrink towards the same values given the same seed', () => {
+    assertGenerateProducesSameValueGivenSameSeed(tupleBuilder);
+  });
+
+  it('should be able to shrink without any context if underlyings do', () => {
+    assertShrinkProducesSameValueWithoutInitialContext(tupleBuilder);
+  });
+
+  it('should only shrink towards correct values', () => {
+    assertShrinkProducesCorrectValues(tupleBuilder, (value) => Array.isArray(value) && value.length === 3);
+  });
+
+  it('should recognize values that would have been generated using it during shrink', () => {
+    assertShrinkProducesValuesFlaggedAsCanGenerate(tupleBuilder);
+  });
+
+  it('should preserve strictly smaller ordering in shrink (underlyings do)', () => {
+    assertShrinkProducesStrictlySmallerValue(tupleBuilder, (t1, t2) => t1.findIndex((v, idx) => v < t2[idx]) !== -1);
+  });
+
+  it('should produce the right shrinking tree', () => {
+    // Arrange
+    const arb = tuple(new FirstArbitrary(), new SecondArbitrary());
+
+    // Act
+    const g = arb.generate(mrngNoCall, undefined);
+    const renderedTree = renderTree(buildNextShrinkTree(arb, g)).join('\n');
+
+    // Assert
+    expect(g.hasToBeCloned).toBe(false);
+    expect(g.value).toBe(g.value_);
+    expect(g.value).toEqual([expectedFirst, expectedSecond]);
+    expect(renderedTree).toMatchInlineSnapshot(`
+      "[4,97]
+      ├> [2,97]
+      |  ├> [0,97]
+      |  |  ├> [0,99]
+      |  |  └> [0,98]
+      |  |     └> [0,100]
+      |  ├> [2,99]
+      |  |  └> [0,99]
+      |  └> [2,98]
+      |     ├> [0,98]
+      |     |  └> [0,100]
+      |     └> [2,100]
+      |        └> [0,100]
+      ├> [3,97]
+      |  ├> [0,97]
+      |  |  ├> [0,99]
+      |  |  └> [0,98]
+      |  |     └> [0,100]
+      |  ├> [1,97]
+      |  |  ├> [1,99]
+      |  |  └> [1,98]
+      |  |     └> [1,100]
+      |  ├> [3,99]
+      |  |  ├> [0,99]
+      |  |  └> [1,99]
+      |  └> [3,98]
+      |     ├> [0,98]
+      |     |  └> [0,100]
+      |     ├> [1,98]
+      |     |  └> [1,100]
+      |     └> [3,100]
+      |        ├> [0,100]
+      |        └> [1,100]
+      ├> [4,99]
+      |  ├> [2,99]
+      |  |  └> [0,99]
+      |  └> [3,99]
+      |     ├> [0,99]
+      |     └> [1,99]
+      └> [4,98]
+         ├> [2,98]
+         |  ├> [0,98]
+         |  |  └> [0,100]
+         |  └> [2,100]
+         |     └> [0,100]
+         ├> [3,98]
+         |  ├> [0,98]
+         |  |  └> [0,100]
+         |  ├> [1,98]
+         |  |  └> [1,100]
+         |  └> [3,100]
+         |     ├> [0,100]
+         |     └> [1,100]
+         └> [4,100]
+            ├> [2,100]
+            |  └> [0,100]
+            └> [3,100]
+               ├> [0,100]
+               └> [1,100]"
+    `);
+  });
+
+  it('should not re-use twice the same instance of cloneable', () => {
+    // Arrange
+    const alreadySeenCloneable = new Set<unknown>();
+    const arb = tuple(new FirstArbitrary(), new CloneableArbitrary(), new SecondArbitrary());
+
+    // Act
+    const g = arb.generate(mrngNoCall, undefined);
+    const treeA = buildNextShrinkTree(arb, g);
+    const treeB = buildNextShrinkTree(arb, g);
+
+    // Assert
+    walkTree(treeA, ([_first, cloneable, _second]) => {
+      expect(alreadySeenCloneable.has(cloneable)).toBe(false);
+      alreadySeenCloneable.add(cloneable);
+    });
+    walkTree(treeB, ([_first, cloneable, _second]) => {
+      expect(alreadySeenCloneable.has(cloneable)).toBe(false);
+      alreadySeenCloneable.add(cloneable);
+    });
+  });
+});
+
+// Helpers
+
+const expectedFirst = 4;
+const expectedSecond = 97;
+
+class FirstArbitrary extends NextArbitrary<number> {
+  generate(_mrng: Random): NextValue<number> {
+    return new NextValue(expectedFirst, { step: 2 });
+  }
+  canGenerate(_value: unknown): _value is number {
+    throw new Error('No call expected in that scenario');
+  }
+  shrink(value: number, context?: unknown): Stream<NextValue<number>> {
+    if (typeof context !== 'object' || context === null || !('step' in context)) {
+      throw new Error('Invalid context for FirstArbitrary');
+    }
+    if (value <= 0) {
+      return Stream.nil();
+    }
+    const currentStep = (context as { step: number }).step;
+    const nextStep = currentStep + 1;
+    return Stream.of(
+      ...(value - currentStep >= 0 ? [new NextValue(value - currentStep, { step: nextStep })] : []),
+      ...(value - currentStep + 1 >= 0 ? [new NextValue(value - currentStep + 1, { step: nextStep })] : [])
+    );
+  }
+}
+class SecondArbitrary extends NextArbitrary<number> {
+  generate(_mrng: Random): NextValue<number> {
+    return new NextValue(expectedSecond, { step: 2 });
+  }
+  canGenerate(_value: unknown): _value is number {
+    throw new Error('No call expected in that scenario');
+  }
+  shrink(value: number, context?: unknown): Stream<NextValue<number>> {
+    if (typeof context !== 'object' || context === null || !('step' in context)) {
+      throw new Error('Invalid context for SecondArbitrary');
+    }
+    if (value >= 100) {
+      return Stream.nil();
+    }
+    const currentStep = (context as { step: number }).step;
+    const nextStep = currentStep + 1;
+    return Stream.of(
+      ...(value + currentStep <= 100 ? [new NextValue(value + currentStep, { step: nextStep })] : []),
+      ...(value + currentStep - 1 <= 100 ? [new NextValue(value + currentStep - 1, { step: nextStep })] : [])
+    );
+  }
+}
+
+class CloneableArbitrary extends NextArbitrary<number> {
+  private instance() {
+    return Object.defineProperty([], cloneMethod, { value: () => this.instance() });
+  }
+  generate(_mrng: Random): NextValue<number> {
+    return new NextValue(this.instance(), { shrunkOnce: false });
+  }
+  canGenerate(_value: unknown): _value is number {
+    throw new Error('No call expected in that scenario');
+  }
+  shrink(value: number, context?: unknown): Stream<NextValue<number>> {
+    if (typeof context !== 'object' || context === null || !('shrunkOnce' in context)) {
+      throw new Error('Invalid context for CloneableArbitrary');
+    }
+    const safeContext = context as { shrunkOnce: boolean };
+    if (safeContext.shrunkOnce) {
+      return Stream.nil();
+    }
+    return Stream.of(new NextValue(this.instance(), { shrunkOnce: true }));
+  }
+}
diff --git a/test/unit/check/arbitrary/TupleArbitrary.properties.ts b/test/unit/check/arbitrary/TupleArbitrary.properties.ts
deleted file mode 100644
index 51281eeb..00000000
--- a/test/unit/check/arbitrary/TupleArbitrary.properties.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import { Arbitrary } from '../../../../src/check/arbitrary/definition/Arbitrary';
-import { integer } from '../../../../src/check/arbitrary/IntegerArbitrary';
-import { Shrinkable } from '../../../../src/fast-check';
-import { Random } from '../../../../src/random/generator/Random';
-
-export class DummyArbitrary extends Arbitrary<string> {
-  constructor(public id: number) {
-    super();
-  }
-  generate(mrng: Random): Shrinkable<string> {
-    return integer()
-      .generate(mrng)
-      .map((v) => `key${this.id}_${v}`);
-  }
-  withBias(freq: number): Arbitrary<string> {
-    return new DummyArbitrary(2 * this.id + freq);
-  }
-}
-function dummy(id: number): Arbitrary<string> {
-  return new DummyArbitrary(id);
-}
-
-export { dummy };
diff --git a/test/unit/check/arbitrary/TupleArbitrary.utest.spec.ts b/test/unit/check/arbitrary/TupleArbitrary.utest.spec.ts
new file mode 100644
index 00000000..93ad38d6
--- /dev/null
+++ b/test/unit/check/arbitrary/TupleArbitrary.utest.spec.ts
@@ -0,0 +1,236 @@
+import { tuple as tupleOld } from '../../../../src/check/arbitrary/TupleArbitrary';
+
+import { convertFromNext, convertToNext } from '../../../../src/check/arbitrary/definition/Converters';
+import { Arbitrary } from '../../../../src/check/arbitrary/definition/Arbitrary';
+import { NextArbitrary } from '../../../../src/check/arbitrary/definition/NextArbitrary';
+import { NextValue } from '../../../../src/check/arbitrary/definition/NextValue';
+
+import * as stubRng from '../../stubs/generators';
+
+import { fakeNextArbitrary } from './generic/NextArbitraryHelpers';
+import { cloneMethod, hasCloneMethod } from '../../../../src/check/symbols';
+import { Stream } from '../../../../src/stream/Stream';
+
+const mrngNoCall = stubRng.mutable.nocall();
+function tuple<Ts extends unknown[]>(...arbs: { [K in keyof Ts]: NextArbitrary<Ts[K]> }): NextArbitrary<Ts> {
+  const oldArbs = arbs.map((arb) => convertFromNext(arb)) as { [K in keyof Ts]: Arbitrary<Ts[K]> };
+  return convertToNext(tupleOld<Ts>(...oldArbs));
+}
+
+describe('tuple', () => {
+  describe('generate', () => {
+    it('should merge results coming from underlyings and call them with the exact same inputs as the received ones', () => {
+      // Arrange
+      const expectedBiasFactor = 48;
+      const vA = Symbol();
+      const vB = Symbol();
+      const vC = Symbol();
+      const { instance: instanceA, generate: generateA } = fakeNextArbitrary<symbol>();
+      const { instance: instanceB, generate: generateB } = fakeNextArbitrary<symbol>();
+      const { instance: instanceC, generate: generateC } = fakeNextArbitrary<symbol>();
+      generateA.mockReturnValueOnce(new NextValue(vA));
+      generateB.mockReturnValueOnce(new NextValue(vB));
+      generateC.mockReturnValueOnce(new NextValue(vC));
+
+      // Act
+      const arb = tuple(instanceA, instanceB, instanceC);
+      const g = arb.generate(mrngNoCall, expectedBiasFactor);
+
+      // Assert
+      expect(g.value).toEqual([vA, vB, vC]);
+      expect(generateA).toHaveBeenCalledWith(mrngNoCall, expectedBiasFactor);
+      expect(generateB).toHaveBeenCalledWith(mrngNoCall, expectedBiasFactor);
+      expect(generateC).toHaveBeenCalledWith(mrngNoCall, expectedBiasFactor);
+    });
+
+    it('should produce a cloneable instance if provided one cloneable underlying', () => {
+      // Arrange
+      const { instance: fakeArbitraryNotCloneableA, generate: generateA } = fakeNextArbitrary<string[]>();
+      const { instance: fakeArbitraryCloneableB, generate: generateB } = fakeNextArbitrary<string[]>();
+      generateA.mockReturnValue(new NextValue([]));
+      generateB.mockReturnValue(new NextValue(Object.defineProperty([], cloneMethod, { value: jest.fn() })));
+
+      // Act
+      const arb = tuple(fakeArbitraryNotCloneableA, fakeArbitraryCloneableB);
+      const g = arb.generate(mrngNoCall, undefined);
+
+      // Assert
+      expect(g.hasToBeCloned).toBe(true);
+      expect(hasCloneMethod(g.value)).toBe(true);
+    });
+
+    it('should not produce a cloneable instance if no cloneable underlyings', () => {
+      // Arrange
+      const { instance: fakeArbitraryNotCloneableA, generate: generateA } = fakeNextArbitrary<string[]>();
+      const { instance: fakeArbitraryNotCloneableB, generate: generateB } = fakeNextArbitrary<string[]>();
+      generateA.mockReturnValue(new NextValue([]));
+      generateB.mockReturnValue(new NextValue([]));
+
+      // Act
+      const arb = tuple(fakeArbitraryNotCloneableA, fakeArbitraryNotCloneableB);
+      const g = arb.generate(mrngNoCall, undefined);
+
+      // Assert
+      expect(g.hasToBeCloned).toBe(false);
+      expect(hasCloneMethod(g.value)).toBe(false);
+    });
+
+    it('should not clone cloneable on generate', () => {
+      // Arrange
+      const { instance: fakeArbitraryNotCloneableA, generate: generateA } = fakeNextArbitrary<string[]>();
+      const { instance: fakeArbitraryCloneableB, generate: generateB } = fakeNextArbitrary<string[]>();
+      const cloneMethodImpl = jest.fn();
+      generateA.mockReturnValue(new NextValue([]));
+      generateB.mockReturnValue(new NextValue(Object.defineProperty([], cloneMethod, { value: cloneMethodImpl })));
+
+      // Act
+      const arb = tuple(fakeArbitraryNotCloneableA, fakeArbitraryCloneableB);
+      arb.generate(mrngNoCall, undefined);
+
+      // Assert
+      expect(cloneMethodImpl).not.toHaveBeenCalled();
+    });
+  });
+
+  describe('canGenerate', () => {
+    it.each`
+      canA     | canB     | canC
+      ${false} | ${false} | ${false}
+      ${false} | ${true}  | ${true}
+      ${true}  | ${false} | ${true}
+      ${true}  | ${true}  | ${false}
+      ${true}  | ${true}  | ${true}
+    `(
+      'should merge results coming from underlyings for canGenerate if received array has the right size',
+      ({ canA, canB, canC }) => {
+        // Arrange
+        const vA = Symbol();
+        const vB = Symbol();
+        const vC = Symbol();
+        const { instance: instanceA, canGenerate: canGenerateA } = fakeNextArbitrary<symbol>();
+        const { instance: instanceB, canGenerate: canGenerateB } = fakeNextArbitrary<symbol>();
+        const { instance: instanceC, canGenerate: canGenerateC } = fakeNextArbitrary<symbol>();
+        canGenerateA.mockReturnValueOnce(canA);
+        canGenerateB.mockReturnValueOnce(canB);
+        canGenerateC.mockReturnValueOnce(canC);
+
+        // Act
+        const arb = tuple(instanceA, instanceB, instanceC);
+        const out = arb.canGenerate([vA, vB, vC]);
+
+        // Assert
+        expect(out).toBe(canA && canB && canC);
+        expect(canGenerateA).toHaveBeenCalledWith(vA);
+        if (canA) expect(canGenerateB).toHaveBeenCalledWith(vB);
+        else expect(canGenerateB).not.toHaveBeenCalled();
+        if (canA && canB) expect(canGenerateC).toHaveBeenCalledWith(vC);
+        else expect(canGenerateC).not.toHaveBeenCalled();
+      }
+    );
+
+    it('should not call underlyings on canGenerate if size is invalid', () => {
+      // Arrange
+      const { instance: instanceA, canGenerate: canGenerateA } = fakeNextArbitrary<symbol>();
+      const { instance: instanceB, canGenerate: canGenerateB } = fakeNextArbitrary<symbol>();
+      const { instance: instanceC, canGenerate: canGenerateC } = fakeNextArbitrary<symbol>();
+
+      // Act
+      const arb = tuple(instanceA, instanceB, instanceC);
+      const out = arb.canGenerate([Symbol(), Symbol(), Symbol(), Symbol()]);
+
+      // Assert
+      expect(out).toBe(false);
+      expect(canGenerateA).not.toHaveBeenCalled();
+      expect(canGenerateB).not.toHaveBeenCalled();
+      expect(canGenerateC).not.toHaveBeenCalled();
+    });
+  });
+
+  describe('shrink', () => {
+    it('should call back arbitraries on shrink with the initially returned contextq', () => {
+      // Arrange
+      const expectedBiasFactor = 48;
+      const vA = Symbol();
+      const vB = Symbol();
+      const vC = Symbol();
+      const contextA = Symbol();
+      const contextB = Symbol();
+      const contextC = Symbol();
+      const { instance: instanceA, generate: generateA, shrink: shrinkA } = fakeNextArbitrary<symbol>();
+      const { instance: instanceB, generate: generateB, shrink: shrinkB } = fakeNextArbitrary<symbol>();
+      const { instance: instanceC, generate: generateC, shrink: shrinkC } = fakeNextArbitrary<symbol>();
+      generateA.mockReturnValueOnce(new NextValue(vA, contextA));
+      generateB.mockReturnValueOnce(new NextValue(vB, contextB));
+      generateC.mockReturnValueOnce(new NextValue(vC, contextC));
+      const shrinkA1 = Symbol();
+      const shrinkA2 = Symbol();
+      const shrinkB1 = Symbol();
+      const shrinkC1 = Symbol();
+      const shrinkC2 = Symbol();
+      const shrinkC3 = Symbol();
+      shrinkA.mockReturnValueOnce(Stream.of(new NextValue(shrinkA1 as symbol), new NextValue(shrinkA2)));
+      shrinkB.mockReturnValueOnce(Stream.of(new NextValue(shrinkB1 as symbol)));
+      shrinkC.mockReturnValueOnce(
+        Stream.of(new NextValue(shrinkC1 as symbol), new NextValue(shrinkC2), new NextValue(shrinkC3))
+      );
+
+      // Act
+      const arb = tuple(instanceA, instanceB, instanceC);
+      const g = arb.generate(mrngNoCall, expectedBiasFactor);
+      const shrinks = [...arb.shrink(g.value, g.context)];
+
+      // Assert
+      expect(shrinks).toHaveLength(2 /* A */ + 1 /* B */ + 3 /* C */);
+      expect(shrinks.map((v) => v.value)).toEqual([
+        [shrinkA1, vB, vC],
+        [shrinkA2, vB, vC],
+        [vA, shrinkB1, vC],
+        [vA, vB, shrinkC1],
+        [vA, vB, shrinkC2],
+        [vA, vB, shrinkC3],
+      ]);
+      expect(shrinkA).toHaveBeenCalledWith(vA, contextA);
+      expect(shrinkB).toHaveBeenCalledWith(vB, contextB);
+      expect(shrinkC).toHaveBeenCalledWith(vC, contextC);
+    });
+
+    it('should clone cloneable on shrink', () => {
+      // Arrange
+      const { instance: fakeArbitraryNotCloneableA, generate: generateA, shrink: shrinkA } = fakeNextArbitrary<
+        string[]
+      >();
+      const { instance: fakeArbitraryCloneableB, generate: generateB, shrink: shrinkB } = fakeNextArbitrary<string[]>();
+
+      const { instance: fakeArbitraryNotCloneableC, generate: generateC, shrink: shrinkC } = fakeNextArbitrary<
+        string[]
+      >();
+      const cloneMethodImpl = jest
+        .fn()
+        .mockImplementation(() => Object.defineProperty([], cloneMethod, { value: cloneMethodImpl }));
+      generateA.mockReturnValue(new NextValue([]));
+      shrinkA.mockReturnValue(Stream.of(new NextValue([]), new NextValue([])));
+      generateB.mockReturnValue(new NextValue(Object.defineProperty([], cloneMethod, { value: cloneMethodImpl })));
+      shrinkB.mockReturnValue(
+        Stream.of(
+          new NextValue(Object.defineProperty([], cloneMethod, { value: cloneMethodImpl })),
+          new NextValue(Object.defineProperty([], cloneMethod, { value: cloneMethodImpl })),
+          new NextValue(Object.defineProperty([], cloneMethod, { value: cloneMethodImpl }))
+        )
+      );
+      generateC.mockReturnValue(new NextValue([]));
+      shrinkC.mockReturnValue(Stream.of(new NextValue([]), new NextValue([]), new NextValue([]), new NextValue([])));
+
+      // Act
+      const arb = tuple(fakeArbitraryNotCloneableA, fakeArbitraryCloneableB, fakeArbitraryNotCloneableC);
+      const g = arb.generate(mrngNoCall, undefined);
+      expect(cloneMethodImpl).not.toHaveBeenCalled();
+      const shrinkLazy = arb.shrink(g.value, g.context);
+      expect(cloneMethodImpl).not.toHaveBeenCalled();
+      const shrinks = [...shrinkLazy];
+
+      // Assert
+      expect(shrinks).toHaveLength(2 /* A */ + 3 /* B */ + 4 /* C */);
+      expect(cloneMethodImpl).toHaveBeenCalledTimes(shrinks.length);
+    });
+  });
+});
diff --git a/test/unit/check/arbitrary/generic/NextArbitraryAssertions.ts b/test/unit/check/arbitrary/generic/NextArbitraryAssertions.ts
new file mode 100644
index 00000000..f7f5134b
--- /dev/null
+++ b/test/unit/check/arbitrary/generic/NextArbitraryAssertions.ts
@@ -0,0 +1,267 @@
+import * as prand from 'pure-rand';
+import * as fc from '../../../../../lib/fast-check';
+
+import { NextArbitrary } from '../../../../../src/check/arbitrary/definition/NextArbitrary';
+import { NextValue } from '../../../../../src/check/arbitrary/definition/NextValue';
+import { Random } from '../../../../../src/random/generator/Random';
+
+// Minimal requirements
+// > The following assertions are supposed to be fulfilled by any of the arbitraries
+// > provided by fast-check.
+
+export function assertGenerateProducesSameValueGivenSameSeed<T, U = never>(
+  arbitraryBuilder: (extraParameters: U) => NextArbitrary<T>,
+  options: {
+    isEqual?: (v1: T, v2: T, extraParameters: U) => void | boolean;
+    extraParameters?: fc.Arbitrary<U>;
+    assertParameters?: fc.Parameters<unknown>;
+  } = {}
+): void {
+  const {
+    isEqual,
+    extraParameters: extra = fc.constant((undefined as unknown) as U) as fc.Arbitrary<U>,
+    assertParameters,
+  } = options;
+  fc.assert(
+    fc.property(fc.integer().noShrink(), biasFactorArbitrary(), extra, (seed, biasFactor, extraParameters) => {
+      // Arrange
+      const arb = arbitraryBuilder(extraParameters);
+
+      // Act
+      const g1 = arb.generate(randomFromSeed(seed), biasFactor);
+      const g2 = arb.generate(randomFromSeed(seed), biasFactor);
+
+      // Assert
+      assertEquality(isEqual, g1.value, g2.value, extraParameters);
+    }),
+    assertParameters
+  );
+}
+
+export function assertShrinkProducesSameValueGivenSameSeed<T, U = never>(
+  arbitraryBuilder: (extraParameters: U) => NextArbitrary<T>,
+  options: {
+    isEqual?: (v1: T, v2: T, extraParameters: U) => void | boolean;
+    noInitialContext?: boolean;
+    extraParameters?: fc.Arbitrary<U>;
+    assertParameters?: fc.Parameters<unknown>;
+  } = {}
+): void {
+  const {
+    isEqual,
+    noInitialContext,
+    extraParameters: extra = fc.constant((undefined as unknown) as U) as fc.Arbitrary<U>,
+    assertParameters,
+  } = options;
+  fc.assert(
+    fc.property(
+      fc.integer().noShrink(),
+      biasFactorArbitrary(),
+      fc.array(fc.nat(100), { minLength: 1 }),
+      extra,
+      (seed, biasFactor, shrinkPath, extraParameters) => {
+        // Arrange
+        const arb = arbitraryBuilder(extraParameters);
+
+        // Act / Assert
+        let g1: NextValue<T> | null = arb.generate(randomFromSeed(seed), biasFactor);
+        let g2: NextValue<T> | null = arb.generate(randomFromSeed(seed), biasFactor);
+        if (noInitialContext) {
+          const originalG2 = g2!;
+          g2 = new NextValue(originalG2.value_, undefined, () => originalG2.value);
+        }
+        let id = 0;
+        while (g1 !== null && g2 !== null) {
+          assertEquality(isEqual, g1.value, g2.value, extraParameters);
+          g1 = arb.shrink(g1.value, g1.context).getNthOrLast(id);
+          g2 = arb.shrink(g2.value, g2.context).getNthOrLast(id);
+          id = (id + 1) % shrinkPath.length;
+        }
+        expect(g1).toBe(null);
+        expect(g2).toBe(null);
+      }
+    ),
+    assertParameters
+  );
+}
+
+// Optional requirements
+// > The following requirements are optional as they do not break the design of fast-check when they are not totally ensured
+// > But some of them are really recommended to build valid arbitraries that can be used.
+
+export function assertGenerateProducesCorrectValues<T, U = never>(
+  arbitraryBuilder: (extraParameters: U) => NextArbitrary<T>,
+  isCorrect: (v: T, extraParameters: U, arb: NextArbitrary<T>) => void | boolean,
+  options: {
+    extraParameters?: fc.Arbitrary<U>;
+    assertParameters?: fc.Parameters<unknown>;
+  } = {}
+): void {
+  const {
+    extraParameters: extra = fc.constant((undefined as unknown) as U) as fc.Arbitrary<U>,
+    assertParameters,
+  } = options;
+  fc.assert(
+    fc.property(fc.integer().noShrink(), biasFactorArbitrary(), extra, (seed, biasFactor, extraParameters) => {
+      // Arrange
+      const arb = arbitraryBuilder(extraParameters);
+
+      // Act
+      const g = arb.generate(randomFromSeed(seed), biasFactor);
+
+      // Assert
+      assertCorrectness(isCorrect, g.value, extraParameters, arb);
+    }),
+    assertParameters
+  );
+}
+
+export function assertShrinkProducesCorrectValues<T, U = never>(
+  arbitraryBuilder: (extraParameters: U) => NextArbitrary<T>,
+  isCorrect: (v: T, extraParameters: U, arb: NextArbitrary<T>) => void | boolean,
+  options: {
+    extraParameters?: fc.Arbitrary<U>;
+    assertParameters?: fc.Parameters<unknown>;
+  } = {}
+): void {
+  const {
+    extraParameters: extra = fc.constant((undefined as unknown) as U) as fc.Arbitrary<U>,
+    assertParameters,
+  } = options;
+  fc.assert(
+    fc.property(
+      fc.integer().noShrink(),
+      biasFactorArbitrary(),
+      fc.array(fc.nat(100), { minLength: 1 }),
+      extra,
+      (seed, biasFactor, shrinkPath, extraParameters) => {
+        // Arrange
+        const arb = arbitraryBuilder(extraParameters);
+
+        // Act / Assert
+        let g: NextValue<T> | null = arb.generate(randomFromSeed(seed), biasFactor);
+        let id = 0;
+        while (g !== null) {
+          assertCorrectness(isCorrect, g.value, extraParameters, arb);
+          g = arb.shrink(g.value, g.context).getNthOrLast(id);
+          id = (id + 1) % shrinkPath.length;
+        }
+        expect(g).toBe(null);
+      }
+    ),
+    assertParameters
+  );
+}
+
+// Extra requirements
+// The assertions above can be configured to push generators even further. They ensure more complex invariants.
+// Following assertions are mostly derived from the one above.
+// > assertShrinkProducesSameValueGivenSameSeed with option {noInitialContext:true}
+// > assertGenerateProducesCorrectValues with option isCorrect: (v, _, arb) => arb.canGenerate(v)
+// > assertShrinkProducesCorrectValues with option (v, _, arb) => arb.canGenerate(v)
+
+export function assertShrinkProducesSameValueWithoutInitialContext<T, U = never>(
+  arbitraryBuilder: (extraParameters: U) => NextArbitrary<T>,
+  options: {
+    isEqual?: (v1: T, v2: T, extraParameters: U) => void | boolean;
+    extraParameters?: fc.Arbitrary<U>;
+    assertParameters?: fc.Parameters<unknown>;
+  } = {}
+): void {
+  return assertShrinkProducesSameValueGivenSameSeed(arbitraryBuilder, { ...options, noInitialContext: true });
+}
+
+export function assertGenerateProducesValuesFlaggedAsCanGenerate<T, U = never>(
+  arbitraryBuilder: (extraParameters: U) => NextArbitrary<T>,
+  options: {
+    extraParameters?: fc.Arbitrary<U>;
+    assertParameters?: fc.Parameters<unknown>;
+  } = {}
+): void {
+  return assertGenerateProducesCorrectValues(arbitraryBuilder, (v, _, arb) => arb.canGenerate(v), options);
+}
+
+export function assertShrinkProducesValuesFlaggedAsCanGenerate<T, U = never>(
+  arbitraryBuilder: (extraParameters: U) => NextArbitrary<T>,
+  options: {
+    extraParameters?: fc.Arbitrary<U>;
+    assertParameters?: fc.Parameters<unknown>;
+  } = {}
+): void {
+  return assertShrinkProducesCorrectValues(arbitraryBuilder, (v, _, arb) => arb.canGenerate(v), options);
+}
+
+export function assertShrinkProducesStrictlySmallerValue<T, U = never>(
+  arbitraryBuilder: (extraParameters: U) => NextArbitrary<T>,
+  isStrictlySmaller: (vNew: T, vOld: T, extraParameters: U) => void | boolean,
+  options: {
+    extraParameters?: fc.Arbitrary<U>;
+    assertParameters?: fc.Parameters<unknown>;
+  } = {}
+): void {
+  const previousValue: { value?: T } = {};
+  function arbitraryBuilderInternal(...args: Parameters<typeof arbitraryBuilder>) {
+    delete previousValue.value;
+    return arbitraryBuilder(...args);
+  }
+  function isStrictlySmallerInternal(v: T, extraParameters: U) {
+    try {
+      if (!('value' in previousValue)) {
+        return true;
+      }
+      const vNew = v;
+      const vOld = previousValue.value!;
+      try {
+        const out = isStrictlySmaller(vNew, vOld, extraParameters);
+        expect(out).not.toBe(false);
+      } catch (err) {
+        throw new Error(
+          `Expect: ${fc.stringify(vNew)} to be strictly smaller than ${fc.stringify(vOld)}\n\nGot error: ${err}`
+        );
+      }
+    } finally {
+      previousValue.value = v;
+    }
+  }
+  return assertShrinkProducesCorrectValues(arbitraryBuilderInternal, isStrictlySmallerInternal, options);
+}
+
+// Various helpers
+
+function randomFromSeed(seed: number): Random {
+  return new Random(prand.xorshift128plus(seed));
+}
+
+function biasFactorArbitrary() {
+  return fc.option(fc.integer({ min: 2 }), { freq: 2, nil: undefined });
+}
+
+function assertEquality<T, U>(
+  isEqual: ((v1: T, v2: T, extraParameters: U) => void | boolean) | undefined,
+  v1: T,
+  v2: T,
+  extraParameters: U
+): void {
+  if (isEqual) {
+    try {
+      const out = isEqual(v1, v2, extraParameters);
+      expect(out).not.toBe(false);
+    } catch (err) {
+      throw new Error(`Expect: ${fc.stringify(v1)} to be equal to ${fc.stringify(v2)}\n\nGot error: ${err}`);
+    }
+  } else expect(v1).toStrictEqual(v2);
+}
+
+function assertCorrectness<T, U>(
+  isCorrect: (v: T, extraParameters: U, arb: NextArbitrary<T>) => void | boolean,
+  v: T,
+  extraParameters: U,
+  arb: NextArbitrary<T>
+): void {
+  try {
+    const out = isCorrect(v, extraParameters, arb);
+    expect(out).not.toBe(false);
+  } catch (err) {
+    throw new Error(`Expect: ${fc.stringify(v)} to be a correct value\n\nGot error: ${err}`);
+  }
+}
diff --git a/test/unit/check/arbitrary/generic/NextArbitraryHelpers.ts b/test/unit/check/arbitrary/generic/NextArbitraryHelpers.ts
new file mode 100644
index 00000000..a3842461
--- /dev/null
+++ b/test/unit/check/arbitrary/generic/NextArbitraryHelpers.ts
@@ -0,0 +1,73 @@
+import { mocked } from 'ts-jest';
+import { MaybeMocked, MockWithArgs } from 'ts-jest/dist/utils/testing';
+import { NextArbitrary } from '../../../../../src/check/arbitrary/definition/NextArbitrary';
+import { NextValue } from '../../../../../src/check/arbitrary/definition/NextValue';
+import { Random } from '../../../../../src/random/generator/Random';
+import { Stream } from '../../../../../src/stream/Stream';
+
+/**
+ * Generate a fake Class inheriting from NextArbitrary with all methods being mocked
+ */
+export function fakeNextArbitraryClass<T = any>(): { Class: new () => NextArbitrary<T> } & MaybeMocked<
+  NextArbitrary<T>
+> {
+  const generate = jest.fn();
+  const canGenerate = (jest.fn() as any) as ((value: unknown) => value is T) &
+    MockWithArgs<(value: unknown) => value is T>;
+  const shrink = jest.fn();
+  const filter = jest.fn();
+  const map = jest.fn();
+  const chain = jest.fn();
+  const noShrink = jest.fn();
+  const noBias = jest.fn();
+  mocked;
+  class FakeNextArbitrary extends NextArbitrary<T> {
+    generate = generate;
+    canGenerate = canGenerate;
+    shrink = shrink;
+    filter = filter;
+    map = map;
+    chain = chain;
+    noShrink = noShrink;
+    noBias = noBias;
+  }
+  return { Class: FakeNextArbitrary, generate, canGenerate, shrink, filter, map, chain, noShrink, noBias };
+}
+
+/**
+ * Generate a fake instance inheriting from NextArbitrary with all methods being mocked
+ */
+export function fakeNextArbitrary<T = any>(): { instance: NextArbitrary<T> } & MaybeMocked<NextArbitrary<T>> {
+  const { Class, ...mockedMethods } = fakeNextArbitraryClass<T>();
+  return { instance: new Class(), ...mockedMethods };
+}
+
+/**
+ * Fake instance with the following capabilities:
+ * - shrink to strictly smaller values
+ * - fully implemented canGenerate
+ * - take bias into account on generate
+ * - context less shrink like context full (for first iteration only)
+ */
+export class FakeIntegerArbitrary extends NextArbitrary<number> {
+  generate(mrng: Random, biasFactor: number | undefined): NextValue<number> {
+    // Worst case: 0-10
+    // No bias   : 0-100
+    const maxLimit = biasFactor !== undefined ? Math.max(10, 100 - biasFactor) : 100;
+    return new NextValue(mrng.nextInt(0, maxLimit), { step: 2 });
+  }
+  canGenerate(value: unknown): value is number {
+    return typeof value === 'number' && value >= 0 && value <= 100 && Number.isInteger(value);
+  }
+  shrink(value: number, context?: unknown): Stream<NextValue<number>> {
+    if (value <= 0) {
+      return Stream.nil();
+    }
+    const currentStep = context !== undefined ? (context as { step: number }).step : 2;
+    const nextStep = currentStep + 1;
+    return Stream.of(
+      ...(value - currentStep >= 0 ? [new NextValue(value - currentStep, { step: nextStep })] : []),
+      ...(value - currentStep + 1 >= 0 ? [new NextValue(value - currentStep + 1, { step: nextStep })] : [])
+    );
+  }
+}