Repository: dubzzz/fast-check
Pull request #2668 (2022-01-22 08:45:13)

✨ Introduce the notion of relative `size`
<!-- Context of the PR: short description and potentially linked issues -->

Related to #2648
<!-- ...a few words to describe the content of this PR...               -->
<!-- ... -->

<!-- Type of PR: [ ] unchecked / [ ] checked -->
**_Category:_**

- [x] ✨ Introduce new features
- [ ] 📝 Add or update documentation
- [ ] ✅ Add or update tests
- [ ] 🐛 Fix a bug
- [ ] 🏷️ Add or update types
- [ ] ⚡️ Improve performance
- [ ] _Other(s):_ ...
<!-- Don't forget to add the gitmoji icon in the name of the PR -->
<!-- See: https://gitmoji.dev/                                  -->

<!-- Fixing bugs, adding features... may impact existing ones           -->
<!-- in order to track potential issues that could be related to your PR -->
<!-- please check the impacts and describe more precisely what to expect -->

**_Potential impacts:_**

<!-- Generated values: Can your change impact any of the existing generators in terms of generated values, if so which ones? when? -->
<!-- Shrink values:    Can your change impact any of the existing generators in terms of shrink values, if so which ones? when? -->
<!-- Performance:      Can it require some typings changes on user side? Please give more details -->
<!-- Typings:          Is there a potential performance impact? In which cases? -->
- [ ] Generated values
- [ ] Shrink values
- [ ] Performance
- [ ] Typings
- [ ] _Other(s):_ ...


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

Latest deployment of this branch, based on commit cc20ed647c3e64a2360ca8e0e7497b4b368a7ce3:

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

codecov[bot]: # [Codecov](https://codecov.io/gh/dubzzz/fast-check/pull/2668?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN) Report
> Merging [#2668](https://codecov.io/gh/dubzzz/fast-check/pull/2668?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN) (6e94dcf) into [main](https://codecov.io/gh/dubzzz/fast-check/commit/b6bc1ea96d1925c8ece8265594b8c4f4a97d19ab?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN) (b6bc1ea) will **decrease** coverage by `0.02%`.
> The diff coverage is `94.44%`.

> :exclamation: Current head 6e94dcf differs from pull request most recent head f61cb73. Consider uploading reports for the commit f61cb73 to get more accurate results
[![Impacted file tree graph](https://codecov.io/gh/dubzzz/fast-check/pull/2668/graphs/tree.svg?width=650&height=150&src=pr&token=fRi2fJN730&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN)](https://codecov.io/gh/dubzzz/fast-check/pull/2668?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN)

```diff
@@            Coverage Diff             @@
##             main    #2668      +/-   ##
==========================================
- Coverage   95.01%   94.98%   -0.03%     
==========================================
  Files         209      209              
  Lines        4851     4867      +16     
  Branches      953      959       +6     
==========================================
+ Hits         4609     4623      +14     
- Misses        238      240       +2     
  Partials        4        4              
```

| Flag | Coverage Δ | |
|---|---|---|
| unit-tests | `94.98% <94.44%> (-0.03%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/dubzzz/fast-check/pull/2668?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN) | Coverage Δ | |
|---|---|---|
| [...trary/\_internals/helpers/MaxLengthFromMinLength.ts](https://codecov.io/gh/dubzzz/fast-check/pull/2668/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN#diff-c3JjL2FyYml0cmFyeS9faW50ZXJuYWxzL2hlbHBlcnMvTWF4TGVuZ3RoRnJvbU1pbkxlbmd0aC50cw==) | `92.50% <94.44%> (+0.83%)` | :arrow_up: |
| [src/arbitrary/\_internals/mappers/ArrayToSet.ts](https://codecov.io/gh/dubzzz/fast-check/pull/2668/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN#diff-c3JjL2FyYml0cmFyeS9faW50ZXJuYWxzL21hcHBlcnMvQXJyYXlUb1NldC50cw==) | `87.50% <0.00%> (-12.50%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/dubzzz/fast-check/pull/2668?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/dubzzz/fast-check/pull/2668?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN). Last update [b6bc1ea...f61cb73](https://codecov.io/gh/dubzzz/fast-check/pull/2668?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN).


Patch:
diff --git a/documentation/Arbitraries.md b/documentation/Arbitraries.md
index 646bec4c..7391bf03 100644
--- a/documentation/Arbitraries.md
+++ b/documentation/Arbitraries.md
@@ -2425,8 +2425,11 @@ fc.array(fc.nat(), {minLength: 5, maxLength: 7})
 // • …
 
 fc.array(fc.nat(), {maxLength: 50, size: 'max'})
-// Note: By specifying size to max, we enforce the fact that we want generated values to have
-// between 0 and 50 items. In other words, we want to use the full range of specified lengths.
+// Note: By specifying size to "max", we enforce the fact that we want generated values to have between
+// 0 and 50 items. In other words, we want to use the full range of specified lengths.
+// Note: If not defined, by default, the size is "=" except if there is a maxLength provided and the
+// global setting defaultSizeToMaxWhenMaxSpecified explicitely set to true (the default in v2). In such
+// case it will automatically be defaulted to "max".
 // Examples of generated values:
 // • [4,26,725992281,7,1186699848]
 // • [1503239805,742382696,478977019,1206184056,992934701,1081616342,1979615602,100017132,1937766941,1785237624,89742033,1144227677,1828223609,1661385382,1964114158,526345403,1355567259,101888470,985865568,1687809116,147253818,1849736419,89389898,137520571,1171150892,127470621,827241914,1255338411]
@@ -2434,6 +2437,24 @@ fc.array(fc.nat(), {maxLength: 50, size: 'max'})
 // • [20]
 // • [19,3,15]
 // • …
+
+fc.array(fc.nat(), {maxLength: 100000, size: '+1'})
+// Note: From a specification point of view, the algorithm is supposed to handle up to 100,000 items.
+// But, even if I want to test the algorithm on large entries I don't want to spend hours in it (it may
+// not scale linearly...). By setting size to "+1" I tell fast-check that I want values larger than usual
+// ones (~10x factor). If I wanted even larger ones I could have used "+2" (~100x factor), "+3" (~1000x factor)
+// or "+4" (~10000x factor). On the opposite, if I wanted smaller arrays I could have used "-1" (~10x smaller)...
+// Note: Size could also have been set explicitely to "=" to say: "I want the size used by default no matter the
+// specified maxLength". If not defined, by default, the size is "=" except if there is a maxLength provided
+// and the global setting defaultSizeToMaxWhenMaxSpecified explicitely set to true (the default in v2). In such
+// case it will automatically be defaulted to "max".
+// Examples of generated values:
+// • [2013730136,353952753,1490777806,634915573,1978586276,205766418,1175483977,32404726,52946578,1069691063,626810743,719356509,1263272304,1824194201,1899370697,1015020872,1705828766,1764355915,1972277951,1015470108,2117817188,449185274,666877613,1210503432,1201056411,777517406,772222564,821394475,850614998,717040931,2031665271,1786858369,1041895508,1725564736,1214876488,1554660788,1241812368]
+// • [11,2147483643,1549284389]
+// • [131262217]
+// • [29,1410245876,741880759,944485652,27,15,870882976,20,184434798,2147483622,344218127,27,409824723,2147483642,329043996,927489807,2035126132,11,2039439877,5,493467004,124950538,26,405637559,2147483620,471069585,931537132,667497301,1621370022,1798147982,10,251298872,867523191,1446431080,1609229900,2147483639,1618986483,1213793840,2147483618,23,2147483639,717045226,928729912,16,2147483637,2147483626,14977076,340466387,13,2042862990,2147483618,2147483631,2147483628,2147483627,18,11,2147483626,2147483640,2147483647,275841729,21,2090499420,983160949,188709474,18,30,1192240225,0,2147483635,22952275,825333491,1138859947,2147483624,5,26,689872800,17,1697943758,384986459,2147483628,1947943844,218900368,12]
+// • [1558059373,1486409544,138880328,1775525007,1289633061,2110277820,2132428886,243113350,370748226,1289875763,1926931276,777271555,200391383,382812004,767046802,1658449850,471365442,258979782,1763577358,875799138,1041944829,769854926,874760332,442170309,91717126,113325162,88812665,1097842037,804561500,1870859458,853896552,50228752,492015973,149076083,2093833652,220810263,257405203]
+// • …
 ```
 </details>
 
diff --git a/src/arbitrary/_internals/helpers/MaxLengthFromMinLength.ts b/src/arbitrary/_internals/helpers/MaxLengthFromMinLength.ts
index 023d775b..4562193b 100644
--- a/src/arbitrary/_internals/helpers/MaxLengthFromMinLength.ts
+++ b/src/arbitrary/_internals/helpers/MaxLengthFromMinLength.ts
@@ -22,12 +22,24 @@ export const MaxLengthUpperBound = 0x7fffffff;
  */
 export type Size = 'xsmall' | 'small' | 'medium' | 'large' | 'xlarge';
 
+/** @internal */
+const orderedSize = ['xsmall', 'small', 'medium', 'large', 'xlarge'] as const;
+
+/**
+ * @remarks Since 2.22.0
+ * @public
+ */
+export type RelativeSize = '-4' | '-3' | '-2' | '-1' | '=' | '+1' | '+2' | '+3' | '+4';
+
+/** @internal */
+const orderedRelativeSize = ['-4', '-3', '-2', '-1', '=', '+1', '+2', '+3', '+4'] as const;
+
 /**
  * Superset of {@link Size} to override the default defined for size
  * @remarks Since 2.22.0
  * @public
  */
-export type SizeForArbitrary = Size | 'max' | undefined;
+export type SizeForArbitrary = RelativeSize | Size | 'max' | undefined;
 
 /**
  * The default size used by fast-check
@@ -56,6 +68,27 @@ export function maxLengthFromMinLength(minLength: number, size: Size): number {
   }
 }
 
+/**
+ * Transform a RelativeSize|Size into a Size
+ * @internal
+ */
+export function relativeSizeToSize(size: Size | RelativeSize, defaultSize: Size): Size {
+  const sizeInRelative = orderedRelativeSize.indexOf(size as RelativeSize);
+  if (sizeInRelative === -1) {
+    return size as Size;
+  }
+  const defaultSizeInSize = orderedSize.indexOf(defaultSize);
+  if (defaultSizeInSize === -1) {
+    throw new Error(`Unable to offset size based on the unknown defaulted one: ${defaultSize}`);
+  }
+  const resultingSizeInSize = defaultSizeInSize + sizeInRelative - 4;
+  return resultingSizeInSize < 0
+    ? orderedSize[0]
+    : resultingSizeInSize >= orderedSize.length
+    ? orderedSize[orderedSize.length - 1]
+    : orderedSize[resultingSizeInSize];
+}
+
 /**
  * Compute `maxGeneratedLength` based on `minLength`, `maxLength` and `size`
  * @param size - Size defined by the caller on the arbitrary
@@ -80,11 +113,12 @@ export function maxGeneratedLengthFromSizeForArbitrary(
   // - Otherwise (size=undefined), we default it to:
   //   - If caller specified a maxLength and asked for defaultSizeToMaxWhenMaxSpecified, then 'max'
   //   - Otherwise, defaultSize
-  const resultingSize =
+  const definedSize =
     size !== undefined ? size : specifiedMaxLength && defaultSizeToMaxWhenMaxSpecified ? 'max' : defaultSize;
 
-  if (resultingSize === 'max') {
+  if (definedSize === 'max') {
     return maxLength;
   }
-  return maxLengthFromMinLength(minLength, resultingSize);
+  const finalSize = relativeSizeToSize(definedSize, defaultSize);
+  return maxLengthFromMinLength(minLength, finalSize);
 }


Test patch:
diff --git a/test/unit/arbitrary/_internals/helpers/MaxLengthFromMinLength.spec.ts b/test/unit/arbitrary/_internals/helpers/MaxLengthFromMinLength.spec.ts
index 8049e3c7..cc459b87 100644
--- a/test/unit/arbitrary/_internals/helpers/MaxLengthFromMinLength.spec.ts
+++ b/test/unit/arbitrary/_internals/helpers/MaxLengthFromMinLength.spec.ts
@@ -5,6 +5,8 @@ import {
   maxGeneratedLengthFromSizeForArbitrary,
   maxLengthFromMinLength,
   MaxLengthUpperBound,
+  RelativeSize,
+  relativeSizeToSize,
   Size,
   SizeForArbitrary,
 } from '../../../../../src/arbitrary/_internals/helpers/MaxLengthFromMinLength';
@@ -68,7 +70,35 @@ describe('maxGeneratedLengthFromSizeForArbitrary', () => {
     );
   });
 
-  it('should only consider the received size when set to "max"', () => {
+  it('should behave as its equivalent Size taking into account global settings when receiving a RelativeSize', () => {
+    fc.assert(
+      fc.property(
+        relativeSizeArb,
+        fc.integer({ min: 0, max: MaxLengthUpperBound }),
+        fc.integer({ min: 0, max: MaxLengthUpperBound }),
+        fc.boolean(),
+        (size, lengthA, lengthB, specifiedMaxLength) => {
+          // Arrange
+          const equivalentSize = relativeSizeToSize(size, DefaultSize);
+          const [minLength, maxLength] = lengthA < lengthB ? [lengthA, lengthB] : [lengthB, lengthA];
+
+          // Act
+          const computedLength = maxGeneratedLengthFromSizeForArbitrary(size, minLength, maxLength, specifiedMaxLength);
+          const expectedLength = maxGeneratedLengthFromSizeForArbitrary(
+            equivalentSize,
+            minLength,
+            maxLength,
+            specifiedMaxLength
+          );
+
+          // Assert
+          expect(computedLength).toBe(expectedLength);
+        }
+      )
+    );
+  });
+
+  it('should only consider the received maxLength when set to "max"', () => {
     fc.assert(
       fc.property(
         fc.integer({ min: 0, max: MaxLengthUpperBound }),
@@ -151,20 +181,72 @@ describe('maxGeneratedLengthFromSizeForArbitrary', () => {
   });
 });
 
+describe('relativeSizeToSize', () => {
+  it('should offset by -4 when "-4"', () => {
+    const relativeSize = '-4';
+    expect(relativeSizeToSize(relativeSize, 'xsmall')).toBe('xsmall');
+    expect(relativeSizeToSize(relativeSize, 'small')).toBe('xsmall');
+    expect(relativeSizeToSize(relativeSize, 'medium')).toBe('xsmall');
+    expect(relativeSizeToSize(relativeSize, 'large')).toBe('xsmall');
+    expect(relativeSizeToSize(relativeSize, 'xlarge')).toBe('xsmall');
+  });
+
+  it('should offset by -1 when "-1"', () => {
+    const relativeSize = '-1';
+    expect(relativeSizeToSize(relativeSize, 'xsmall')).toBe('xsmall');
+    expect(relativeSizeToSize(relativeSize, 'small')).toBe('xsmall');
+    expect(relativeSizeToSize(relativeSize, 'medium')).toBe('small');
+    expect(relativeSizeToSize(relativeSize, 'large')).toBe('medium');
+    expect(relativeSizeToSize(relativeSize, 'xlarge')).toBe('large');
+  });
+
+  it('should not offset when "="', () => {
+    const relativeSize = '=';
+    expect(relativeSizeToSize(relativeSize, 'xsmall')).toBe('xsmall');
+    expect(relativeSizeToSize(relativeSize, 'small')).toBe('small');
+    expect(relativeSizeToSize(relativeSize, 'medium')).toBe('medium');
+    expect(relativeSizeToSize(relativeSize, 'large')).toBe('large');
+    expect(relativeSizeToSize(relativeSize, 'xlarge')).toBe('xlarge');
+  });
+
+  it('should offset by +1 when "+1"', () => {
+    const relativeSize = '+1';
+    expect(relativeSizeToSize(relativeSize, 'xsmall')).toBe('small');
+    expect(relativeSizeToSize(relativeSize, 'small')).toBe('medium');
+    expect(relativeSizeToSize(relativeSize, 'medium')).toBe('large');
+    expect(relativeSizeToSize(relativeSize, 'large')).toBe('xlarge');
+    expect(relativeSizeToSize(relativeSize, 'xlarge')).toBe('xlarge');
+  });
+
+  it('should offset by +4 when "+4"', () => {
+    const relativeSize = '+4';
+    expect(relativeSizeToSize(relativeSize, 'xsmall')).toBe('xlarge');
+    expect(relativeSizeToSize(relativeSize, 'small')).toBe('xlarge');
+    expect(relativeSizeToSize(relativeSize, 'medium')).toBe('xlarge');
+    expect(relativeSizeToSize(relativeSize, 'large')).toBe('xlarge');
+    expect(relativeSizeToSize(relativeSize, 'xlarge')).toBe('xlarge');
+  });
+});
+
 // Helpers
 
 const allSizeOrdered = ['xsmall', 'small', 'medium', 'large', 'xlarge'] as const;
 const sizeArb = fc.constantFrom<Size>(...allSizeOrdered);
 const isSmallerSize = (sa: Size, sb: Size) => allSizeOrdered.indexOf(sa) < allSizeOrdered.indexOf(sb);
 
-const allSizeForArbitrary = [...allSizeOrdered, 'max'] as const; // WARNING/ it does not include undefined
+const allRelativeSize = ['-4', '-3', '-2', '-1', '=', '+1', '+2', '+3', '+4'] as const;
+const relativeSizeArb = fc.constantFrom<RelativeSize>(...allRelativeSize);
+
+const allSizeForArbitrary = [...allSizeOrdered, ...allRelativeSize, 'max'] as const; // WARNING: it does not include undefined
 const sizeForArbitraryArb = fc.constantFrom<SizeForArbitrary>(...allSizeForArbitrary);
 
 // Type check that helpers are covering all the possibilities
 
 const failIfMissingSize: Size extends typeof allSizeOrdered[number] ? true : never = true;
+const failIfMissingRelativeSize: RelativeSize extends typeof allRelativeSize[number] ? true : never = true;
 const failIfMissingSizeForArbitrary: NonNullable<SizeForArbitrary> extends typeof allSizeForArbitrary[number]
   ? true
   : never = true;
 assert(failIfMissingSize); // just not to appear unused
+assert(failIfMissingRelativeSize); // just not to appear unused
 assert(failIfMissingSizeForArbitrary); // just not to appear unused