Skip to content

Commit

Permalink
feat: add Schemable implementation for fast-check in contrib
Browse files Browse the repository at this point in the history
fast-check does some nice stuff with property based testing and we can implement
Schemable for the fast-check Arbitrary, allowing us to somewhat generate fake
data from a Schema. Not sure if the IntersectArbitrary is a mess or not but
we'll see.
  • Loading branch information
baetheus committed Nov 27, 2023
1 parent 5ccf5f5 commit 299a1a3
Show file tree
Hide file tree
Showing 5 changed files with 235 additions and 4 deletions.
62 changes: 62 additions & 0 deletions contrib/fast-check.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import type { Arbitrary, Random, Stream, Value } from "npm:fast-check@3.14.0";
import type {
IntersectSchemable,
LiteralSchemable,
Schemable,
TupleSchemable,
} from "../schemable.ts";
import type { Kind, Out } from "../kind.ts";

import * as fc from "npm:fast-check@3.14.0";
export * from "npm:fast-check@3.14.0";

export interface KindArbitrary extends Kind {
readonly kind: Arbitrary<Out<this, 0>>;
}

export class IntersectArbitrary<U, V> extends fc.Arbitrary<U & V> {
constructor(private first: Arbitrary<U>, private second: Arbitrary<V>) {
super();
}

generate(mrng: Random, biasFactor: number | undefined): Value<U & V> {
const fst = this.first.generate(mrng, biasFactor);
const snd = this.second.generate(mrng, biasFactor);
return new fc.Value(
Object.assign({}, fst.value, snd.value),
mrng.nextInt(),
);
}

canShrinkWithoutContext(value: unknown): value is U & V {
return this.first.canShrinkWithoutContext(value) &&
this.second.canShrinkWithoutContext(value);
}

shrink(value: U & V, context: unknown): Stream<Value<U & V>> {
return fc.Stream.of(new fc.Value(value, context));
}
}

export const SchemableArbitrary: Schemable<KindArbitrary> = {
unknown: fc.anything,
string: fc.string,
number: fc.float,
boolean: fc.boolean,
literal: fc.constantFrom as LiteralSchemable<KindArbitrary>["literal"],
nullable: fc.option,
undefinable: fc.option,
record: <T>(arb: Arbitrary<T>) => fc.dictionary(fc.string(), arb),
array: fc.array,
tuple: fc.tuple as TupleSchemable<KindArbitrary>["tuple"],
struct: fc.record,
partial: (items) => fc.record(items, { requiredKeys: [] }),
intersect:
((second) => (first) =>
new IntersectArbitrary(first, second)) as IntersectSchemable<
KindArbitrary
>["intersect"],
union: (second) => (first) => fc.oneof(first, second),

lazy: (_id, builder) => fc.memo(builder)(),
};
76 changes: 76 additions & 0 deletions examples/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { schema } from "../schemable.ts";
import { SchemableDecoder } from "../decoder.ts";
import { print, SchemableJsonBuilder } from "../json_schema.ts";
import { sample, SchemableArbitrary } from "../contrib/fast-check.ts";
import { pipe } from "../fn.ts";

const Vector = schema((s) => s.tuple(s.number(), s.number(), s.number()));

const Asteroid = schema((s) =>
s.struct({
type: s.literal("asteroid"),
location: Vector(s),
mass: s.number(),
})
);

const Planet = schema((s) =>
s.struct({
type: s.literal("planet"),
location: Vector(s),
mass: s.number(),
population: s.number(),
habitable: s.boolean(),
})
);

const Rank = schema((s) =>
pipe(
s.literal("captain"),
s.union(s.literal("first mate")),
s.union(s.literal("officer")),
s.union(s.literal("ensign")),
)
);

const CrewMember = schema((s) =>
s.struct({
name: s.string(),
age: s.number(),
rank: Rank(s),
home: Planet(s),
})
);

const Ship = schema((s) =>
s.struct({
type: s.literal("ship"),
location: Vector(s),
mass: s.number(),
name: s.string(),
crew: s.array(CrewMember(s)),
})
);

const SpaceObject = schema((s) =>
pipe(Asteroid(s), s.union(Planet(s)), s.union(Ship(s)))
);

const decoder = SpaceObject(SchemableDecoder);
const arbitrary = SpaceObject(SchemableArbitrary);
const json_schema = print(SpaceObject(SchemableJsonBuilder));

const rands = sample(arbitrary, 10);
const checks = rands.map(decoder);

console.log({ json_schema, rands, checks });

const intersect = schema((s) =>
pipe(
s.struct({ one: s.number() }),
s.intersect(s.partial({ two: s.string() })),
)
);

const iarbitrary = intersect(SchemableArbitrary);
console.log("Intersect", sample(iarbitrary, 20));
6 changes: 3 additions & 3 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion iterable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
* @since 2.0.0
*/

import type { $, Kind, Out } from "./kind.ts";
import type { Kind, Out } from "./kind.ts";
import type { Applicable } from "./applicable.ts";
import type { Combinable } from "./combinable.ts";
import type { Either } from "./either.ts";
Expand Down
93 changes: 93 additions & 0 deletions testing/contrib/fast-check.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { assertEquals } from "https://deno.land/std@0.103.0/testing/asserts.ts";

import * as F from "../../contrib/fast-check.ts";
import * as R from "../../refinement.ts";
import { schema } from "../../schemable.ts";
import { pipe } from "../../fn.ts";

Deno.test("Fast Check Schemable", () => {
const Vector = schema((s) => s.tuple(s.number(), s.number(), s.number()));

const Asteroid = schema((s) =>
s.struct({
type: s.literal("asteroid"),
location: Vector(s),
mass: s.number(),
tags: s.record(s.boolean()),
})
);

const Planet = schema((s) =>
s.struct({
type: s.literal("planet"),
location: Vector(s),
mass: s.number(),
population: s.number(),
habitable: s.boolean(),
})
);

const Rank = schema((s) =>
pipe(
s.literal("captain"),
s.union(s.literal("first mate")),
s.union(s.literal("officer")),
s.union(s.literal("ensign")),
)
);

const CrewMember = schema((s) =>
pipe(
s.struct({
name: s.string(),
age: s.number(),
rank: Rank(s),
home: Planet(s),
}),
s.intersect(s.partial({
tags: s.record(s.string()),
})),
)
);

const Ship = schema((s) =>
s.struct({
type: s.literal("ship"),
location: Vector(s),
mass: s.number(),
name: s.string(),
crew: s.array(CrewMember(s)),
lazy: s.lazy("lazy", () => s.string()),
})
);

const SpaceObject = schema((s) =>
pipe(Asteroid(s), s.union(Planet(s)), s.union(Ship(s)))
);

const refinement = SpaceObject(R.SchemableRefinement);
const arbitrary = SpaceObject(F.SchemableArbitrary);
const rands = F.sample(arbitrary, 10);

for (const rand of rands) {
assertEquals(refinement(rand), true);
}
});

Deno.test("Fast Check IntersectArbitrary", () => {
const intersect = new F.IntersectArbitrary(
F.record({ one: F.integer() }),
F.record({ two: F.string() }),
);

assertEquals(intersect.canShrinkWithoutContext(null), false);
assertEquals(
intersect.canShrinkWithoutContext({ one: 1, two: "two" }),
false,
);

assertEquals(
intersect.shrink({ one: 1, two: "two" }, null),
F.Stream.of(new F.Value({ one: 1, two: "two" }, 0)),
);
});

0 comments on commit 299a1a3

Please sign in to comment.