Repository: dubzzz/fast-check
Pull request #1837 (2021-05-06 22:12:01)

🐛 `maxCount` of `0` was overriden to `5` in `lorem` instead of being rejected


<!-- Why is this PR for? -->
<!-- Describe the reason why you opened this PR or give a link towards the associated issue. -->

## In a nutshell

❌ New feature
✔️ Fix an issue
❌ Documentation improvement
❌ Other: *please explain*

(✔️: yes, ❌: no)

## Potential impacts

None


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/1837/builds/127524) or the icon next to each commit SHA.

Latest deployment of this branch, based on commit c84310fef1412416c606e13f1dfe06cc88efa50b:

|Sandbox| Source |
|--|--|
|[Vanilla](https://codesandbox.io/s/vanilla-1dni5)| Configuration |
|[fast-check-examples](https://codesandbox.io/s/fast-check-examples-6ovdp)| Configuration |


Patch:
diff --git a/src/arbitrary/_internals/mappers/IndexToMappedConstant.ts b/src/arbitrary/_internals/mappers/IndexToMappedConstant.ts
index 9450273f..663a1191 100644
--- a/src/arbitrary/_internals/mappers/IndexToMappedConstant.ts
+++ b/src/arbitrary/_internals/mappers/IndexToMappedConstant.ts
@@ -1,7 +1,7 @@
 /** @internal */
 export function indexToMappedConstantMapperFor<T>(
   entries: { num: number; build: (idInGroup: number) => T }[]
-): (index: number) => T {
+): (choiceIndex: number) => T {
   return function indexToMappedConstantMapper(choiceIndex: number): T {
     let idx = -1;
     let numSkips = 0;
@@ -11,3 +11,42 @@ export function indexToMappedConstantMapperFor<T>(
     return entries[idx].build(choiceIndex - numSkips + entries[idx].num);
   };
 }
+
+/** @internal */
+type ReverseMapping = { mapping: Map<unknown, number>; negativeZeroIndex: number | undefined };
+
+/** @internal */
+function buildReverseMapping(entries: { num: number; build: (idInGroup: number) => unknown }[]): ReverseMapping {
+  const reverseMapping: ReverseMapping = { mapping: new Map(), negativeZeroIndex: undefined };
+  let choiceIndex = 0;
+  for (let entryIdx = 0; entryIdx !== entries.length; ++entryIdx) {
+    const entry = entries[entryIdx];
+    for (let idxInEntry = 0; idxInEntry !== entry.num; ++idxInEntry) {
+      const value = entry.build(idxInEntry);
+      if (Object.is(value, -0)) {
+        reverseMapping.negativeZeroIndex = choiceIndex;
+      } else {
+        reverseMapping.mapping.set(value, choiceIndex);
+      }
+      ++choiceIndex;
+    }
+  }
+  return reverseMapping;
+}
+
+/** @internal */
+export function indexToMappedConstantUnmapperFor<T>(
+  entries: { num: number; build: (idInGroup: number) => T }[]
+): (value: unknown) => number {
+  let reverseMapping: ReverseMapping | null = null;
+  return function indexToMappedConstantUnmapper(value: unknown): number {
+    if (reverseMapping === null) {
+      reverseMapping = buildReverseMapping(entries);
+    }
+    const choiceIndex = Object.is(value, -0) ? reverseMapping.negativeZeroIndex : reverseMapping.mapping.get(value);
+    if (choiceIndex === undefined) {
+      throw new Error('Unknown value encountered cannot be built using this mapToConstant');
+    }
+    return choiceIndex;
+  };
+}
diff --git a/src/arbitrary/lorem.ts b/src/arbitrary/lorem.ts
index e7b3ff0c..d7762c64 100644
--- a/src/arbitrary/lorem.ts
+++ b/src/arbitrary/lorem.ts
@@ -263,7 +263,7 @@ function lorem(constraints: LoremConstraints): Arbitrary<string>;
 function lorem(...args: [] | [number] | [number, boolean] | [LoremConstraints]): Arbitrary<string> {
   const maxWordsCount = typeof args[0] === 'object' ? args[0].maxCount : args[0];
   const sentencesMode = typeof args[0] === 'object' ? args[0].mode === 'sentences' : args[1];
-  const maxCount = maxWordsCount || 5;
+  const maxCount = maxWordsCount !== undefined ? maxWordsCount : 5;
   if (maxCount < 1) {
     throw new Error(`lorem has to produce at least one word/sentence`);
   }
diff --git a/src/arbitrary/mapToConstant.ts b/src/arbitrary/mapToConstant.ts
index d61b715d..6e02a5bf 100644
--- a/src/arbitrary/mapToConstant.ts
+++ b/src/arbitrary/mapToConstant.ts
@@ -1,6 +1,10 @@
 import { Arbitrary } from '../check/arbitrary/definition/Arbitrary';
+import { convertFromNext, convertToNext } from '../check/arbitrary/definition/Converters';
 import { nat } from './nat';
-import { indexToMappedConstantMapperFor } from './_internals/mappers/IndexToMappedConstant';
+import {
+  indexToMappedConstantMapperFor,
+  indexToMappedConstantUnmapperFor,
+} from './_internals/mappers/IndexToMappedConstant';
 
 /** @internal */
 function computeNumChoices<T>(options: { num: number; build: (idInGroup: number) => T }[]): number {
@@ -35,5 +39,10 @@ function computeNumChoices<T>(options: { num: number; build: (idInGroup: number)
  */
 export function mapToConstant<T>(...entries: { num: number; build: (idInGroup: number) => T }[]): Arbitrary<T> {
   const numChoices = computeNumChoices(entries);
-  return nat({ max: numChoices - 1 }).map(indexToMappedConstantMapperFor(entries));
+  return convertFromNext(
+    convertToNext(nat({ max: numChoices - 1 })).map(
+      indexToMappedConstantMapperFor(entries),
+      indexToMappedConstantUnmapperFor(entries)
+    )
+  );
 }


Test patch:
diff --git a/test/unit/arbitrary/LoremArbitrary.spec.ts b/test/unit/arbitrary/LoremArbitrary.spec.ts
index 4f59450a..1ab214ad 100644
--- a/test/unit/arbitrary/LoremArbitrary.spec.ts
+++ b/test/unit/arbitrary/LoremArbitrary.spec.ts
@@ -21,7 +21,7 @@ describe('LoremArbitrary', () => {
       ));
     it('Should generate words by default', () =>
       fc.assert(
-        fc.property(fc.integer(), fc.integer(0, 100), (seed, num) => {
+        fc.property(fc.integer(), fc.integer(1, 100), (seed, num) => {
           const mrng = new Random(prand.xorshift128plus(seed));
           const g = lorem({ maxCount: num }).generate(mrng).value;
           expect(g).not.toContain('.');
@@ -29,7 +29,7 @@ describe('LoremArbitrary', () => {
       ));
     it('Should not generate commas for words', () =>
       fc.assert(
-        fc.property(fc.integer(), fc.integer(0, 100), (seed, num) => {
+        fc.property(fc.integer(), fc.integer(1, 100), (seed, num) => {
           const mrng = new Random(prand.xorshift128plus(seed));
           const g = lorem({ maxCount: num }).generate(mrng).value;
           expect(g).not.toContain(',');
@@ -61,7 +61,7 @@ describe('LoremArbitrary', () => {
     describe('Still support non recommended signatures', () => {
       it('Should support fc.lorem(maxWordsCount)', () => {
         fc.assert(
-          fc.property(fc.integer(), fc.nat(100), (seed, maxWordsCount) => {
+          fc.property(fc.integer(), fc.integer(1, 100), (seed, maxWordsCount) => {
             const refArbitrary = lorem({ maxCount: maxWordsCount });
             const nonRecommendedArbitrary = lorem(maxWordsCount);
             expect(generateOneValue(seed, nonRecommendedArbitrary)).toEqual(generateOneValue(seed, refArbitrary));
@@ -70,7 +70,7 @@ describe('LoremArbitrary', () => {
       });
       it('Should support fc.lorem(maxWordsCount, sentencesMode)', () => {
         fc.assert(
-          fc.property(fc.integer(), fc.nat(100), fc.boolean(), (seed, maxWordsCount, sentencesMode) => {
+          fc.property(fc.integer(), fc.integer(1, 100), fc.boolean(), (seed, maxWordsCount, sentencesMode) => {
             const refArbitrary = lorem({ maxCount: maxWordsCount, mode: sentencesMode ? 'sentences' : 'words' });
             const nonRecommendedArbitrary = lorem(maxWordsCount, sentencesMode);
             expect(generateOneValue(seed, nonRecommendedArbitrary)).toEqual(generateOneValue(seed, refArbitrary));
diff --git a/test/unit/arbitrary/MapToConstantArbitrary.spec.ts b/test/unit/arbitrary/MapToConstantArbitrary.spec.ts
deleted file mode 100644
index 5a17afe6..00000000
--- a/test/unit/arbitrary/MapToConstantArbitrary.spec.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-import * as prand from 'pure-rand';
-import * as fc from '../../../lib/fast-check';
-
-import { mapToConstant } from '../../../src/arbitrary/mapToConstant';
-import { Random } from '../../../src/random/generator/Random';
-import { minMax } from '../check/arbitrary/generic/GenericArbitraryHelper';
-import { nat } from '../../../src/arbitrary/nat';
-
-describe('MapToConstantArbitrary', () => {
-  describe('mapToConstant', () => {
-    it('Single non-zero builder should be equivalent to nat and map', () =>
-      fc.assert(
-        fc.property(
-          fc.integer(),
-          fc.integer(1, Number.MAX_SAFE_INTEGER),
-          minMax(fc.nat(100)),
-          (seed, numValues, others) => {
-            const { min: pos, max: numBuilders } = others;
-            fc.pre(pos < numBuilders);
-
-            const entries: { num: number; build: (v: number) => string }[] = [];
-            for (let idx = 0; idx !== numBuilders; ++idx) {
-              entries.push({ num: idx === pos ? numValues : 0, build: (v: number) => `Builder #${idx}: ${v}` });
-            }
-
-            const refArb = nat(numValues - 1).map((v) => entries[pos].build(v));
-            const arb = mapToConstant(...entries);
-
-            const refMrng = new Random(prand.xorshift128plus(seed));
-            const mrng = new Random(prand.xorshift128plus(seed));
-
-            expect(arb.generate(mrng).value).toEqual(refArb.generate(refMrng).value);
-          }
-        )
-      ));
-    it('Should call the right builder', () =>
-      fc.assert(
-        fc.property(fc.integer(), fc.array(fc.nat(100)), (seed, builderSizes) => {
-          const totalSize = builderSizes.reduce((a, b) => a + b, 0);
-          fc.pre(totalSize > 0);
-
-          const entries: { num: number; build: (v: number) => any }[] = [];
-          for (let idx = 0, currentSize = 0; idx !== builderSizes.length; currentSize += builderSizes[idx], ++idx) {
-            entries.push({ num: builderSizes[idx], build: (v) => v + currentSize });
-          }
-
-          const refArb = nat(totalSize - 1);
-          const arb = mapToConstant(...entries);
-
-          const refMrng = new Random(prand.xorshift128plus(seed));
-          const mrng = new Random(prand.xorshift128plus(seed));
-
-          expect(arb.generate(mrng).value).toEqual(refArb.generate(refMrng).value);
-        })
-      ));
-  });
-});
diff --git a/test/unit/arbitrary/__snapshots__/mapToConstant.spec.ts.snap b/test/unit/arbitrary/__snapshots__/mapToConstant.spec.ts.snap
new file mode 100644
index 00000000..fd918e32
--- /dev/null
+++ b/test/unit/arbitrary/__snapshots__/mapToConstant.spec.ts.snap
@@ -0,0 +1,87 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`mapToConstant (integration) should be able to shrink c given hexa-like entries 1`] = `
+"\\"c\\"
+├> \\"0\\"
+├> \\"6\\"
+|  ├> \\"3\\"
+|  |  └> \\"2\\"
+|  |     └> \\"1\\"
+|  |        └> \\"0\\"
+|  └> \\"5\\"
+|     └> \\"4\\"
+|        └> \\"3\\"
+|           ├> \\"0\\"
+|           └> \\"2\\"
+|              └> \\"1\\"
+|                 └> \\"0\\"
+├> \\"9\\"
+|  └> \\"8\\"
+|     └> \\"7\\"
+|        └> \\"6\\"
+|           ├> \\"0\\"
+|           ├> \\"3\\"
+|           |  └> \\"2\\"
+|           |     └> \\"1\\"
+|           |        └> \\"0\\"
+|           └> \\"5\\"
+|              └> \\"4\\"
+|                 └> \\"3\\"
+|                    ├> \\"0\\"
+|                    └> \\"2\\"
+|                       └> \\"1\\"
+|                          └> \\"0\\"
+└> \\"b\\"
+   └> \\"a\\"
+      └> \\"9\\"
+         ├> \\"0\\"
+         ├> \\"5\\"
+         |  ├> \\"3\\"
+         |  |  └> \\"2\\"
+         |  |     └> \\"1\\"
+         |  |        └> \\"0\\"
+         |  └> \\"4\\"
+         |     └> \\"3\\"
+         |        ├> \\"0\\"
+         |        └> \\"2\\"
+         |           └> \\"1\\"
+         |              └> \\"0\\"
+         ├> \\"7\\"
+         |  └> \\"6\\"
+         |     └> \\"5\\"
+         |        ├> \\"0\\"
+         |        ├> \\"3\\"
+         |        |  └> \\"2\\"
+         |        |     └> \\"1\\"
+         |        |        └> \\"0\\"
+         |        └> \\"4\\"
+         |           └> \\"3\\"
+         |              ├> \\"0\\"
+         |              └> \\"2\\"
+         |                 └> \\"1\\"
+         |                    └> \\"0\\"
+         └> \\"8\\"
+            └> \\"7\\"
+               ├> \\"0\\"
+               ├> \\"4\\"
+               |  ├> \\"2\\"
+               |  |  └> \\"1\\"
+               |  |     └> \\"0\\"
+               |  └> \\"3\\"
+               |     └> \\"2\\"
+               |        ├> \\"0\\"
+               |        └> \\"1\\"
+               |           └> \\"0\\"
+               └> \\"6\\"
+                  └> \\"5\\"
+                     └> \\"4\\"
+                        ├> \\"0\\"
+                        ├> \\"2\\"
+                        |  └> \\"1\\"
+                        |     └> \\"0\\"
+                        └> \\"3\\"
+                           └> \\"2\\"
+                              ├> \\"0\\"
+                              └> \\"1\\"
+                                 └> \\"0\\""
+`;
diff --git a/test/unit/arbitrary/_internals/mappers/IndexToMappedConstant.spec.ts b/test/unit/arbitrary/_internals/mappers/IndexToMappedConstant.spec.ts
new file mode 100644
index 00000000..2aa86771
--- /dev/null
+++ b/test/unit/arbitrary/_internals/mappers/IndexToMappedConstant.spec.ts
@@ -0,0 +1,131 @@
+import {
+  indexToMappedConstantMapperFor,
+  indexToMappedConstantUnmapperFor,
+} from '../../../../../src/arbitrary/_internals/mappers/IndexToMappedConstant';
+
+describe('indexToMappedConstantMapperFor', () => {
+  it.each`
+    index | value
+    ${0}  | ${'A0'}
+    ${1}  | ${'A1'}
+    ${9}  | ${'A9'}
+    ${10} | ${'B0'}
+    ${11} | ${'D0'}
+    ${12} | ${'D1'}
+    ${15} | ${'D4'}
+  `('should properly map index $index to value $value', ({ index, value }) => {
+    // Arrange
+    const entries = [
+      { num: 10, build: (index: number) => `A${index}` },
+      { num: 1, build: (index: number) => `B${index}` },
+      { num: 0, build: (index: number) => `C${index}` },
+      { num: 5, build: (index: number) => `D${index}` },
+    ];
+
+    // Act
+    const computedValue = indexToMappedConstantMapperFor(entries)(index);
+
+    // Assert
+    expect(computedValue).toBe(value);
+  });
+});
+
+describe('indexToMappedConstantUnmapperFor', () => {
+  it.each`
+    index | value
+    ${0}  | ${'A0'}
+    ${1}  | ${'A1'}
+    ${9}  | ${'A9'}
+    ${10} | ${'B0'}
+    ${11} | ${'D0'}
+    ${12} | ${'D1'}
+    ${15} | ${'D4'}
+  `('should properly unmap value $value to index $index', ({ index, value }) => {
+    // Arrange
+    const entries = [
+      { num: 10, build: (index: number) => `A${index}` },
+      { num: 1, build: (index: number) => `B${index}` },
+      { num: 0, build: (index: number) => `C${index}` },
+      { num: 5, build: (index: number) => `D${index}` },
+    ];
+
+    // Act
+    const computedIndex = indexToMappedConstantUnmapperFor(entries)(value);
+
+    // Assert
+    expect(computedIndex).toBe(index);
+  });
+
+  it('should properly unmap 0, -0 and NaN when defined in the mappings', () => {
+    // Arrange
+    const entries = [
+      { num: 10, build: (index: number) => `A${index}` },
+      { num: 1, build: (_index: number) => 0 },
+      { num: 2, build: (index: number) => `C${index}` },
+      { num: 1, build: (_index: number) => -0 },
+      { num: 1, build: (_index: number) => Number.NaN },
+      { num: 5, build: (index: number) => `D${index}` },
+    ];
+
+    // Act
+    const unmapper = indexToMappedConstantUnmapperFor<unknown>(entries);
+    const computedIndexZero = unmapper(0);
+    const computedIndexMinusZero = unmapper(-0);
+    const computedIndexNaN = unmapper(Number.NaN);
+    const computedIndexOther = unmapper('D2');
+
+    // Assert
+    expect(computedIndexZero).toBe(10);
+    expect(computedIndexMinusZero).toBe(13);
+    expect(computedIndexNaN).toBe(14);
+    expect(computedIndexOther).toBe(17);
+  });
+
+  it('should not call any build function when creating the unmapper', () => {
+    // Arrange
+    const entries = [
+      { num: 10, build: jest.fn() },
+      { num: 3, build: jest.fn() },
+    ];
+
+    // Act
+    indexToMappedConstantUnmapperFor(entries);
+
+    // Assert
+    expect(entries[0].build).not.toHaveBeenCalled();
+    expect(entries[1].build).not.toHaveBeenCalled();
+  });
+
+  it('should call all build functions on first call to unmapper', () => {
+    // Arrange
+    const entries = [
+      { num: 10, build: jest.fn().mockReturnValue(0) },
+      { num: 3, build: jest.fn().mockReturnValue(1) },
+    ];
+
+    // Act
+    const unmapper = indexToMappedConstantUnmapperFor(entries);
+    unmapper(0);
+
+    // Assert
+    expect(entries[0].build).toHaveBeenCalledTimes(entries[0].num);
+    expect(entries[1].build).toHaveBeenCalledTimes(entries[1].num);
+  });
+
+  it('should not call again build functions on second call to unmapper', () => {
+    // Arrange
+    const entries = [
+      { num: 10, build: jest.fn().mockReturnValue(0) },
+      { num: 3, build: jest.fn().mockReturnValue(1) },
+    ];
+
+    // Act
+    const unmapper = indexToMappedConstantUnmapperFor(entries);
+    unmapper(0);
+    unmapper(1);
+
+    // Assert
+    expect(entries[0].build).toHaveBeenCalledTimes(entries[0].num);
+    expect(entries[1].build).toHaveBeenCalledTimes(entries[1].num);
+  });
+});
diff --git a/test/unit/arbitrary/mapToConstant.spec.ts b/test/unit/arbitrary/mapToConstant.spec.ts
new file mode 100644
index 00000000..260f5411
--- /dev/null
+++ b/test/unit/arbitrary/mapToConstant.spec.ts
@@ -0,0 +1,121 @@
+import fc from '../../../lib/fast-check';
+import { mapToConstant } from '../../../src/arbitrary/mapToConstant';
+import { convertToNext } from '../../../src/check/arbitrary/definition/Converters';
+import { NextValue } from '../../../src/check/arbitrary/definition/NextValue';
+import {
+  assertGenerateProducesCorrectValues,
+  assertGenerateProducesSameValueGivenSameSeed,
+  assertGenerateProducesValuesFlaggedAsCanGenerate,
+  assertShrinkProducesCorrectValues,
+  assertShrinkProducesSameValueWithoutInitialContext,
+  assertShrinkProducesValuesFlaggedAsCanGenerate,
+} from '../check/arbitrary/generic/NextArbitraryAssertions';
+import { buildNextShrinkTree, renderTree } from '../check/arbitrary/generic/ShrinkTree';
+
+describe('mapToConstant', () => {
+  it('should reject any inputs containing at least one strictly negative entry', () =>
+    fc.assert(
+      fc.property(fc.array(fc.nat()), fc.array(fc.nat()), fc.integer({ max: -1 }), (beforeNeg, afterNeg, neg) => {
+        // Arrange
+        const entries = [...beforeNeg, neg, ...afterNeg].map((num) => ({ num, build: jest.fn() }));
+
+        // Act / Assert
+        expect(() => mapToConstant(...entries)).toThrowError();
+      })
+    ));
+
+  it('should reject any inputs summing to zero', () =>
+    fc.assert(
+      fc.property(fc.nat({ max: 1000 }), (length) => {
+        // Arrange
+        const entries = [...Array(length)].map(() => ({ num: 0, build: jest.fn() }));
+
+        // Act / Assert
+        expect(() => mapToConstant(...entries)).toThrowError();
+      })
+    ));
+
+  it('should accept any inputs not summing to zero and with positive or null values', () =>
+    fc.assert(
+      fc.property(fc.array(fc.nat(), { minLength: 1 }), (nums) => {
+        // Arrange
+        fc.pre(nums.some((n) => n > 0));
+        const entries = nums.map((num) => ({ num, build: jest.fn() }));
+
+        // Act / Assert
+        expect(() => mapToConstant(...entries)).not.toThrowError();
+      })
+    ));
+});
+
+describe('mapToConstant (integration)', () => {
+  type Extra = unknown[][];
+  const extraParameters: fc.Arbitrary<Extra> = fc.array(fc.set(fc.anything(), { minLength: 1 }), { minLength: 1 });
+
+  const isCorrect = (value: unknown, extra: Extra) => {
+    for (const entryCandidate of extra) {
+      if (entryCandidate.includes(value)) {
+        // -0 and 0 are mixed by include
+        return true;
+      }
+    }
+    return false;
+  };
+
+  const mapToConstantBuilder = (extra: Extra) => {
+    const alreadySeenConstants = new Set<unknown>();
+    const entries: { num: number; build: (index: number) => unknown }[] = [];
+    for (const entryCandidate of extra) {
+      // Only keep values (aka constants) never seen before (-0 and 0) will be mixed due to Set
+      const trimmedConstants = entryCandidate.filter((c) => !alreadySeenConstants.has(c));
+      entries.push({ num: trimmedConstants.length, build: (index: number) => trimmedConstants[index] });
+      trimmedConstants.forEach((c) => alreadySeenConstants.add(c));
+    }
+    return convertToNext(mapToConstant(...entries));
+  };
+
+  it('should generate the same values given the same seed', () => {
+    assertGenerateProducesSameValueGivenSameSeed(mapToConstantBuilder, { extraParameters });
+  });
+
+  it('should only generate correct values', () => {
+    assertGenerateProducesCorrectValues(mapToConstantBuilder, isCorrect, { extraParameters });
+  });
+
+  it('should recognize values that would have been generated using it during generate', () => {
+    assertGenerateProducesValuesFlaggedAsCanGenerate(mapToConstantBuilder, { extraParameters });
+  });
+
+  it('should shrink towards the same values given the same seed', () => {
+    assertGenerateProducesSameValueGivenSameSeed(mapToConstantBuilder, { extraParameters });
+  });
+
+  it('should be able to shrink without any context', () => {
+    assertShrinkProducesSameValueWithoutInitialContext(mapToConstantBuilder, { extraParameters });
+  });
+
+  it('should only shrink towards correct values', () => {
+    assertShrinkProducesCorrectValues(mapToConstantBuilder, isCorrect, { extraParameters });
+  });
+
+  it('should recognize values that would have been generated using it during shrink', () => {
+    assertShrinkProducesValuesFlaggedAsCanGenerate(mapToConstantBuilder, { extraParameters });
+  });
+
+  it('should be able to shrink c given hexa-like entries', () => {
+    // Arrange
+    const arb = convertToNext(
+      mapToConstant(
+        { num: 10, build: (index) => String.fromCodePoint(index + 48) }, // 0-9
+        { num: 6, build: (index) => String.fromCodePoint(index + 97) } // a-f
+      )
+    );
+    const value = new NextValue('c');
+
+    // Act
+    const renderedTree = renderTree(buildNextShrinkTree(arb, value)).join('\n');
+
+    // Assert
+    expect(renderedTree).toMatchSnapshot();
+  });
+});