Repository: dubzzz/fast-check
Pull request #2177 (2021-08-05 17:58:13)

🔨 Add `test:debug` script for easier debugging
<!-- Context of the PR: short description and potentially linked issues -->

Adding yet another script to ease debugging and troubleshooting of issues.

<!-- ...a few words to describe the content of this PR...               -->
<!-- ... -->

<!-- Type of PR: [ ] unchecked / [ ] checked -->
**_Category:_**

- [ ] ✨ Introduce new features
- [ ] 📝 Add or update documentation
- [ ] ✅ Add or update tests
- [ ] 🐛 Fix a bug
- [ ] 🏷️ Add or update types
- [ ] ⚡️ Improve performance
- [x] _Other(s):_ Script
<!-- 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):_ ...



Patch:
diff --git a/package.json b/package.json
index 501d577c..a4176814 100644
--- a/package.json
+++ b/package.json
@@ -32,6 +32,7 @@
     "watch": "tsc -w",
     "test": "jest --config jest.unit.config.cjs --coverage",
     "test:watch": "jest --config jest.unit.config.cjs --watch",
+    "test:debug": "node --inspect node_modules/jest/bin/jest.js --config jest.unit.config.cjs --watch --runInBand",
     "e2e": "jest --config jest.e2e.config.cjs --coverage",
     "e2e:watch": "jest --config jest.e2e.config.cjs --watch",
     "update:examples": "cross-env UPDATE_CODE_SNIPPETS=true jest --config jest.examples.config.cjs",
diff --git a/src/arbitrary/_internals/SubarrayArbitrary.ts b/src/arbitrary/_internals/SubarrayArbitrary.ts
new file mode 100644
index 00000000..6edeccac
--- /dev/null
+++ b/src/arbitrary/_internals/SubarrayArbitrary.ts
@@ -0,0 +1,95 @@
+import { NextArbitrary } from '../../check/arbitrary/definition/NextArbitrary';
+import { NextValue } from '../../check/arbitrary/definition/NextValue';
+import { Random } from '../../random/generator/Random';
+import { makeLazy } from '../../stream/LazyIterableIterator';
+import { Stream } from '../../stream/Stream';
+import { isSubarrayOf } from './helpers/IsSubarrayOf';
+import { IntegerArbitrary } from './IntegerArbitrary';
+
+/** @internal */
+export class SubarrayArbitrary<T> extends NextArbitrary<T[]> {
+  readonly lengthArb: NextArbitrary<number>;
+  readonly biasedLengthArb: NextArbitrary<number>;
+  constructor(
+    readonly originalArray: T[],
+    readonly isOrdered: boolean,
+    readonly minLength: number,
+    readonly maxLength: number
+  ) {
+    super();
+    if (minLength < 0 || minLength > originalArray.length)
+      throw new Error(
+        'fc.*{s|S}ubarrayOf expects the minimal length to be between 0 and the size of the original array'
+      );
+    if (maxLength < 0 || maxLength > originalArray.length)
+      throw new Error(
+        'fc.*{s|S}ubarrayOf expects the maximal length to be between 0 and the size of the original array'
+      );
+    if (minLength > maxLength)
+      throw new Error('fc.*{s|S}ubarrayOf expects the minimal length to be inferior or equal to the maximal length');
+
+    this.lengthArb = new IntegerArbitrary(minLength, maxLength);
+    this.biasedLengthArb =
+      minLength !== maxLength
+        ? new IntegerArbitrary(minLength, minLength + Math.floor(Math.log(maxLength - minLength) / Math.log(2)))
+        : this.lengthArb;
+  }
+
+  generate(mrng: Random, biasFactor: number | undefined): NextValue<T[]> {
+    const lengthArb =
+      biasFactor !== undefined && mrng.nextInt(1, biasFactor) === 1 ? this.biasedLengthArb : this.lengthArb;
+    const size = lengthArb.generate(mrng, undefined);
+    const sizeValue = size.value;
+
+    const remainingElements = this.originalArray.map((_v, idx) => idx);
+    const ids: number[] = [];
+    for (let index = 0; index !== sizeValue; ++index) {
+      const selectedIdIndex = mrng.nextInt(0, remainingElements.length - 1);
+      ids.push(remainingElements[selectedIdIndex]);
+      remainingElements.splice(selectedIdIndex, 1);
+    }
+    if (this.isOrdered) {
+      ids.sort((a, b) => a - b);
+    }
+
+    return new NextValue(
+      ids.map((i) => this.originalArray[i]),
+      size.context
+    );
+  }
+
+  canShrinkWithoutContext(value: unknown): value is T[] {
+    if (!Array.isArray(value)) {
+      return false;
+    }
+    if (!this.lengthArb.canShrinkWithoutContext(value.length)) {
+      return false;
+    }
+    return isSubarrayOf(this.originalArray, value);
+  }
+
+  shrink(value: T[], context: unknown): Stream<NextValue<T[]>> {
+    // shrinking one by one is not the most comprehensive
+    // but allows a reasonable number of entries in the shrink
+    if (value.length === 0) {
+      return Stream.nil<NextValue<T[]>>();
+    }
+    return this.lengthArb
+      .shrink(value.length, context)
+      .map((newSize) => {
+        return new NextValue(
+          value.slice(value.length - newSize.value), // array of length newSize.value
+          newSize.context // integer context for value newSize.value (the length)
+        );
+      })
+      .join(
+        value.length > this.minLength
+          ? makeLazy(() =>
+              this.shrink(value.slice(1), undefined)
+                .filter((newValue) => this.minLength <= newValue.value.length + 1)
+                .map((newValue) => new NextValue([value[0]].concat(newValue.value), undefined))
+            )
+          : Stream.nil<NextValue<T[]>>()
+      );
+  }
+}
diff --git a/src/arbitrary/_internals/helpers/IsSubarrayOf.ts b/src/arbitrary/_internals/helpers/IsSubarrayOf.ts
new file mode 100644
index 00000000..852681f0
--- /dev/null
+++ b/src/arbitrary/_internals/helpers/IsSubarrayOf.ts
@@ -0,0 +1,27 @@
+export function isSubarrayOf(source: unknown[], small: unknown[]): boolean {
+  const countMap = new Map<unknown, number>();
+  let countMinusZero = 0;
+  for (const sourceEntry of source) {
+    if (Object.is(sourceEntry, -0)) {
+      ++countMinusZero;
+    } else {
+      const oldCount = countMap.get(sourceEntry) || 0;
+      countMap.set(sourceEntry, oldCount + 1);
+    }
+  }
+  for (let index = 0; index !== small.length; ++index) {
+    if (!(index in small)) {
+      return false;
+    }
+    const smallEntry = small[index];
+    if (Object.is(smallEntry, -0)) {
+      if (countMinusZero === 0) return false;
+      --countMinusZero;
+    } else {
+      const oldCount = countMap.get(smallEntry) || 0;
+      if (oldCount === 0) return false;
+      countMap.set(smallEntry, oldCount - 1);
+    }
+  }
+  return true;
+}
diff --git a/src/arbitrary/shuffledSubarray.ts b/src/arbitrary/shuffledSubarray.ts
new file mode 100644
index 00000000..faf6ce88
--- /dev/null
+++ b/src/arbitrary/shuffledSubarray.ts
@@ -0,0 +1,71 @@
+import { Arbitrary } from '../check/arbitrary/definition/Arbitrary';
+import { convertFromNext } from '../check/arbitrary/definition/Converters';
+import { SubarrayArbitrary } from './_internals/SubarrayArbitrary';
+
+/**
+ * Constraints to be applied on {@link shuffledSubarray}
+ * @remarks Since 2.18.0
+ * @public
+ */
+export interface ShuffledSubarrayConstraints {
+  /**
+   * Lower bound of the generated subarray size (included)
+   * @defaultValue 0
+   * @remarks Since 2.4.0
+   */
+  minLength?: number;
+  /**
+   * Upper bound of the generated subarray size (included)
+   * @defaultValue The length of the original array itself
+   * @remarks Since 2.4.0
+   */
+  maxLength?: number;
+}
+
+/**
+ * For subarrays of `originalArray`
+ *
+ * @param originalArray - Original array
+ *
+ * @remarks Since 1.5.0
+ * @public
+ */
+function shuffledSubarray<T>(originalArray: T[]): Arbitrary<T[]>;
+/**
+ * For subarrays of `originalArray`
+ *
+ * @param originalArray - Original array
+ * @param minLength - Lower bound of the generated array size
+ * @param maxLength - Upper bound of the generated array size
+ *
+ * @deprecated
+ * Superceded by `fc.shuffledSubarray(originalArray, {minLength, maxLength})` - see {@link https://github.com/dubzzz/fast-check/issues/992 | #992}.
+ * Ease the migration with {@link https://github.com/dubzzz/fast-check/tree/main/codemods/unify-signatures | our codemod script}.
+ *
+ * @remarks Since 1.5.0
+ * @public
+ */
+function shuffledSubarray<T>(originalArray: T[], minLength: number, maxLength: number): Arbitrary<T[]>;
+/**
+ * For subarrays of `originalArray`
+ *
+ * @param originalArray - Original array
+ * @param constraints - Constraints to apply when building instances
+ *
+ * @remarks Since 2.4.0
+ * @public
+ */
+function shuffledSubarray<T>(originalArray: T[], constraints: ShuffledSubarrayConstraints): Arbitrary<T[]>;
+function shuffledSubarray<T>(
+  originalArray: T[],
+  ...args: [] | [number, number] | [ShuffledSubarrayConstraints]
+): Arbitrary<T[]> {
+  if (typeof args[0] === 'number' && typeof args[1] === 'number') {
+    return convertFromNext(new SubarrayArbitrary(originalArray, false, args[0], args[1]));
+  }
+  const ct = args[0] as ShuffledSubarrayConstraints | undefined;
+  const minLength = ct !== undefined && ct.minLength !== undefined ? ct.minLength : 0;
+  const maxLength = ct !== undefined && ct.maxLength !== undefined ? ct.maxLength : originalArray.length;
+  return convertFromNext(new SubarrayArbitrary(originalArray, false, minLength, maxLength));
+}
+export { shuffledSubarray };
diff --git a/src/arbitrary/subarray.ts b/src/arbitrary/subarray.ts
new file mode 100644
index 00000000..cf9f80b6
--- /dev/null
+++ b/src/arbitrary/subarray.ts
@@ -0,0 +1,68 @@
+import { Arbitrary } from '../check/arbitrary/definition/Arbitrary';
+import { convertFromNext } from '../check/arbitrary/definition/Converters';
+import { SubarrayArbitrary } from './_internals/SubarrayArbitrary';
+
+/**
+ * Constraints to be applied on {@link subarray}
+ * @remarks Since 2.4.0
+ * @public
+ */
+export interface SubarrayConstraints {
+  /**
+   * Lower bound of the generated subarray size (included)
+   * @defaultValue 0
+   * @remarks Since 2.4.0
+   */
+  minLength?: number;
+  /**
+   * Upper bound of the generated subarray size (included)
+   * @defaultValue The length of the original array itself
+   * @remarks Since 2.4.0
+   */
+  maxLength?: number;
+}
+
+/**
+ * For subarrays of `originalArray` (keeps ordering)
+ *
+ * @param originalArray - Original array
+ *
+ * @remarks Since 1.5.0
+ * @public
+ */
+function subarray<T>(originalArray: T[]): Arbitrary<T[]>;
+/**
+ * For subarrays of `originalArray` (keeps ordering)
+ *
+ * @param originalArray - Original array
+ * @param minLength - Lower bound of the generated array size
+ * @param maxLength - Upper bound of the generated array size
+ *
+ * @deprecated
+ * Superceded by `fc.subarray(originalArray, {minLength, maxLength})` - see {@link https://github.com/dubzzz/fast-check/issues/992 | #992}.
+ * Ease the migration with {@link https://github.com/dubzzz/fast-check/tree/main/codemods/unify-signatures | our codemod script}.
+ *
+ * @remarks Since 1.5.0
+ * @public
+ */
+function subarray<T>(originalArray: T[], minLength: number, maxLength: number): Arbitrary<T[]>;
+/**
+ * For subarrays of `originalArray` (keeps ordering)
+ *
+ * @param originalArray - Original array
+ * @param constraints - Constraints to apply when building instances
+ *
+ * @remarks Since 2.4.0
+ * @public
+ */
+function subarray<T>(originalArray: T[], constraints: SubarrayConstraints): Arbitrary<T[]>;
+function subarray<T>(originalArray: T[], ...args: [] | [number, number] | [SubarrayConstraints]): Arbitrary<T[]> {
+  if (typeof args[0] === 'number' && typeof args[1] === 'number') {
+    return convertFromNext(new SubarrayArbitrary(originalArray, true, args[0], args[1]));
+  }
+  const ct = args[0] as SubarrayConstraints | undefined;
+  const minLength = ct !== undefined && ct.minLength !== undefined ? ct.minLength : 0;
+  const maxLength = ct !== undefined && ct.maxLength !== undefined ? ct.maxLength : originalArray.length;
+  return convertFromNext(new SubarrayArbitrary(originalArray, true, minLength, maxLength));
+}
+export { subarray };
diff --git a/src/check/arbitrary/SubarrayArbitrary.ts b/src/check/arbitrary/SubarrayArbitrary.ts
deleted file mode 100644
index a0a6560e..00000000
--- a/src/check/arbitrary/SubarrayArbitrary.ts
+++ /dev/null
@@ -1,204 +0,0 @@
-import { Random } from '../../random/generator/Random';
-import { Stream } from '../../stream/Stream';
-import { Arbitrary } from './definition/Arbitrary';
-import { ArbitraryWithContextualShrink } from './definition/ArbitraryWithContextualShrink';
-import { biasWrapper } from './definition/BiasedArbitraryWrapper';
-import { Shrinkable } from './definition/Shrinkable';
-import { integer } from '../../arbitrary/integer';
-import { makeLazy } from '../../stream/LazyIterableIterator';
-
-/** @internal */
-class SubarrayArbitrary<T> extends Arbitrary<T[]> {
-  readonly lengthArb: ArbitraryWithContextualShrink<number>;
-  constructor(
-    readonly originalArray: T[],
-    readonly isOrdered: boolean,
-    readonly minLength: number,
-    readonly maxLength: number
-  ) {
-    super();
-    if (minLength < 0 || minLength > originalArray.length)
-      throw new Error(
-        'fc.*{s|S}ubarrayOf expects the minimal length to be between 0 and the size of the original array'
-      );
-    if (maxLength < 0 || maxLength > originalArray.length)
-      throw new Error(
-        'fc.*{s|S}ubarrayOf expects the maximal length to be between 0 and the size of the original array'
-      );
-    if (minLength > maxLength)
-      throw new Error('fc.*{s|S}ubarrayOf expects the minimal length to be inferior or equal to the maximal length');
-    this.lengthArb = integer(minLength, maxLength);
-  }
-  private wrapper(items: T[], itemsLengthContext: unknown): Shrinkable<T[]> {
-    return new Shrinkable(items, () =>
-      this.shrinkImpl(items, itemsLengthContext).map((contextualValue) =>
-        this.wrapper(contextualValue[0], contextualValue[1])
-      )
-    );
-  }
-  generate(mrng: Random): Shrinkable<T[]> {
-    const remainingElements = this.originalArray.map((_v, idx) => idx);
-    const size = this.lengthArb.generate(mrng).value;
-    const ids: number[] = [];
-    for (let idx = 0; idx !== size; ++idx) {
-      const selectedIdIndex = mrng.nextInt(0, remainingElements.length - 1);
-      ids.push(remainingElements[selectedIdIndex]);
-      remainingElements.splice(selectedIdIndex, 1);
-    }
-    if (this.isOrdered) ids.sort((a, b) => a - b);
-    return this.wrapper(
-      ids.map((i) => this.originalArray[i]),
-      undefined
-    );
-  }
-  private shrinkImpl(items: T[], itemsLengthContext: unknown): Stream<[T[], unknown]> {
-    // shrinking one by one is the not the most comprehensive
-    // but allows a reasonable number of entries in the shrink
-    if (items.length === 0) {
-      return Stream.nil<[T[], unknown]>();
-    }
-    return this.lengthArb
-      .contextualShrink(items.length, itemsLengthContext)
-      .map((contextualValue): [T[], unknown] => {
-        return [
-          items.slice(items.length - contextualValue[0]), // array of length contextualValue[0]
-          contextualValue[1], // integer context for value contextualValue[0] (the length)
-        ];
-      })
-      .join(
-        items.length > this.minLength
-          ? makeLazy(() =>
-              this.shrinkImpl(items.slice(1), undefined)
-                .filter((contextualValue) => this.minLength <= contextualValue[0].length + 1)
-                .map((contextualValue) => [[items[0]].concat(contextualValue[0]), undefined])
-            )
-          : Stream.nil<[T[], unknown]>()
-      );
-  }
-  withBias(freq: number): Arbitrary<T[]> {
-    return this.minLength !== this.maxLength
-      ? biasWrapper(freq, this, (originalArbitrary: SubarrayArbitrary<T>) => {
-          return new SubarrayArbitrary(
-            originalArbitrary.originalArray,
-            originalArbitrary.isOrdered,
-            originalArbitrary.minLength,
-            originalArbitrary.minLength +
-              Math.floor(Math.log(originalArbitrary.maxLength - originalArbitrary.minLength) / Math.log(2))
-          );
-        })
-      : this;
-  }
-}
-
-/**
- * Constraints to be applied on {@link subarray} and {@link shuffledSubarray}
- * @remarks Since 2.4.0
- * @public
- */
-export interface SubarrayConstraints {
-  /**
-   * Lower bound of the generated subarray size (included)
-   * @defaultValue 0
-   * @remarks Since 2.4.0
-   */
-  minLength?: number;
-  /**
-   * Upper bound of the generated subarray size (included)
-   * @defaultValue The length of the original array itself
-   * @remarks Since 2.4.0
-   */
-  maxLength?: number;
-}
-
-/**
- * For subarrays of `originalArray` (keeps ordering)
- *
- * @param originalArray - Original array
- *
- * @remarks Since 1.5.0
- * @public
- */
-function subarray<T>(originalArray: T[]): Arbitrary<T[]>;
-/**
- * For subarrays of `originalArray` (keeps ordering)
- *
- * @param originalArray - Original array
- * @param minLength - Lower bound of the generated array size
- * @param maxLength - Upper bound of the generated array size
- *
- * @deprecated
- * Superceded by `fc.subarray(originalArray, {minLength, maxLength})` - see {@link https://github.com/dubzzz/fast-check/issues/992 | #992}.
- * Ease the migration with {@link https://github.com/dubzzz/fast-check/tree/main/codemods/unify-signatures | our codemod script}.
- *
- * @remarks Since 1.5.0
- * @public
- */
-function subarray<T>(originalArray: T[], minLength: number, maxLength: number): Arbitrary<T[]>;
-/**
- * For subarrays of `originalArray` (keeps ordering)
- *
- * @param originalArray - Original array
- * @param constraints - Constraints to apply when building instances
- *
- * @remarks Since 2.4.0
- * @public
- */
-function subarray<T>(originalArray: T[], constraints: SubarrayConstraints): Arbitrary<T[]>;
-function subarray<T>(originalArray: T[], ...args: [] | [number, number] | [SubarrayConstraints]): Arbitrary<T[]> {
-  if (typeof args[0] === 'number' && typeof args[1] === 'number') {
-    return new SubarrayArbitrary(originalArray, true, args[0], args[1]);
-  }
-  const ct = args[0] as SubarrayConstraints | undefined;
-  const minLength = ct !== undefined && ct.minLength !== undefined ? ct.minLength : 0;
-  const maxLength = ct !== undefined && ct.maxLength !== undefined ? ct.maxLength : originalArray.length;
-  return new SubarrayArbitrary(originalArray, true, minLength, maxLength);
-}
-
-/**
- * For subarrays of `originalArray`
- *
- * @param originalArray - Original array
- *
- * @remarks Since 1.5.0
- * @public
- */
-function shuffledSubarray<T>(originalArray: T[]): Arbitrary<T[]>;
-/**
- * For subarrays of `originalArray`
- *
- * @param originalArray - Original array
- * @param minLength - Lower bound of the generated array size
- * @param maxLength - Upper bound of the generated array size
- *
- * @deprecated
- * Superceded by `fc.shuffledSubarray(originalArray, {minLength, maxLength})` - see {@link https://github.com/dubzzz/fast-check/issues/992 | #992}.
- * Ease the migration with {@link https://github.com/dubzzz/fast-check/tree/main/codemods/unify-signatures | our codemod script}.
- *
- * @remarks Since 1.5.0
- * @public
- */
-function shuffledSubarray<T>(originalArray: T[], minLength: number, maxLength: number): Arbitrary<T[]>;
-/**
- * For subarrays of `originalArray`
- *
- * @param originalArray - Original array
- * @param constraints - Constraints to apply when building instances
- *
- * @remarks Since 2.4.0
- * @public
- */
-function shuffledSubarray<T>(originalArray: T[], constraints: SubarrayConstraints): Arbitrary<T[]>;
-function shuffledSubarray<T>(
-  originalArray: T[],
-  ...args: [] | [number, number] | [SubarrayConstraints]
-): Arbitrary<T[]> {
-  if (typeof args[0] === 'number' && typeof args[1] === 'number') {
-    return new SubarrayArbitrary(originalArray, false, args[0], args[1]);
-  }
-  const ct = args[0] as SubarrayConstraints | undefined;
-  const minLength = ct !== undefined && ct.minLength !== undefined ? ct.minLength : 0;
-  const maxLength = ct !== undefined && ct.maxLength !== undefined ? ct.maxLength : originalArray.length;
-  return new SubarrayArbitrary(originalArray, false, minLength, maxLength);
-}
-
-export { subarray, shuffledSubarray };
diff --git a/src/fast-check-default.ts b/src/fast-check-default.ts
index df2797a0..57d1c0c7 100644
--- a/src/fast-check-default.ts
+++ b/src/fast-check-default.ts
@@ -82,7 +82,8 @@ import { string, StringSharedConstraints } from './arbitrary/string';
 import { string16bits } from './arbitrary/string16bits';
 import { stringOf } from './arbitrary/stringOf';
 import { unicodeString } from './arbitrary/unicodeString';
-import { shuffledSubarray, subarray, SubarrayConstraints } from './check/arbitrary/SubarrayArbitrary';
+import { subarray, SubarrayConstraints } from './arbitrary/subarray';
+import { shuffledSubarray, ShuffledSubarrayConstraints } from './arbitrary/shuffledSubarray';
 import { genericTuple } from './arbitrary/genericTuple';
 import { tuple } from './arbitrary/tuple';
 import { uuid } from './arbitrary/uuid';
@@ -381,6 +382,7 @@ export {
   SparseArrayConstraints,
   StringSharedConstraints,
   SubarrayConstraints,
+  ShuffledSubarrayConstraints,
   WebAuthorityConstraints,
   WebUrlConstraints,
   WeightedArbitrary,


Test patch:
diff --git a/test/unit/check/arbitrary/SubarrayArbitrary.spec.ts b/test/unit/check/arbitrary/SubarrayArbitrary.spec.ts
index 1819c916..ea0026f4 100644
--- a/test/unit/check/arbitrary/SubarrayArbitrary.spec.ts
+++ b/test/unit/check/arbitrary/SubarrayArbitrary.spec.ts
@@ -1,6 +1,7 @@
 import * as fc from '../../../../lib/fast-check';
 
-import { subarray, shuffledSubarray } from '../../../../src/check/arbitrary/SubarrayArbitrary';
+import { subarray } from '../../../../src/arbitrary/subarray';
+import { shuffledSubarray } from '../../../../src/arbitrary/shuffledSubarray';
 
 import { generateOneValue } from './generic/GenerateOneValue';
 import * as genericHelper from './generic/GenericArbitraryHelper';