Skip to content
This repository has been archived by the owner on Jan 2, 2024. It is now read-only.

Commit

Permalink
feat: support io-ts-types (#18)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: Fuzzers now have an input and decoded type.  Fuzzer generate as output the *input* of the t.Type, not the decoded or output types.
  • Loading branch information
holvonixAdvay authored Aug 9, 2019
1 parent 9be1c28 commit 5489f03
Show file tree
Hide file tree
Showing 9 changed files with 299 additions and 120 deletions.
28 changes: 22 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,20 +65,36 @@ Currently supports (along with their closure under composition):
- `t.UnknownArray`
- `t.void`

If you `yarn add monocle-ts io-ts-types` and register the `io-ts-types` extra
fuzzers via `r.register(...await loadIoTsTypesFuzzers())`, the following
additional types will be supported:
If you `yarn add monocle-ts io-ts-types` and register the
[`io-ts-types`](https://github.com/gcanti/io-ts) extra fuzzers via
`r.register(...await loadIoTsTypesFuzzers())`, the following additional types
will be supported:

- `date`
- `BooleanFromString`
- `DateFromISOString`
- `DateFromNumber`
- `DateFromUnixTime`
- `IntFromString`
- `NonEmptyString`
- `NumberFromString`
- `UUID`
- `regexp`

The following `io-ts-types` are composed of core types and you do not need to
`loadIoTsTypesFuzzers` to fuzz them:

- `either`
- `option`

## How To

### Generate Conforming Examples and Verifying Decoder Behavior

Given a `d = t.Decoder<I,A>` (aka a `t.Type`), `fuzz.exampleGenerator` will
build a `t.Encoder<[number,FuzzContext],A>` that will give example instances of
`A`. The example instances should all pass on `d.decode`, which should return an
identical example. No exceptions should be thrown.
build a `t.Encoder<[number,FuzzContext],I>` that will give example instances of
`I`. The example instances should all pass on `d.decode` (which should return an
identical example in the case of basic types). No exceptions should be thrown.

### Configure Core Fuzzers

Expand Down
62 changes: 35 additions & 27 deletions src/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,20 +38,22 @@ type BasicType =

type basicFuzzGenerator<
T,
C extends t.Decoder<unknown, T> & BasicType
> = fuzzGenerator<T, C>;
U,
C extends t.Decoder<U, T> & BasicType
> = fuzzGenerator<T, U, C>;

type basicLiteralConcreteFuzzer<
T,
C extends t.Decoder<unknown, T> & BasicType
> = ConcreteFuzzer<T>['func'];
U,
C extends t.Decoder<U, T> & BasicType
> = ConcreteFuzzer<T, U>['func'];

type BasicFuzzer<T, C extends t.Decoder<unknown, T> & BasicType> = Fuzzer<T, C>;
type BasicFuzzer<T, U, C extends t.Decoder<U, T> & BasicType> = Fuzzer<T, U, C>;

function concrete<T, C extends t.Decoder<unknown, T> & BasicType>(
func: basicLiteralConcreteFuzzer<T, C>,
function concrete<T, U, C extends t.Decoder<U, T> & BasicType>(
func: basicLiteralConcreteFuzzer<T, U, C>,
tag: C['_tag']
): BasicFuzzer<T, C> {
): BasicFuzzer<T, U, C> {
return {
impl: {
type: 'fuzzer',
Expand All @@ -63,10 +65,10 @@ function concrete<T, C extends t.Decoder<unknown, T> & BasicType>(
};
}

function gen<T, C extends t.Decoder<unknown, T> & BasicType>(
func: basicFuzzGenerator<T, C>,
function gen<T, U, C extends t.Decoder<U, T> & BasicType>(
func: basicFuzzGenerator<T, U, C>,
tag: C['_tag']
): BasicFuzzer<T, C> {
): BasicFuzzer<T, U, C> {
return {
impl: {
type: 'generator',
Expand Down Expand Up @@ -107,7 +109,7 @@ export const defaultUnknownType = t.type({ __default_any_0: t.number });

const fuzzUnknownWithType = (codec: t.Decoder<unknown, unknown>) => (
b: t.UnknownType
): ConcreteFuzzer<unknown> => {
): ConcreteFuzzer<unknown, unknown> => {
return {
// unknown recursion handled specially below
mightRecurse: false,
Expand All @@ -125,7 +127,7 @@ export function unknownFuzzer(

export function fuzzLiteral(
b: t.LiteralType<string | number | boolean>
): ConcreteFuzzer<unknown> {
): ConcreteFuzzer<string | number | boolean, unknown> {
return {
mightRecurse: false,
func: () => {
Expand All @@ -134,7 +136,9 @@ export function fuzzLiteral(
};
}

export function fuzzUnion(b: t.UnionType<t.Mixed[]>): ConcreteFuzzer<unknown> {
export function fuzzUnion(
b: t.UnionType<t.Mixed[]>
): ConcreteFuzzer<unknown, unknown> {
return {
mightRecurse: false,
children: b.types,
Expand All @@ -149,7 +153,7 @@ export function fuzzUnion(b: t.UnionType<t.Mixed[]>): ConcreteFuzzer<unknown> {

export function fuzzKeyof(
b: t.KeyofType<{ [key: string]: unknown }>
): ConcreteFuzzer<unknown> {
): ConcreteFuzzer<unknown, unknown> {
return {
mightRecurse: false,
func: (_, n) => {
Expand All @@ -161,7 +165,7 @@ export function fuzzKeyof(

export function fuzzTuple(
b: t.TupleType<t.Mixed[]>
): ConcreteFuzzer<unknown[]> {
): ConcreteFuzzer<unknown[], unknown> {
return {
mightRecurse: false,
children: b.types,
Expand All @@ -172,7 +176,9 @@ export function fuzzTuple(
};
}

export function fuzzExact(b: t.ExactC<t.HasProps>): ConcreteFuzzer<unknown> {
export function fuzzExact(
b: t.ExactC<t.HasProps>
): ConcreteFuzzer<unknown, unknown> {
return {
mightRecurse: false,
children: [b.type],
Expand All @@ -190,7 +196,7 @@ export function fuzzExact(b: t.ExactC<t.HasProps>): ConcreteFuzzer<unknown> {

export function fuzzReadonly(
b: t.ReadonlyType<t.Any>
): ConcreteFuzzer<unknown> {
): ConcreteFuzzer<unknown, unknown> {
return {
mightRecurse: false,
children: [b.type],
Expand All @@ -203,7 +209,7 @@ export function fuzzReadonly(

export function fuzzRecursive(
b: t.RecursiveType<t.Mixed>
): ConcreteFuzzer<unknown> {
): ConcreteFuzzer<unknown, unknown> {
return {
mightRecurse: true,
children: [b.type],
Expand Down Expand Up @@ -232,7 +238,7 @@ export const defaultMaxArrayLength = 5;

const fuzzArrayWithMaxLength = (maxLength: number) => (
b: t.ArrayType<t.Mixed>
): ConcreteFuzzer<unknown[]> => {
): ConcreteFuzzer<unknown[], unknown> => {
return {
mightRecurse: false,
children: [b.type],
Expand All @@ -246,7 +252,7 @@ export function arrayFuzzer(maxLength: number = defaultMaxArrayLength) {

const fuzzReadonlyArrayWithMaxLength = (maxLength: number) => (
b: t.ReadonlyArrayType<t.Mixed>
): ConcreteFuzzer<unknown[]> => {
): ConcreteFuzzer<unknown[], unknown> => {
return {
mightRecurse: false,
children: [b.type],
Expand All @@ -271,7 +277,7 @@ export function readonlyArrayFuzzer(maxLength: number = defaultMaxArrayLength) {

const fuzzAnyArrayWithMaxLength = (maxLength: number) => (
b: t.AnyArrayType
): ConcreteFuzzer<unknown[]> => {
): ConcreteFuzzer<unknown[], unknown> => {
return {
mightRecurse: false,
children: [t.unknown],
Expand All @@ -287,7 +293,7 @@ export const defaultExtraProps = { ___0000_extra_: t.number };

export const fuzzPartialWithExtraCodec = (extra: t.Props) => (
b: t.PartialType<t.Props>
): ConcreteFuzzer<unknown> => {
): ConcreteFuzzer<unknown, unknown> => {
const kk = Object.getOwnPropertyNames(b.props);
const xx = Object.getOwnPropertyNames(extra);
const keys = Object.getOwnPropertyNames(b.props).concat(
Expand Down Expand Up @@ -322,7 +328,7 @@ export function partialFuzzer(extra: t.Props = defaultExtraProps) {

export const fuzzInterfaceWithExtraCodec = (extra: t.Props) => (
b: t.InterfaceType<t.Props>
): ConcreteFuzzer<unknown> => {
): ConcreteFuzzer<unknown, unknown> => {
const kk = Object.getOwnPropertyNames(b.props);
const xx = Object.getOwnPropertyNames(extra);
const keys = Object.getOwnPropertyNames(b.props).concat(
Expand Down Expand Up @@ -361,7 +367,7 @@ export function interfaceFuzzer(extra: t.Props = defaultExtraProps) {

export function fuzzIntersection(
b: t.IntersectionType<t.Any[]>
): ConcreteFuzzer<unknown> {
): ConcreteFuzzer<unknown, unknown> {
return {
mightRecurse: false,
children: b.types,
Expand Down Expand Up @@ -392,7 +398,8 @@ export function fuzzIntersection(
};
}

export const coreFuzzers: ReadonlyArray<Fuzzer> = [
// tslint:disable-next-line:no-any
export const coreFuzzers: ReadonlyArray<Fuzzer<unknown, unknown, any>> = [
concrete(fuzzNumber, 'NumberType'),
concreteFuzzerByName(fuzzInt, 'Int'),
concrete(fuzzBoolean, 'BooleanType'),
Expand All @@ -414,4 +421,5 @@ export const coreFuzzers: ReadonlyArray<Fuzzer> = [
gen(fuzzKeyof, 'KeyofType'),
gen(fuzzTuple, 'TupleType'),
gen(fuzzRecursive, 'RecursiveType'),
] as ReadonlyArray<Fuzzer>;
// tslint:disable-next-line:no-any
] as ReadonlyArray<Fuzzer<unknown, unknown, any>>;
89 changes: 86 additions & 3 deletions src/extra-fuzzers/io-ts-types.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,91 @@
import { FuzzContext, concreteFuzzerByName } from '../fuzzer';
import { rngi } from '../rng';
import {
FuzzContext,
concreteFuzzerByName,
FuzzerUnit,
ImmediateConcreteFuzzer,
fuzzerByName,
} from '../fuzzer';
import { rngi, rng } from '../rng';

import * as t from 'io-ts';
import { isRight } from 'fp-ts/lib/Either';
import { date } from 'io-ts-types/lib/date';
import { DateFromISOString } from 'io-ts-types/lib/DateFromISOString';
import { DateFromNumber } from 'io-ts-types/lib/DateFromNumber';
import { DateFromUnixTime } from 'io-ts-types/lib/DateFromUnixTime';
import { BooleanFromString } from 'io-ts-types/lib/BooleanFromString';
import { IntFromString } from 'io-ts-types/lib/IntFromString';
import { NumberFromString } from 'io-ts-types/lib/NumberFromString';

function decode<A1, U, I1>(t: t.Type<A1, U, I1>, v: I1): A1 {
const d = t.decode(v);
// istanbul ignore else
if (isRight(d)) {
return d.right;
} else {
throw new Error('IOTSF0003: cannot decode piped fuzzer output');
}
}

function pipeFuzzer<A1, I1, V, U, T>(
t: t.Type<A1, U, I1>,
e: t.Encode<A1, V>
): ImmediateConcreteFuzzer<T, V> {
return {
type: 'fuzzer',
mightRecurse: false,
children: [t],
func: (ctx, n, h0) => {
return e(decode(t, (h0 as FuzzerUnit<I1>).encode([rngi(n), ctx])));
},
};
}

export function fuzzDate(_: FuzzContext, n: number): Date {
return new Date(rngi(n) / 1000.0);
}

export const fuzzers = [concreteFuzzerByName(fuzzDate, 'Date')];
export function fuzzUUID(_: FuzzContext, n: number): string {
const r = rng(n);
const ret: string[] = [];
for (const i of new Array(32).keys()) {
switch (i) {
case 8:
case 12:
case 16:
case 20:
ret.push('-');
break;
default:
// nothing
}
ret.push((Math.abs(r.int32()) % 16).toString(16));
}
return ret.join('');
}

export function fuzzRegExp(i: string): RegExp {
return new RegExp(i);
}

export const fuzzers = [
concreteFuzzerByName(fuzzDate, 'Date'),
fuzzerByName(
pipeFuzzer(t.boolean, BooleanFromString.encode),
'BooleanFromString'
),
fuzzerByName(pipeFuzzer(date, DateFromISOString.encode), 'DateFromISOString'),
fuzzerByName(pipeFuzzer(date, DateFromNumber.encode), 'DateFromNumber'),
fuzzerByName(
pipeFuzzer(date, x => Math.floor(DateFromUnixTime.encode(x))),
'DateFromUnixTime'
),
fuzzerByName(pipeFuzzer(t.Int, IntFromString.encode), 'IntFromString'),
fuzzerByName(pipeFuzzer(t.string, x => `n${x}`), 'NonEmptyString'),
fuzzerByName(
pipeFuzzer(t.number, NumberFromString.encode),
'NumberFromString'
),
concreteFuzzerByName(fuzzUUID, 'UUID'),
fuzzerByName(pipeFuzzer(t.string, fuzzRegExp), 'RegExp'),
];
Loading

0 comments on commit 5489f03

Please sign in to comment.