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 mixedCase arbitrary #427

Merged
merged 16 commits into from
Sep 8, 2019
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