Skip to content

Commit

Permalink
Add mixedCase arbitrary (#427)
Browse files Browse the repository at this point in the history
* Add mixedCase arbitrary

Fixes #426

* Extract countToggledBits outside of the class

* Remove unused code

* Extract computeNextFlags

* Draft units

* Fix MixedCaseArbitrary tests

* Add basic itest for mixedCase

* Add more units

* Bug for flags=2n, nextSize=1

* Add units related to shrinker

* Update snapshots

* Better jsdoc

* Do not shrink on string if we shrunk on bigint before

* Update markdown documentation

* Add warning concerning the need for bigint support

* Lint code
  • Loading branch information
dubzzz authored Sep 8, 2019
1 parent 29bad49 commit 147e0b3
Show file tree
Hide file tree
Showing 7 changed files with 428 additions and 0 deletions.
1 change: 1 addition & 0 deletions documentation/1-Guides/Arbitraries.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ More specific strings:
- `fc.webSegment()` Web URL path segment
- `fc.webUrl()` Web URL following the specs specified by RFC 3986 and WHATWG URL Standard
- `fc.emailAddress()` Email address following RFC 1123 and RFC 5322
- `fc.mixedCase(stringArb: Arbitrary<string>)` or `fc.mixedCase(stringArb: Arbitrary<string>, constraints: MixedCaseConstraints)` Randomly switch the case of characters generated by `stringArb`

## Date (:Date)

Expand Down
123 changes: 123 additions & 0 deletions src/check/arbitrary/MixedCaseArbitrary.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { Random } from '../../random/generator/Random';
import { Stream } from '../../stream/Stream';
import { bigUintN } from './BigIntArbitrary';
import { Arbitrary } from './definition/Arbitrary';
import { Shrinkable } from './definition/Shrinkable';

export interface MixedCaseConstraints {
/** Transform a character to its upper and/or lower case version */
toggleCase?: (rawChar: string) => string;
}

/** @hidden */
export function countToggledBits(n: bigint): number {
let count = 0;
while (n > BigInt(0)) {
if (n & BigInt(1)) ++count;
n >>= BigInt(1);
}
return count;
}

/** @hidden */
export function computeNextFlags(flags: bigint, nextSize: number): bigint {
// whenever possible we want to preserve the same number of toggled positions
// whenever possible we want to keep them at the same place
// flags: 1000101 -> 10011 or 11001 (second choice for the moment)
const allowedMask = (BigInt(1) << BigInt(nextSize)) - BigInt(1);
const preservedFlags = flags & allowedMask;
let numMissingFlags = countToggledBits(flags - preservedFlags);
let nFlags = preservedFlags;
for (let mask = BigInt(1); mask <= allowedMask && numMissingFlags !== 0; mask <<= BigInt(1)) {
if (!(nFlags & mask)) {
nFlags |= mask;
--numMissingFlags;
}
}
return nFlags;
}

/** @hidden */
class MixedCaseArbitrary extends Arbitrary<string> {
constructor(private readonly stringArb: Arbitrary<string>, private readonly toggleCase: (rawChar: string) => string) {
super();
}
private computeTogglePositions(chars: string[]): number[] {
const positions: number[] = [];
for (let idx = 0; idx !== chars.length; ++idx) {
if (this.toggleCase(chars[idx]) !== chars[idx]) positions.push(idx);
}
return positions;
}
private wrapper(
rawCase: Shrinkable<string>,
chars: string[],
togglePositions: number[],
flags: bigint
): Shrinkable<string> {
const newChars = chars.slice();
for (let idx = 0, mask = BigInt(1); idx !== togglePositions.length; ++idx, mask <<= BigInt(1)) {
if (flags & mask) newChars[togglePositions[idx]] = this.toggleCase(newChars[togglePositions[idx]]);
}
return new Shrinkable(newChars.join(''), () => this.shrinkImpl(rawCase, chars, togglePositions, flags));
}
private shrinkImpl(
rawCase: Shrinkable<string>,
chars: string[],
togglePositions: number[],
flags: bigint
): Stream<Shrinkable<string>> {
return rawCase
.shrink()
.map(s => {
const nChars = [...s.value_];
const nTogglePositions = this.computeTogglePositions(nChars);
const nFlags = computeNextFlags(flags, nTogglePositions.length);
return this.wrapper(s, nChars, nTogglePositions, nFlags);
})
.join(
bigUintN(togglePositions.length)
.shrinkableFor(flags)
.shrink()
.map(nFlags => {
return this.wrapper(new Shrinkable(rawCase.value), chars, togglePositions, nFlags.value_);
})
);
}
generate(mrng: Random): Shrinkable<string> {
const rawCaseShrinkable = this.stringArb.generate(mrng);

const chars = [...rawCaseShrinkable.value_]; // split into valid unicode (keeps surrogate pairs)
const togglePositions = this.computeTogglePositions(chars);

const flagsArb = bigUintN(togglePositions.length);
const flags = flagsArb.generate(mrng).value_; // true => toggle the char, false => keep it as-is

return this.wrapper(rawCaseShrinkable, chars, togglePositions, flags);
}
}

/** @hidden */
function defaultToggleCase(rawChar: string) {
const upper = rawChar.toUpperCase();
if (upper !== rawChar) return upper;
return rawChar.toLowerCase();
}

/**
* Randomly switch the case of characters generated by `stringArb` (upper/lower)
*
* WARNING:
* Require bigint support.
* Under-the-hood the arbitrary relies on bigint to compute the flags that should be toggled or not.
*
* @param stringArb Arbitrary able to build string values
* @param constraints Constraints to be applied when computing upper/lower case version
*/
export function mixedCase(stringArb: Arbitrary<string>, constraints?: MixedCaseConstraints): Arbitrary<string> {
if (typeof BigInt === 'undefined') {
throw new Error(`mixedCase requires BigInt support`);
}
const toggleCase = (constraints && constraints.toggleCase) || defaultToggleCase;
return new MixedCaseArbitrary(stringArb, toggleCase);
}
3 changes: 3 additions & 0 deletions src/fast-check-default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { letrec } from './check/arbitrary/LetRecArbitrary';
import { lorem } from './check/arbitrary/LoremArbitrary';
import { mapToConstant } from './check/arbitrary/MapToConstantArbitrary';
import { memo, Memo } from './check/arbitrary/MemoArbitrary';
import { mixedCase, MixedCaseConstraints } from './check/arbitrary/MixedCaseArbitrary';
import {
anything,
json,
Expand Down Expand Up @@ -117,6 +118,7 @@ export {
fullUnicode,
hexa,
base64,
mixedCase,
string,
asciiString,
string16bits,
Expand Down Expand Up @@ -184,6 +186,7 @@ export {
ExecutionStatus,
ExecutionTree,
Memo,
MixedCaseConstraints,
ObjectConstraints,
Parameters,
RecordConstraints,
Expand Down
5 changes: 5 additions & 0 deletions test/e2e/NoRegressionBigInt.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,9 @@ describe(`NoRegression BigInt`, () => {
it('bigUint', () => {
expect(() => fc.assert(fc.property(fc.bigUint(), v => testFunc(v)), settings)).toThrowErrorMatchingSnapshot();
});
it('mixedCase', () => {
expect(() =>
fc.assert(fc.property(fc.mixedCase(fc.hexaString()), v => testFunc(v)), settings)
).toThrowErrorMatchingSnapshot();
});
});
42 changes: 42 additions & 0 deletions test/e2e/__snapshots__/NoRegressionBigInt.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -725,3 +725,45 @@ Execution summary:
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . √ [987n]
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . √ [989n]"
`;

exports[`NoRegression BigInt mixedCase 1`] = `
"Property failed after 2 tests
{ seed: 42, path: \\"1:2:1:13:10\\", endOnFailure: true }
Counterexample: [\\"aa\\"]
Shrunk 4 time(s)
Got error: Property failed by returning false
Execution summary:
√ [\\"a3A0\\"]
× [\\"53dAA379\\"]
. √ [\\"\\"]
. √ [\\"A379\\"]
. × [\\"dAA379\\"]
. . √ [\\"379\\"]
. . × [\\"AA379\\"]
. . . √ [\\"379\\"]
. . . √ [\\"A379\\"]
. . . √ [\\"0A379\\"]
. . . √ [\\"5A379\\"]
. . . √ [\\"8A379\\"]
. . . √ [\\"9A379\\"]
. . . √ [\\"A\\"]
. . . √ [\\"A79\\"]
. . . √ [\\"A379\\"]
. . . √ [\\"A0379\\"]
. . . √ [\\"A5379\\"]
. . . √ [\\"A8379\\"]
. . . √ [\\"A9379\\"]
. . . × [\\"AA\\"]
. . . . √ [\\"A\\"]
. . . . √ [\\"0A\\"]
. . . . √ [\\"5A\\"]
. . . . √ [\\"8A\\"]
. . . . √ [\\"9A\\"]
. . . . √ [\\"A\\"]
. . . . √ [\\"A0\\"]
. . . . √ [\\"A5\\"]
. . . . √ [\\"A8\\"]
. . . . √ [\\"A9\\"]
. . . . × [\\"aa\\"]"
`;
25 changes: 25 additions & 0 deletions test/unit/check/arbitrary/MixedCaseArbitrary.itest.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { nat } from '../../../../src/check/arbitrary/IntegerArbitrary';
import { stringOf } from '../../../../src/check/arbitrary/StringArbitrary';
import { mixedCase } from '../../../../src/check/arbitrary/MixedCaseArbitrary';

import * as genericHelper from './generic/GenericArbitraryHelper';

declare function BigInt(n: number | bigint | string): bigint;

describe('MixedCaseArbitrary', () => {
if (typeof BigInt === 'undefined') {
it('no test', () => {
expect(true).toBe(true);
});
return;
}
describe('mixedCase', () => {
const stringArb = stringOf(nat(3).map(id => ['0', '1', 'A', 'B'][id]));
genericHelper.isValidArbitrary(() => mixedCase(stringArb), {
isStrictlySmallerValue: (v1, v2) => {
return v1.length < v2.length || v1 < v2 /* '0' < 'A' < 'a' */;
},
isValidValue: (g: string) => typeof g === 'string' && [...g].every(c => '01abAB'.includes(c))
});
});
});
Loading

0 comments on commit 147e0b3

Please sign in to comment.