1import * as BigDecimal from "effect/BigDecimal"
2import * as DateTime from "effect/DateTime"
3import * as Option from "effect/Option"
4import * as Schema from "effect/Schema"
5import * as FastCheck from "fast-check"
6import assert from "node:assert/strict"
7import type { Tree } from "./schema.ts"
8import {
9 makeBigDecimalSchema,
10 makeDateTimeUtcSchema,
11 makeDateTimeZonedSchema,
12 makeOptionalStructSchema,
13 regularExpression,
14 validateNumbers,
15 validateRegExpCoverage,
16 validateRegExpValues,
17 validateSchemaValues,
18 validateStrings,
19 validateTrees,
20 validateUint8Arrays
21} from "./schema.ts"
22
23const seed = 42
24const recursiveSeed = 188
25const regExpShrinkSeed = 0
26const namedTimeZones = ["UTC", "Europe/London", "America/New_York", "Asia/Tokyo", "Australia/Sydney"] as const
27
28const makeRegExpArbitrary = () => FastCheck.stringMatching(regularExpression)
29
30interface FlatMapValue {
31 readonly length: number
32 readonly values: ReadonlyArray<number>
33}
34
35const flatMapTargets = globalThis.Array.from({ length: 8 }, (_, index) => {
36 const length = index + 1
37 return FastCheck.array(FastCheck.integer({ min: -1_000, max: 1_000 }), { minLength: length, maxLength: length })
38 .map((values): FlatMapValue => ({ length, values }))
39})
40const flatMapArbitrary = FastCheck.integer({ min: 1, max: 8 }).chain((length) => flatMapTargets[length - 1])
41
42const scoreArbitrary = FastCheck.oneof(
43 FastCheck.constant(Option.none()),
44 FastCheck.integer({ min: 0, max: 100 }).map(Option.some)
45)
46
47const treeFields = (
48 score: FastCheck.Arbitrary<Option.Option<number>>,
49 children: FastCheck.Arbitrary<ReadonlyArray<Tree>>
50) => ({
51 label: FastCheck.string({ minLength: 2, maxLength: 12 }),
52 score,
53 children
54})
55
56const treeArbitrary = (maxDepth = 1) => {
57 const depthIdentifier = FastCheck.createDepthIdentifier()
58 const recursion = { maxDepth, depthIdentifier }
59 const recursive = FastCheck.letrec<{ readonly Tree: Tree }>((tie) => ({
60 Tree: FastCheck.oneof(
61 recursion,
62 FastCheck.record(treeFields(FastCheck.constant(Option.none()), FastCheck.constant([]))),
63 FastCheck.constant(null).chain(() =>
64 FastCheck.record(
65 treeFields(
66 FastCheck.oneof(recursion, FastCheck.constant(Option.none()), scoreArbitrary),
67 FastCheck.array(tie("Tree"), { maxLength: 3 })
68 )
69 )
70 )
71 )
72 })).Tree
73 return FastCheck.record(
74 treeFields(scoreArbitrary, FastCheck.array(recursive, { maxLength: 3 }))
75 )
76}
77
78const timeZoneArbitrary = () =>
79 FastCheck.oneof(
80 FastCheck.integer({ min: -12 * 60 * 60 * 1_000, max: 14 * 60 * 60 * 1_000 }).map(DateTime.zoneMakeOffset),
81 FastCheck.constantFrom(...namedTimeZones).map(DateTime.zoneMakeNamedUnsafe)
82 )
83
84export const coldRecursiveFirstSample = () => ({
85 run: () => FastCheck.sample(treeArbitrary(0), { numRuns: 1, seed }),
86 validate: validateTrees(1, 2, 2)
87})
88
89export const recursiveSample32 = () => {
90 const arbitrary = treeArbitrary()
91 return {
92 run: () => FastCheck.sample(arbitrary, { numRuns: 32, seed: recursiveSeed }),
93 validate: validateTrees(32, 90, 110)
94 }
95}
96
97export const optionalStructSample128 = () => {
98 const schema = makeOptionalStructSchema()
99 const item = FastCheck.integer({ min: 0, max: 1_000 })
100 const arbitrary = FastCheck.record({ a: item, b: item, c: item, d: item, e: item, f: item, g: item, h: item }, {
101 requiredKeys: []
102 })
103 return {
104 run: () => FastCheck.sample(arbitrary, { numRuns: 128, seed }),
105 validate: validateSchemaValues(schema, 128)
106 }
107}
108
109export const constrainedStringSample128 = () => {
110 const arbitrary = FastCheck.string({ minLength: 32, maxLength: 32 })
111 return {
112 run: () => FastCheck.sample(arbitrary, { numRuns: 128, seed }),
113 validate: validateStrings(128)
114 }
115}
116
117export const coldRegExpFirstSample = () => ({
118 run: () => FastCheck.sample(makeRegExpArbitrary(), { numRuns: 1, seed }),
119 validate: validateRegExpValues(1)
120})
121
122export const regExpSample64 = () => {
123 const arbitrary = makeRegExpArbitrary()
124 return {
125 run: () => FastCheck.sample(arbitrary, { numRuns: 64, seed }),
126 validate: validateRegExpCoverage
127 }
128}
129
130export const regExpCheckFalsifyAndShrink = () => {
131 const property = FastCheck.property(makeRegExpArbitrary(), () => false)
132 return {
133 run: () => FastCheck.check(property, { numRuns: 1, seed: regExpShrinkSeed }),
134 validate: (result: FastCheck.RunDetails<[string]>) => {
135 assert.equal(result.failed, true)
136 if (!result.failed) return
137 validateRegExpValues(1)(result.counterexample)
138 assert.equal(result.numShrinks > 0, true)
139 }
140 }
141}
142
143export const boundedNumberSample128 = () => {
144 const arbitrary = FastCheck.double({ min: 2, max: 4, noNaN: true })
145 return {
146 run: () => FastCheck.sample(arbitrary, { numRuns: 128, seed }),
147 validate: validateNumbers(128)
148 }
149}
150
151export const uint8ArraySample128 = () => {
152 const arbitrary = FastCheck.uint8Array({ maxLength: 10 })
153 return {
154 run: () => FastCheck.sample(arbitrary, { numRuns: 128, seed }),
155 validate: validateUint8Arrays(128)
156 }
157}
158
159export const bigDecimalSample128 = () => {
160 const schema = makeBigDecimalSchema()
161 const scale = 20
162 const factor = BigInt(10) ** BigInt(17)
163 const arbitrary = FastCheck.bigInt({
164 min: BigInt(1234) * factor + BigInt(1),
165 max: BigInt(1236) * factor - BigInt(1)
166 }).map((value) => BigDecimal.make(value, scale))
167 return {
168 run: () => FastCheck.sample(arbitrary, { numRuns: 128, seed }),
169 validate: validateSchemaValues(schema, 128)
170 }
171}
172
173export const dateTimeUtcSample128 = () => {
174 const schema = makeDateTimeUtcSchema()
175 const arbitrary = FastCheck.integer({ min: -1_000_000_000, max: 1_000_000_000 }).map(DateTime.makeUnsafe)
176 return {
177 run: () => FastCheck.sample(arbitrary, { numRuns: 128, seed }),
178 validate: validateSchemaValues(schema, 128)
179 }
180}
181
182export const timeZoneNamedSample128 = () => {
183 const arbitrary = FastCheck.constantFrom(...namedTimeZones).map(DateTime.zoneMakeNamedUnsafe)
184 return {
185 run: () => FastCheck.sample(arbitrary, { numRuns: 128, seed }),
186 validate: validateSchemaValues(Schema.TimeZoneNamed, 128)
187 }
188}
189
190export const timeZoneSample128 = () => {
191 const arbitrary = timeZoneArbitrary()
192 return {
193 run: () => FastCheck.sample(arbitrary, { numRuns: 128, seed }),
194 validate: validateSchemaValues(Schema.TimeZone, 128)
195 }
196}
197
198export const dateTimeZonedSample128 = () => {
199 const schema = makeDateTimeZonedSchema()
200 const arbitrary = FastCheck.tuple(
201 FastCheck.integer({ min: -1_000_000_000, max: 1_000_000_000 }),
202 timeZoneArbitrary()
203 ).map(([epochMilliseconds, timeZone]) => DateTime.makeZonedUnsafe(epochMilliseconds, { timeZone }))
204 return {
205 run: () => FastCheck.sample(arbitrary, { numRuns: 128, seed }),
206 validate: validateSchemaValues(schema, 128)
207 }
208}
209
210export const rareFilterSample32 = () => {
211 const arbitrary = FastCheck.integer({ min: 0, max: 255 }).filter((value) => value % 16 === 0)
212 return {
213 run: () => FastCheck.sample(arbitrary, { numRuns: 32, seed }),
214 validate: (values: ReadonlyArray<number>) => {
215 assert.equal(values.length, 32)
216 assert.equal(values.every((value) => value % 16 === 0), true)
217 }
218 }
219}
220
221export const uniqueArraySample32 = () => {
222 const arbitrary = FastCheck.uniqueArray(FastCheck.integer({ min: 0, max: 1_023 }), {
223 minLength: 32,
224 maxLength: 32
225 })
226 return {
227 run: () => FastCheck.sample(arbitrary, { numRuns: 32, seed }),
228 validate: (values: ReadonlyArray<ReadonlyArray<number>>) => {
229 assert.equal(values.length, 32)
230 assert.equal(values.every((value) => value.length === 32 && new Set(value).size === 32), true)
231 }
232 }
233}
234
235export const literalSample128 = () => {
236 const arbitrary = FastCheck.constant("value")
237 return {
238 run: () => FastCheck.sample(arbitrary, { numRuns: 128, seed }),
239 validate: (values: ReadonlyArray<unknown>) => {
240 assert.equal(values.length, 128)
241 assert.equal(values.every((value) => value === "value"), true)
242 }
243 }
244}
245
246export const mapSample128 = () => {
247 const arbitrary = FastCheck.integer({ min: 0, max: 1_000 }).map((value) => value + 1)
248 return {
249 run: () => FastCheck.sample(arbitrary, { numRuns: 128, seed }),
250 validate: (values: ReadonlyArray<number>) => {
251 assert.equal(values.length, 128)
252 assert.equal(values.every((value) => value >= 1 && value <= 1_001), true)
253 }
254 }
255}
256
257export const passingFilterSample128 = () => {
258 const arbitrary = FastCheck.integer({ min: 0, max: 1_000 }).filter((value) => value >= 0)
259 return {
260 run: () => FastCheck.sample(arbitrary, { numRuns: 128, seed }),
261 validate: (values: ReadonlyArray<number>) => {
262 assert.equal(values.length, 128)
263 assert.equal(values.every((value) => value >= 0 && value <= 1_000), true)
264 }
265 }
266}
267
268export const selectiveFilterSample32 = () => {
269 const arbitrary = FastCheck.integer({ min: 0, max: 255 }).filter((value) => value % 16 === 0)
270 return {
271 run: () => FastCheck.sample(arbitrary, { numRuns: 32, seed }),
272 validate: (values: ReadonlyArray<number>) => {
273 assert.equal(values.length, 32)
274 assert.equal(values.every((value) => value % 16 === 0), true)
275 }
276 }
277}
278
279export const filterMapSample128 = () => {
280 const arbitrary = FastCheck.integer({ min: 0, max: 255 })
281 .filter((value) => value % 2 === 0)
282 .map((value) => value / 2)
283 return {
284 run: () => FastCheck.sample(arbitrary, { numRuns: 128, seed }),
285 validate: (values: ReadonlyArray<number>) => {
286 assert.equal(values.length, 128)
287 assert.equal(values.every((value) => Number.isInteger(value) && value >= 0 && value <= 127), true)
288 }
289 }
290}
291
292export const filterCheckFalsifyAndShrink = () => {
293 const arbitrary = FastCheck.integer({ min: 1, max: 8 }).filter(
294 (value) => value === 8 || value === 5 || value === 4
295 )
296 const property = FastCheck.property(arbitrary, () => false)
297 return {
298 run: () => FastCheck.check(property, { examples: [[8]], numRuns: 1, seed }),
299 validate: (result: FastCheck.RunDetails<[number]>) => {
300 assert.equal(result.failed, true)
301 assert.deepEqual(result.counterexample, [4])
302 assert.equal(result.numShrinks, 2)
303 }
304 }
305}
306
307export const allTupleSample128 = () => {
308 const arbitrary = FastCheck.tuple(FastCheck.constant("left"), FastCheck.constant(1))
309 return {
310 run: () => FastCheck.sample(arbitrary, { numRuns: 128, seed }),
311 validate: (values: ReadonlyArray<["left", 1]>) => {
312 assert.equal(values.length, 128)
313 assert.equal(values.every(([left, right]) => left === "left" && right === 1), true)
314 }
315 }
316}
317
318export const allRecordSample128 = () => {
319 const arbitrary = FastCheck.record({
320 name: FastCheck.constantFrom("Ada", "Grace"),
321 age: FastCheck.integer()
322 })
323 return {
324 run: () => FastCheck.sample(arbitrary, { numRuns: 128, seed }),
325 validate: (values: ReadonlyArray<{ readonly name: string; readonly age: number }>) => {
326 assert.equal(values.length, 128)
327 assert.equal(values.every((value) => value.name === "Ada" || value.name === "Grace"), true)
328 }
329 }
330}
331
332export const flatMapSample128 = () => ({
333 run: () => FastCheck.sample(flatMapArbitrary, { numRuns: 128, seed }),
334 validate: (values: ReadonlyArray<FlatMapValue>) => {
335 assert.equal(values.length, 128)
336 assert.equal(values.every((value) => value.values.length === value.length), true)
337 }
338})
339
340export const flatMapCheckFalsifyAndShrink = () => {
341 const property = FastCheck.property(flatMapArbitrary, () => false)
342 return {
343 run: () => FastCheck.check(property, { numRuns: 1, seed }),
344 validate: (result: FastCheck.RunDetails<[FlatMapValue]>) => {
345 assert.equal(result.failed, true)
346 assert.equal(result.counterexample?.[0].length, 1)
347 assert.equal(result.counterexample?.[0].values.length, 1)
348 }
349 }
350}
351
352export const flatMapCheckReplay = () => {
353 const property = FastCheck.property(flatMapArbitrary, () => false)
354 const initial = FastCheck.check(property, { numRuns: 1, seed })
355 assert.equal(initial.failed, true)
356 return {
357 run: () => FastCheck.check(property, { numRuns: 1, seed: initial.seed, path: initial.counterexamplePath }),
358 validate: (result: FastCheck.RunDetails<[FlatMapValue]>) => {
359 assert.equal(result.failed, true)
360 assert.equal(result.counterexample?.[0].length, 1)
361 assert.equal(result.counterexample?.[0].values.length, 1)
362 }
363 }
364}
365
366export const checkPass100 = () => {
367 const arbitrary = FastCheck.integer()
368 const property = FastCheck.property(arbitrary, () => true)
369 return {
370 run: () => FastCheck.check(property, { numRuns: 100, seed }),
371 validate: (result: FastCheck.RunDetails<[number]>) => {
372 assert.equal(result.failed, false)
373 assert.equal(result.numRuns, 100)
374 assert.equal(result.numSkips, 0)
375 }
376 }
377}
378
379export const arrayCheckPass100 = () => {
380 const property = FastCheck.property(FastCheck.array(FastCheck.integer(), { maxLength: 50 }), () => true)
381 return {
382 run: () => FastCheck.check(property, { numRuns: 100, seed }),
383 validate: (result: FastCheck.RunDetails<[Array<number>]>) => {
384 assert.equal(result.failed, false)
385 assert.equal(result.numRuns, 100)
386 assert.equal(result.numSkips, 0)
387 }
388 }
389}
390
391export const arrayCheckFalsifyAndShrink = () => {
392 const arbitrary = FastCheck.array(FastCheck.constantFrom(8, 27, 0, 1), { maxLength: 4 })
393 const property = FastCheck.property(
394 arbitrary,
395 (values) => !(values.includes(27) && values.indexOf(0) > values.indexOf(27))
396 )
397 return {
398 run: () => FastCheck.check(property, { examples: [[[8, 27, 0, 1]]], numRuns: 1, seed }),
399 validate: (result: FastCheck.RunDetails<[Array<number>]>) => {
400 assert.equal(result.failed, true)
401 assert.deepEqual(result.counterexample, [[27, 0]])
402 }
403 }
404}
405
406export const testSchemaVerifyGeneration100 = () => ({
407 run: () => {
408 const schema = Schema.Int
409 const arbitrary = FastCheck.integer()
410 FastCheck.assert(FastCheck.property(arbitrary, Schema.is(schema)), { numRuns: 100, seed })
411 },
412 validate: (result: void) => assert.equal(result, undefined)
413})
414
415export const checkFalsifyAndShrink = () => {
416 const arbitrary = FastCheck.integer({ min: 1, max: 1_000 })
417 const property = FastCheck.property(arbitrary, (value) => value < 0)
418 return {
419 run: () => FastCheck.check(property, { examples: [[1_000]], numRuns: 1, seed }),
420 validate: (result: FastCheck.RunDetails<[number]>) => {
421 assert.equal(result.failed, true)
422 assert.deepEqual(result.counterexample, [1])
423 assert.equal(result.numShrinks, 1)
424 }
425 }
426}
427
428export const checkReplay = () => {
429 const arbitrary = FastCheck.integer({ min: 1, max: 1_000 })
430 const property = FastCheck.property(arbitrary, (value) => value < 0)
431 const initial = FastCheck.check(property, { examples: [[1_000]], numRuns: 1, seed })
432 assert.equal(initial.failed, true)
433 return {
434 run: () => FastCheck.check(property, { numRuns: 1, seed: initial.seed, path: initial.counterexamplePath }),
435 validate: (result: FastCheck.RunDetails<[number]>) => {
436 assert.equal(result.failed, true)
437 assert.deepEqual(result.counterexample, [1])
438 assert.equal(result.numShrinks, 0)
439 }
440 }
441}