Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add mapToConstant arbitrary #340

Merged
merged 2 commits into from
Apr 10, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions documentation/Arbitraries.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ More specific strings:
- `fc.constant<T>(value: T): Arbitrary<T>` constant arbitrary only able to produce `value: T`
- `fc.constantFrom<T>(...values: T[]): Arbitrary<T>` randomly chooses among the values provided. It considers the first value as the default value so that in case of failure it will shrink to it. It expects a minimum of one value and throws whether it receives no value as parameters. It can easily be used on arrays with `fc.constantFrom(...myArray)` (or `fc.constantFrom.apply(null, myArray)` for older versions of TypeScript/JavaScript)
- `fc.clonedConstant<T>(value: T): Arbitrary<T>` constant arbitrary only able to produce `value: T`. If it exists, it called its `[fc.cloneMethod]` at each call to generate
- `fc.mapToConstant<T>(...entries: { num: number; build: (idInGroup: number) => T }[]): Arbitrary<T>` generates non-contiguous ranges of values by mapping integer values to constant
- `fc.oneof<T>(...arbs: Arbitrary<T>[]): Arbitrary<T>` randomly chooses an arbitrary at each new generation. Should be provided with at least one arbitrary. All arbitraries are equally probable and shrink is still working for the selected arbitrary. `fc.oneof` is able to shrink inside the failing arbitrary but not accross arbitraries (contrary to `fc.constantFrom` when dealing with constant arbitraries)
- `fc.option<T>(arb: Arbitrary<T>): Arbitrary<T | null>` or `fc.option<T>(arb: Arbitrary<T>, freq: number): Arbitrary<T | null>` arbitrary able to nullify its generated value. When provided a custom `freq` value it changes the frequency of `null` values so that they occur one time over `freq` tries (eg.: `freq=5` means that 20% of generated values will be `null` and 80% would be produced through `arb`). By default: `freq=5`
- `fc.subarray<T>(originalArray: T[]): Arbitrary<T[]>`, or `fc.subarray<T>(originalArray: T[], minLength: number, maxLength: number): Arbitrary<T[]>` subarray of `originalArray`. Values inside the subarray are ordered the same way they are in `originalArray`. By setting the parameters `minLength` and/or `maxLength`, the user can change the minimal (resp. maximal) size allowed for the generated subarray. By default: `minLength=0` and `maxLength=originalArray.length`
Expand Down
42 changes: 42 additions & 0 deletions src/check/arbitrary/MapToConstantArbitrary.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Arbitrary } from './definition/Arbitrary';
import { nat } from './IntegerArbitrary';

/** @hidden */
function computeNumChoices<T>(options: { num: number; build: (idInGroup: number) => T }[]): number {
if (options.length === 0) throw new Error(`fc.mapToConstant expects at least one option`);
let numChoices = 0;
for (let idx = 0; idx !== options.length; ++idx) {
if (options[idx].num < 0)
throw new Error(`fc.mapToConstant expects all options to have a number of entries greater or equal to zero`);
numChoices += options[idx].num;
}
if (numChoices === 0) throw new Error(`fc.mapToConstant expects at least one choice among options`);
return numChoices;
}

/**
* Generate non-contiguous ranges of values
* by mapping integer values to constant
*
* @param options Builders to be called to generate the values
*
* @example
* ```
* // generate alphanumeric values (a-z0-9)
* mapToConstant(
* { num: 26, build: v => String.fromCharCode(v + 0x61) },
* { num: 10, build: v => String.fromCharCode(v + 0x30) },
* )
* ```
*/
export function mapToConstant<T>(...entries: { num: number; build: (idInGroup: number) => T }[]): Arbitrary<T> {
const numChoices = computeNumChoices(entries);
return nat(numChoices - 1).map(choice => {
let idx = -1;
let numSkips = 0;
while (choice >= numSkips) {
numSkips += entries[++idx].num;
}
return entries[idx].build(choice - numSkips + entries[idx].num);
});
}
2 changes: 2 additions & 0 deletions src/fast-check-default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { compareBooleanFunc, compareFunc, func } from './check/arbitrary/Functio
import { integer, maxSafeInteger, maxSafeNat, nat } from './check/arbitrary/IntegerArbitrary';
import { ipV4, ipV6 } from './check/arbitrary/IpArbitrary';
import { lorem } from './check/arbitrary/LoremArbitrary';
import { mapToConstant } from './check/arbitrary/MapToConstantArbitrary';
import {
anything,
json,
Expand Down Expand Up @@ -115,6 +116,7 @@ export {
constant,
constantFrom,
clonedConstant,
mapToConstant,
option,
oneof,
frequency,
Expand Down
16 changes: 16 additions & 0 deletions test/legacy/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,19 @@ testArbitrary(fc.string());
testArbitrary(fc.fullUnicodeString());
testArbitrary(fc.lorem());
testArbitrary(fc.frequency({ weight: 1, arbitrary: fc.nat() }, { weight: 2, arbitrary: fc.double() }));
testArbitrary(
fc.mapToConstant(
{
num: 26,
build: function(v) {
return String.fromCharCode(v + 0x61);
}
},
{
num: 10,
build: function(v) {
return String.fromCharCode(v + 0x30);
}
}
)
);
57 changes: 57 additions & 0 deletions test/unit/check/arbitrary/MapToConstantArbitrary.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import * as prand from 'pure-rand';
import * as fc from '../../../../lib/fast-check';

import { mapToConstant } from '../../../../src/check/arbitrary/MapToConstantArbitrary';
import { Random } from '../../../../src/random/generator/Random';
import { minMax } from './generic/GenericArbitraryHelper';
import { nat } from '../../../../src/check/arbitrary/IntegerArbitrary';

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);
})
));
});
});