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 new flags for object: withBoxedValues, withSet, withMap #321

Merged
merged 4 commits into from
Feb 26, 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
3 changes: 3 additions & 0 deletions documentation/Arbitraries.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ export module ObjectConstraints {
maxDepth?: number; // maximal depth allowed for this object
key?: Arbitrary<string>; // arbitrary for key
values?: Arbitrary<any>[]; // arbitrary responsible for base value
withBoxedValues?: boolean; // adapt all entries within `values` to generate boxed version of the value too
withMap?: boolean; // also generate Map
withSet?: boolean; // also generate Set
};
};
```
Expand Down
71 changes: 64 additions & 7 deletions src/check/arbitrary/ObjectArbitrary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,18 @@ import { double } from './FloatingPointArbitrary';
import { integer } from './IntegerArbitrary';
import { oneof } from './OneOfArbitrary';
import { string, unicodeString } from './StringArbitrary';
import { tuple } from './TupleArbitrary';

export class ObjectConstraints {
constructor(readonly key: Arbitrary<string>, readonly values: Arbitrary<any>[], readonly maxDepth: number) {}
constructor(
readonly key: Arbitrary<string>,
readonly values: Arbitrary<any>[],
readonly maxDepth: number,
readonly withSet: boolean,
readonly withMap: boolean
) {}
next(): ObjectConstraints {
return new ObjectConstraints(this.key, this.values, this.maxDepth - 1);
return new ObjectConstraints(this.key, this.values, this.maxDepth - 1, this.withSet, this.withMap);
}

/**
Expand Down Expand Up @@ -41,14 +48,45 @@ export class ObjectConstraints {
];
}

/** @hidden */
private static boxArbitraries(arbs: Arbitrary<any>[]): Arbitrary<any>[] {
return arbs.map(arb =>
arb.map(v => {
switch (typeof v) {
case 'boolean':
// tslint:disable-next-line:no-construct
return new Boolean(v);
case 'number':
// tslint:disable-next-line:no-construct
return new Number(v);
case 'string':
// tslint:disable-next-line:no-construct
return new String(v);
default:
return v;
}
})
);
}

/** @hidden */
private static boxArbitrariesIfNeeded(arbs: Arbitrary<any>[], boxEnabled: boolean): Arbitrary<any>[] {
return boxEnabled ? this.boxArbitraries(arbs).concat(arbs) : arbs;
}

static from(settings?: ObjectConstraints.Settings): ObjectConstraints {
function getOr<T>(access: () => T | undefined, value: T): T {
return settings != null && access() != null ? access()! : value;
}
return new ObjectConstraints(
getOr(() => settings!.key, string()),
getOr(() => settings!.values, ObjectConstraints.defaultValues()),
getOr(() => settings!.maxDepth, 2)
this.boxArbitrariesIfNeeded(
getOr(() => settings!.values, ObjectConstraints.defaultValues()),
getOr(() => settings!.withBoxedValues, false)
),
getOr(() => settings!.maxDepth, 2),
getOr(() => settings!.withSet, false),
getOr(() => settings!.withMap, false)
);
}
}
Expand Down Expand Up @@ -87,19 +125,38 @@ export namespace ObjectConstraints {
* - `Number.NEGATIVE_INFINITY`
*/
values?: Arbitrary<any>[];
/** Also generate boxed versions of values */
withBoxedValues?: boolean;
/** Also generate Set */
withSet?: boolean;
/** Also generate Map */
withMap?: boolean;
}
}

/** @hidden */
const anythingInternal = (subConstraints: ObjectConstraints): Arbitrary<any> => {
const potentialArbValue = [...subConstraints.values]; // base
if (subConstraints.maxDepth > 0) {
potentialArbValue.push(objectInternal(subConstraints.next())); // sub-object
const subAnythingArb = anythingInternal(subConstraints.next());
potentialArbValue.push(dictionary(subConstraints.key, subAnythingArb)); // sub-object
potentialArbValue.push(...subConstraints.values.map(arb => array(arb))); // arrays of base
potentialArbValue.push(array(anythingInternal(subConstraints.next()))); // mixed content arrays
potentialArbValue.push(array(subAnythingArb)); // mixed content arrays
if (subConstraints.withMap) {
potentialArbValue.push(array(tuple(subConstraints.key, subAnythingArb)).map(v => new Map(v))); // map string -> obj
potentialArbValue.push(array(tuple(subAnythingArb, subAnythingArb)).map(v => new Map(v))); // map obj -> obj
}
if (subConstraints.withSet) {
potentialArbValue.push(...subConstraints.values.map(arb => array(arb).map(v => new Set(v)))); // arrays of base
potentialArbValue.push(array(subAnythingArb).map(v => new Set(v))); // mixed content arrays
}
}
if (subConstraints.maxDepth > 1) {
potentialArbValue.push(array(objectInternal(subConstraints.next().next()))); // array of Object
const subSubObjectArb = objectInternal(subConstraints.next().next());
potentialArbValue.push(array(subSubObjectArb)); // array of Object
if (subConstraints.withSet) {
potentialArbValue.push(array(subSubObjectArb).map(v => new Set(v))); // set of Object
}
}
return oneof(...potentialArbValue);
};
Expand Down
27 changes: 27 additions & 0 deletions test/e2e/GenerateAllValues.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,31 @@ describe(`Generate all values (seed: ${seed})`, () => {
fc.property(fc.set(fc.string(), 1, 40), csts => lookForMissing(fc.constantFrom(...csts), csts.length))
));
});
describe('fc.anything()', () => {
const checkCanProduce = (label: string, typeofLabel: string, toStringLabel: string) => {
it(`should be able to generate ${label}`, () => {
let numTries = 0;
const mrng = new fc.Random(prand.xorshift128plus(seed));
const arb = fc.anything({ withBoxedValues: true, withMap: true, withSet: true });
while (++numTries <= 10000) {
const { value } = arb.generate(mrng);
if (typeof value === typeofLabel && Object.prototype.toString.call(value) === toStringLabel) {
return;
}
}
fail(`Was not able to generate ${label}`);
});
};
checkCanProduce('null', 'object', '[object Null]');
checkCanProduce('undefined', 'undefined', '[object Undefined]');
checkCanProduce('boolean', 'boolean', '[object Boolean]');
checkCanProduce('number', 'number', '[object Number]');
checkCanProduce('string', 'string', '[object String]');
checkCanProduce('boxed Boolean', 'object', '[object Boolean]');
checkCanProduce('boxed Number', 'object', '[object Number]');
checkCanProduce('boxed String', 'object', '[object String]');
checkCanProduce('Array', 'object', '[object Array]');
checkCanProduce('Set', 'object', '[object Set]');
checkCanProduce('Map', 'object', '[object Map]');
});
});
92 changes: 91 additions & 1 deletion test/unit/check/arbitrary/ObjectArbitrary.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import {
jsonObject,
unicodeJsonObject,
json,
unicodeJson
unicodeJson,
ObjectConstraints
} from '../../../../src/check/arbitrary/ObjectArbitrary';

import * as stubRng from '../../stubs/generators';
Expand Down Expand Up @@ -105,6 +106,95 @@ describe('ObjectArbitrary', () => {
assertShrinkedValue(originalValue, shrinkable.value);
})
));
const checkProduce = (settings: ObjectConstraints.Settings, f: (v: any) => boolean) => {
let numRuns = 0;
const seed = 0;
const mrng = stubRng.mutable.fastincrease(seed);
const arb = anything(settings);
while (++numRuns <= 1000) {
if (f(arb.generate(mrng).value)) return;
}
fail('Failed to generate the expected value');
};
const checkProduceBoxed = <T>(className: string, basicValue: T) => {
return checkProduce(
{ values: [constant(basicValue)], maxDepth: 0, withBoxedValues: true },
v => typeof v === 'object' && Object.prototype.toString.call(v) === `[object ${className}]`
);
};
const checkProduceUnboxed = <T>(basicValue: T) => {
return checkProduce(
{ values: [constant(basicValue)], maxDepth: 0, withBoxedValues: true },
v => v === basicValue
);
};
it('Should be able to produce boxed Boolean', () => checkProduceBoxed('Boolean', true));
it('Should be able to produce boxed Number', () => checkProduceBoxed('Number', 1));
it('Should be able to produce boxed String', () => checkProduceBoxed('String', ''));
it('Should be able to produce unboxed Boolean', () => checkProduceUnboxed(true));
it('Should be able to produce unboxed Number', () => checkProduceUnboxed(1));
it('Should be able to produce unboxed String', () => checkProduceUnboxed(''));
it('Should be able to produce Set', () =>
checkProduce({ values: [constant(0)], maxDepth: 1, withSet: true }, v => v instanceof Set));
it('Should be able to produce Map', () =>
checkProduce({ values: [constant(0)], maxDepth: 1, withMap: true }, v => v instanceof Map));
it('Should not be able to produce Array if maxDepth is zero', () =>
fc.assert(
fc.property(fc.integer(), seed => {
const settings = { maxDepth: 0 };
const mrng = stubRng.mutable.fastincrease(seed);
return !(anything(settings).generate(mrng).value instanceof Array);
})
));
it('Should not be able to produce Set if maxDepth is zero', () =>
fc.assert(
fc.property(fc.integer(), seed => {
const settings = { maxDepth: 0, withSet: true };
const mrng = stubRng.mutable.fastincrease(seed);
return !(anything(settings).generate(mrng).value instanceof Set);
})
));
it('Should not be able to produce Map if maxDepth is zero', () =>
fc.assert(
fc.property(fc.integer(), seed => {
const settings = { maxDepth: 0, withMap: true };
const mrng = stubRng.mutable.fastincrease(seed);
return !(anything(settings).generate(mrng).value instanceof Map);
})
));
it('Should take maxDepth into account whatever the other settings', () =>
fc.assert(
fc.property(
fc.integer(),
fc.nat(10),
fc.record(
{
key: fc.constant(constant('single-key')),
values: fc.constant([constant('single-value')]),
withBoxedValues: fc.boolean(),
withMap: fc.boolean(),
withSet: fc.boolean()
},
{ withDeletedKeys: true }
),
(seed, maxDepth, settings) => {
const mrng = stubRng.mutable.fastincrease(seed);
const v = anything({ ...settings, maxDepth }).generate(mrng).value;
const depthEvaluator = (node: any): number => {
let subNodes: any[] = [];
if (Array.isArray(node)) subNodes.concat(node);
else if (node instanceof Set) subNodes.concat(Array.from(node));
else if (node instanceof Map)
subNodes.concat(Array.from(node).map(t => t[0]), Array.from(node).map(t => t[1]));
else if (Object.prototype.toString.call(node) === '[object Object]') {
for (const k of Object.keys(node)) subNodes.push(node[k]);
} else return 0;
return subNodes.reduce((max, subNode) => Math.max(max, depthEvaluator(subNode)), 0) + 1;
};
return depthEvaluator(v) <= maxDepth;
}
)
));
});
describe('json', () => {
it('Should produce strings', () =>
Expand Down