Skip to content

Commit

Permalink
✨ Add experimental "custom slices" constraint on array
Browse files Browse the repository at this point in the history
This new constraint not officially exposed to fast-check's users (in other words, name might change in patch, minor... without any precise warning), should unlock the feature "Helper to deal with dangerous strings" #484 by making it feasible.

So far, it only provides an extra constraint on the API of `array` but it will soon be leveraged for arbitraries on strings.
  • Loading branch information
dubzzz committed Jul 4, 2022
1 parent 8607cb7 commit 7220c57
Show file tree
Hide file tree
Showing 6 changed files with 113 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ export class ArrayArbitrary<T> extends Arbitrary<T[]> {
depthIdentifier: DepthIdentifier | string | undefined,
// Whenever passing a isEqual to ArrayArbitrary, you also have to filter
// it's output just in case produced values are too small (below minLength)
readonly setBuilder?: CustomSetBuilder<Value<T>>
readonly setBuilder: CustomSetBuilder<Value<T>> | undefined,
readonly customSlices: T[][]
) {
super();
this.lengthArb = integer({ min: minLength, max: maxGeneratedLength });
Expand Down Expand Up @@ -76,7 +77,7 @@ export class ArrayArbitrary<T> extends Arbitrary<T[]> {
): Value<T>[] {
let numSkippedInRow = 0;
const s = setBuilder();
const slicedGenerator = buildSlicedGenerator(this.arb, mrng, [], biasFactorItems);
const slicedGenerator = buildSlicedGenerator(this.arb, mrng, this.customSlices, biasFactorItems);
// Try to append into items up to the target size
// We may reject some items as they are already part of the set
// so we need to retry and generate other ones. In order to prevent infinite loop,
Expand Down Expand Up @@ -110,7 +111,7 @@ export class ArrayArbitrary<T> extends Arbitrary<T[]> {

private generateNItems(N: number, mrng: Random, biasFactorItems: number | undefined): Value<T>[] {
const items: Value<T>[] = [];
const slicedGenerator = buildSlicedGenerator(this.arb, mrng, [], biasFactorItems);
const slicedGenerator = buildSlicedGenerator(this.arb, mrng, this.customSlices, biasFactorItems);
slicedGenerator.attemptExact(N);
for (let index = 0; index !== N; ++index) {
const current = slicedGenerator.next();
Expand Down
18 changes: 17 additions & 1 deletion packages/fast-check/src/arbitrary/array.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,21 @@ export interface ArrayConstraints {
depthIdentifier?: DepthIdentifier | string;
}

/**
* Extra but internal constraints that can be passed to array.
* This extra set is made of constraints mostly targets experimental and internal features not yet mature to be exposed.
* @internal
*/
export interface ArrayConstraintsInternal<T> extends ArrayConstraints {
/**
* Extra user-definable and hardcoded values.
* Each entry in the array could be used to build the final generated value outputed by the arbitrary of array on generate.
* Each entry must have at least one element of type T into it.
* Each T must be a value acceptable for the arbitrary passed to the array.
*/
experimentalCustomSlices?: T[][];
}

/**
* For arrays of values coming from `arb`
*
Expand All @@ -66,6 +81,7 @@ function array<T>(arb: Arbitrary<T>, constraints: ArrayConstraints = {}): Arbitr
const maxLength = maxLengthOrUnset !== undefined ? maxLengthOrUnset : MaxLengthUpperBound;
const specifiedMaxLength = maxLengthOrUnset !== undefined;
const maxGeneratedLength = maxGeneratedLengthFromSizeForArbitrary(size, minLength, maxLength, specifiedMaxLength);
return new ArrayArbitrary<T>(arb, minLength, maxGeneratedLength, maxLength, depthIdentifier);
const customSlices = (constraints as ArrayConstraintsInternal<T>).experimentalCustomSlices || [];
return new ArrayArbitrary<T>(arb, minLength, maxGeneratedLength, maxLength, depthIdentifier, undefined, customSlices);
}
export { array };
10 changes: 9 additions & 1 deletion packages/fast-check/src/arbitrary/uniqueArray.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,15 @@ export function uniqueArray<T, U>(arb: Arbitrary<T>, constraints: UniqueArrayCon
const depthIdentifier = constraints.depthIdentifier;
const setBuilder = buildUniqueArraySetBuilder(constraints);

const arrayArb = new ArrayArbitrary<T>(arb, minLength, maxGeneratedLength, maxLength, depthIdentifier, setBuilder);
const arrayArb = new ArrayArbitrary<T>(
arb,
minLength,
maxGeneratedLength,
maxLength,
depthIdentifier,
setBuilder,
[]
);
if (minLength === 0) return arrayArb;
return arrayArb.filter((tab) => tab.length >= minLength);
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,15 @@ describe('ArrayArbitrary', () => {
const { instance: mrng } = fakeRandom();

// Act
const arb = new ArrayArbitrary(instance, minLength, maxGeneratedLength, maxLength, undefined);
const arb = new ArrayArbitrary(
instance,
minLength,
maxGeneratedLength,
maxLength,
undefined,
undefined,
[]
);
const g = arb.generate(mrng, undefined);

// Assert
Expand Down Expand Up @@ -82,7 +90,15 @@ describe('ArrayArbitrary', () => {
const { instance: mrng } = fakeRandom();

// Act
const arb = new ArrayArbitrary(instance, minLength, maxGeneratedLength, maxLength, undefined, setBuilder);
const arb = new ArrayArbitrary(
instance,
minLength,
maxGeneratedLength,
maxLength,
undefined,
setBuilder,
[]
);
const g = arb.generate(mrng, undefined);

// Assert
Expand Down Expand Up @@ -132,7 +148,8 @@ describe('ArrayArbitrary', () => {
minLength,
maxLength,
undefined,
withSetBuilder ? setBuilder : undefined
withSetBuilder ? setBuilder : undefined,
[]
);
const g = arb.generate(mrng, biasFactor);

Expand Down Expand Up @@ -198,7 +215,8 @@ describe('ArrayArbitrary', () => {
maxGeneratedLength,
maxLength,
undefined,
withSetBuilder ? setBuilder : undefined
withSetBuilder ? setBuilder : undefined,
[]
);
arb.generate(mrng, biasFactor);

Expand Down Expand Up @@ -230,7 +248,7 @@ describe('ArrayArbitrary', () => {
const { instance: mrng } = fakeRandom();

// Act
const arb = new ArrayArbitrary(instance, 0, 10, 100, undefined);
const arb = new ArrayArbitrary(instance, 0, 10, 100, undefined, undefined, []);
const g = arb.generate(mrng, undefined);

// Assert
Expand Down Expand Up @@ -258,7 +276,7 @@ describe('ArrayArbitrary', () => {
const { instance: mrng } = fakeRandom();

// Act
const arb = new ArrayArbitrary(instance, 0, 10, 100, undefined);
const arb = new ArrayArbitrary(instance, 0, 10, 100, undefined, undefined, []);
const g = arb.generate(mrng, undefined);

// Assert
Expand Down Expand Up @@ -307,7 +325,8 @@ describe('ArrayArbitrary', () => {
maxGeneratedLength,
maxLength,
undefined,
withSetBuilder ? setBuilder : undefined
withSetBuilder ? setBuilder : undefined,
[]
);
const out = arb.canShrinkWithoutContext(value);

Expand Down Expand Up @@ -358,7 +377,8 @@ describe('ArrayArbitrary', () => {
maxGeneratedLength,
maxLength,
undefined,
withSetBuilder ? setBuilder : undefined
withSetBuilder ? setBuilder : undefined,
[]
);
const out = arb.canShrinkWithoutContext(value.map((v) => v[0]));

Expand Down Expand Up @@ -404,7 +424,15 @@ describe('ArrayArbitrary', () => {
setBuilder.mockReturnValue(customSet);

// Act
const arb = new ArrayArbitrary(instance, minLength, maxGeneratedLength, maxLength, undefined, setBuilder);
const arb = new ArrayArbitrary(
instance,
minLength,
maxGeneratedLength,
maxLength,
undefined,
setBuilder,
[]
);
const out = arb.canShrinkWithoutContext(value.map((v) => v[0]));

// Assert
Expand Down Expand Up @@ -450,7 +478,8 @@ describe('ArrayArbitrary', () => {
maxGeneratedLength,
maxLength,
undefined,
withSetBuilder ? setBuilder : undefined
withSetBuilder ? setBuilder : undefined,
[]
);
const out = arb.canShrinkWithoutContext(value);

Expand Down Expand Up @@ -494,7 +523,8 @@ describe('ArrayArbitrary', () => {
maxGeneratedLength,
maxLength,
undefined,
withSetBuilder ? setBuilder : undefined
withSetBuilder ? setBuilder : undefined,
[]
);
const out = arb.canShrinkWithoutContext(value);

Expand All @@ -512,7 +542,7 @@ describe('ArrayArbitrary (integration)', () => {
// Arrange
const alreadySeenCloneable = new Set<unknown>();
const mrng = new Random(prand.mersenne(0));
const arb = new ArrayArbitrary(new CloneableArbitrary(), 0, 5, 100, undefined); // 0 to 5 generated items
const arb = new ArrayArbitrary(new CloneableArbitrary(), 0, 5, 100, undefined, undefined, []); // 0 to 5 generated items

// Act
let g = arb.generate(mrng, undefined);
Expand Down
36 changes: 31 additions & 5 deletions packages/fast-check/test/unit/arbitrary/array.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,15 @@ describe('array', () => {
const arb = withConfiguredGlobal(config, () => array(childInstance));

// Assert
expect(ArrayArbitrary).toHaveBeenCalledWith(childInstance, 0, expect.any(Number), 0x7fffffff, undefined);
expect(ArrayArbitrary).toHaveBeenCalledWith(
childInstance,
0,
expect.any(Number),
0x7fffffff,
undefined,
undefined,
[]
);
const receivedGeneratedMaxLength = ArrayArbitrary.mock.calls[0][2]; // Expecting the real value would check an implementation detail
expect(receivedGeneratedMaxLength).toBeGreaterThan(0);
expect(receivedGeneratedMaxLength).toBeLessThanOrEqual(2 ** 31 - 1);
Expand All @@ -61,13 +69,21 @@ describe('array', () => {
const arb = withConfiguredGlobal(config, () => array(childInstance, { maxLength }));

// Assert
expect(ArrayArbitrary).toHaveBeenCalledWith(childInstance, 0, expect.any(Number), maxLength, undefined);
expect(ArrayArbitrary).toHaveBeenCalledWith(
childInstance,
0,
expect.any(Number),
maxLength,
undefined,
undefined,
[]
);
const receivedGeneratedMaxLength = ArrayArbitrary.mock.calls[0][2]; // Expecting the real value would check an implementation detail
expect(receivedGeneratedMaxLength).toBeGreaterThanOrEqual(0);
expect(receivedGeneratedMaxLength).toBeLessThanOrEqual(maxLength);
expect(Number.isInteger(receivedGeneratedMaxLength)).toBe(true);
if (config.defaultSizeToMaxWhenMaxSpecified) {
expect(ArrayArbitrary).toHaveBeenCalledWith(childInstance, 0, maxLength, maxLength, undefined);
expect(ArrayArbitrary).toHaveBeenCalledWith(childInstance, 0, maxLength, maxLength, undefined, undefined, []);
}
expect(arb).toBe(instance);
})
Expand Down Expand Up @@ -137,7 +153,15 @@ describe('array', () => {
expect(receivedGeneratedMaxLength).toBeLessThanOrEqual(maxLength);
expect(Number.isInteger(receivedGeneratedMaxLength)).toBe(true);
if (config.defaultSizeToMaxWhenMaxSpecified) {
expect(ArrayArbitrary).toHaveBeenCalledWith(childInstance, minLength, maxLength, maxLength, undefined);
expect(ArrayArbitrary).toHaveBeenCalledWith(
childInstance,
minLength,
maxLength,
maxLength,
undefined,
undefined,
[]
);
}
expect(arb).toBe(instance);
}
Expand Down Expand Up @@ -183,7 +207,9 @@ describe('array', () => {
minLength,
maxLength,
maxLength,
depthIdentifier
depthIdentifier,
undefined,
[]
);
}
expect(arb).toBe(instance);
Expand Down
15 changes: 10 additions & 5 deletions packages/fast-check/test/unit/arbitrary/uniqueArray.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ describe('uniqueArray', () => {
expect.any(Number),
0x7fffffff,
undefined,
expect.any(Function)
expect.any(Function),
[]
);
const receivedGeneratedMaxLength = ArrayArbitrary.mock.calls[0][2]; // Expecting the real value would check an implementation detail
expect(receivedGeneratedMaxLength).toBeGreaterThan(0);
Expand Down Expand Up @@ -72,7 +73,8 @@ describe('uniqueArray', () => {
expect.any(Number),
maxLength,
undefined,
expect.any(Function)
expect.any(Function),
[]
);
const receivedGeneratedMaxLength = ArrayArbitrary.mock.calls[0][2]; // Expecting the real value would check an implementation detail
expect(receivedGeneratedMaxLength).toBeGreaterThanOrEqual(0);
Expand Down Expand Up @@ -113,7 +115,8 @@ describe('uniqueArray', () => {
expect.any(Number),
0x7fffffff,
undefined,
expect.any(Function)
expect.any(Function),
[]
);
const receivedGeneratedMaxLength = ArrayArbitrary.mock.calls[0][2]; // Expecting the real value would check an implementation detail
if (minLength !== 2 ** 31 - 1) {
Expand Down Expand Up @@ -153,7 +156,8 @@ describe('uniqueArray', () => {
expect.any(Number),
maxLength,
undefined,
expect.any(Function)
expect.any(Function),
[]
);
const receivedGeneratedMaxLength = ArrayArbitrary.mock.calls[0][2]; // Expecting the real value would check an implementation detail
expect(receivedGeneratedMaxLength).toBeGreaterThanOrEqual(minLength);
Expand Down Expand Up @@ -219,7 +223,8 @@ describe('uniqueArray', () => {
expect.any(Number),
constraints.maxLength !== undefined ? constraints.maxLength : expect.any(Number),
constraints.depthIdentifier,
expect.any(Function)
expect.any(Function),
[]
);
expect(arb).toBe(instance);
}
Expand Down

0 comments on commit 7220c57

Please sign in to comment.