Repository: dubzzz/fast-check
Pull request #2953 (2022-05-09 12:54:24)

✨ Extend usage of `defaultSizeToMaxWhenMaxSpecified` to depth
Up-to-now the only usage of `defaultSizeToMaxWhenMaxSpecified` has been length. It was initially introduced to provide a backward compatible way to build array-like structures in v3.

But as the feature may be useful for some users, we don't plan to remove it and rather pln to make it opt-in. With that choice it looks to be the best option to make depth and length even closer by adopting the same logic regarding that flag for both of them.

As a consequence, this PR adds support for `defaultSizeToMaxWhenMaxSpecified` on depth factors. When set to true and if a maximal depth has been specified and no size hardcoded on arbitrary level, then we will apply the same as if the user set the size to 'max'. Otherwise it means '='.

<!-- Context of the PR: short description and potentially linked issues -->

<!-- ...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/2953/builds/248047) or the icon next to each commit SHA.

Latest deployment of this branch, based on commit b44bd423b63520d99964bcdc5d66d94a03e8ff9c:

|Sandbox| Source |
|--|--|
|[Vanilla](https://codesandbox.io/s/vanilla-3njgjh)| Configuration |
|[dubzzz/fast-check](https://codesandbox.io/s/dubzzz-fast-check-h89isq)| Configuration |

codecov[bot]: # [Codecov](https://codecov.io/gh/dubzzz/fast-check/pull/2953?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN) Report
> Merging [#2953](https://codecov.io/gh/dubzzz/fast-check/pull/2953?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN) (b44bd42) into [main](https://codecov.io/gh/dubzzz/fast-check/commit/918e559f1781d3e3e2c701b489e79af09161b896?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN) (918e559) will **increase** coverage by `0.00%`.
> The diff coverage is `100.00%`.

```diff
@@           Coverage Diff           @@
##             main    #2953   +/-   ##
=======================================
  Coverage   96.27%   96.27%           
=======================================
  Files         193      193           
  Lines        4505     4510    +5     
  Branches      973      976    +3     
=======================================
+ Hits         4337     4342    +5     
  Misses        158      158           
  Partials       10       10           
```

| Flag | Coverage Δ | |
|---|---|---|
| unit-tests | `96.27% <100.00%> (+<0.01%)` | :arrow_up: |

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/2953?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN) | Coverage Δ | |
|---|---|---|
| [src/arbitrary/\_internals/FrequencyArbitrary.ts](https://codecov.io/gh/dubzzz/fast-check/pull/2953/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN#diff-c3JjL2FyYml0cmFyeS9faW50ZXJuYWxzL0ZyZXF1ZW5jeUFyYml0cmFyeS50cw==) | `98.03% <ø> (ø)` | |
| [src/check/runner/configuration/GlobalParameters.ts](https://codecov.io/gh/dubzzz/fast-check/pull/2953/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN#diff-c3JjL2NoZWNrL3J1bm5lci9jb25maWd1cmF0aW9uL0dsb2JhbFBhcmFtZXRlcnMudHM=) | `100.00% <ø> (ø)` | |
| [...trary/\_internals/helpers/MaxLengthFromMinLength.ts](https://codecov.io/gh/dubzzz/fast-check/pull/2953/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN#diff-c3JjL2FyYml0cmFyeS9faW50ZXJuYWxzL2hlbHBlcnMvTWF4TGVuZ3RoRnJvbU1pbkxlbmd0aC50cw==) | `95.65% <100.00%> (+0.33%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/dubzzz/fast-check/pull/2953?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/2953?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN). Last update [918e559...b44bd42](https://codecov.io/gh/dubzzz/fast-check/pull/2953?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 882f7f55..a3550c17 100644
--- a/documentation/Arbitraries.md
+++ b/documentation/Arbitraries.md
@@ -3797,7 +3797,7 @@ was pretty cool, selecting the right value was not trivial from a user point of
 number of items defined within an iterable structure and depth. Except very complex and ad-hoc cases, we expect size to
 be the only kind of configuration used to setup depth factors.
 
-Depth factor derived from size works exactly the same way as size, it can rely on Relative Size but also Explicit Size. By default when not explicitely set it will be defaulted to `=` and will ignore `defaultSizeToMaxWhenMaxSpecified` which has been added for length-based sizes.
+Depth factor derived from size works exactly the same way as size, it can rely on Relative Size but also Explicit Size. As for length, if not specified the size will either be defaulted to `"="` or to `"max"` depending on the flag `defaultSizeToMaxWhenMaxSpecified` and on the fact that the user specified a maximal depth or not. The only case defaulting to `"max"` is: user specified a maximal depth onto the instance but not size and `defaultSizeToMaxWhenMaxSpecified` is set to `true`. Any other setup will fallback to `"="`.
 
 Here is how a size translates into depth factors:
 - `xsmall` — `1`
diff --git a/documentation/Tips.md b/documentation/Tips.md
index 621e1e29..717f045b 100644
--- a/documentation/Tips.md
+++ b/documentation/Tips.md
@@ -894,6 +894,6 @@ There are two main ways to change this upper bound:
 At global level, there are two main options:
 
 - `baseSize` — defaulted to `"small"` — define what should be the default size when nothing has been specified at instantiation level
-- `defaultSizeToMaxWhenMaxSpecified` — when set to `true`, any arbitrary being instantiated with an upper bound (such as `maxLength`) and no size will see it's size defaulted to `max` / when set to `false`, if not defined the size will be defaulted to `baseSize` (see above)
+- `defaultSizeToMaxWhenMaxSpecified` — when set to `true`, any arbitrary being instantiated with an upper bound (such as `maxLength` or `maxDepth`) and no size will see it's size defaulted to `max` / when set to `false`, if not defined the size will be defaulted to `baseSize` (see above)
 
 You may want to read more about ways to configure global settings at [Setup global settings](#setup-global-settings).
diff --git a/src/arbitrary/_internals/FrequencyArbitrary.ts b/src/arbitrary/_internals/FrequencyArbitrary.ts
index 0580e58d..e3b5f4bd 100644
--- a/src/arbitrary/_internals/FrequencyArbitrary.ts
+++ b/src/arbitrary/_internals/FrequencyArbitrary.ts
@@ -33,7 +33,7 @@ export class FrequencyArbitrary<T> extends Arbitrary<T> {
       throw new Error(`${label} expects the sum of weights to be strictly superior to 0`);
     }
     const sanitizedConstraints: _SanitizedConstraints = {
-      depthFactor: depthFactorFromSizeForArbitrary(constraints.depthFactor),
+      depthFactor: depthFactorFromSizeForArbitrary(constraints.depthFactor, constraints.maxDepth !== undefined),
       depthIdentifier: constraints.depthIdentifier,
       maxDepth: constraints.maxDepth,
       withCrossShrink: constraints.withCrossShrink,
diff --git a/src/arbitrary/_internals/helpers/MaxLengthFromMinLength.ts b/src/arbitrary/_internals/helpers/MaxLengthFromMinLength.ts
index b13d3ad3..482012d6 100644
--- a/src/arbitrary/_internals/helpers/MaxLengthFromMinLength.ts
+++ b/src/arbitrary/_internals/helpers/MaxLengthFromMinLength.ts
@@ -144,17 +144,26 @@ export function maxGeneratedLengthFromSizeForArbitrary(
 /**
  * Compute `depthFactor` based on `size`
  * @param size - Size or depthFactor defined by the caller on the arbitrary
+ * @param specifiedMaxDepth - Whether or not the caller specified a max depth
  * @internal
  */
-export function depthFactorFromSizeForArbitrary(depthFactorOrSize: DepthFactorSizeForArbitrary): number {
+export function depthFactorFromSizeForArbitrary(
+  depthFactorOrSize: DepthFactorSizeForArbitrary,
+  specifiedMaxDepth: boolean
+): number {
   if (typeof depthFactorOrSize === 'number') {
     return depthFactorOrSize;
   }
-  if (depthFactorOrSize === 'max') {
+  const { baseSize: defaultSize = DefaultSize, defaultSizeToMaxWhenMaxSpecified } = readConfigureGlobal() || {};
+  const definedSize =
+    depthFactorOrSize !== undefined
+      ? depthFactorOrSize
+      : specifiedMaxDepth && defaultSizeToMaxWhenMaxSpecified
+      ? 'max'
+      : defaultSize;
+  if (definedSize === 'max') {
     return 0;
   }
-  const { baseSize: defaultSize = DefaultSize } = readConfigureGlobal() || {};
-  const definedSize = depthFactorOrSize !== undefined ? depthFactorOrSize : defaultSize;
   const finalSize = relativeSizeToSize(definedSize, defaultSize);
   switch (finalSize) {
     case 'xsmall':
diff --git a/src/check/runner/configuration/GlobalParameters.ts b/src/check/runner/configuration/GlobalParameters.ts
index 9d8c4a0b..f99fc7f6 100644
--- a/src/check/runner/configuration/GlobalParameters.ts
+++ b/src/check/runner/configuration/GlobalParameters.ts
@@ -76,7 +76,8 @@ export type GlobalParameters = Pick<Parameters<unknown>, Exclude<keyof Parameter
   baseSize?: Size;
   /**
    * When set to `true` and if the size has not been defined for this precise instance,
-   * it will automatically default to `"max"` if the user specified a upper bound for the range.
+   * it will automatically default to `"max"` if the user specified a upper bound for the range
+   * (applies to length and to depth).
    *
    * When `false`, the size will be defaulted to `baseSize` even if the user specified
    * a upper bound for the range.


Test patch:
diff --git a/test/unit/arbitrary/_internals/helpers/MaxLengthFromMinLength.spec.ts b/test/unit/arbitrary/_internals/helpers/MaxLengthFromMinLength.spec.ts
index ba72515f..4e93b00c 100644
--- a/test/unit/arbitrary/_internals/helpers/MaxLengthFromMinLength.spec.ts
+++ b/test/unit/arbitrary/_internals/helpers/MaxLengthFromMinLength.spec.ts
@@ -267,21 +267,30 @@ describe('maxGeneratedLengthFromSizeForArbitrary', () => {
 describe('depthFactorFromSizeForArbitrary', () => {
   it('should only consider the received depthFactor when set to a numeric value', () => {
     fc.assert(
-      fc.property(sizeRelatedGlobalConfigArb, fc.double({ min: 0 }), (config, size) => {
-        // Arrange / Act
-        const computedDepthFactor = withConfiguredGlobal(config, () => depthFactorFromSizeForArbitrary(size));
+      fc.property(
+        sizeRelatedGlobalConfigArb,
+        fc.double({ min: 0 }),
+        fc.boolean(),
+        (config, size, specifiedMaxDepth) => {
+          // Arrange / Act
+          const computedDepthFactor = withConfiguredGlobal(config, () =>
+            depthFactorFromSizeForArbitrary(size, specifiedMaxDepth)
+          );
 
-        // Assert
-        expect(computedDepthFactor).toBe(size);
-      })
+          // Assert
+          expect(computedDepthFactor).toBe(size);
+        }
+      )
     );
   });
 
   it('should only consider the received size when set to Size', () => {
     fc.assert(
-      fc.property(sizeRelatedGlobalConfigArb, sizeArb, (config, size) => {
+      fc.property(sizeRelatedGlobalConfigArb, sizeArb, fc.boolean(), (config, size, specifiedMaxDepth) => {
         // Arrange / Act
-        const computedDepthFactor = withConfiguredGlobal(config, () => depthFactorFromSizeForArbitrary(size));
+        const computedDepthFactor = withConfiguredGlobal(config, () =>
+          depthFactorFromSizeForArbitrary(size, specifiedMaxDepth)
+        );
         const expectedDepthFactor = { xsmall: 1, small: 1 / 2, medium: 1 / 4, large: 1 / 8, xlarge: 1 / 16 }[size];
 
         // Assert
@@ -292,14 +301,16 @@ describe('depthFactorFromSizeForArbitrary', () => {
 
   it('should behave as its equivalent Size taking into account global settings when receiving a RelativeSize', () => {
     fc.assert(
-      fc.property(sizeRelatedGlobalConfigArb, relativeSizeArb, (config, size) => {
+      fc.property(sizeRelatedGlobalConfigArb, relativeSizeArb, fc.boolean(), (config, size, specifiedMaxDepth) => {
         // Arrange
         const { baseSize: defaultSize = DefaultSize } = config;
         const equivalentSize = relativeSizeToSize(size, defaultSize);
 
         // Act
-        const computedDepthFactor = withConfiguredGlobal(config, () => depthFactorFromSizeForArbitrary(size));
-        const expectedDepthFactor = depthFactorFromSizeForArbitrary(equivalentSize);
+        const computedDepthFactor = withConfiguredGlobal(config, () =>
+          depthFactorFromSizeForArbitrary(size, specifiedMaxDepth)
+        );
+        const expectedDepthFactor = depthFactorFromSizeForArbitrary(equivalentSize, false);
 
         // Assert
         expect(computedDepthFactor).toBe(expectedDepthFactor);
@@ -308,10 +319,26 @@ describe('depthFactorFromSizeForArbitrary', () => {
   });
 
   it('should always return 0 if size is max whatever the global configuration', () => {
+    fc.assert(
+      fc.property(sizeRelatedGlobalConfigArb, fc.boolean(), (config, specifiedMaxDepth) => {
+        // Arrange / Act
+        const computedDepthFactor = withConfiguredGlobal(config, () =>
+          depthFactorFromSizeForArbitrary('max', specifiedMaxDepth)
+        );
+
+        // Assert
+        expect(computedDepthFactor).toBe(0);
+      })
+    );
+  });
+
+  it('should always return 0 if both specifiedMaxDepth and defaultSizeToMaxWhenMaxSpecified are true and size unset', () => {
     fc.assert(
       fc.property(sizeRelatedGlobalConfigArb, (config) => {
         // Arrange / Act
-        const computedDepthFactor = withConfiguredGlobal(config, () => depthFactorFromSizeForArbitrary('max'));
+        const computedDepthFactor = withConfiguredGlobal({ ...config, defaultSizeToMaxWhenMaxSpecified: true }, () =>
+          depthFactorFromSizeForArbitrary(undefined, true)
+        );
 
         // Assert
         expect(computedDepthFactor).toBe(0);