1import { assert, describe, it } from "@effect/vitest"
2import {
3 Deferred,
4 Effect,
5 Exit,
6 Fiber,
7 Option,
8 Order,
9 Random,
10 Result,
11 Schema,
12 SchemaIssue,
13 SchemaTransformation
14} from "effect"
15import * as Arbitrary from "effect/Arbitrary"
16import * as BigDecimal from "effect/BigDecimal"
17import * as Chunk from "effect/Chunk"
18import * as DateTime from "effect/DateTime"
19import * as HashMap from "effect/HashMap"
20import * as HashSet from "effect/HashSet"
21import * as Scheduler from "effect/Scheduler"
22
23const makeSuspendChain = (count: number): Schema.Codec<unknown> => {
24 let schema: Schema.Codec<unknown> = Schema.Null
25 for (let index = 0; index < count; index++) {
26 const next = schema
27 schema = Schema.suspend(() => next)
28 }
29 return schema
30}
31
32interface SchemaCatalogEntry {
33 readonly name: string
34 readonly schema: Schema.Top
35}
36
37const verifySchemaCatalog = Effect.fnUntraced(function*(entries: ReadonlyArray<SchemaCatalogEntry>) {
38 for (const entry of entries) {
39 const result = yield* Arbitrary.checkEffect(Arbitrary.schema(entry.schema), Schema.is(entry.schema), {
40 runs: 100,
41 maxDiscards: 200,
42 seed: `schema-catalog:${entry.name}`
43 })
44 assert.strictEqual(result._tag, "Passed", entry.name)
45 if (result._tag === "Passed") {
46 assert.strictEqual(result.runs, 100, entry.name)
47 assert.isAtMost(result.discards, 200, entry.name)
48 }
49 }
50})
51
52describe("Arbitrary", () => {
53 describe("isArbitrary", () => {
54 it("identifies Arbitrary values", () => {
55 assert.isTrue(Arbitrary.isArbitrary(Arbitrary.schema(Schema.String)))
56 assert.isFalse(Arbitrary.isArbitrary(Schema.String))
57 assert.isFalse(Arbitrary.isArbitrary(null))
58 })
59 })
60
61 describe("schema", () => {
62 it.effect("uses a custom Schema shrinker", () =>
63 Effect.gen(function*() {
64 const arbitrary = Arbitrary.schema(Schema.Literals(["large", "small"]), {
65 shrink: (value) => value === "large" ? ["small"] as const : []
66 })
67 const result = yield* Arbitrary.checkEffect(arbitrary, () => false, {
68 runs: 1,
69 maxDiscards: 0,
70 seed: 0
71 })
72
73 assert.strictEqual(result._tag, "Falsified")
74 if (result._tag === "Falsified") {
75 assert.strictEqual(result.initialInput, "large")
76 assert.strictEqual(result.shrunkInput, "small")
77 assert.strictEqual(result.shrinks, 1)
78
79 const replayed = yield* Arbitrary.checkEffect(arbitrary, () => false, { replay: result.replay })
80 assert.deepStrictEqual(replayed, result)
81 }
82 }))
83
84 it.effect("validates custom shrink candidates before evaluating the property", () =>
85 Effect.gen(function*() {
86 const arbitrary = Arbitrary.schema(Schema.Literals(["large", "small"]), {
87 shrink: (value) => value === "large" ? ["invalid" as "large", "small"] as const : []
88 })
89 const limitedEvaluated: Array<string> = []
90 const limited = yield* Arbitrary.checkEffect(arbitrary, (value) => {
91 limitedEvaluated.push(value)
92 return false
93 }, {
94 runs: 1,
95 maxDiscards: 0,
96 maxShrinks: 1,
97 seed: 0
98 })
99
100 assert.strictEqual(limited._tag, "Falsified")
101 if (limited._tag === "Falsified") {
102 assert.strictEqual(limited.shrunkInput, "large")
103 assert.strictEqual(limited.shrinks, 0)
104 assert.deepStrictEqual(limitedEvaluated, ["large"])
105 }
106
107 const evaluated: Array<string> = []
108 const result = yield* Arbitrary.checkEffect(arbitrary, (value) => {
109 evaluated.push(value)
110 return false
111 }, {
112 runs: 1,
113 maxDiscards: 0,
114 maxShrinks: 2,
115 seed: 0
116 })
117
118 assert.strictEqual(result._tag, "Falsified")
119 if (result._tag === "Falsified") {
120 assert.strictEqual(result.shrunkInput, "small")
121 assert.strictEqual(result.shrinks, 1)
122 assert.deepStrictEqual(evaluated, ["large", "small"])
123 }
124 }))
125
126 it.effect("evaluates a custom shrinker only while shrinking a failure", () =>
127 Effect.gen(function*() {
128 let calls = 0
129 const arbitrary = Arbitrary.schema(Schema.Literals(["large", "small"]), {
130 shrink: () => {
131 calls++
132 return []
133 }
134 })
135
136 yield* Arbitrary.sampleEffect(arbitrary, { count: 1, maxDiscards: 0, seed: 0 })
137 yield* Arbitrary.checkEffect(arbitrary, () => true, { runs: 1, maxDiscards: 0, seed: 0 })
138 assert.strictEqual(calls, 0)
139
140 yield* Arbitrary.checkEffect(arbitrary, () => false, { runs: 1, maxDiscards: 0, seed: 0 })
141 assert.strictEqual(calls, 1)
142 }))
143
144 it.effect("replaces Schema-derived shrinking with the custom shrinker", () =>
145 Effect.gen(function*() {
146 const schema = Schema.BigInt.check(Schema.isBetweenBigInt({ minimum: 100n, maximum: 1_000n }))
147 const result = yield* Arbitrary.checkEffect(Arbitrary.schema(schema, { shrink: () => [] }), () => false, {
148 runs: 1,
149 seed: 21
150 })
151
152 assert.strictEqual(result._tag, "Falsified")
153 if (result._tag === "Falsified") {
154 assert.isTrue(result.initialInput > 100n)
155 assert.strictEqual(result.shrunkInput, result.initialInput)
156 assert.strictEqual(result.shrinks, 0)
157 }
158 }))
159
160 it.effect("generates deterministic samples and pushes constraints into primitive constructors", () =>
161 Effect.gen(function*() {
162 const schema = Schema.String.check(Schema.isMinLength(8), Schema.isMaxLength(8))
163 const arbitrary = Arbitrary.schema(schema)
164 const first = yield* Arbitrary.sampleEffect(arbitrary, { count: 20, maxDiscards: 0, seed: "constraint" })
165 const second = yield* Arbitrary.sampleEffect(arbitrary, { count: 20, maxDiscards: 0, seed: "constraint" })
166
167 assert.deepStrictEqual(first, second)
168 assert.isTrue(first.every((value) => value.length === 8))
169 }))
170
171 it.effect("constructs strings for the regular pattern subset", () =>
172 Effect.gen(function*() {
173 const schemas = [
174 Schema.String.check(Schema.isPattern(/^[A-Z]{3}[0-9]{3}$/)),
175 Schema.String.check(Schema.isPattern(/^(?:foo|bar)-[^0-9]\s?\w+$/)),
176 Schema.String.check(Schema.isPattern(/^a+?$/)),
177 Schema.String.check(Schema.isPattern(/^\x41\u0042\cC$/)),
178 Schema.String.check(Schema.isPattern(/^\u{1f600}{2}$/u)),
179 Schema.String.check(Schema.isPattern(/^effect$/y))
180 ]
181 for (let index = 0; index < schemas.length; index++) {
182 const values = yield* Arbitrary.sampleEffect(Arbitrary.schema(schemas[index]), {
183 count: 20,
184 maxDiscards: 20,
185 seed: `regular-pattern-${index}`
186 })
187 assert.isTrue(values.every(Schema.is(schemas[index])))
188 }
189 }))
190
191 it.effect("constructs strings with code point bounds and UTF-16 length bounds", () =>
192 Effect.gen(function*() {
193 const schemas = [
194 Schema.String.check(Schema.isMinCodePoints(128)),
195 Schema.String.check(Schema.isMaxCodePoints(0)),
196 Schema.String.check(Schema.isBetweenCodePoints(2, 4)),
197 Schema.String.check(Schema.isMinCodePoints(3), Schema.isMinCodePoints(4), Schema.isMaxCodePoints(4)),
198 Schema.String.check(Schema.isMinCodePoints(4), Schema.isMaxCodePoints(5), Schema.isMaxCodePoints(4)),
199 Schema.String.check(Schema.isBetweenLength(8, 8), Schema.isBetweenCodePoints(4, 4)),
200 Schema.String.check(Schema.isMaxLength(4), Schema.isMinCodePoints(4)),
201 Schema.String.check(Schema.isPattern(/^\u{1f600}{4}$/u), Schema.isBetweenCodePoints(4, 4))
202 ]
203 for (let index = 0; index < schemas.length; index++) {
204 const schema = schemas[index]
205 const values = yield* Arbitrary.sampleEffect(Arbitrary.schema(schema), {
206 count: 20,
207 maxDiscards: 0,
208 size: 0,
209 seed: `code-points-${index}`
210 })
211 assert.isTrue(values.every(Schema.is(schema)))
212 }
213 }))
214
215 it.effect("generates variable-length supplementary patterns above the default size", () =>
216 Effect.gen(function*() {
217 const schema = Schema.String.check(Schema.isPattern(/^\u{1f600}+$/u), Schema.isMinCodePoints(128))
218 const values = yield* Arbitrary.sampleEffect(Arbitrary.schema(schema), {
219 count: 5,
220 maxDiscards: 100,
221 seed: 42
222 })
223 assert.isTrue(values.every(Schema.is(schema)))
224 }))
225
226 it.effect("generates variable-length patterns with code point bounds at size zero", () =>
227 Effect.gen(function*() {
228 const schemas = [
229 Schema.String.check(
230 Schema.isPattern(/^\u{1f600}+$/u),
231 Schema.isBetweenCodePoints(4, 4),
232 Schema.isMaxLength(8)
233 ),
234 Schema.String.check(Schema.isPattern(/^a+$/), Schema.isBetweenCodePoints(4, 4), Schema.isMaxLength(4)),
235 Schema.String.check(
236 Schema.isPattern(/^(?:a|\u{1f600})+$/u),
237 Schema.isBetweenCodePoints(4, 4),
238 Schema.isMaxLength(6)
239 )
240 ]
241 for (let index = 0; index < schemas.length; index++) {
242 const schema = schemas[index]
243 const values = yield* Arbitrary.sampleEffect(Arbitrary.schema(schema), {
244 count: 20,
245 maxDiscards: 100,
246 size: 0,
247 seed: `code-point-pattern-${index}`
248 })
249 assert.isTrue(values.every(Schema.is(schema)))
250 }
251 }))
252
253 it.effect("reaches longer pattern alternatives under a maximum code point bound", () =>
254 Effect.gen(function*() {
255 for (const size of [undefined, 0]) {
256 const schema = Schema.String.check(
257 Schema.isPattern(/^(?:a{129}|\u{1f600}{128})$/u),
258 Schema.isMaxCodePoints(128)
259 )
260 const values = yield* Arbitrary.sampleEffect(Arbitrary.schema(schema), {
261 count: 10,
262 maxDiscards: 100,
263 seed: 42,
264 ...(size === undefined ? {} : { size })
265 })
266 assert.deepStrictEqual(values, Array(10).fill("😀".repeat(128)))
267 }
268 }))
269
270 it.effect("respects UTF-16 limits and large code point maxima for patterns", () =>
271 Effect.gen(function*() {
272 const schemas = [
273 Schema.String.check(
274 Schema.isPattern(/^(?:aaa|\u{1f600}{2})$/u),
275 Schema.isMaxCodePoints(2),
276 Schema.isMaxLength(4)
277 ),
278 Schema.String.check(Schema.isPattern(/^a+$/), Schema.isMaxCodePoints(1_000_000))
279 ]
280 for (const schema of schemas) {
281 const values = yield* Arbitrary.sampleEffect(Arbitrary.schema(schema), {
282 count: 10,
283 maxDiscards: 100,
284 seed: 42,
285 size: 0
286 })
287 assert.isTrue(values.every(Schema.is(schema)))
288 }
289 }))
290
291 it.effect("generates supplementary code points and preserves bounds while shrinking", () =>
292 Effect.gen(function*() {
293 const schema = Schema.String.check(Schema.isBetweenCodePoints(1, 1))
294 const arbitrary = Arbitrary.schema(schema)
295 const values = yield* Arbitrary.sampleEffect(arbitrary, {
296 count: 100,
297 maxDiscards: 0,
298 seed: "supplementary-code-points"
299 })
300 assert.isTrue(values.some((value) => value.length === 2))
301 assert.isTrue(values.every((value) => [...value].length === 1))
302
303 const evaluated: Array<string> = []
304 const result = yield* Arbitrary.checkEffect(arbitrary, (value) => {
305 evaluated.push(value)
306 return false
307 }, { runs: 1, seed: "code-point-shrink" })
308 assert.strictEqual(result._tag, "Falsified")
309 assert.isTrue(evaluated.every((value) => [...value].length === 1))
310 if (result._tag === "Falsified") assert.strictEqual(result.shrunkInput, "\0")
311 }))
312
313 it("rejects contradictory code point and UTF-16 length constraints", () => {
314 for (
315 const schema of [
316 Schema.String.check(Schema.isMinCodePoints(3), Schema.isMaxCodePoints(2)),
317 Schema.String.check(Schema.isMinCodePoints(3), Schema.isMaxLength(2)),
318 Schema.String.check(Schema.isMinLength(3), Schema.isMaxCodePoints(1))
319 ]
320 ) {
321 assert.throws(() => Arbitrary.schema(schema), /Unable to derive an arbitrary for .*constraints/)
322 }
323 })
324
325 it.effect("covers feasible regular-expression alternatives under a length bound", () =>
326 Effect.gen(function*() {
327 const pattern = /^(?:a|[B-D]{2}|[0-9]{3}){16}$/
328 const schema = Schema.String.check(Schema.isPattern(pattern), Schema.isMaxLength(32))
329 const values = yield* Arbitrary.sampleEffect(Arbitrary.schema(schema), {
330 count: 64,
331 maxDiscards: 0,
332 seed: 42,
333 size: 32
334 })
335
336 assert.isTrue(values.every(Schema.is(schema)))
337 assert.isAbove(new Set(values.map((value) => value.length)).size, 1)
338 assert.isTrue(values.some((value) => value.includes("a")))
339 assert.isTrue(values.some((value) => /[B-D]{2}/.test(value)))
340 assert.isTrue(values.some((value) => /[0-9]{3}/.test(value)))
341 }))
342
343 it.effect("honors dot-all and sticky regular-expression flags", () =>
344 Effect.gen(function*() {
345 const dotAll = Schema.String.check(Schema.isPattern(/^.$/s))
346 const dotAllValues = yield* Arbitrary.sampleEffect(Arbitrary.schema(dotAll), {
347 count: 200,
348 maxDiscards: 0,
349 seed: 0
350 })
351 assert.isTrue(dotAllValues.every(Schema.is(dotAll)))
352 assert.isTrue(dotAllValues.some((value) => /[\n\r\u2028\u2029]/.test(value)))
353
354 const sticky = Schema.String.check(Schema.isPattern(/a/y))
355 const stickyValues = yield* Arbitrary.sampleEffect(Arbitrary.schema(sticky), {
356 count: 100,
357 maxDiscards: 0,
358 seed: "sticky-pattern"
359 })
360 assert.isTrue(stickyValues.every(Schema.is(sticky)))
361 assert.isTrue(stickyValues.some((value) => value.length > 1))
362 }))
363
364 it.effect("biases broad character classes toward common characters while exploring their full domain", () =>
365 Effect.gen(function*() {
366 const schema = Schema.String.check(Schema.isPattern(/^.$/u))
367 const values = yield* Arbitrary.sampleEffect(Arbitrary.schema(schema), {
368 count: 200,
369 maxDiscards: 0,
370 seed: "broad-character-class"
371 })
372
373 assert.isTrue(values.some((value) => /^[\x20-\x7e]$/u.test(value)))
374 assert.isTrue(values.some((value) => value.codePointAt(0)! > 0x1f600))
375 }))
376
377 it.effect("constructs strings for built-in pattern checks", () =>
378 Effect.gen(function*() {
379 const schemas = [
380 Schema.String.check(Schema.isTrimmed()),
381 Schema.String.check(Schema.isUppercased()),
382 Schema.String.check(Schema.isLowercased()),
383 Schema.String.check(Schema.isCapitalized()),
384 Schema.String.check(Schema.isUncapitalized()),
385 Schema.String.check(Schema.isStartingWith("a.b")),
386 Schema.String.check(Schema.isEndingWith("a.b")),
387 Schema.String.check(Schema.isIncluding("a.b"))
388 ]
389 for (let index = 0; index < schemas.length; index++) {
390 const values = yield* Arbitrary.sampleEffect(Arbitrary.schema(schemas[index]), {
391 count: 20,
392 maxDiscards: 100,
393 seed: `built-in-pattern-${index}`
394 })
395 assert.isTrue(values.every(Schema.is(schemas[index])))
396 }
397 }))
398
399 it.effect("generates RegExp declarations without codec filtering", () =>
400 Effect.gen(function*() {
401 const values = yield* Arbitrary.sampleEffect(Arbitrary.schema(Schema.RegExp), {
402 count: 100,
403 maxDiscards: 0,
404 seed: "regexp-declaration"
405 })
406
407 assert.isTrue(values.every((value) => value instanceof globalThis.RegExp))
408 assert.isTrue(new Set(values.map((value) => value.source)).size > 1)
409 assert.isTrue(new Set(values.map((value) => value.flags)).size > 1)
410 }))
411
412 it.effect("generates and shrinks web URL declarations without codec filtering", () =>
413 Effect.gen(function*() {
414 const arbitrary = Arbitrary.schema(Schema.URL)
415 const values = yield* Arbitrary.sampleEffect(arbitrary, {
416 count: 200,
417 maxDiscards: 0,
418 seed: "url-declaration",
419 size: 10
420 })
421
422 assert.isTrue(values.every((value) => value instanceof globalThis.URL))
423 assert.deepStrictEqual(new Set(values.map((value) => value.protocol)), new Set(["http:", "https:"]))
424 assert.isTrue(values.some((value) => value.pathname !== "/"))
425
426 const result = yield* Arbitrary.checkEffect(arbitrary, () => false, {
427 runs: 1,
428 maxDiscards: 0,
429 seed: "url-shrink",
430 size: 10
431 })
432 assert.strictEqual(result._tag, "Falsified")
433 if (result._tag === "Falsified") {
434 assert.isAbove(result.shrinks, 0)
435 assert.match(result.shrunkInput.protocol, /^https?:$/)
436 }
437 }))
438
439 it.effect("generates and shrinks Date declarations constructively across their full domain", () =>
440 Effect.gen(function*() {
441 const values = yield* Arbitrary.sampleEffect(Arbitrary.schema(Schema.Date), {
442 count: 25_000,
443 maxDiscards: 0,
444 seed: "date-declaration"
445 })
446 const timestamps = values.map((value) => value.getTime())
447
448 assert.include(timestamps, -8_640_000_000_000_000)
449 assert.include(timestamps, 0)
450 assert.include(timestamps, 8_640_000_000_000_000)
451 assert.isTrue(values.every(Schema.is(Schema.Date)))
452
453 const constrained = Schema.Date.check(Schema.isBetweenDate({
454 minimum: new globalThis.Date(0),
455 maximum: new globalThis.Date(10),
456 exclusiveMinimum: true,
457 exclusiveMaximum: true
458 }))
459 const constrainedValues = yield* Arbitrary.sampleEffect(Arbitrary.schema(constrained), {
460 count: 500,
461 maxDiscards: 0,
462 seed: "date-constraints"
463 })
464 const constrainedTimestamps = constrainedValues.map((value) => value.getTime())
465 assert.include(constrainedTimestamps, 1)
466 assert.include(constrainedTimestamps, 9)
467 assert.isTrue(constrainedValues.every(Schema.is(constrained)))
468
469 const result = yield* Arbitrary.checkEffect(Arbitrary.schema(Schema.Date), () => false, {
470 runs: 1,
471 maxDiscards: 0,
472 seed: "date-shrink"
473 })
474 assert.strictEqual(result._tag, "Falsified")
475 if (result._tag === "Falsified") assert.strictEqual(result.shrunkInput.getTime(), 0)
476 }))
477
478 it.effect("generates and shrinks BigDecimal declarations constructively", () =>
479 Effect.gen(function*() {
480 const arbitrary = Arbitrary.schema(Schema.BigDecimal)
481 const values = yield* Arbitrary.sampleEffect(arbitrary, {
482 count: 500,
483 maxDiscards: 0,
484 seed: "big-decimal-declaration",
485 size: 10
486 })
487
488 assert.isTrue(values.every(Schema.is(Schema.BigDecimal)))
489 assert.isAbove(new Set(values.map((value) => value.scale)).size, 1)
490
491 const constrained = Schema.BigDecimal.check(Schema.isBetweenBigDecimal({
492 minimum: BigDecimal.make(1_234n, 3),
493 maximum: BigDecimal.make(1_236n, 3),
494 exclusiveMinimum: true,
495 exclusiveMaximum: true
496 }))
497 const constrainedValues = yield* Arbitrary.sampleEffect(Arbitrary.schema(constrained), {
498 count: 500,
499 maxDiscards: 0,
500 seed: "big-decimal-constraints",
501 size: 10
502 })
503
504 assert.isTrue(constrainedValues.every(Schema.is(constrained)))
505
506 const highScale = Schema.BigDecimal.check(Schema.isBetweenBigDecimal({
507 minimum: BigDecimal.make(-12_345n, 22),
508 maximum: BigDecimal.make(678_901n, 24)
509 }))
510 const highScaleValues = yield* Arbitrary.sampleEffect(Arbitrary.schema(highScale), {
511 count: 500,
512 maxDiscards: 0,
513 seed: "big-decimal-high-scale-constraints",
514 size: 10
515 })
516
517 assert.isTrue(highScaleValues.every(Schema.is(highScale)))
518
519 const result = yield* Arbitrary.checkEffect(arbitrary, () => false, {
520 runs: 1,
521 maxDiscards: 0,
522 seed: "big-decimal-shrink",
523 size: 10
524 })
525 assert.strictEqual(result._tag, "Falsified")
526 if (result._tag === "Falsified") {
527 assert.isTrue(BigDecimal.Equivalence(result.shrunkInput, BigDecimal.make(0n, 0)))
528 }
529 }))
530
531 it.effect("covers independent BigDecimal precision and exponent ranges", () =>
532 Effect.gen(function*() {
533 const values = yield* Arbitrary.sampleEffect(Arbitrary.schema(Schema.BigDecimal), {
534 count: 1_000,
535 maxDiscards: 0,
536 seed: "big-decimal-precision-and-exponent",
537 size: 1
538 })
539
540 assert.isTrue(values.some((value) => Math.abs(value.scale) > 20))
541 assert.isTrue(values.some((value) => {
542 const magnitude = value.value < 0n ? -value.value : value.value
543 return magnitude.toString().length > 20
544 }))
545 }))
546
547 it.effect("constructs BigDecimals in narrow and extreme-scale intervals", () =>
548 Effect.gen(function*() {
549 const narrow = Schema.BigDecimal.check(Schema.isBetweenBigDecimal({
550 minimum: BigDecimal.make(123_456_789n, 1_000),
551 maximum: BigDecimal.make(123_456_799n, 1_000),
552 exclusiveMinimum: true,
553 exclusiveMaximum: true
554 }))
555 const narrowValues = yield* Arbitrary.sampleEffect(Arbitrary.schema(narrow), {
556 count: 200,
557 maxDiscards: 0,
558 seed: "big-decimal-narrow-extreme"
559 })
560 assert.isTrue(narrowValues.every(Schema.is(narrow)))
561
562 const maximumScale = Schema.BigDecimal.check(Schema.isBetweenBigDecimal({
563 minimum: BigDecimal.make(0n, Number.MAX_SAFE_INTEGER),
564 maximum: BigDecimal.make(1n, Number.MAX_SAFE_INTEGER)
565 }))
566 const maximumScaleValues = yield* Arbitrary.sampleEffect(Arbitrary.schema(maximumScale), {
567 count: 50,
568 maxDiscards: 0,
569 seed: "big-decimal-maximum-scale"
570 })
571 assert.isTrue(maximumScaleValues.every(Schema.is(maximumScale)))
572 }))
573
574 it.effect("recognizes equivalent BigDecimal singleton bounds", () =>
575 Effect.gen(function*() {
576 for (
577 const [minimum, maximum] of [
578 [BigDecimal.make(1n, 0), BigDecimal.make(10n, 1)],
579 [BigDecimal.make(10n ** 4097n, 0), BigDecimal.make(1n, -4097)],
580 [BigDecimal.make(-1n, -4097), BigDecimal.make(-(10n ** 4097n), 0)],
581 [BigDecimal.make(0n, Number.MIN_SAFE_INTEGER), BigDecimal.make(0n, Number.MAX_SAFE_INTEGER)]
582 ]
583 ) {
584 const schema = Schema.BigDecimal.check(Schema.isBetweenBigDecimal({ minimum, maximum }))
585 const values = yield* Arbitrary.sampleEffect(Arbitrary.schema(schema), {
586 count: 20,
587 maxDiscards: 0,
588 seed: "big-decimal-singleton"
589 })
590
591 assert.isTrue(values.every((value) => BigDecimal.Equivalence(value, minimum)))
592 }
593 }))
594
595 it.effect("preserves both BigDecimal bounds across large scale differences", () =>
596 Effect.gen(function*() {
597 const near = BigDecimal.make(10n ** 4097n - 1n, 0)
598 const far = BigDecimal.make(1n, -4097)
599 for (const [minimum, maximum] of [[near, far], [BigDecimal.negate(far), BigDecimal.negate(near)]]) {
600 for (const exclusive of [false, true]) {
601 const schema = Schema.BigDecimal.check(Schema.isBetweenBigDecimal({
602 minimum,
603 maximum,
604 exclusiveMinimum: exclusive,
605 exclusiveMaximum: exclusive
606 }))
607 const values = yield* Arbitrary.sampleEffect(Arbitrary.schema(schema), {
608 count: 100,
609 maxDiscards: 0,
610 seed: "big-decimal-large-scale-difference"
611 })
612
613 assert.isTrue(values.every(Schema.is(schema)))
614 assert.isTrue(
615 values.some((value) => BigDecimal.Order(value, minimum) > 0 && BigDecimal.Order(value, maximum) < 0)
616 )
617 }
618 }
619 }))
620
621 it("rejects empty BigDecimal intervals at the maximum scale", () => {
622 const schema = Schema.BigDecimal.check(Schema.isBetweenBigDecimal({
623 minimum: BigDecimal.make(0n, Number.MAX_SAFE_INTEGER),
624 maximum: BigDecimal.make(1n, Number.MAX_SAFE_INTEGER),
625 exclusiveMinimum: true,
626 exclusiveMaximum: true
627 }))
628
629 assert.throws(() => Arbitrary.schema(schema), /Unable to derive an arbitrary for BigDecimal constraints/)
630 })
631
632 it.effect("shrinks BigDecimals by numeric value across representations", () =>
633 Effect.gen(function*() {
634 const one = BigDecimal.make(1n, 0)
635 const onePointOne = BigDecimal.make(11n, 1)
636 const schema = Schema.BigDecimal.check(Schema.isGreaterThanOrEqualToBigDecimal(BigDecimal.make(1n, 1)))
637 const arbitrary = Arbitrary.schema(schema)
638 const result = yield* Arbitrary.checkEffect(arbitrary, (value) => BigDecimal.Order(value, one) <= 0, {
639 runs: 100,
640 maxDiscards: 0,
641 maxShrinks: 100,
642 seed: "big-decimal-numeric-shrink"
643 })
644
645 assert.strictEqual(result._tag, "Falsified")
646 if (result._tag === "Falsified") {
647 assert.isTrue(BigDecimal.Order(result.shrunkInput, one) > 0)
648 assert.isTrue(BigDecimal.Order(result.shrunkInput, onePointOne) <= 0)
649 const replayed = yield* Arbitrary.checkEffect(arbitrary, (value) => BigDecimal.Order(value, one) <= 0, {
650 replay: result.replay
651 })
652 assert.strictEqual(replayed._tag, "Falsified")
653 if (replayed._tag === "Falsified") {
654 assert.isTrue(BigDecimal.Equivalence(replayed.initialInput, result.initialInput))
655 assert.isTrue(BigDecimal.Equivalence(replayed.shrunkInput, result.shrunkInput))
656 assert.strictEqual(replayed.shrinks, result.shrinks)
657 }
658 }
659 }))
660
661 it.effect("shrinks BigDecimals toward ordinary thresholds between simple candidates", () =>
662 Effect.gen(function*() {
663 for (const [low, high, boundary] of [[1, 10, 7], [100, 101, 100.5], [0.1, 10, 1]]) {
664 for (const sign of [1, -1]) {
665 const minimum = BigDecimal.fromNumberUnsafe(sign > 0 ? low : -high)
666 const maximum = BigDecimal.fromNumberUnsafe(sign > 0 ? high : -low)
667 const threshold = BigDecimal.fromNumberUnsafe(sign * boundary)
668 const tolerance = BigDecimal.fromNumberUnsafe(sign * (boundary + 0.01))
669 const schema = Schema.BigDecimal.check(Schema.isBetweenBigDecimal({ minimum, maximum }))
670 const arbitrary = Arbitrary.schema(schema)
671 const property = (value: BigDecimal.BigDecimal) => sign * BigDecimal.Order(value, threshold) <= 0
672 const result = yield* Arbitrary.checkEffect(arbitrary, property, {
673 seed: "b",
674 runs: 100,
675 maxShrinks: 100,
676 maxDiscards: 0
677 })
678
679 assert.strictEqual(result._tag, "Falsified")
680 if (result._tag === "Falsified") {
681 assert.isTrue(sign * BigDecimal.Order(result.initialInput, tolerance) > 0)
682 assert.isTrue(sign * BigDecimal.Order(result.shrunkInput, threshold) > 0)
683 assert.isTrue(sign * BigDecimal.Order(result.shrunkInput, tolerance) <= 0)
684 assert.isTrue(Schema.is(schema)(result.shrunkInput))
685 const replayed = yield* Arbitrary.checkEffect(arbitrary, property, { replay: result.replay })
686 assert.strictEqual(replayed._tag, "Falsified")
687 if (replayed._tag === "Falsified") {
688 assert.isTrue(BigDecimal.Equivalence(replayed.initialInput, result.initialInput))
689 assert.isTrue(BigDecimal.Equivalence(replayed.shrunkInput, result.shrunkInput))
690 assert.strictEqual(replayed.shrinks, result.shrinks)
691 assert.strictEqual(replayed.replay, result.replay)
692 }
693 }
694 }
695 }
696 }))
697
698 it.effect("generates and shrinks DateTime.Utc declarations constructively", () =>
699 Effect.gen(function*() {
700 const arbitrary = Arbitrary.schema(Schema.DateTimeUtc)
701 const values = yield* Arbitrary.sampleEffect(arbitrary, {
702 count: 1_000,
703 maxDiscards: 0,
704 seed: "date-time-utc-declaration",
705 size: 10
706 })
707
708 assert.isTrue(values.every(Schema.is(Schema.DateTimeUtc)))
709
710 const isBetweenDateTime = Schema.makeIsBetween({ order: DateTime.Order })
711 const constrained = Schema.DateTimeUtc.check(isBetweenDateTime({
712 minimum: DateTime.makeUnsafe(0),
713 maximum: DateTime.makeUnsafe(10),
714 exclusiveMinimum: true,
715 exclusiveMaximum: true
716 }))
717 const constrainedValues = yield* Arbitrary.sampleEffect(Arbitrary.schema(constrained), {
718 count: 500,
719 maxDiscards: 0,
720 seed: "date-time-utc-constraints",
721 size: 10
722 })
723
724 assert.isTrue(constrainedValues.every(Schema.is(constrained)))
725 assert.isTrue(constrainedValues.every((value) => value.epochMilliseconds > 0 && value.epochMilliseconds < 10))
726
727 const result = yield* Arbitrary.checkEffect(arbitrary, () => false, {
728 runs: 1,
729 maxDiscards: 0,
730 seed: "date-time-utc-shrink",
731 size: 10
732 })
733 assert.strictEqual(result._tag, "Falsified")
734 if (result._tag === "Falsified") assert.strictEqual(result.shrunkInput.epochMilliseconds, 0)
735 }))
736
737 it.effect("generates named and offset TimeZone declarations constructively", () =>
738 Effect.gen(function*() {
739 const named = yield* Arbitrary.sampleEffect(Arbitrary.schema(Schema.TimeZoneNamed), {
740 count: 200,
741 maxDiscards: 0,
742 seed: "time-zone-named-declaration",
743 size: 10
744 })
745 const zones = yield* Arbitrary.sampleEffect(Arbitrary.schema(Schema.TimeZone), {
746 count: 1_000,
747 maxDiscards: 0,
748 seed: "time-zone-declaration",
749 size: 10
750 })
751
752 assert.isTrue(named.every(Schema.is(Schema.TimeZoneNamed)))
753 assert.isAbove(new Set(named.map((value) => value.id)).size, 1)
754 assert.isTrue(zones.every(Schema.is(Schema.TimeZone)))
755 assert.isTrue(zones.some(DateTime.isTimeZoneNamed))
756 assert.isTrue(zones.some(DateTime.isTimeZoneOffset))
757 }))
758
759 it.effect("generates and shrinks DateTime.Zoned declarations constructively", () =>
760 Effect.gen(function*() {
761 const arbitrary = Arbitrary.schema(Schema.DateTimeZoned)
762 const values = yield* Arbitrary.sampleEffect(arbitrary, {
763 count: 1_000,
764 maxDiscards: 0,
765 seed: "date-time-zoned-declaration",
766 size: 10
767 })
768
769 assert.isTrue(values.every(Schema.is(Schema.DateTimeZoned)))
770 assert.isTrue(values.some((value) => DateTime.isTimeZoneNamed(value.zone)))
771 assert.isTrue(values.some((value) => DateTime.isTimeZoneOffset(value.zone)))
772
773 const isBetweenDateTime = Schema.makeIsBetween({ order: DateTime.Order })
774 const constrained = Schema.DateTimeZoned.check(isBetweenDateTime({
775 minimum: DateTime.makeZonedUnsafe(0, { timeZone: "UTC" }),
776 maximum: DateTime.makeZonedUnsafe(10, { timeZone: "UTC" }),
777 exclusiveMinimum: true,
778 exclusiveMaximum: true
779 }))
780 const constrainedValues = yield* Arbitrary.sampleEffect(Arbitrary.schema(constrained), {
781 count: 500,
782 maxDiscards: 0,
783 seed: "date-time-zoned-constraints",
784 size: 10
785 })
786
787 assert.isTrue(constrainedValues.every(Schema.is(constrained)))
788 assert.isTrue(constrainedValues.every((value) => value.epochMilliseconds > 0 && value.epochMilliseconds < 10))
789
790 const result = yield* Arbitrary.checkEffect(arbitrary, () => false, {
791 runs: 1,
792 maxDiscards: 0,
793 seed: "date-time-zoned-shrink",
794 size: 10
795 })
796 assert.strictEqual(result._tag, "Falsified")
797 if (result._tag === "Falsified") assert.strictEqual(result.shrunkInput.epochMilliseconds, 0)
798 }))
799
800 it.effect("generates and shrinks Uint8Array declarations through Array semantics", () =>
801 Effect.gen(function*() {
802 const schema = Schema.Uint8Array.check(Schema.isMinLength(2), Schema.isMaxLength(10))
803 const arbitrary = Arbitrary.schema(schema)
804 const values = yield* Arbitrary.sampleEffect(arbitrary, {
805 count: 8_192,
806 maxDiscards: 0,
807 seed: "uint8-array-declaration",
808 size: 10
809 })
810 const lengths = values.map((value) => value.length)
811 const bytes = values.flatMap((value) => [...value])
812
813 assert.isTrue(values.every((value) => value instanceof globalThis.Uint8Array))
814 assert.isTrue(values.every(Schema.is(schema)))
815 assert.include(lengths, 2)
816 assert.include(lengths, 10)
817 assert.include(bytes, 0)
818 assert.include(bytes, 255)
819
820 const result = yield* Arbitrary.checkEffect(arbitrary, () => false, {
821 runs: 1,
822 maxDiscards: 0,
823 seed: "uint8-array-shrink",
824 size: 10
825 })
826 assert.strictEqual(result._tag, "Falsified")
827 if (result._tag === "Falsified") {
828 assert.instanceOf(result.shrunkInput, globalThis.Uint8Array)
829 assert.deepStrictEqual([...result.shrunkInput], [0, 0])
830 }
831 }))
832
833 it.effect("explores strings around an unanchored match", () =>
834 Effect.gen(function*() {
835 const schema = Schema.String.check(Schema.isIncluding("needle"))
836 const values = yield* Arbitrary.sampleEffect(Arbitrary.schema(schema), {
837 count: 100,
838 maxDiscards: 0,
839 seed: "unanchored-pattern"
840 })
841
842 assert.isTrue(values.every(Schema.is(schema)))
843 assert.isTrue(values.some((value) => value !== "needle"))
844 assert.isTrue(values.some((value) => value.startsWith("needle")))
845 assert.isTrue(values.some((value) => value.endsWith("needle")))
846 }))
847
848 it.effect("combines constructive pattern generation with string length constraints", () =>
849 Effect.gen(function*() {
850 const schema = Schema.String.check(
851 Schema.isPattern(/^(ab){3,10}$/),
852 Schema.isMinLength(8),
853 Schema.isMaxLength(12)
854 )
855 const values = yield* Arbitrary.sampleEffect(Arbitrary.schema(schema), {
856 count: 100,
857 maxDiscards: 0,
858 seed: "pattern-length"
859 })
860
861 assert.isTrue(values.every(Schema.is(schema)))
862 assert.isTrue(values.every((value) => value.length === 8 || value.length === 10 || value.length === 12))
863 }))
864
865 it.effect("uses every supported pattern as a candidate and validates against every pattern", () =>
866 Effect.gen(function*() {
867 const schema = Schema.String.check(
868 Schema.isPattern(/^[^A-Z]*$/),
869 Schema.isPattern(/^0x[0-9a-f]{40}$/)
870 )
871 const values = yield* Arbitrary.sampleEffect(Arbitrary.schema(schema), {
872 count: 20,
873 maxDiscards: 200,
874 seed: "multiple-patterns"
875 })
876
877 assert.isTrue(values.every(Schema.is(schema)))
878 }))
879
880 it.effect("keeps unsupported patterns as residual filters", () =>
881 Effect.gen(function*() {
882 const schema = Schema.String.check(
883 Schema.isPattern(/^(?=a)a$/),
884 Schema.isPattern(/^a$/)
885 )
886 const values = yield* Arbitrary.sampleEffect(Arbitrary.schema(schema), {
887 count: 20,
888 maxDiscards: 0,
889 seed: "unsupported-pattern-candidate"
890 })
891
892 assert.deepStrictEqual(values, globalThis.Array.from({ length: 20 }, () => "a"))
893 }))
894
895 it.effect("bounds fallback filtering for unsupported regular expression constructs", () =>
896 Effect.gen(function*() {
897 const schema = Schema.String.check(Schema.isPattern(/^(?=a)b$/))
898 const result = yield* Effect.result(Arbitrary.sampleEffect(Arbitrary.schema(schema), {
899 count: 1,
900 maxDiscards: 2,
901 seed: "unsupported-pattern"
902 }))
903
904 assert.isTrue(Result.isFailure(result))
905 if (Result.isFailure(result)) {
906 assert.deepStrictEqual(result.failure, {
907 _tag: "SampleError",
908 generated: 0,
909 discards: 3,
910 seed: "unsupported-pattern"
911 })
912 }
913 }))
914
915 it.effect("uses generic generation for permissive unsupported regular expressions", () =>
916 Effect.gen(function*() {
917 const schema = Schema.String.check(Schema.isPattern(/^(?=)[ -~]*$/))
918 const values = yield* Arbitrary.sampleEffect(Arbitrary.schema(schema), {
919 count: 20,
920 maxDiscards: 20,
921 seed: "unsupported-permissive-pattern"
922 })
923
924 assert.isTrue(values.every(Schema.is(schema)))
925 }))
926
927 it.effect("bounds incompatible pattern and length constraints", () =>
928 Effect.gen(function*() {
929 const schemas = [
930 Schema.String.check(
931 Schema.isPattern(/^(aa)+$/),
932 Schema.isMinLength(3),
933 Schema.isMaxLength(3)
934 ),
935 Schema.String.check(
936 Schema.isPattern(/^$/),
937 Schema.isMinLength(1)
938 )
939 ]
940
941 for (const schema of schemas) {
942 const result = yield* Effect.result(Arbitrary.sampleEffect(Arbitrary.schema(schema), {
943 count: 1,
944 maxDiscards: 2,
945 seed: "incompatible-pattern-length"
946 }))
947
948 assert.isTrue(Result.isFailure(result))
949 if (Result.isFailure(result)) {
950 assert.deepStrictEqual(result.failure, {
951 _tag: "SampleError",
952 generated: 0,
953 discards: 3,
954 seed: "incompatible-pattern-length"
955 })
956 }
957 }
958 }))
959
960 it.effect("keeps pattern constraints while shrinking strings", () =>
961 Effect.gen(function*() {
962 const schema = Schema.String.check(Schema.isPattern(/^a+$/), Schema.isMaxLength(8))
963 const evaluated: Array<string> = []
964 const result = yield* Arbitrary.checkEffect(
965 Arbitrary.schema(schema),
966 (value) => {
967 evaluated.push(value)
968 return value.length < 4
969 },
970 { runs: 100, seed: "pattern-shrink" }
971 )
972
973 assert.strictEqual(result._tag, "Falsified")
974 assert.isTrue(evaluated.every(Schema.is(schema)))
975 if (result._tag === "Falsified") assert.strictEqual(result.shrunkInput.length, 4)
976 }))
977
978 it.effect("shrinks fixed-length String code units toward null", () =>
979 Effect.gen(function*() {
980 const schema = Schema.String.check(Schema.isMinLength(3), Schema.isMaxLength(3))
981 const result = yield* Arbitrary.checkEffect(Arbitrary.schema(schema), () => false, {
982 runs: 1,
983 seed: "string-code-unit-shrink",
984 size: 3
985 })
986
987 assert.strictEqual(result._tag, "Falsified")
988 if (result._tag === "Falsified") {
989 assert.notStrictEqual(result.initialInput, "\0\0\0")
990 assert.strictEqual(result.shrunkInput, "\0\0\0")
991 }
992 }))
993
994 it.effect("structurally shrinks regular-expression repetitions", () =>
995 Effect.gen(function*() {
996 const schema = Schema.String.check(Schema.isPattern(/^a*b$/), Schema.isMinLength(1), Schema.isMaxLength(8))
997 const result = yield* Arbitrary.checkEffect(
998 Arbitrary.schema(schema),
999 () => false,
1000 { runs: 1, seed: "structural-pattern-shrink", size: 8 }
1001 )
1002
1003 assert.strictEqual(result._tag, "Falsified")
1004 if (result._tag === "Falsified") {
1005 assert.notStrictEqual(result.initialInput, "b")
1006 assert.strictEqual(result.shrunkInput, "b")
1007 }
1008 }))
1009
1010 it.effect("structurally shrinks regular-expression alternatives to the first one", () =>
1011 Effect.gen(function*() {
1012 const schema = Schema.String.check(Schema.isPattern(/^(foo|bar|baz)$/))
1013 const result = yield* Arbitrary.checkEffect(
1014 Arbitrary.schema(schema),
1015 () => false,
1016 { runs: 1, seed: "structural-alternative-shrink" }
1017 )
1018
1019 assert.strictEqual(result._tag, "Falsified")
1020 if (result._tag === "Falsified") {
1021 assert.notStrictEqual(result.initialInput, "foo")
1022 assert.strictEqual(result.shrunkInput, "foo")
1023 }
1024 }))
1025
1026 it.effect("derives independent random streams for adjacent attempts", () =>
1027 Effect.gen(function*() {
1028 const values = yield* Arbitrary.sampleEffect(
1029 Arbitrary.schema(Schema.Int.check(Schema.isBetween({ minimum: 0, maximum: Number.MAX_SAFE_INTEGER }))),
1030 { count: 1_000, maxDiscards: 0, seed: "attempt-streams" }
1031 )
1032
1033 // Numeric bias deliberately repeats small values, but attempt-local streams must still explore broadly.
1034 assert.isAtLeast(new Set(values).size, 750)
1035 }))
1036
1037 it.effect("reaches both integer parities across the complete safe range", () =>
1038 Effect.gen(function*() {
1039 const schema = Schema.Int.check(Schema.isBetween({
1040 minimum: Number.MIN_SAFE_INTEGER,
1041 maximum: Number.MAX_SAFE_INTEGER
1042 }))
1043 const values = yield* Arbitrary.sampleEffect(Arbitrary.schema(schema), {
1044 count: 200,
1045 maxDiscards: 0,
1046 seed: "complete-safe-integer-range"
1047 })
1048
1049 assert.isTrue(values.every(Number.isSafeInteger))
1050 assert.isTrue(values.some((value) => Math.abs(value % 2) === 0))
1051 assert.isTrue(values.some((value) => Math.abs(value % 2) === 1))
1052 }))
1053
1054 it.effect("spreads bounded integer samples across the complete domain", () =>
1055 Effect.gen(function*() {
1056 const values = yield* Arbitrary.sampleEffect(
1057 Arbitrary.schema(Schema.Int.check(Schema.isBetween({ minimum: 0, maximum: 7 }))),
1058 { count: 8_192, maxDiscards: 0, seed: "bounded-integer-buckets" }
1059 )
1060 const buckets = Array.from({ length: 8 }, () => 0)
1061 for (const value of values) buckets[value]!++
1062
1063 // Like fast-check v4.9.0's broad numeric smoke assertions, these bounds detect collapsed PRNG buckets without
1064 // specifying a public distribution.
1065 // https://github.com/dubzzz/fast-check/blob/v4.9.0/packages/fast-check/test/e2e/arbitraries/DoubleArbitrary.spec.ts
1066 for (const bucket of buckets) {
1067 assert.isAtLeast(bucket, values.length * 0.05)
1068 assert.isAtMost(bucket, values.length * 0.3)
1069 }
1070 }))
1071
1072 it.effect("spreads bounded Number samples across one IEEE-754 binade", () =>
1073 Effect.gen(function*() {
1074 const schema = Schema.Number.check(Schema.isBetween({
1075 minimum: 1,
1076 maximum: 2,
1077 exclusiveMaximum: true
1078 }))
1079 const values = yield* Arbitrary.sampleEffect(Arbitrary.schema(schema), {
1080 count: 8_192,
1081 maxDiscards: 0,
1082 seed: "bounded-number-buckets"
1083 })
1084 const buckets = Array.from({ length: 4 }, () => 0)
1085 for (const value of values) buckets[Math.floor((value - 1) * 4)]!++
1086
1087 // Edge weighting may change privately, but it must preserve broad coverage of the binade.
1088 for (const bucket of buckets) {
1089 assert.isAtLeast(bucket, values.length * 0.1)
1090 assert.isAtMost(bucket, values.length * 0.4)
1091 }
1092 }))
1093
1094 it.effect("targets bounded integer and BigInt edges while preserving broad coverage", () =>
1095 Effect.gen(function*() {
1096 const minimumInt = -1_000_000
1097 const maximumInt = 1_000_000
1098 const ints = yield* Arbitrary.sampleEffect(
1099 Arbitrary.schema(Schema.Int.check(Schema.isBetween({ minimum: minimumInt, maximum: maximumInt }))),
1100 { count: 25_000, maxDiscards: 0, seed: "numeric-edge-bias-int" }
1101 )
1102 assert.include(ints, minimumInt)
1103 assert.include(ints, 0)
1104 assert.include(ints, maximumInt)
1105 assert.isAtLeast(new Set(ints).size, 15_000)
1106
1107 const minimumBigInt = -(BigInt(1) << BigInt(255))
1108 const maximumBigInt = (BigInt(1) << BigInt(255)) - BigInt(1)
1109 const bigints = yield* Arbitrary.sampleEffect(
1110 Arbitrary.schema(Schema.BigInt.check(Schema.isBetweenBigInt({
1111 minimum: minimumBigInt,
1112 maximum: maximumBigInt
1113 }))),
1114 { count: 25_000, maxDiscards: 0, seed: "numeric-edge-bias-bigint" }
1115 )
1116 assert.include(bigints, minimumBigInt)
1117 assert.include(bigints, BigInt(0))
1118 assert.include(bigints, maximumBigInt)
1119 assert.isAtLeast(new Set(bigints).size, 15_000)
1120 }))
1121
1122 it.effect("targets String, Array, and Record cardinality boundaries while preserving intermediate sizes", () =>
1123 Effect.gen(function*() {
1124 const count = 8_192
1125 const assertBoundaryBias = Effect.fnUntraced(function*<A>(
1126 schema: Schema.Schema<A>,
1127 cardinality: (value: A) => number
1128 ) {
1129 const values = yield* Arbitrary.sampleEffect(Arbitrary.schema(schema), {
1130 count,
1131 maxDiscards: count,
1132 seed: "length-edge-bias",
1133 size: 10
1134 })
1135 const buckets = Array.from({ length: 11 }, () => 0)
1136 for (const value of values) buckets[cardinality(value)]!++
1137
1138 assert.isAtLeast(buckets[0], count * 0.12)
1139 assert.isAtLeast(buckets[10], count * 0.12)
1140 for (const bucket of buckets.slice(1, -1)) assert.isAtLeast(bucket, count * 0.04)
1141 })
1142
1143 yield* assertBoundaryBias(Schema.String.check(Schema.isMaxLength(10)), (value) => value.length)
1144 yield* assertBoundaryBias(Schema.Array(Schema.Null).check(Schema.isMaxLength(10)), (value) => value.length)
1145 yield* assertBoundaryBias(
1146 Schema.Record(Schema.String, Schema.Null).check(Schema.isMaxProperties(10)),
1147 (value) => Object.keys(value).length
1148 )
1149 }))
1150
1151 it.effect("starts progressive checks for Records with an explicit minimum property count", () =>
1152 Effect.gen(function*() {
1153 const schema = Schema.Record(Schema.String, Schema.Number).check(Schema.isMinProperties(2))
1154 const result = yield* Arbitrary.checkEffect(Arbitrary.schema(schema), Schema.is(schema), {
1155 runs: 20,
1156 maxDiscards: 200,
1157 seed: "record-min-properties-progressive"
1158 })
1159
1160 assert.strictEqual(result._tag, "Passed")
1161 }))
1162
1163 it.effect("generates ordinary and null-prototype structural objects", () =>
1164 Effect.gen(function*() {
1165 const schema = Schema.Struct({ value: Schema.Int })
1166 const values = yield* Arbitrary.sampleEffect(Arbitrary.schema(schema), {
1167 count: 512,
1168 maxDiscards: 0,
1169 seed: "object-prototypes"
1170 })
1171 const prototypes = new Set(values.map(Object.getPrototypeOf))
1172
1173 assert.isTrue(values.every(Schema.is(schema)))
1174 assert.isTrue(prototypes.has(Object.prototype))
1175 assert.isTrue(prototypes.has(null))
1176 }))
1177
1178 describe("indexed objects", () => {
1179 it.effect("combines fixed field constraints at size zero and starts progressive checks", () =>
1180 Effect.gen(function*() {
1181 const schema = Schema.StructWithRest(
1182 Schema.Struct({ fixed: Schema.String }),
1183 [Schema.Record(Schema.String, Schema.NonEmptyString)]
1184 )
1185 const arbitrary = Arbitrary.schema(schema)
1186 const values = yield* Arbitrary.sampleEffect(arbitrary, {
1187 count: 128,
1188 maxDiscards: 0,
1189 seed: "indexed-fixed-fields",
1190 size: 0
1191 })
1192 assert.isTrue(values.every(Schema.is(schema)))
1193 assert.isTrue(values.every((value) => value.fixed.length === 1))
1194 const prototypes = new Set(values.map(Object.getPrototypeOf))
1195 assert.isTrue(prototypes.has(null))
1196 assert.isTrue(prototypes.has(Object.prototype))
1197
1198 const result = yield* Arbitrary.checkEffect(arbitrary, Schema.is(schema), {
1199 runs: 100,
1200 maxDiscards: 128,
1201 seed: "indexed-fixed-fields"
1202 })
1203 assert.strictEqual(result._tag, "Passed")
1204 }))
1205
1206 it.effect("does not generate omitted fixed property names from indexes", () =>
1207 Effect.gen(function*() {
1208 const cases = [
1209 ["a", Schema.Struct({ a: Schema.optionalKey(Schema.Never) })],
1210 [0, Schema.Record(Schema.Literal(0), Schema.optionalKey(Schema.Never))]
1211 ] as const
1212 for (const [key, fields] of cases) {
1213 const schema = Schema.StructWithRest(fields, [
1214 Schema.Record(Schema.String.check(Schema.isPattern(new RegExp(`^${key}$`))), Schema.Boolean)
1215 ])
1216 const values = yield* Arbitrary.sampleEffect(Arbitrary.schema(schema), {
1217 count: 20,
1218 maxDiscards: 100,
1219 seed: "indexed-optional-never",
1220 size: 1
1221 })
1222
1223 assert.isTrue(values.every(Schema.is(schema)))
1224 assert.isTrue(values.every((value) => !(key in value)))
1225
1226 const shrinkingSchema = Schema.StructWithRest(fields, [
1227 Schema.Record(Schema.String.check(Schema.isPattern(new RegExp(`^${key}+$`))), Schema.Boolean)
1228 ]).check(Schema.isBetweenProperties(1, 1))
1229 const result = yield* Arbitrary.checkEffect(Arbitrary.schema(shrinkingSchema), () => false, {
1230 runs: 1,
1231 maxDiscards: 100,
1232 maxShrinks: 100,
1233 seed: 0,
1234 size: 4
1235 })
1236
1237 assert.strictEqual(result._tag, "Falsified")
1238 if (result._tag === "Falsified") {
1239 assert.deepStrictEqual(result.shrunkInput, { [`${key}${key}`]: false })
1240 assert.isTrue(Schema.is(shrinkingSchema)(result.shrunkInput))
1241 }
1242 }
1243 }))
1244
1245 it.effect("uses indexes when optional properties cannot satisfy the minimum", () =>
1246 Effect.gen(function*() {
1247 const schema = Schema.StructWithRest(
1248 Schema.Struct({ a: Schema.optionalKey(Schema.Never) }),
1249 [Schema.Record(Schema.String.check(Schema.isPattern(/^b$/)), Schema.Boolean)]
1250 ).check(Schema.isBetweenProperties(1, 1))
1251 const values = yield* Arbitrary.sampleEffect(Arbitrary.schema(schema), {
1252 count: 20,
1253 maxDiscards: 0,
1254 seed: "indexed-optional-never-minimum",
1255 size: 0
1256 })
1257
1258 assert.isTrue(values.every(Schema.is(schema)))
1259 assert.isTrue(values.every((value) => "b" in value && !("a" in value)))
1260 }))
1261
1262 it.effect("shrinks and replays fixed fields within the combined constraints", () =>
1263 Effect.gen(function*() {
1264 const schema = Schema.StructWithRest(
1265 Schema.Struct({ fixed: Schema.String }),
1266 [Schema.Record(Schema.String, Schema.NonEmptyString)]
1267 )
1268 const arbitrary = Arbitrary.schema(schema)
1269 const seen: Array<typeof schema.Type> = []
1270 const property = (value: typeof schema.Type) => {
1271 seen.push(value)
1272 return false
1273 }
1274 const result = yield* Arbitrary.checkEffect(arbitrary, property, {
1275 runs: 1,
1276 maxDiscards: 128,
1277 maxShrinks: 64,
1278 seed: "indexed-object-shrinking",
1279 size: 8
1280 })
1281 assert.strictEqual(result._tag, "Falsified")
1282 if (result._tag !== "Falsified") return
1283 assert.isAbove(result.shrinks, 0)
1284 assert.strictEqual(result.shrunkInput.fixed.length, 1)
1285 assert.strictEqual(Object.getPrototypeOf(result.shrunkInput), Object.getPrototypeOf(result.initialInput))
1286 const replayed = yield* Arbitrary.checkEffect(arbitrary, property, { replay: result.replay })
1287 assert.strictEqual(replayed._tag, "Falsified")
1288 if (replayed._tag === "Falsified") {
1289 assert.deepStrictEqual(replayed.shrunkInput, result.shrunkInput)
1290 assert.strictEqual(replayed.shrinks, result.shrinks)
1291 }
1292 assert.isTrue(seen.every(Schema.is(schema)))
1293 }))
1294
1295 it.effect("combines numeric and bigint bounds with fixed field constraints", () =>
1296 Effect.gen(function*() {
1297 const schemas = [
1298 Schema.StructWithRest(
1299 Schema.Struct({ fixed: Schema.Number }),
1300 [Schema.Record(Schema.String, Schema.Int.check(Schema.isBetween({ minimum: 10, maximum: 20 })))]
1301 ),
1302 Schema.StructWithRest(
1303 Schema.Struct({ fixed: Schema.BigInt.check(Schema.isLessThanOrEqualToBigInt(BigInt(15))) }),
1304 [Schema.Record(Schema.String, Schema.BigInt.check(Schema.isGreaterThanOrEqualToBigInt(BigInt(10))))]
1305 )
1306 ]
1307 for (const schema of schemas) {
1308 const values = yield* Arbitrary.sampleEffect(Arbitrary.schema(schema), {
1309 count: 32,
1310 maxDiscards: 0,
1311 seed: "indexed-numeric-bounds",
1312 size: 0
1313 })
1314 assert.isTrue(values.every(Schema.is(schema)))
1315 }
1316 }))
1317
1318 it.effect("combines overlapping index constraints after generating the key", () =>
1319 Effect.gen(function*() {
1320 const schema = Schema.StructWithRest(Schema.Struct({}), [
1321 Schema.Record(Schema.String, Schema.String),
1322 Schema.Record(Schema.String, Schema.NonEmptyString)
1323 ]).check(Schema.isMinProperties(1))
1324 const values = yield* Arbitrary.sampleEffect(Arbitrary.schema(schema), {
1325 count: 128,
1326 maxDiscards: 0,
1327 seed: "indexed-overlap",
1328 size: 0
1329 })
1330 assert.isTrue(values.every(Schema.is(schema)))
1331 }))
1332
1333 it.effect("discards empty scalar intersections without failing compilation", () =>
1334 Effect.gen(function*() {
1335 const schemas = [
1336 Schema.StructWithRest(
1337 Schema.Struct({ fixed: Schema.String.check(Schema.isMaxLength(0)) }),
1338 [Schema.Record(Schema.String, Schema.NonEmptyString)]
1339 ),
1340 Schema.StructWithRest(
1341 Schema.Struct({ fixed: Schema.Int }),
1342 [Schema.Record(Schema.String, Schema.Number.check(Schema.isBetween({ minimum: 0.2, maximum: 0.8 })))]
1343 ),
1344 Schema.StructWithRest(
1345 Schema.Struct({ fixed: Schema.BigInt.check(Schema.isLessThanOrEqualToBigInt(BigInt(1))) }),
1346 [Schema.Record(Schema.String, Schema.BigInt.check(Schema.isGreaterThanBigInt(BigInt(1))))]
1347 )
1348 ]
1349 for (const schema of schemas) {
1350 const result = yield* Effect.result(Arbitrary.sampleEffect(Arbitrary.schema(schema), {
1351 count: 1,
1352 maxDiscards: 0,
1353 seed: "indexed-empty-intersection",
1354 size: 0
1355 }))
1356 assert.isTrue(Result.isFailure(result))
1357 if (Result.isFailure(result)) assert.strictEqual(result.failure._tag, "SampleError")
1358 }
1359 }))
1360
1361 it.effect("selects matching pattern, template, numeric and symbol indexes", () =>
1362 Effect.gen(function*() {
1363 const symbol = Symbol.for("indexed-field")
1364 const schema = Schema.StructWithRest(
1365 Schema.Struct({
1366 a: Schema.String,
1367 other: Schema.String,
1368 "pre_x": Schema.String,
1369 0: Schema.String,
1370 [symbol]: Schema.String
1371 }),
1372 [
1373 Schema.Record(Schema.String.check(Schema.isPattern(/^a/)), Schema.NonEmptyString),
1374 Schema.Record(Schema.TemplateLiteral(["pre_", Schema.String]), Schema.NonEmptyString),
1375 Schema.Record(Schema.Int, Schema.NonEmptyString),
1376 Schema.Record(Schema.Symbol, Schema.NonEmptyString)
1377 ]
1378 )
1379 const values = yield* Arbitrary.sampleEffect(Arbitrary.schema(schema), {
1380 count: 32,
1381 maxDiscards: 0,
1382 seed: "indexed-key-matching",
1383 size: 0
1384 })
1385 assert.isTrue(values.every(Schema.is(schema)))
1386 assert.isTrue(values.every((value) => value.other === ""))
1387 }))
1388
1389 it.effect("validates residual index filters without constructive constraints", () =>
1390 Effect.gen(function*() {
1391 const schema = Schema.StructWithRest(
1392 Schema.Struct({ fixed: Schema.String }),
1393 [Schema.Record(Schema.String, Schema.String.check(Schema.makeFilter((value) => value.length > 0)))]
1394 )
1395 const arbitrary = Arbitrary.schema(schema)
1396 const result = yield* Effect.result(Arbitrary.sampleEffect(arbitrary, {
1397 count: 1,
1398 maxDiscards: 0,
1399 seed: "indexed-residual-filter",
1400 size: 0
1401 }))
1402 assert.isTrue(Result.isFailure(result))
1403 if (Result.isFailure(result)) assert.strictEqual(result.failure._tag, "SampleError")
1404 const values = yield* Arbitrary.sampleEffect(arbitrary, {
1405 count: 32,
1406 maxDiscards: 128,
1407 seed: "indexed-residual-filter",
1408 size: 4
1409 })
1410 assert.isTrue(values.every(Schema.is(schema)))
1411 const seen: Array<typeof schema.Type> = []
1412 const checked = yield* Arbitrary.checkEffect(arbitrary, (value) => {
1413 seen.push(value)
1414 return false
1415 }, {
1416 runs: 1,
1417 maxDiscards: 128,
1418 maxShrinks: 64,
1419 seed: "indexed-residual-filter",
1420 size: 4
1421 })
1422 assert.strictEqual(checked._tag, "Falsified")
1423 if (checked._tag === "Falsified") assert.isAbove(checked.shrinks, 0)
1424 assert.isTrue(seen.every(Schema.is(schema)))
1425 }))
1426
1427 it.effect("rejects key shrinks that enter an incompatible index", () =>
1428 Effect.gen(function*() {
1429 const schema = Schema.StructWithRest(Schema.Struct({}), [
1430 Schema.Record(Schema.String, Schema.String.check(Schema.isMaxLength(0))),
1431 Schema.Record(Schema.String.check(Schema.isMaxLength(1)), Schema.NonEmptyString)
1432 ]).check(Schema.isMinProperties(1), Schema.isMaxProperties(1))
1433 const seen: Array<typeof schema.Type> = []
1434 const result = yield* Arbitrary.checkEffect(Arbitrary.schema(schema), (value) => {
1435 seen.push(value)
1436 return false
1437 }, {
1438 runs: 1,
1439 maxDiscards: 128,
1440 maxShrinks: 64,
1441 seed: "indexed-key-shrinking",
1442 size: 8
1443 })
1444 assert.strictEqual(result._tag, "Falsified")
1445 if (result._tag === "Falsified") assert.isAbove(result.shrinks, 0)
1446 assert.isTrue(seen.every(Schema.is(schema)))
1447 assert.isTrue(seen.every((value) => Object.keys(value)[0].length > 1))
1448 }))
1449 })
1450
1451 it.effect("preserves special Record keys with null-prototype objects", () =>
1452 Effect.gen(function*() {
1453 const schema = Schema.Record(Schema.Literal("__proto__"), Schema.Literal("value"))
1454 const values = yield* Arbitrary.sampleEffect(Arbitrary.schema(schema), {
1455 count: 512,
1456 maxDiscards: 0,
1457 seed: "record-object-prototypes"
1458 })
1459
1460 assert.isTrue(values.every(Schema.is(schema)))
1461 assert.isTrue(values.some((value) => Object.getPrototypeOf(value) === null))
1462 assert.isTrue(values.every((value) => Object.hasOwn(value, "__proto__") && value.__proto__ === "value"))
1463 }))
1464
1465 it.effect("preserves null prototypes while shrinking and replaying", () =>
1466 Effect.gen(function*() {
1467 const schema = Schema.Struct({
1468 value: Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 100 }))
1469 })
1470 const arbitrary = Arbitrary.schema(schema)
1471 const property = (value: typeof schema.Type) => Object.getPrototypeOf(value) !== null
1472 const result = yield* Arbitrary.checkEffect(arbitrary, property, {
1473 runs: 100,
1474 maxDiscards: 0,
1475 seed: "null-prototype-shrink",
1476 size: 100
1477 })
1478
1479 assert.strictEqual(result._tag, "Falsified")
1480 if (result._tag === "Falsified") {
1481 assert.strictEqual(Object.getPrototypeOf(result.initialInput), null)
1482 assert.strictEqual(Object.getPrototypeOf(result.shrunkInput), null)
1483 assert.isTrue(Schema.is(schema)(result.shrunkInput))
1484 const replayed = yield* Arbitrary.checkEffect(arbitrary, property, { replay: result.replay })
1485 assert.strictEqual(replayed._tag, "Falsified")
1486 if (replayed._tag === "Falsified") {
1487 assert.deepStrictEqual(replayed.initialInput, result.initialInput)
1488 assert.deepStrictEqual(replayed.shrunkInput, result.shrunkInput)
1489 assert.deepStrictEqual(replayed.failure, result.failure)
1490 assert.strictEqual(replayed.shrinks, result.shrinks)
1491 assert.strictEqual(replayed.replay, result.replay)
1492 }
1493 }
1494 }))
1495
1496 it.effect("injects the approved JavaScript String edge corpus", () =>
1497 Effect.gen(function*() {
1498 const corpus = [
1499 "",
1500 " ",
1501 "\t",
1502 "\n",
1503 "\0",
1504 "0",
1505 "-1",
1506 "4294967295",
1507 "__proto__",
1508 "constructor",
1509 "prototype",
1510 "toString",
1511 "\uD800",
1512 "\uDC00",
1513 "😀"
1514 ]
1515 const values = yield* Arbitrary.sampleEffect(Arbitrary.schema(Schema.String), {
1516 count: 8_192,
1517 maxDiscards: 0,
1518 seed: "string-edge-corpus",
1519 size: 16
1520 })
1521
1522 for (const value of corpus) assert.include(values, value)
1523 }))
1524
1525 it.effect("scales unconstrained String, Array, and Record sizes without hidden ceilings", () =>
1526 Effect.gen(function*() {
1527 const size = 32
1528 const strings = yield* Arbitrary.sampleEffect(Arbitrary.schema(Schema.String), {
1529 count: 200,
1530 maxDiscards: 0,
1531 seed: "unconstrained-size",
1532 size
1533 })
1534 const arrays = yield* Arbitrary.sampleEffect(Arbitrary.schema(Schema.Array(Schema.Null)), {
1535 count: 200,
1536 maxDiscards: 0,
1537 seed: "unconstrained-size",
1538 size
1539 })
1540 const records = yield* Arbitrary.sampleEffect(Arbitrary.schema(Schema.Record(Schema.String, Schema.Null)), {
1541 count: 200,
1542 maxDiscards: 200,
1543 seed: "unconstrained-size",
1544 size
1545 })
1546
1547 assert.isTrue(strings.some((value) => value.length > 16))
1548 assert.isTrue(arrays.some((value) => value.length > 8))
1549 assert.isTrue(records.some((value) => Object.keys(value).length > 8))
1550 }))
1551
1552 it.effect("scales the specialized Json generator with size", () =>
1553 Effect.gen(function*() {
1554 const values = yield* Arbitrary.sampleEffect(Arbitrary.schema(Schema.Json), {
1555 count: 200,
1556 maxDiscards: 0,
1557 seed: "json-size",
1558 size: 32
1559 })
1560
1561 assert.isTrue(values.some((value) => typeof value === "string" && value.length > 8))
1562 assert.isTrue(values.some((value) => Array.isArray(value) && value.length > 3))
1563 const objects = values.filter((value): value is Record<string, Schema.Json> => {
1564 return typeof value === "object" && value !== null && !Array.isArray(value)
1565 })
1566 assert.isTrue(objects.some((value) => Object.keys(value).length > 3))
1567 assert.isTrue(objects.flatMap((value) => Object.keys(value)).some((key) => !/^key\d+$/.test(key)))
1568 const prototypes = new Set(objects.map(Object.getPrototypeOf))
1569 assert.isTrue(prototypes.has(Object.prototype))
1570 assert.isTrue(prototypes.has(null))
1571 }))
1572
1573 it.effect("targets IEEE-754 edge cases while preserving intermediate values", () =>
1574 Effect.gen(function*() {
1575 // This follows fast-check v4.9.0's broad edge-coverage contract without fixing exact frequencies.
1576 // https://github.com/dubzzz/fast-check/blob/v4.9.0/packages/fast-check/test/e2e/arbitraries/DoubleArbitrary.spec.ts
1577 const values = yield* Arbitrary.sampleEffect(Arbitrary.schema(Schema.Number), {
1578 count: 25_000,
1579 maxDiscards: 0,
1580 seed: "numeric-edge-bias-number"
1581 })
1582 const expected = [
1583 Number.NEGATIVE_INFINITY,
1584 -Number.MAX_VALUE,
1585 -Number.MIN_VALUE,
1586 -0,
1587 0,
1588 Number.MIN_VALUE,
1589 Number.MAX_VALUE,
1590 Number.POSITIVE_INFINITY,
1591 Number.NaN
1592 ]
1593 for (const value of expected) {
1594 assert.isTrue(values.some((candidate) => Object.is(candidate, value)))
1595 }
1596 const intermediate = values.filter((value) => {
1597 const absolute = Math.abs(value)
1598 return absolute >= 2 ** -1021 && absolute < 2 ** 1023
1599 })
1600 assert.isAtLeast(intermediate.length, values.length * 0.5)
1601 }))
1602
1603 it.effect("reaches NaN through a one-sided upper Number bound", () =>
1604 Effect.gen(function*() {
1605 const schema = Schema.Number.check(Schema.isLessThanOrEqualTo(0))
1606 const values = yield* Arbitrary.sampleEffect(Arbitrary.schema(schema), {
1607 count: 1_000,
1608 maxDiscards: 0,
1609 seed: "upper-bounded-number-nan"
1610 })
1611
1612 assert.isTrue(values.every(Schema.is(schema)))
1613 assert.isTrue(values.some(Number.isNaN))
1614 }))
1615
1616 it("rejects integer bounds outside the safe range", () => {
1617 assert.throws(
1618 () => Arbitrary.schema(Schema.Int.check(Schema.isGreaterThan(Number.MAX_SAFE_INTEGER))),
1619 /Unable to derive an arbitrary for integer constraints/
1620 )
1621 assert.throws(
1622 () => Arbitrary.schema(Schema.Int.check(Schema.isLessThan(Number.MIN_SAFE_INTEGER))),
1623 /Unable to derive an arbitrary for integer constraints/
1624 )
1625 })
1626
1627 it("rejects NaN numeric bounds", () => {
1628 const isBetweenNumber = Schema.makeIsBetween({ order: Order.Number })
1629 assert.throws(
1630 () => Arbitrary.schema(Schema.Number.check(isBetweenNumber({ minimum: Number.NaN, maximum: 1 }))),
1631 /Unable to derive an arbitrary for number constraints/
1632 )
1633 assert.throws(
1634 () => Arbitrary.schema(Schema.Int.check(isBetweenNumber({ minimum: 0, maximum: Number.NaN }))),
1635 /Unable to derive an arbitrary for integer constraints/
1636 )
1637 })
1638
1639 it.effect("generates BigInts across arbitrary-width bounded ranges", () =>
1640 Effect.gen(function*() {
1641 const maximum = BigInt(1) << BigInt(1024)
1642 const schema = Schema.BigInt.check(Schema.isBetweenBigInt({ minimum: BigInt(0), maximum }))
1643 const values = yield* Arbitrary.sampleEffect(Arbitrary.schema(schema), {
1644 count: 20,
1645 maxDiscards: 0,
1646 seed: "arbitrary-width-bigint"
1647 })
1648
1649 assert.isTrue(values.every((value) => value >= BigInt(0) && value <= maximum))
1650 assert.isTrue(values.some((value) => value > BigInt(Number.MAX_SAFE_INTEGER)))
1651 }))
1652
1653 it.effect("generates unbounded BigInts across intermediate bit lengths", () =>
1654 Effect.gen(function*() {
1655 const values = yield* Arbitrary.sampleEffect(Arbitrary.schema(Schema.BigInt), {
1656 count: 1_000,
1657 maxDiscards: 0,
1658 seed: "unbounded-bigint-bit-lengths",
1659 size: 1
1660 })
1661 const bitLengths = values.map((value) => {
1662 const magnitude = value < 0n ? -value : value
1663 return magnitude === 0n ? 0 : magnitude.toString(2).length
1664 })
1665
1666 assert.isTrue(values.some((value) => value < 0n))
1667 assert.isTrue(values.some((value) => value > 0n))
1668 assert.isTrue(bitLengths.some((length) => length > 512))
1669 assert.isAtLeast(new Set(bitLengths).size, 100)
1670 }))
1671
1672 it.effect("constructs one-sided BigInts beyond a large finite bound", () =>
1673 Effect.gen(function*() {
1674 const minimum = 1n << 4_096n
1675 const schema = Schema.BigInt.check(Schema.isGreaterThanOrEqualToBigInt(minimum))
1676 const values = yield* Arbitrary.sampleEffect(Arbitrary.schema(schema), {
1677 count: 100,
1678 maxDiscards: 0,
1679 seed: "one-sided-large-bigint"
1680 })
1681
1682 assert.isTrue(values.every((value) => value >= minimum))
1683 assert.isTrue(values.some((value) => value > minimum))
1684 }))
1685
1686 it.effect("covers intermediate BigInt magnitudes on the finite side of one-sided bounds", () =>
1687 Effect.gen(function*() {
1688 const bound = 1n << 4096n
1689 for (const sign of [1n, -1n]) {
1690 const schema = Schema.BigInt.check(
1691 sign > 0n
1692 ? Schema.isLessThanBigInt(bound)
1693 : Schema.isGreaterThanBigInt(-bound)
1694 )
1695 for (const seed of ["one-sided-intermediate", "one-sided-intermediate-2"]) {
1696 const values = yield* Arbitrary.sampleEffect(Arbitrary.schema(schema), {
1697 count: 1_000,
1698 maxDiscards: 0,
1699 seed,
1700 size: 1
1701 })
1702
1703 assert.isTrue(values.every(Schema.is(schema)))
1704 assert.isTrue(values.some((value) => sign * value > (1n << 2500n) && sign * value < (1n << 3500n)))
1705 assert.isTrue(values.some((value) => value < 0n))
1706 assert.isTrue(values.some((value) => value > 0n))
1707 assert.isTrue(values.some((value) => value !== 0n && value > -100n && value < 100n))
1708 }
1709 }
1710 }))
1711
1712 it.effect("shrinks BigInts to a common semantic boundary", () =>
1713 Effect.gen(function*() {
1714 const boundary = BigInt(Number.MAX_SAFE_INTEGER)
1715 const arbitrary = Arbitrary.schema(Schema.BigInt)
1716 const result = yield* Arbitrary.checkEffect(arbitrary, (value) => value <= boundary, {
1717 runs: 200,
1718 maxDiscards: 0,
1719 maxShrinks: 100,
1720 seed: "bigint-semantic-boundary"
1721 })
1722
1723 assert.strictEqual(result._tag, "Falsified")
1724 if (result._tag === "Falsified") {
1725 assert.strictEqual(result.shrunkInput, boundary + 1n)
1726 const replayed = yield* Arbitrary.checkEffect(arbitrary, (value) => value <= boundary, {
1727 replay: result.replay
1728 })
1729 assert.strictEqual(replayed._tag, "Falsified")
1730 if (replayed._tag === "Falsified") {
1731 assert.strictEqual(replayed.initialInput, result.initialInput)
1732 assert.strictEqual(replayed.shrunkInput, result.shrunkInput)
1733 assert.strictEqual(replayed.shrinks, result.shrinks)
1734 }
1735 }
1736 }))
1737
1738 it.effect("generates the sole Number between adjacent exclusive IEEE-754 bounds", () =>
1739 Effect.gen(function*() {
1740 const minimum = 2 ** 100
1741 const unit = 2 ** 48
1742 const expected = minimum + unit
1743 const schema = Schema.Number.check(Schema.isBetween({
1744 minimum,
1745 maximum: minimum + unit * 2,
1746 exclusiveMinimum: true,
1747 exclusiveMaximum: true
1748 }))
1749 const values = yield* Arbitrary.sampleEffect(Arbitrary.schema(schema), {
1750 count: 20,
1751 maxDiscards: 0,
1752 seed: "adjacent-ieee-bounds"
1753 })
1754
1755 assert.deepStrictEqual(values, Array.from({ length: 20 }, () => expected))
1756 }))
1757
1758 it.effect("preserves both signed zeros in an inclusive zero interval", () =>
1759 Effect.gen(function*() {
1760 const schema = Schema.Number.check(Schema.isBetween({ minimum: 0, maximum: 0 }))
1761 const values = yield* Arbitrary.sampleEffect(Arbitrary.schema(schema), {
1762 count: 50,
1763 maxDiscards: 0,
1764 seed: "signed-zero"
1765 })
1766
1767 assert.isTrue(values.every((value) => value === 0))
1768 assert.isTrue(values.some((value) => Object.is(value, -0)))
1769 assert.isTrue(values.some((value) => Object.is(value, 0)))
1770 }))
1771
1772 it.effect("generates exact subnormal and infinite singleton intervals", () =>
1773 Effect.gen(function*() {
1774 const expected = [
1775 Number.NEGATIVE_INFINITY,
1776 -Number.MAX_VALUE,
1777 -Number.MIN_VALUE,
1778 Number.MIN_VALUE,
1779 Number.MAX_VALUE,
1780 Number.POSITIVE_INFINITY
1781 ]
1782 const values: Array<number> = []
1783 const isBetweenNumber = Schema.makeIsBetween({ order: Order.Number })
1784 for (const value of expected) {
1785 const schema = Schema.Number.check(isBetweenNumber({ minimum: value, maximum: value }))
1786 const [sample] = yield* Arbitrary.sampleEffect(Arbitrary.schema(schema), {
1787 count: 1,
1788 maxDiscards: 0,
1789 seed: "ieee-singleton"
1790 })
1791 values.push(sample)
1792 }
1793
1794 assert.deepStrictEqual(values, expected)
1795 }))
1796
1797 it.effect("generates infinity from one-sided inclusive bounds", () =>
1798 Effect.gen(function*() {
1799 const greaterThanOrEqualTo = Schema.makeIsGreaterThanOrEqualTo({ order: Order.Number })
1800 const lessThanOrEqualTo = Schema.makeIsLessThanOrEqualTo({ order: Order.Number })
1801 const [positive] = yield* Arbitrary.sampleEffect(
1802 Arbitrary.schema(Schema.Number.check(greaterThanOrEqualTo(Number.POSITIVE_INFINITY))),
1803 { count: 1, maxDiscards: 0, seed: "positive-infinity" }
1804 )
1805 const negativeSchema = Schema.Number.check(lessThanOrEqualTo(Number.NEGATIVE_INFINITY))
1806 const negatives = yield* Arbitrary.sampleEffect(
1807 Arbitrary.schema(negativeSchema),
1808 { count: 20, maxDiscards: 0, seed: "negative-infinity" }
1809 )
1810
1811 assert.strictEqual(positive, Number.POSITIVE_INFINITY)
1812 assert.isTrue(negatives.every(Schema.is(negativeSchema)))
1813 assert.include(negatives, Number.NEGATIVE_INFINITY)
1814 assert.isTrue(negatives.some(Number.isNaN))
1815 }))
1816
1817 it("rejects infinite intervals excluded by a finite constraint", () => {
1818 const greaterThanOrEqualTo = Schema.makeIsGreaterThanOrEqualTo({ order: Order.Number })
1819 const schema = Schema.Number.check(
1820 Schema.isFinite(),
1821 greaterThanOrEqualTo(Number.POSITIVE_INFINITY)
1822 )
1823
1824 assert.throws(() => Arbitrary.schema(schema), /Unable to derive an arbitrary for number constraints/)
1825 })
1826
1827 it.effect("uses the same generated value for sampling and a single-run check", () =>
1828 Effect.gen(function*() {
1829 const arbitrary = Arbitrary.schema(Schema.Struct({
1830 values: Schema.Array(Schema.Int).check(Schema.isMinLength(2), Schema.isMaxLength(4)),
1831 unique: Schema.UniqueArray(Schema.Int).check(Schema.isMinLength(2), Schema.isMaxLength(4))
1832 }))
1833 const sampled = yield* Arbitrary.sampleEffect(arbitrary, { count: 1, seed: "sample-check", size: 10 })
1834 let checked: (typeof sampled)[number] | undefined
1835 yield* Arbitrary.checkEffect(arbitrary, (value) => {
1836 checked = value
1837 return true
1838 }, { runs: 1, seed: "sample-check", size: 10 })
1839
1840 assert.deepStrictEqual(checked, sampled[0])
1841 }))
1842
1843 it.effect("supports unique collections", () =>
1844 Effect.gen(function*() {
1845 const schema = Schema.UniqueArray(Schema.Int).check(Schema.isMinLength(4), Schema.isMaxLength(4))
1846 const values = yield* Arbitrary.sampleEffect(Arbitrary.schema(schema), {
1847 count: 20,
1848 maxDiscards: 20,
1849 seed: "unique"
1850 })
1851
1852 assert.isTrue(values.every((value) => value.length === 4 && new Set(value).size === 4))
1853 }))
1854
1855 it.effect("uses Effect equality for constructive uniqueness", () =>
1856 Effect.gen(function*() {
1857 const schema = Schema.UniqueArray(Schema.Struct({ value: Schema.Literal(1) })).check(
1858 Schema.isMinLength(2),
1859 Schema.isMaxLength(2)
1860 )
1861 const result = yield* Effect.result(Arbitrary.sampleEffect(Arbitrary.schema(schema), {
1862 count: 1,
1863 maxDiscards: 2,
1864 seed: "structural-unique"
1865 }))
1866
1867 assert.isTrue(Result.isFailure(result))
1868 if (Result.isFailure(result)) {
1869 assert.deepStrictEqual(result.failure, {
1870 _tag: "SampleError",
1871 generated: 0,
1872 discards: 3,
1873 seed: "structural-unique"
1874 })
1875 }
1876 }))
1877
1878 it.effect("preserves unique collections while shrinking", () =>
1879 Effect.gen(function*() {
1880 const schema = Schema.UniqueArray(Schema.Int).check(Schema.isMinLength(2), Schema.isMaxLength(4))
1881 const result = yield* Arbitrary.checkEffect(Arbitrary.schema(schema), () => false, {
1882 runs: 1,
1883 seed: "unique-shrink",
1884 size: 10
1885 })
1886
1887 assert.strictEqual(result._tag, "Falsified")
1888 if (result._tag === "Falsified") {
1889 assert.isTrue(Schema.is(schema)(result.shrunkInput))
1890 }
1891 }))
1892
1893 it.effect("generates optional, rest, record, and union structure", () =>
1894 Effect.gen(function*() {
1895 const schema = Schema.Struct({
1896 tuple: Schema.TupleWithRest(
1897 Schema.Tuple([Schema.String, Schema.optionalKey(Schema.Int)]),
1898 [Schema.Boolean]
1899 ),
1900 optional: Schema.optionalKey(Schema.String),
1901 record: Schema.Record(Schema.String, Schema.Int).check(Schema.isMaxProperties(3)),
1902 union: Schema.Union([Schema.String, Schema.Int])
1903 })
1904 const values = yield* Arbitrary.sampleEffect(Arbitrary.schema(schema), {
1905 count: 50,
1906 seed: "structural",
1907 size: 5
1908 })
1909
1910 assert.isTrue(values.every(Schema.is(schema)))
1911 }))
1912
1913 it.effect("omits optional Never properties", () =>
1914 Effect.gen(function*() {
1915 const schema = Schema.Struct({ value: Schema.optionalKey(Schema.Never) })
1916 const values = yield* Arbitrary.sampleEffect(Arbitrary.schema(schema), {
1917 count: 1,
1918 maxDiscards: 0,
1919 seed: "optional-never"
1920 })
1921
1922 assert.deepStrictEqual(values, [{}])
1923 }))
1924
1925 it.effect("uses inhabited branches around Never", () =>
1926 Effect.gen(function*() {
1927 const union = yield* Arbitrary.sampleEffect(
1928 Arbitrary.schema(Schema.Union([Schema.Never, Schema.Literal("value")])),
1929 { count: 1, maxDiscards: 0, seed: "never-union" }
1930 )
1931 const array = yield* Arbitrary.sampleEffect(Arbitrary.schema(Schema.Array(Schema.Never)), {
1932 count: 1,
1933 maxDiscards: 0,
1934 seed: "never-array"
1935 })
1936
1937 assert.deepStrictEqual(union, ["value"])
1938 assert.deepStrictEqual(array, [[]])
1939 }))
1940
1941 it.effect("constructively selects optional properties affordable by the recursion budget", () =>
1942 Effect.gen(function*() {
1943 interface Node {
1944 readonly next: null | Node
1945 }
1946 const Node: Schema.Codec<Node> = Schema.Struct({
1947 next: Schema.suspend(() => Schema.Union([Schema.Null, Node]))
1948 })
1949 const schema = Schema.Struct({
1950 cheap: Schema.optionalKey(Schema.Null),
1951 recursive: Schema.optionalKey(Node)
1952 }).check(Schema.isBetweenProperties(1, 1))
1953 const values = yield* Arbitrary.sampleEffect(Arbitrary.schema(schema), {
1954 count: 100,
1955 maxDiscards: 0,
1956 seed: "optional-property-budget",
1957 size: 0
1958 })
1959
1960 assert.isTrue(values.every((value) => "cheap" in value && !("recursive" in value)))
1961 }))
1962
1963 it.effect("shrinks Record values before keys while preserving key uniqueness", () =>
1964 Effect.gen(function*() {
1965 const schema = Schema.Record(
1966 Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(1)),
1967 Schema.Int.check(Schema.isBetween({ minimum: 0, maximum: 10 }))
1968 ).check(Schema.isMinProperties(1), Schema.isMaxProperties(1))
1969 const result = yield* Arbitrary.checkEffect(Arbitrary.schema(schema), () => false, {
1970 runs: 1,
1971 seed: "record-key-shrink",
1972 size: 1
1973 })
1974
1975 assert.strictEqual(result._tag, "Falsified")
1976 if (result._tag === "Falsified") {
1977 assert.deepStrictEqual(result.shrunkInput, { "\0": 0 })
1978 }
1979
1980 const uniqueSchema = Schema.Record(
1981 Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(1)),
1982 Schema.Null
1983 ).check(Schema.isMinProperties(2), Schema.isMaxProperties(2))
1984 const uniqueResult = yield* Arbitrary.checkEffect(Arbitrary.schema(uniqueSchema), () => false, {
1985 runs: 1,
1986 seed: "record-key-unique-shrink",
1987 size: 2
1988 })
1989
1990 assert.strictEqual(uniqueResult._tag, "Falsified")
1991 if (uniqueResult._tag === "Falsified") {
1992 const keys = Object.keys(uniqueResult.shrunkInput).map((key) => key.charCodeAt(0)).sort((a, b) => a - b)
1993 assert.deepStrictEqual(keys, [0, 1])
1994 }
1995 }))
1996
1997 it.effect("generates finite numeric template literal segments", () =>
1998 Effect.gen(function*() {
1999 const schema = Schema.TemplateLiteral([Schema.Number])
2000 const values = yield* Arbitrary.sampleEffect(Arbitrary.schema(schema), {
2001 count: 200,
2002 maxDiscards: 0,
2003 seed: "template-number"
2004 })
2005
2006 assert.isTrue(values.every(Schema.is(schema)))
2007 }))
2008
2009 it.effect("generates bit templates and preserves checked numeric template shrinks", () =>
2010 Effect.gen(function*() {
2011 const bits = Schema.TemplateLiteral([Schema.Literals([0, 1])])
2012 const values = yield* Arbitrary.sampleEffect(Arbitrary.schema(bits), {
2013 count: 50,
2014 maxDiscards: 0,
2015 seed: 0
2016 })
2017 assert.deepStrictEqual(new Set(values), new Set(["0", "1"]))
2018
2019 const schema = Schema.TemplateLiteral(["a", Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 100 }))])
2020 const seen: Array<string> = []
2021 const result = yield* Arbitrary.checkEffect(Arbitrary.schema(schema), (value) => {
2022 seen.push(value)
2023 return false
2024 }, { runs: 1, maxDiscards: 0, maxShrinks: 100, seed: 0, size: 10 })
2025
2026 assert.strictEqual(result._tag, "Falsified")
2027 assert.isAbove(seen.length, 1)
2028 assert.isTrue(seen.every(Schema.is(schema)))
2029 if (result._tag === "Falsified") assert.strictEqual(result.shrunkInput, "a1")
2030 }))
2031
2032 it.effect("validates overlapping oneOf Union members", () =>
2033 Effect.gen(function*() {
2034 const schema = Schema.Union([Schema.String, Schema.Literal("a")], { mode: "oneOf" })
2035 const values = yield* Arbitrary.sampleEffect(Arbitrary.schema(schema), {
2036 count: 100,
2037 maxDiscards: 200,
2038 seed: "overlapping-one-of"
2039 })
2040
2041 assert.isTrue(values.every(Schema.is(schema)))
2042 assert.notInclude(values, "a")
2043 }))
2044
2045 it.effect("bounds exhaustion for an uninhabited oneOf Union", () =>
2046 Effect.gen(function*() {
2047 const schema = Schema.Union([Schema.Literal("a"), Schema.Literal("a")], { mode: "oneOf" })
2048 const result = yield* Effect.result(Arbitrary.sampleEffect(Arbitrary.schema(schema), {
2049 count: 1,
2050 maxDiscards: 2,
2051 seed: "uninhabited-one-of"
2052 }))
2053
2054 assert.isTrue(Result.isFailure(result))
2055 if (Result.isFailure(result)) {
2056 assert.deepStrictEqual(result.failure, {
2057 _tag: "SampleError",
2058 generated: 0,
2059 discards: 3,
2060 seed: "uninhabited-one-of"
2061 })
2062 }
2063 }))
2064
2065 it.effect("generates finite numeric template literal union segments", () =>
2066 Effect.gen(function*() {
2067 const schema = Schema.TemplateLiteral([Schema.Union([Schema.Number, Schema.Literal("a")])])
2068 const values = yield* Arbitrary.sampleEffect(Arbitrary.schema(schema), {
2069 count: 200,
2070 maxDiscards: 0,
2071 seed: "template-number-union"
2072 })
2073
2074 assert.isTrue(values.every(Schema.is(schema)))
2075 }))
2076
2077 it.effect("generates symbols", () =>
2078 Effect.gen(function*() {
2079 const values = yield* Arbitrary.sampleEffect(Arbitrary.schema(Schema.Symbol), {
2080 count: 20,
2081 maxDiscards: 0,
2082 seed: "symbol"
2083 })
2084
2085 assert.isTrue(values.every((value) => typeof value === "symbol"))
2086 }))
2087
2088 it.effect("generates Unknown and Any through Json", () =>
2089 Effect.gen(function*() {
2090 for (const schema of [Schema.Unknown, Schema.Any]) {
2091 const values = yield* Arbitrary.sampleEffect(Arbitrary.schema(schema), {
2092 count: 30,
2093 maxDiscards: 0,
2094 seed: "unknown-any",
2095 size: 5
2096 })
2097
2098 assert.isTrue(values.every(Schema.is(Schema.Json)))
2099 }
2100 }))
2101
2102 it.effect("derives Json through its built-in representation", () =>
2103 Effect.gen(function*() {
2104 const values = yield* Arbitrary.sampleEffect(Arbitrary.schema(Schema.Json), {
2105 count: 30,
2106 seed: "json",
2107 size: 5
2108 })
2109
2110 assert.isTrue(values.every(Schema.is(Schema.Json)))
2111 }))
2112
2113 it.effect("prefers an explicit toCodecArbitrary over a built-in representation", () =>
2114 Effect.gen(function*() {
2115 const expected = "https://effect.website/custom"
2116 const schema = Schema.URL.annotate({
2117 toCodecArbitrary: () =>
2118 Schema.link<URL>()(
2119 Schema.Literal(expected),
2120 SchemaTransformation.transform<globalThis.URL, typeof expected>({
2121 decode: (value) => new URL(value),
2122 encode: () => expected
2123 })
2124 )
2125 })
2126 const values = yield* Arbitrary.sampleEffect(Arbitrary.schema(schema), {
2127 count: 10,
2128 maxDiscards: 0,
2129 seed: "built-in-explicit-arbitrary"
2130 })
2131
2132 assert.deepStrictEqual(values.map((value) => value.href), globalThis.Array(10).fill(expected))
2133 }))
2134
2135 it.effect("derives canonical declarations through their codec", () =>
2136 Effect.gen(function*() {
2137 const schema = Schema.Option(Schema.Int)
2138 const values = yield* Arbitrary.sampleEffect(Arbitrary.schema(schema), { count: 30, seed: "option", size: 5 })
2139
2140 assert.isTrue(values.every(Option.isOption))
2141 assert.isTrue(values.every(Schema.is(schema)))
2142 }))
2143
2144 it.effect("prefers toCodecArbitrary and passes normalized constraints without Order", () =>
2145 Effect.gen(function*() {
2146 let received: Schema.Annotations.ToArbitrary.GenerationConstraint<number> | undefined
2147 const declaration = Schema.declare<number>((input): input is number => typeof input === "number", {
2148 toCodec: () =>
2149 Schema.link<number>()(
2150 Schema.Literal(99),
2151 SchemaTransformation.transform<number, 99>({ decode: (value) => value, encode: () => 99 })
2152 ),
2153 toCodecArbitrary: ({ constraint }) => {
2154 received = constraint
2155 return Schema.link<number>()(
2156 Schema.Int.check(Schema.isBetween({
2157 minimum: constraint?.minimum ?? Number.MIN_SAFE_INTEGER,
2158 maximum: constraint?.maximum ?? Number.MAX_SAFE_INTEGER,
2159 exclusiveMinimum: constraint?.exclusiveMinimum,
2160 exclusiveMaximum: constraint?.exclusiveMaximum
2161 })),
2162 SchemaTransformation.passthrough()
2163 )
2164 }
2165 }).check(Schema.isBetween({ minimum: 1, maximum: 4, exclusiveMinimum: true }))
2166
2167 const values = yield* Arbitrary.sampleEffect(Arbitrary.schema(declaration), {
2168 count: 100,
2169 maxDiscards: 0,
2170 seed: "to-codec-arbitrary-constraint"
2171 })
2172
2173 assert.deepStrictEqual(received, { minimum: 1, maximum: 4, exclusiveMinimum: true })
2174 assert.isTrue(values.every((value) => value > 1 && value <= 4))
2175 }))
2176
2177 it("rejects contradictory generation constraints before invoking toCodecArbitrary", () => {
2178 let invoked = false
2179 const declaration = Schema.declare<number>((input): input is number => typeof input === "number", {
2180 toCodecArbitrary: () => {
2181 invoked = true
2182 return Schema.link<number>()(Schema.Number, SchemaTransformation.passthrough())
2183 }
2184 }).check(
2185 Schema.makeFilter(() => true, { arbitraryConstraint: { minLength: 2 } }),
2186 Schema.makeFilter(() => true, { arbitraryConstraint: { maxLength: 1 } })
2187 )
2188
2189 assert.throws(() => Arbitrary.schema(declaration), /Unable to derive an arbitrary for constraints/)
2190 assert.isFalse(invoked)
2191 })
2192
2193 it.effect("derives mutually recursive declarations through toCodecArbitrary", () =>
2194 Effect.gen(function*() {
2195 interface A {
2196 readonly _tag: "A"
2197 readonly next: B | null
2198 }
2199 interface B {
2200 readonly _tag: "B"
2201 readonly next: A | null
2202 }
2203 let A: Schema.declare<A>
2204 let B: Schema.declare<B>
2205 A = Schema.declare<A>(
2206 (input): input is A => typeof input === "object" && input !== null && (input as A)._tag === "A",
2207 {
2208 toCodecArbitrary: () =>
2209 Schema.link<A>()(
2210 Schema.Struct({
2211 _tag: Schema.Literal("A"),
2212 next: Schema.NullOr(Schema.suspend(() => B))
2213 }),
2214 SchemaTransformation.passthrough()
2215 )
2216 }
2217 )
2218 B = Schema.declare<B>(
2219 (input): input is B => typeof input === "object" && input !== null && (input as B)._tag === "B",
2220 {
2221 toCodecArbitrary: () =>
2222 Schema.link<B>()(
2223 Schema.Struct({
2224 _tag: Schema.Literal("B"),
2225 next: Schema.NullOr(Schema.suspend(() => A))
2226 }),
2227 SchemaTransformation.passthrough()
2228 )
2229 }
2230 )
2231
2232 const values = yield* Arbitrary.sampleEffect(Arbitrary.schema(A), {
2233 count: 100,
2234 maxDiscards: 0,
2235 seed: "mutual-to-codec-arbitrary",
2236 size: 10
2237 })
2238
2239 assert.isTrue(values.every(Schema.is(A)))
2240 assert.isTrue(values.some((value) => value.next?._tag === "B"))
2241 }))
2242
2243 it.effect("promotes valid shrink descendants through a rejecting canonical codec", () =>
2244 Effect.gen(function*() {
2245 const encoded = Schema.Int.check(Schema.isBetween({ minimum: 0, maximum: 100 }))
2246 const schema = Schema.declare<number>(
2247 (input): input is number => input === 25 || input === 100,
2248 {
2249 toCodec: () =>
2250 Schema.link<number>()(
2251 encoded,
2252 SchemaTransformation.transformEffect({
2253 decode: (value) =>
2254 value === 25 || value === 100
2255 ? Effect.succeed(value)
2256 : Effect.fail(new SchemaIssue.Forbidden({ message: "unsupported value" })),
2257 encode: Effect.succeed
2258 })
2259 )
2260 }
2261 )
2262
2263 const result = yield* Arbitrary.checkEffect(Arbitrary.schema(schema), () => false, {
2264 runs: 1,
2265 maxDiscards: 0,
2266 seed: 47
2267 })
2268
2269 assert.strictEqual(result._tag, "Falsified")
2270 if (result._tag === "Falsified") {
2271 assert.strictEqual(result.initialInput, 100)
2272 assert.strictEqual(result.shrunkInput, 25)
2273 assert.strictEqual(result.shrinks, 1)
2274 }
2275 }))
2276
2277 it.effect("keeps asynchronous canonical codecs interruptible", () =>
2278 Effect.gen(function*() {
2279 const started = yield* Deferred.make<void>()
2280 const schema = Schema.declare<number>((input): input is number => typeof input === "number", {
2281 toCodecArbitrary: () =>
2282 Schema.link<number>()(
2283 Schema.Literal(1),
2284 SchemaTransformation.transformEffect<number, 1>({
2285 decode: () => {
2286 Deferred.doneUnsafe(started, Effect.void)
2287 return Effect.never
2288 },
2289 encode: () => Effect.succeed(1)
2290 })
2291 )
2292 })
2293 const fiber = yield* Effect.forkChild(Arbitrary.sampleEffect(Arbitrary.schema(schema), {
2294 count: 1,
2295 seed: "interrupt-asynchronous-codec"
2296 }))
2297
2298 yield* Deferred.await(started)
2299 yield* Fiber.interrupt(fiber)
2300 const exit = yield* Fiber.await(fiber)
2301
2302 assert.isTrue(Exit.hasInterrupts(exit))
2303 }))
2304
2305 it.effect("derives JSON-canonical declarations through their codec", () =>
2306 Effect.gen(function*() {
2307 const values = yield* Arbitrary.sampleEffect(Arbitrary.schema(Schema.URLSearchParams), {
2308 count: 30,
2309 seed: "url-search-params"
2310 })
2311
2312 assert.isTrue(values.every((value) => value instanceof URLSearchParams))
2313 }))
2314
2315 it.effect("derives productive structural declarations through canonical codecs", () =>
2316 Effect.gen(function*() {
2317 const schemas: ReadonlyArray<Schema.Top> = [
2318 Schema.Result(Schema.Number, Schema.String),
2319 Schema.Redacted(Schema.String),
2320 Schema.ReadonlySet(Schema.Int),
2321 Schema.HashSet(Schema.Int),
2322 Schema.ReadonlyMap(Schema.String, Schema.Int),
2323 Schema.HashMap(Schema.String, Schema.Int),
2324 Schema.Chunk(Schema.Int),
2325 Schema.Duration,
2326 Schema.Cause(Schema.String, Schema.String),
2327 Schema.Exit(Schema.Int, Schema.String, Schema.String),
2328 Schema.TimeZoneOffset
2329 ]
2330
2331 for (const schema of schemas) {
2332 const result = yield* Arbitrary.checkEffect(Arbitrary.schema(schema), Schema.is(schema), {
2333 runs: 20,
2334 maxDiscards: 0,
2335 seed: "structural-declarations"
2336 })
2337 assert.deepStrictEqual(result, { _tag: "Passed", runs: 20, discards: 0 })
2338 }
2339 }))
2340
2341 it.effect("constructs collection cardinality through declaration annotations", () =>
2342 Effect.gen(function*() {
2343 const hashSet = Schema.HashSet(Schema.Int).check(Schema.makeFilter(
2344 (value: HashSet.HashSet<number>) => HashSet.size(value) === 3,
2345 { arbitraryConstraint: { minSize: 3, maxSize: 3 } }
2346 ))
2347 const hashMap = Schema.HashMap(Schema.Literals(["a", "b", "c"]), Schema.Boolean).check(Schema.makeFilter(
2348 (value: HashMap.HashMap<string, boolean>) => HashMap.size(value) === 3,
2349 { arbitraryConstraint: { minSize: 3, maxSize: 3 } }
2350 ))
2351 const schemas: ReadonlyArray<readonly [Schema.Top, (value: any) => number]> = [
2352 [Schema.Chunk(Schema.Int).check(Schema.isBetweenLength(3, 3)), Chunk.size],
2353 [Schema.ReadonlySet(Schema.Int).check(Schema.isBetweenSize(3, 3)), (value) => value.size],
2354 [hashSet, HashSet.size],
2355 [
2356 Schema.ReadonlyMap(Schema.Literals(["a", "b", "c"]), Schema.Boolean).check(Schema.isBetweenSize(3, 3)),
2357 (value) => value.size
2358 ],
2359 [hashMap, HashMap.size]
2360 ]
2361
2362 for (const [schema, size] of schemas) {
2363 const values = yield* Arbitrary.sampleEffect(Arbitrary.schema(schema), {
2364 count: 30,
2365 maxDiscards: 100,
2366 seed: "collection-to-codec-arbitrary",
2367 size: 10
2368 })
2369 assert.isTrue(values.every((value) => size(value) === 3))
2370 }
2371 }))
2372
2373 it.effect("preserves Map key uniqueness while shrinking", () =>
2374 Effect.gen(function*() {
2375 const schema = Schema.ReadonlyMap(
2376 Schema.Literals(["a", "b", "c"]),
2377 Schema.Int
2378 ).check(Schema.isBetweenSize(2, 3))
2379 const result = yield* Arbitrary.checkEffect(Arbitrary.schema(schema), () => false, {
2380 runs: 1,
2381 seed: "map-key-shrinking",
2382 size: 10
2383 })
2384
2385 assert.strictEqual(result._tag, "Falsified")
2386 if (result._tag === "Falsified") {
2387 assert.strictEqual(result.shrunkInput.size, 2)
2388 assert.strictEqual(new Set(result.shrunkInput.keys()).size, result.shrunkInput.size)
2389 }
2390 }))
2391
2392 describe("Schema catalog", () => {
2393 it.effect("derives primitive and structural schemas", () => {
2394 const enumValues = {
2395 Apple: "apple",
2396 Banana: "banana",
2397 Cantaloupe: 3
2398 } as const
2399 class CatalogClass extends Schema.Class<CatalogClass>("ArbitraryCatalogClass")({
2400 value: Schema.String
2401 }) {}
2402 return verifySchemaCatalog([
2403 { name: "Any", schema: Schema.Any },
2404 { name: "Unknown", schema: Schema.Unknown },
2405 { name: "Void", schema: Schema.Void },
2406 { name: "Undefined", schema: Schema.Undefined },
2407 { name: "Null", schema: Schema.Null },
2408 { name: "String", schema: Schema.String },
2409 { name: "Number", schema: Schema.Number },
2410 { name: "Boolean", schema: Schema.Boolean },
2411 { name: "BigInt", schema: Schema.BigInt },
2412 { name: "Symbol", schema: Schema.Symbol },
2413 { name: "UniqueSymbol", schema: Schema.UniqueSymbol(Symbol.for("arbitrary-parity")) },
2414 { name: "ObjectKeyword", schema: Schema.ObjectKeyword },
2415 { name: "Literals", schema: Schema.Literals(["a", 1, true, 1n]) },
2416 { name: "Enum", schema: Schema.Enum(enumValues) },
2417 {
2418 name: "TemplateLiteral",
2419 schema: Schema.TemplateLiteral(["user_", Schema.String.check(Schema.isUUID())])
2420 },
2421 { name: "Union", schema: Schema.Union([Schema.String, Schema.Number]) },
2422 {
2423 name: "Tuple",
2424 schema: Schema.Tuple([Schema.String, Schema.optionalKey(Schema.Number)])
2425 },
2426 {
2427 name: "TupleWithRest",
2428 schema: Schema.TupleWithRest(Schema.Tuple([Schema.Boolean]), [Schema.Number, Schema.String])
2429 },
2430 { name: "Array", schema: Schema.Array(Schema.String) },
2431 {
2432 name: "Struct",
2433 schema: Schema.Struct({
2434 required: Schema.String,
2435 optional: Schema.optionalKey(Schema.Number)
2436 })
2437 },
2438 { name: "Record(String)", schema: Schema.Record(Schema.String, Schema.Number) },
2439 { name: "Record(Symbol)", schema: Schema.Record(Schema.Symbol, Schema.Number) },
2440 { name: "Class", schema: CatalogClass },
2441 {
2442 name: "StructWithRest",
2443 schema: Schema.StructWithRest(
2444 Schema.Struct({ required: Schema.Number }),
2445 [Schema.Record(Schema.String, Schema.Number)]
2446 )
2447 }
2448 ])
2449 })
2450
2451 it.effect("derives schemas with canonical constraints", () =>
2452 verifySchemaCatalog([
2453 {
2454 name: "String length",
2455 schema: Schema.String.check(Schema.isMinLength(2), Schema.isMaxLength(4))
2456 },
2457 { name: "String starts with", schema: Schema.String.check(Schema.isStartingWith("a.b")) },
2458 { name: "String ends with", schema: Schema.String.check(Schema.isEndingWith("a+b")) },
2459 { name: "String includes", schema: Schema.String.check(Schema.isIncluding("[")) },
2460 { name: "Finite", schema: Schema.Finite },
2461 { name: "Int32", schema: Schema.Number.check(Schema.isInt32()) },
2462 {
2463 name: "Int fractional bounds",
2464 schema: Schema.Int.check(Schema.isBetween({
2465 minimum: 1.2,
2466 maximum: 10.8,
2467 exclusiveMinimum: true,
2468 exclusiveMaximum: true
2469 }))
2470 },
2471 {
2472 name: "BigInt bounds",
2473 schema: Schema.BigInt.check(Schema.isBetweenBigInt({
2474 minimum: 0n,
2475 maximum: 10n,
2476 exclusiveMinimum: true,
2477 exclusiveMaximum: true
2478 }))
2479 },
2480 {
2481 name: "Date bounds",
2482 schema: Schema.Date.check(Schema.isBetweenDate({
2483 minimum: new globalThis.Date(0),
2484 maximum: new globalThis.Date(10),
2485 exclusiveMinimum: true,
2486 exclusiveMaximum: true
2487 }))
2488 },
2489 {
2490 name: "BigDecimal bounds",
2491 schema: Schema.BigDecimal.check(Schema.isBetweenBigDecimal({
2492 minimum: BigDecimal.fromStringUnsafe("1.01"),
2493 maximum: BigDecimal.fromStringUnsafe("1.02"),
2494 exclusiveMinimum: true,
2495 exclusiveMaximum: true
2496 }))
2497 },
2498 {
2499 name: "Array length",
2500 schema: Schema.Array(Schema.String).check(Schema.isBetweenLength(2, 4))
2501 },
2502 {
2503 name: "UniqueArray",
2504 schema: Schema.UniqueArray(Schema.String).check(Schema.isMaxLength(2))
2505 },
2506 {
2507 name: "Record properties",
2508 schema: Schema.Record(Schema.String, Schema.Number).check(Schema.isBetweenProperties(2, 4))
2509 },
2510 {
2511 name: "Struct properties",
2512 schema: Schema.Struct({
2513 a: Schema.optionalKey(Schema.String),
2514 b: Schema.optionalKey(Schema.String),
2515 c: Schema.optionalKey(Schema.String)
2516 }).check(Schema.isBetweenProperties(2, 2))
2517 },
2518 {
2519 name: "ReadonlyMap size",
2520 schema: Schema.ReadonlyMap(Schema.String, Schema.Number).check(Schema.isBetweenSize(2, 4))
2521 }
2522 ]))
2523
2524 it.effect("derives built-in and canonical declarations", () =>
2525 verifySchemaCatalog([
2526 { name: "Json", schema: Schema.Json },
2527 { name: "MutableJson", schema: Schema.MutableJson },
2528 { name: "Date", schema: Schema.Date },
2529 { name: "URL", schema: Schema.URL },
2530 { name: "URLSearchParams", schema: Schema.URLSearchParams },
2531 { name: "RegExp", schema: Schema.RegExp },
2532 { name: "Duration", schema: Schema.Duration },
2533 { name: "BigDecimal", schema: Schema.BigDecimal },
2534 { name: "DateTimeUtc", schema: Schema.DateTimeUtc },
2535 { name: "TimeZoneOffset", schema: Schema.TimeZoneOffset },
2536 { name: "TimeZoneNamed", schema: Schema.TimeZoneNamed },
2537 { name: "TimeZone", schema: Schema.TimeZone },
2538 { name: "DateTimeZoned", schema: Schema.DateTimeZoned },
2539 { name: "Uint8Array", schema: Schema.Uint8Array },
2540 { name: "UnknownFromJsonString", schema: Schema.UnknownFromJsonString },
2541 { name: "Option", schema: Schema.Option(Schema.String) },
2542 { name: "Result", schema: Schema.Result(Schema.Number, Schema.String) },
2543 { name: "ReadonlySet", schema: Schema.ReadonlySet(Schema.Number) },
2544 { name: "ReadonlyMap", schema: Schema.ReadonlyMap(Schema.String, Schema.Number) },
2545 { name: "HashSet", schema: Schema.HashSet(Schema.Number) },
2546 { name: "HashMap", schema: Schema.HashMap(Schema.String, Schema.Number) },
2547 { name: "Chunk", schema: Schema.Chunk(Schema.Number) },
2548 { name: "Redacted", schema: Schema.Redacted(Schema.String, { label: "password" }) },
2549 { name: "CauseReason", schema: Schema.CauseReason(Schema.String, Schema.String) },
2550 { name: "Cause", schema: Schema.Cause(Schema.String, Schema.String) },
2551 { name: "ErrorInstance", schema: Schema.ErrorInstance() },
2552 { name: "Exit", schema: Schema.Exit(Schema.Number, Schema.String, Schema.String) },
2553 { name: "File", schema: Schema.File },
2554 { name: "FormData", schema: Schema.FormData }
2555 ]))
2556
2557 it("rejects uninhabited structural schemas", () => {
2558 assert.throws(
2559 () => Arbitrary.schema(Schema.Never),
2560 /Unable to derive an arbitrary for a schema without a finite generation path/
2561 )
2562 assert.throws(
2563 () => Arbitrary.schema(Schema.Struct({ value: Schema.Never })),
2564 /Unable to derive an arbitrary for a schema without a finite generation path/
2565 )
2566 assert.throws(
2567 () => Arbitrary.schema(Schema.Array(Schema.String).check(Schema.isMinLength(2), Schema.isMaxLength(1))),
2568 /Unable to derive an arbitrary for array constraints/
2569 )
2570 assert.throws(
2571 () =>
2572 Arbitrary.schema(
2573 Schema.Struct({ required: Schema.String }).check(Schema.isMaxProperties(0))
2574 ),
2575 /Unable to derive an arbitrary for object property constraints/
2576 )
2577 })
2578 })
2579
2580 it.effect("generates recursive schemas with a finite path", () =>
2581 Effect.gen(function*() {
2582 interface Node {
2583 readonly value: string
2584 readonly children: ReadonlyArray<Node>
2585 }
2586 const Node: Schema.Codec<Node> = Schema.Struct({
2587 value: Schema.String,
2588 children: Schema.Array(Schema.suspend(() => Node)).check(Schema.isMaxLength(3))
2589 })
2590 const values = yield* Arbitrary.sampleEffect(Arbitrary.schema(Node), { count: 30, seed: "recursive", size: 5 })
2591
2592 assert.isTrue(values.every(Schema.is(Node)))
2593 }))
2594
2595 it.effect("uses the same recursive value for sampling and a single-run check", () =>
2596 Effect.gen(function*() {
2597 interface Node {
2598 readonly left: ReadonlyArray<Node | null>
2599 readonly right: ReadonlyArray<Node | null>
2600 }
2601 let Node!: Schema.Codec<Node>
2602 const child = Schema.Union([Schema.Null, Schema.suspend(() => Node)])
2603 const children = Schema.Array(child).check(Schema.isMinLength(1), Schema.isMaxLength(2))
2604 Node = Schema.Struct({ left: children, right: children })
2605 const arbitrary = Arbitrary.schema(Node)
2606 const sampled = yield* Arbitrary.sampleEffect(arbitrary, {
2607 count: 1,
2608 seed: "recursive-parity",
2609 size: 8
2610 })
2611 const checked: Array<Node> = []
2612 yield* Arbitrary.checkEffect(arbitrary, (value) => {
2613 checked.push(value)
2614 return true
2615 }, { runs: 1, seed: "recursive-parity", size: 8 })
2616
2617 assert.deepStrictEqual(checked, sampled)
2618 const assertPropertyOrder = (node: Node | null): void => {
2619 if (node === null) return
2620 assert.deepStrictEqual(Object.keys(node), ["left", "right"])
2621 for (const child of node.left) assertPropertyOrder(child)
2622 for (const child of node.right) assertPropertyOrder(child)
2623 }
2624 assertPropertyOrder(sampled[0])
2625 }))
2626
2627 it.effect("reserves the shared recursion budget for later siblings", () =>
2628 Effect.gen(function*() {
2629 interface Node {
2630 readonly children: ReadonlyArray<Node>
2631 }
2632 const Node: Schema.Codec<Node> = Schema.Struct({
2633 children: Schema.Array(Schema.suspend(() => Node)).check(Schema.isMaxLength(3))
2634 })
2635 const result = yield* Arbitrary.checkEffect(Arbitrary.schema(Node), () => true, {
2636 runs: 100,
2637 seed: "recursive-budget",
2638 size: 10
2639 })
2640
2641 assert.deepStrictEqual(result, { _tag: "Passed", runs: 100, discards: 0 })
2642 }))
2643
2644 it.effect("does not assign recursive depth according to sibling order", () =>
2645 Effect.gen(function*() {
2646 interface Node {
2647 readonly left: Node | null
2648 readonly right: Node | null
2649 }
2650 const Node: Schema.Codec<Node> = Schema.Struct({
2651 left: Schema.Union([Schema.Null, Schema.suspend(() => Node)]),
2652 right: Schema.Union([Schema.Null, Schema.suspend(() => Node)])
2653 })
2654 const values = yield* Arbitrary.sampleEffect(Arbitrary.schema(Node), {
2655 count: 2_000,
2656 maxDiscards: 0,
2657 seed: "recursive-sibling-order",
2658 size: 8
2659 })
2660 const countNodes = (root: Node | null): number => {
2661 if (root === null) return 0
2662 assert.deepStrictEqual(Object.keys(root), ["left", "right"])
2663 return 1 + countNodes(root.left) + countNodes(root.right)
2664 }
2665 let left = 0
2666 let right = 0
2667 for (const value of values) {
2668 left += countNodes(value.left)
2669 right += countNodes(value.right)
2670 }
2671
2672 assert.isAbove(left, 0)
2673 assert.isAbove(right, 0)
2674 assert.isAtMost(Math.abs(left - right), (left + right) * 0.1)
2675 }))
2676
2677 it.effect("reserves the shared recursion budget for record entries", () =>
2678 Effect.gen(function*() {
2679 type Node = null | { readonly [key: string]: Node }
2680 const Node: Schema.Codec<Node> = Schema.Union([
2681 Schema.Null,
2682 Schema.Record(Schema.String, Schema.suspend(() => Node)).check(
2683 Schema.isMinProperties(2),
2684 Schema.isMaxProperties(2)
2685 )
2686 ])
2687 const result = yield* Arbitrary.checkEffect(Arbitrary.schema(Node), () => true, {
2688 runs: 100,
2689 seed: "recursive-record-budget",
2690 size: 10
2691 })
2692
2693 assert.deepStrictEqual(result, { _tag: "Passed", runs: 100, discards: 0 })
2694 }))
2695
2696 it.effect("discovers a recursive finite path through a canonical declaration", () =>
2697 Effect.gen(function*() {
2698 interface Node {
2699 readonly next: Option.Option<Node>
2700 }
2701 const Node: Schema.Codec<Node> = Schema.Struct({
2702 next: Schema.Option(Schema.suspend(() => Node))
2703 })
2704 const values = yield* Arbitrary.sampleEffect(Arbitrary.schema(Node), {
2705 count: 30,
2706 seed: "recursive-option",
2707 size: 5
2708 })
2709
2710 assert.isTrue(values.every(Schema.is(Node)))
2711 }))
2712
2713 it.effect("starts progressive checks from a finite recursive base", () =>
2714 Effect.gen(function*() {
2715 interface Node {
2716 readonly next: Node | null
2717 }
2718 const Node: Schema.Codec<Node> = Schema.suspend(() =>
2719 Schema.Struct({
2720 next: Schema.Union([Schema.Null, Node])
2721 })
2722 )
2723 const result = yield* Arbitrary.checkEffect(Arbitrary.schema(Node), () => true, {
2724 runs: 2,
2725 maxDiscards: 0,
2726 seed: "recursive-base",
2727 size: 5
2728 })
2729
2730 assert.deepStrictEqual(result, { _tag: "Passed", runs: 2, discards: 0 })
2731 }))
2732
2733 it.effect("generates mutually recursive schemas", () =>
2734 Effect.gen(function*() {
2735 interface Expression {
2736 readonly type: "expression"
2737 readonly value: number | Operation
2738 }
2739 interface Operation {
2740 readonly type: "operation"
2741 readonly left: Expression
2742 readonly right: Expression
2743 }
2744 const Expression: Schema.Codec<Expression> = Schema.Struct({
2745 type: Schema.Literal("expression"),
2746 value: Schema.Union([Schema.Int, Schema.suspend((): Schema.Codec<Operation> => Operation)])
2747 })
2748 const Operation: Schema.Codec<Operation> = Schema.Struct({
2749 type: Schema.Literal("operation"),
2750 left: Expression,
2751 right: Expression
2752 })
2753 const values = yield* Arbitrary.sampleEffect(Arbitrary.schema(Operation), {
2754 count: 30,
2755 seed: "mutually-recursive",
2756 size: 5
2757 })
2758
2759 assert.isTrue(values.every(Schema.is(Operation)))
2760 }))
2761
2762 it.effect("derives a five-thousand-node mutually recursive component without overflowing", () =>
2763 Effect.gen(function*() {
2764 const count = 5_000
2765 const schemas: Array<Schema.Codec<unknown>> = []
2766 const suspends = Array.from(
2767 { length: count },
2768 (_, index) => Schema.suspend((): Schema.Codec<unknown> => schemas[(index + 1) % count])
2769 )
2770 for (let index = 0; index < count; index++) {
2771 schemas.push(Schema.Union([Schema.Null, Schema.Struct({ next: suspends[index] })]))
2772 }
2773
2774 const values = yield* Arbitrary.sampleEffect(Arbitrary.schema(schemas[0]), {
2775 count: 1,
2776 maxDiscards: 0,
2777 seed: "deep-mutual-recursion",
2778 size: 10
2779 })
2780
2781 assert.strictEqual(values.length, 1)
2782 }))
2783
2784 it.effect("derives and samples a ten-thousand-node suspend chain without overflowing", () =>
2785 Effect.gen(function*() {
2786 const values = yield* Arbitrary.sampleEffect(Arbitrary.schema(makeSuspendChain(10_000)), {
2787 count: 1,
2788 maxDiscards: 0,
2789 seed: "deep-suspend-chain"
2790 })
2791
2792 assert.deepStrictEqual(values, [null])
2793 }))
2794
2795 it("fails immediately for recursion without a finite path", () => {
2796 const Recursive = Schema.suspend((): Schema.Codec<unknown> => schema)
2797 const schema: Schema.Codec<unknown> = Schema.Struct({ value: Recursive })
2798
2799 assert.throws(
2800 () => Arbitrary.schema(schema),
2801 /Unable to derive an arbitrary for a schema without a finite generation path/
2802 )
2803 })
2804
2805 it.effect("treats unproductive recursive union branches as empty", () =>
2806 Effect.gen(function*() {
2807 const Empty: Schema.Codec<unknown> = Schema.suspend(() => Schema.Struct({ value: Empty }))
2808 const values = yield* Arbitrary.sampleEffect(Arbitrary.schema(Schema.Union([Schema.Null, Empty])), {
2809 count: 10,
2810 maxDiscards: 0,
2811 seed: "empty-recursive-branch"
2812 })
2813
2814 assert.deepStrictEqual(values, Array.from({ length: 10 }, () => null))
2815 }))
2816
2817 it("fails immediately for unsupported declarations", () => {
2818 const declaration = Schema.declare((input): input is URL => input instanceof URL, { expected: "URL" })
2819
2820 assert.throws(() => Arbitrary.schema(declaration), /Unable to derive an arbitrary for an unsupported Declaration/)
2821 })
2822
2823 it.effect("bounds residual-filter exhaustion", () =>
2824 Effect.gen(function*() {
2825 const schema = Schema.String.check(Schema.makeFilter(() => false, { expected: "impossible" }))
2826 const result = yield* Effect.result(Arbitrary.sampleEffect(Arbitrary.schema(schema), {
2827 count: 1,
2828 maxDiscards: 2,
2829 seed: "exhaustion"
2830 }))
2831
2832 assert.isTrue(Result.isFailure(result))
2833 if (Result.isFailure(result)) {
2834 assert.deepStrictEqual(result.failure, {
2835 _tag: "SampleError",
2836 generated: 0,
2837 discards: 3,
2838 seed: "exhaustion"
2839 })
2840 }
2841 }))
2842
2843 it.effect("evaluates a residual filter once per generated value", () =>
2844 Effect.gen(function*() {
2845 let evaluations = 0
2846 const schema = Schema.Null.check(Schema.makeFilter(() => {
2847 evaluations++
2848 return true
2849 }))
2850 const result = yield* Arbitrary.checkEffect(Arbitrary.schema(schema), () => true, {
2851 runs: 1,
2852 seed: "single-filter-evaluation"
2853 })
2854
2855 assert.deepStrictEqual(result, { _tag: "Passed", runs: 1, discards: 0 })
2856 assert.strictEqual(evaluations, 1)
2857 }))
2858
2859 it.effect("interrupts a synchronous residual-filter generation loop", () =>
2860 Effect.gen(function*() {
2861 const schema = Schema.String.check(Schema.makeFilter(() => false, { expected: "impossible" }))
2862 const fiber = yield* Effect.forkChild(Arbitrary.sampleEffect(Arbitrary.schema(schema), {
2863 count: 1,
2864 maxDiscards: 100_000,
2865 seed: "interrupt-generation"
2866 }))
2867
2868 yield* Effect.yieldNow
2869 yield* Fiber.interrupt(fiber)
2870 const exit = yield* Fiber.await(fiber)
2871
2872 assert.isTrue(Exit.hasInterrupts(exit))
2873 }))
2874
2875 it.effect("interrupts a synchronous successful generation loop", () =>
2876 Effect.gen(function*() {
2877 const count = 100_001
2878 let generated = 0
2879 const started = yield* Deferred.make<void>()
2880 const schema = Schema.String.check(Schema.makeFilter(() => {
2881 generated++
2882 if (generated === 1) Deferred.doneUnsafe(started, Effect.void)
2883 return true
2884 }))
2885 const arbitrary = Arbitrary.schema(schema)
2886 generated = 0
2887 const fiber = yield* Effect.forkChild(
2888 Arbitrary.sampleEffect(arbitrary, {
2889 count,
2890 seed: "interrupt-successful-generation"
2891 }).pipe(Effect.provideService(Scheduler.MaxOpsBeforeYield, 16))
2892 )
2893
2894 yield* Deferred.await(started)
2895 assert.isBelow(generated, count)
2896 yield* Fiber.interrupt(fiber)
2897 const exit = yield* Fiber.await(fiber)
2898
2899 assert.isTrue(Exit.hasInterrupts(exit))
2900 }))
2901
2902 it.effect("interrupts deep suspended generation", () =>
2903 Effect.gen(function*() {
2904 const arbitrary = Arbitrary.schema(makeSuspendChain(10_000))
2905 const fiber = yield* Effect.forkChild(Arbitrary.sampleEffect(arbitrary, {
2906 count: 1,
2907 maxDiscards: 0,
2908 seed: "interrupt-suspended-generation"
2909 }))
2910
2911 yield* Effect.yieldNow
2912 yield* Fiber.interrupt(fiber)
2913 const exit = yield* Fiber.await(fiber)
2914
2915 assert.isTrue(Exit.hasInterrupts(exit))
2916 }))
2917 })
2918
2919 describe("combinators", () => {
2920 it.effect("generates a constant without shrink candidates", () =>
2921 Effect.gen(function*() {
2922 const value = { tag: "value" }
2923 const arbitrary = Arbitrary.Constant(value)
2924 const values = yield* Arbitrary.sampleEffect(arbitrary, { count: 3, seed: "constant" })
2925 const result = yield* Arbitrary.checkEffect(arbitrary, () => false, {
2926 runs: 1,
2927 seed: "constant"
2928 })
2929
2930 assert.deepStrictEqual(values, [value, value, value])
2931 assert.isTrue(values.every((sample) => sample === value))
2932 assert.strictEqual(result._tag, "Falsified")
2933 if (result._tag === "Falsified") {
2934 assert.strictEqual(result.initialInput, value)
2935 assert.strictEqual(result.shrunkInput, value)
2936 assert.strictEqual(result.shrinks, 0)
2937 }
2938 }))
2939
2940 it.effect("maps generated values and the complete shrink tree", () =>
2941 Effect.gen(function*() {
2942 const arbitrary = Arbitrary.schema(
2943 Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 1_000 }))
2944 ).pipe(
2945 Arbitrary.map((value) => value * 2)
2946 )
2947 const result = yield* Arbitrary.checkEffect(arbitrary, () => false, { runs: 1, seed: 47, size: 10 })
2948
2949 assert.strictEqual(result._tag, "Falsified")
2950 if (result._tag === "Falsified") {
2951 assert.strictEqual(result.initialInput, 2_000)
2952 assert.strictEqual(result.shrunkInput, 2)
2953 assert.strictEqual(result.shrinks, 1)
2954 }
2955 }))
2956
2957 it.effect("preserves duplicate shrink positions after a non-injective map", () =>
2958 Effect.gen(function*() {
2959 const arbitrary = Arbitrary.map(
2960 Arbitrary.schema(Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 1_000 }))),
2961 () => "same"
2962 )
2963 let evaluations = 0
2964 const result = yield* Arbitrary.checkEffect(arbitrary, () => {
2965 evaluations++
2966 return false
2967 }, { runs: 1, seed: 47, size: 10 })
2968
2969 assert.strictEqual(result._tag, "Falsified")
2970 if (result._tag === "Falsified") {
2971 assert.strictEqual(result.initialInput, "same")
2972 assert.strictEqual(result.shrunkInput, "same")
2973 assert.strictEqual(result.shrinks, 1)
2974 assert.strictEqual(evaluations, 2)
2975 }
2976 }))
2977
2978 it.effect("bounds values rejected by filter", () =>
2979 Effect.gen(function*() {
2980 const arbitrary = Arbitrary.filter(Arbitrary.schema(Schema.Int), () => false)
2981 const sampled = yield* Effect.result(Arbitrary.sampleEffect(arbitrary, {
2982 count: 1,
2983 maxDiscards: 2,
2984 seed: "filter-exhaustion"
2985 }))
2986 const checked = yield* Arbitrary.checkEffect(arbitrary, () => true, {
2987 runs: 1,
2988 maxDiscards: 2,
2989 seed: "filter-exhaustion"
2990 })
2991
2992 assert.isTrue(Result.isFailure(sampled))
2993 if (Result.isFailure(sampled)) {
2994 assert.deepStrictEqual(sampled.failure, {
2995 _tag: "SampleError",
2996 generated: 0,
2997 discards: 3,
2998 seed: "filter-exhaustion"
2999 })
3000 }
3001 assert.deepStrictEqual(checked, {
3002 _tag: "Exhausted",
3003 runs: 0,
3004 discards: 3,
3005 seed: "filter-exhaustion"
3006 })
3007 }))
3008
3009 it.effect("promotes accepted shrink descendants through filter", () =>
3010 Effect.gen(function*() {
3011 const arbitrary = Arbitrary.filter(
3012 Arbitrary.schema(Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 100 }))),
3013 (value) => value === 100 || value === 26
3014 )
3015 const result = yield* Arbitrary.checkEffect(arbitrary, () => false, { runs: 1, seed: 47, size: 10 })
3016
3017 assert.strictEqual(result._tag, "Falsified")
3018 if (result._tag === "Falsified") {
3019 assert.strictEqual(result.initialInput, 100)
3020 assert.strictEqual(result.shrunkInput, 26)
3021 assert.strictEqual(result.shrinks, 1)
3022 }
3023 }))
3024
3025 it.effect("transforms accepted values and discards rejected values with filterMap", () =>
3026 Effect.gen(function*() {
3027 const arbitrary = Arbitrary.filterMap(
3028 Arbitrary.schema(Schema.Int.check(Schema.isBetween({ minimum: 0, maximum: 100 }))),
3029 (value) => value % 2 === 0 ? Result.succeed(`even:${value}`) : Result.fail(value)
3030 )
3031 const values = yield* Arbitrary.sampleEffect(arbitrary, {
3032 count: 20,
3033 maxDiscards: 100,
3034 seed: "filter-map"
3035 })
3036
3037 assert.strictEqual(values.length, 20)
3038 assert.isTrue(values.every((value) => /^even:\d+$/.test(value)))
3039 }))
3040
3041 it.effect("promotes transformed shrink descendants through filterMap", () =>
3042 Effect.gen(function*() {
3043 const arbitrary = Arbitrary.filterMap(
3044 Arbitrary.schema(Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 100 }))),
3045 (value) => value === 100 || value === 26 ? Result.succeed(`value:${value}`) : Result.fail(value)
3046 )
3047 const result = yield* Arbitrary.checkEffect(arbitrary, () => false, { runs: 1, seed: 47, size: 10 })
3048
3049 assert.strictEqual(result._tag, "Falsified")
3050 if (result._tag === "Falsified") {
3051 assert.strictEqual(result.initialInput, "value:100")
3052 assert.strictEqual(result.shrunkInput, "value:26")
3053 assert.strictEqual(result.shrinks, 1)
3054 const replayed = yield* Arbitrary.checkEffect(arbitrary, () => false, { replay: result.replay })
3055 assert.strictEqual(replayed._tag, "Falsified")
3056 if (replayed._tag === "Falsified") {
3057 assert.strictEqual(replayed.initialInput, result.initialInput)
3058 assert.strictEqual(replayed.shrunkInput, result.shrunkInput)
3059 assert.strictEqual(replayed.shrinks, result.shrinks)
3060 }
3061 }
3062 }))
3063
3064 it.effect("keeps root generation stable and shrinks dependent lengths consistently", () =>
3065 Effect.gen(function*() {
3066 const source = Arbitrary.schema(Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 8 })))
3067 const item = Schema.Int.check(Schema.isBetween({ minimum: -1_000, maximum: 1_000 }))
3068 const dependents = globalThis.Array.from({ length: 8 }, (_, index) => {
3069 const length = index + 1
3070 return Arbitrary.schema(
3071 Schema.Array(item).check(Schema.isMinLength(length), Schema.isMaxLength(length))
3072 ).pipe(Arbitrary.map((values) => ({ length, values })))
3073 })
3074 const arbitrary = source.pipe(Arbitrary.flatMap((length) => dependents[length - 1]))
3075
3076 const composite = Arbitrary.all({
3077 dependent: arbitrary,
3078 after: Arbitrary.schema(Schema.Int)
3079 })
3080 const sampled = yield* Arbitrary.sampleEffect(composite, { count: 1, seed: 3, size: 8 })
3081 let checked: {
3082 readonly dependent: { readonly length: number; readonly values: ReadonlyArray<number> }
3083 readonly after: number
3084 } | undefined
3085 yield* Arbitrary.checkEffect(composite, (value) => {
3086 checked = value
3087 return true
3088 }, { runs: 1, seed: 3, size: 8 })
3089 assert.deepStrictEqual(sampled[0], checked)
3090
3091 let failures = 0
3092 for (let seed = 0; seed < 32; seed++) {
3093 const result = yield* Arbitrary.checkEffect(arbitrary, (value) => value.values[0] >= 0, {
3094 runs: 1,
3095 seed,
3096 size: 8
3097 })
3098 if (result._tag === "Falsified") {
3099 failures++
3100 assert.strictEqual(result.shrunkInput.length, 1)
3101 }
3102 }
3103 assert.isAbove(failures, 0)
3104 }))
3105
3106 it.effect("shrinks the source first and closes it after selecting a dependent shrink", () =>
3107 Effect.gen(function*() {
3108 const integers = Arbitrary.schema(Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 100 })))
3109 const sources: Array<number> = []
3110 const arbitrary = Arbitrary.flatMap(integers, (source) => {
3111 sources.push(source)
3112 return Arbitrary.map(integers, (target) => ({ source, target }))
3113 })
3114 let initial: { readonly source: number; readonly target: number } | undefined
3115 const result = yield* Arbitrary.checkEffect(arbitrary, (value) => {
3116 if (initial === undefined) {
3117 initial = value
3118 return false
3119 }
3120 return value.source !== initial.source || value.target === initial.target
3121 }, { runs: 1, seed: 0, size: 10 })
3122
3123 assert.strictEqual(result._tag, "Falsified")
3124 if (result._tag === "Falsified") {
3125 assert.deepStrictEqual(result.initialInput, { source: 2, target: 15 })
3126 assert.deepStrictEqual(result.shrunkInput, { source: 2, target: 1 })
3127 assert.strictEqual(result.shrinks, 1)
3128 assert.deepStrictEqual(sources, [2, 1])
3129 }
3130 }))
3131
3132 it.effect("replays source, dependent, and promoted flatMap shrink paths", () =>
3133 Effect.gen(function*() {
3134 const integers = Arbitrary.schema(Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 100 })))
3135 const cases: ReadonlyArray<{
3136 readonly arbitrary: Arbitrary.Arbitrary<number>
3137 readonly property: (value: number) => boolean
3138 readonly shrunkInput: number
3139 }> = [
3140 {
3141 arbitrary: Arbitrary.flatMap(integers, (value) => Arbitrary.schema(Schema.Literal(value))),
3142 property: (value) => value < 10,
3143 shrunkInput: 10
3144 },
3145 {
3146 arbitrary: Arbitrary.flatMap(Arbitrary.schema(Schema.Null), () => integers),
3147 property: (value) => value < 10,
3148 shrunkInput: 10
3149 },
3150 {
3151 arbitrary: Arbitrary.flatMap(integers, (value) =>
3152 value === 100 || value === 26
3153 ? Arbitrary.schema(Schema.Literal(value))
3154 : Arbitrary.filter(Arbitrary.schema(Schema.Literal(value)), () => false)),
3155 property: () => false,
3156 shrunkInput: 26
3157 }
3158 ]
3159
3160 for (const test of cases) {
3161 const result = yield* Arbitrary.checkEffect(test.arbitrary, test.property, {
3162 runs: 1,
3163 seed: 47,
3164 size: 10
3165 })
3166 assert.strictEqual(result._tag, "Falsified")
3167 if (result._tag !== "Falsified") continue
3168 assert.strictEqual(result.initialInput, 100)
3169 assert.strictEqual(result.shrunkInput, test.shrunkInput)
3170
3171 const replayed = yield* Arbitrary.checkEffect(test.arbitrary, test.property, { replay: result.replay })
3172 assert.strictEqual(replayed._tag, "Falsified")
3173 if (replayed._tag === "Falsified") {
3174 assert.strictEqual(replayed.initialInput, result.initialInput)
3175 assert.strictEqual(replayed.shrunkInput, result.shrunkInput)
3176 assert.deepStrictEqual(replayed.failure, result.failure)
3177 assert.strictEqual(replayed.shrinks, result.shrinks)
3178 }
3179 }
3180 }))
3181
3182 it.effect("bounds initial flatMap discards and terminates an all-discard shrink frontier", () =>
3183 Effect.gen(function*() {
3184 const rejected = Arbitrary.filter(Arbitrary.schema(Schema.Literal("value")), () => false)
3185 const roots = [
3186 Arbitrary.flatMap(rejected, () => Arbitrary.schema(Schema.Literal("target"))),
3187 Arbitrary.flatMap(Arbitrary.schema(Schema.Literal("source")), () => rejected)
3188 ]
3189 for (const arbitrary of roots) {
3190 const sampled = yield* Effect.result(Arbitrary.sampleEffect(arbitrary, {
3191 count: 1,
3192 maxDiscards: 2,
3193 seed: "flat-map-discard"
3194 }))
3195 const checked = yield* Arbitrary.checkEffect(arbitrary, () => true, {
3196 runs: 1,
3197 maxDiscards: 2,
3198 seed: "flat-map-discard"
3199 })
3200 assert.isTrue(Result.isFailure(sampled))
3201 if (Result.isFailure(sampled)) {
3202 assert.deepStrictEqual(sampled.failure, {
3203 _tag: "SampleError",
3204 generated: 0,
3205 discards: 3,
3206 seed: "flat-map-discard"
3207 })
3208 }
3209 assert.deepStrictEqual(checked, {
3210 _tag: "Exhausted",
3211 runs: 0,
3212 discards: 3,
3213 seed: "flat-map-discard"
3214 })
3215 }
3216
3217 const integers = Arbitrary.schema(Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 10 })))
3218 const frontier = Arbitrary.flatMap(integers, (value) =>
3219 value === 10
3220 ? Arbitrary.schema(Schema.Literal(value))
3221 : Arbitrary.filter(Arbitrary.schema(Schema.Literal(value)), () => false))
3222 const result = yield* Arbitrary.checkEffect(frontier, () => false, { runs: 1, seed: 47, size: 10 })
3223 assert.strictEqual(result._tag, "Falsified")
3224 if (result._tag === "Falsified") {
3225 assert.strictEqual(result.initialInput, 10)
3226 assert.strictEqual(result.shrunkInput, 10)
3227 assert.strictEqual(result.shrinks, 0)
3228 }
3229 }))
3230
3231 it.effect("shares recursive fuel and tops up only the dependent minimum cost", () =>
3232 Effect.gen(function*() {
3233 interface Chain {
3234 readonly next: Chain | null
3235 }
3236 const Chain: Schema.Codec<Chain> = Schema.Struct({
3237 next: Schema.suspend(() => Schema.Union([Schema.Null, Chain]))
3238 })
3239 const chain = Arbitrary.schema(Chain)
3240 const triple = chain.pipe(
3241 Arbitrary.flatMap((first) =>
3242 chain.pipe(
3243 Arbitrary.flatMap((second) => chain.pipe(Arbitrary.map((third) => [first, second, third] as const)))
3244 )
3245 )
3246 )
3247 const chainSize = (value: Chain): number => 1 + (value.next === null ? 0 : chainSize(value.next))
3248
3249 const values = yield* Arbitrary.sampleEffect(triple, {
3250 count: 512,
3251 maxDiscards: 0,
3252 seed: 42,
3253 size: 8
3254 })
3255 assert.isTrue(values.every((value) => value.reduce((total, item) => total + chainSize(item), 0) - 3 <= 8))
3256
3257 let checked = 0
3258 const result = yield* Arbitrary.checkEffect(triple, (value) => {
3259 checked++
3260 return value.reduce((total, item) => total + chainSize(item), 0) - 3 <= 8
3261 }, { runs: 128, seed: 42, size: 8 })
3262 assert.deepStrictEqual(result, { _tag: "Passed", runs: 128, discards: 0 })
3263 assert.strictEqual(checked, 128)
3264
3265 const minimum = yield* Arbitrary.sampleEffect(triple, {
3266 count: 64,
3267 maxDiscards: 0,
3268 seed: 42,
3269 size: 0
3270 })
3271 assert.isTrue(minimum.every((value) => value.every((item) => chainSize(item) === 1)))
3272 }))
3273
3274 it.effect("supports mutually recursive Schema Arbitraries as source and dependent", () =>
3275 Effect.gen(function*() {
3276 interface A {
3277 readonly _tag: "A"
3278 readonly next: B | null
3279 }
3280 interface B {
3281 readonly _tag: "B"
3282 readonly next: A | null
3283 }
3284 const A: Schema.Codec<A> = Schema.Struct({
3285 _tag: Schema.Literal("A"),
3286 next: Schema.suspend(() => Schema.Union([Schema.Null, B]))
3287 })
3288 const B: Schema.Codec<B> = Schema.Struct({
3289 _tag: Schema.Literal("B"),
3290 next: Schema.suspend(() => Schema.Union([Schema.Null, A]))
3291 })
3292 const arbitrary = Arbitrary.flatMap(
3293 Arbitrary.schema(A),
3294 (left) => Arbitrary.map(Arbitrary.schema(B), (right) => [left, right] as const)
3295 )
3296 const values = yield* Arbitrary.sampleEffect(arbitrary, {
3297 count: 30,
3298 maxDiscards: 0,
3299 seed: "flat-map-mutual-recursion",
3300 size: 5
3301 })
3302
3303 assert.isTrue(values.every(([left, right]) => Schema.is(A)(left) && Schema.is(B)(right)))
3304 }))
3305
3306 it.effect("keeps dependent generation interruptible", () =>
3307 Effect.gen(function*() {
3308 const started = yield* Deferred.make<void>()
3309 const schema = Schema.declare<number>((input): input is number => typeof input === "number", {
3310 toCodecArbitrary: () =>
3311 Schema.link<number>()(
3312 Schema.Literal(1),
3313 SchemaTransformation.transformEffect<number, 1>({
3314 decode: () => {
3315 Deferred.doneUnsafe(started, Effect.void)
3316 return Effect.never
3317 },
3318 encode: () => Effect.succeed(1)
3319 })
3320 )
3321 })
3322 const arbitrary = Arbitrary.flatMap(
3323 Arbitrary.schema(Schema.Literal("source")),
3324 () => Arbitrary.schema(schema)
3325 )
3326 const fiber = yield* Effect.forkChild(Arbitrary.sampleEffect(arbitrary, {
3327 count: 1,
3328 seed: "interrupt-flat-map-dependent"
3329 }))
3330
3331 yield* Deferred.await(started)
3332 yield* Fiber.interrupt(fiber)
3333 const exit = yield* Fiber.await(fiber)
3334 assert.isTrue(Exit.hasInterrupts(exit))
3335 }))
3336
3337 it.effect("interrupts long discarded-source promotion", () =>
3338 Effect.gen(function*() {
3339 const started = yield* Deferred.make<void>()
3340 const source = Arbitrary.schema(
3341 Schema.Array(Schema.Int).check(Schema.isMinLength(1_000), Schema.isMaxLength(1_000))
3342 )
3343 let callbacks = 0
3344 const arbitrary = Arbitrary.flatMap(source, (value) => {
3345 callbacks++
3346 if (callbacks === 2) Deferred.doneUnsafe(started, Effect.void)
3347 const target = Arbitrary.Constant(value)
3348 return callbacks === 1 ? target : Arbitrary.filter(target, () => false)
3349 })
3350 const fiber = yield* Effect.forkChild(
3351 Arbitrary.checkEffect(arbitrary, () => false, {
3352 runs: 1,
3353 seed: "interrupt-flat-map-promotion"
3354 }).pipe(Effect.provideService(Scheduler.MaxOpsBeforeYield, 16))
3355 )
3356
3357 yield* Deferred.await(started)
3358 yield* Fiber.interrupt(fiber)
3359 const exit = yield* Fiber.await(fiber)
3360 assert.isTrue(Exit.hasInterrupts(exit))
3361 }))
3362
3363 it.effect("keeps synchronous combinator exceptions as defects", () =>
3364 Effect.gen(function*() {
3365 const source = Arbitrary.schema(Schema.Literal("value"))
3366 const defect = (): never => {
3367 throw new Error("combinator defect")
3368 }
3369 const arbitraries: ReadonlyArray<Arbitrary.Arbitrary<unknown>> = [
3370 Arbitrary.map(source, defect),
3371 Arbitrary.filter(source, defect),
3372 Arbitrary.filterMap(source, defect),
3373 Arbitrary.flatMap(source, defect)
3374 ]
3375
3376 for (const arbitrary of arbitraries) {
3377 const exit = yield* Effect.exit(Arbitrary.sampleEffect(arbitrary, { count: 1, seed: "combinator-defect" }))
3378 assert.isTrue(Exit.hasDies(exit))
3379 }
3380 }))
3381
3382 it.effect("combines tuple, iterable, and record Arbitraries with all", () =>
3383 Effect.gen(function*() {
3384 const text = Arbitrary.schema(Schema.Literal("value"))
3385 const count = Arbitrary.schema(Schema.Literal(1))
3386 const tuple = yield* Arbitrary.sampleEffect(Arbitrary.all([text, count]), {
3387 count: 1,
3388 maxDiscards: 0,
3389 seed: "all-tuple"
3390 })
3391 const iterable = yield* Arbitrary.sampleEffect(Arbitrary.all(new Set([text, count])), {
3392 count: 1,
3393 maxDiscards: 0,
3394 seed: "all-iterable"
3395 })
3396 const record = yield* Arbitrary.sampleEffect(Arbitrary.all({ text, count }), {
3397 count: 1,
3398 maxDiscards: 0,
3399 seed: "all-record"
3400 })
3401 const emptyTuple = yield* Arbitrary.sampleEffect(Arbitrary.all([]), { count: 1, seed: "all-empty-tuple" })
3402 const emptyRecord = yield* Arbitrary.sampleEffect(Arbitrary.all({}), { count: 1, seed: "all-empty-record" })
3403
3404 assert.deepStrictEqual(tuple, [["value", 1]])
3405 assert.deepStrictEqual(iterable, [["value", 1]])
3406 assert.deepStrictEqual(record, [{ text: "value", count: 1 }])
3407 assert.deepStrictEqual(emptyTuple, [[]])
3408 assert.deepStrictEqual(emptyRecord, [{}])
3409 }))
3410
3411 it.effect("shrinks all members independently and replays the product", () =>
3412 Effect.gen(function*() {
3413 const member = Arbitrary.schema(Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 100 })))
3414 const arbitrary = Arbitrary.all([member, member])
3415 const result = yield* Arbitrary.checkEffect(arbitrary, () => false, { runs: 1, seed: 47, size: 10 })
3416
3417 assert.strictEqual(result._tag, "Falsified")
3418 if (result._tag === "Falsified") {
3419 assert.deepStrictEqual(result.shrunkInput, [1, 1])
3420 const replayed = yield* Arbitrary.checkEffect(arbitrary, () => false, { replay: result.replay })
3421 assert.deepStrictEqual(replayed, result)
3422 }
3423 }))
3424
3425 it.effect("shares one optional recursion budget across all members", () =>
3426 Effect.gen(function*() {
3427 interface Chain {
3428 readonly next: Chain | null
3429 }
3430 const Chain: Schema.Codec<Chain> = Schema.Struct({
3431 next: Schema.suspend(() => Schema.Union([Schema.Null, Chain]))
3432 })
3433 const values = yield* Arbitrary.sampleEffect(
3434 Arbitrary.all([
3435 Arbitrary.schema(Chain),
3436 Arbitrary.schema(Chain),
3437 Arbitrary.schema(Chain)
3438 ]),
3439 {
3440 count: 100,
3441 maxDiscards: 0,
3442 seed: "all-recursion-budget",
3443 size: 5
3444 }
3445 )
3446 const depth = (chain: Chain): number => chain.next === null ? 1 : 1 + depth(chain.next)
3447
3448 assert.isTrue(values.every((value) => value.reduce((total, chain) => total + depth(chain) - 1, 0) <= 5))
3449 }))
3450
3451 it.effect("preserves special record keys", () =>
3452 Effect.gen(function*() {
3453 const input = Object.fromEntries([
3454 ["__proto__", Arbitrary.schema(Schema.Literal("value"))]
3455 ]) as Record<"__proto__", Arbitrary.Arbitrary<"value">>
3456 const arbitrary = Arbitrary.all(input)
3457 const values = yield* Arbitrary.sampleEffect(arbitrary, { count: 512, seed: "all-proto" })
3458 const prototypes = new Set(values.map(Object.getPrototypeOf))
3459
3460 assert.isTrue(values.every((value) => Object.hasOwn(value, "__proto__") && value.__proto__ === "value"))
3461 assert.isTrue(prototypes.has(Object.prototype))
3462 assert.isTrue(prototypes.has(null))
3463 }))
3464 })
3465
3466 describe("exhaustion", () => {
3467 it.effect("reports and reuses an automatically resolved seed", () =>
3468 Effect.gen(function*() {
3469 const seed = 123_456
3470 const random = {
3471 nextIntUnsafe: () => seed,
3472 nextDoubleUnsafe: () => 0
3473 }
3474 const arbitrary = Arbitrary.filter(Arbitrary.schema(Schema.Literal("value")), () => false)
3475 const sampleOptions = { count: 1, maxDiscards: 2 } as const
3476 const checkOptions = { runs: 1, maxDiscards: 2 } as const
3477
3478 const sampled = yield* Effect.result(Arbitrary.sampleEffect(arbitrary, sampleOptions)).pipe(
3479 Effect.provideService(Random.Random, random)
3480 )
3481 assert.isTrue(Result.isFailure(sampled))
3482 if (Result.isSuccess(sampled)) return
3483 assert.strictEqual(sampled.failure.seed, seed)
3484 const resampled = yield* Effect.result(Arbitrary.sampleEffect(arbitrary, {
3485 ...sampleOptions,
3486 seed: sampled.failure.seed
3487 }))
3488
3489 const checked = yield* Arbitrary.checkEffect(arbitrary, () => true, checkOptions).pipe(
3490 Effect.provideService(Random.Random, random)
3491 )
3492 assert.strictEqual(checked._tag, "Exhausted")
3493 if (checked._tag !== "Exhausted") return
3494 assert.strictEqual(checked.seed, seed)
3495 const rechecked = yield* Arbitrary.checkEffect(arbitrary, () => true, {
3496 ...checkOptions,
3497 seed: checked.seed
3498 })
3499
3500 assert.deepStrictEqual(resampled, sampled)
3501 assert.deepStrictEqual(rechecked, checked)
3502 }))
3503 })
3504
3505 describe("formatCheckFailure", () => {
3506 it("formats diagnostic values without ambiguity", () => {
3507 const falsified = Arbitrary.formatCheckFailure({
3508 _tag: "Falsified",
3509 initialInput: "initial",
3510 shrunkInput: "\n",
3511 failure: { _tag: "PropertyError", error: "boom" },
3512 runs: 1,
3513 discards: 0,
3514 shrinks: 0,
3515 replay: "replay"
3516 })
3517 const exhausted = Arbitrary.formatCheckFailure({
3518 _tag: "Exhausted",
3519 runs: 0,
3520 discards: 1,
3521 seed: "seed"
3522 })
3523 const map = Arbitrary.formatCheckFailure({
3524 _tag: "Falsified",
3525 initialInput: new Map(),
3526 shrunkInput: new Map([["key", 1]]),
3527 failure: { _tag: "ReturnedFalse" },
3528 runs: 1,
3529 discards: 0,
3530 shrinks: 0,
3531 replay: "replay"
3532 })
3533 const throwingInput = Object.defineProperty({}, "value", {
3534 enumerable: true,
3535 get() {
3536 throw new Error("getter defect")
3537 }
3538 })
3539 const throwing = Arbitrary.formatCheckFailure({
3540 _tag: "Falsified",
3541 initialInput: throwingInput,
3542 shrunkInput: throwingInput,
3543 failure: { _tag: "ReturnedFalse" },
3544 runs: 1,
3545 discards: 0,
3546 shrinks: 0,
3547 replay: "replay"
3548 })
3549
3550 assert.strictEqual(
3551 falsified,
3552 "Property falsified after 1 run(s) and 0 shrink(s)\nShrunk input: \"\\n\"\nFailure: \"boom\"\nReplay: replay"
3553 )
3554 assert.strictEqual(exhausted, "Property exhausted after 0 run(s) and 1 discard(s)\nSeed: \"seed\"")
3555 assert.match(map!, /Shrunk input: Map\(/)
3556 assert.match(throwing!, /Shrunk input: {"value":"\[property access threw\]"}/)
3557 assert.match(throwing!, /Replay: replay$/)
3558 })
3559 })
3560
3561 describe("checkEffect", () => {
3562 it.effect("isolates generation Random from property Random", () =>
3563 Effect.gen(function*() {
3564 const arbitrary = Arbitrary.schema(Schema.Int)
3565 const pureInputs: Array<number> = []
3566 const effectfulInputs: Array<number> = []
3567
3568 yield* Arbitrary.checkEffect(arbitrary, (value) => {
3569 pureInputs.push(value)
3570 return true
3571 }, { runs: 20, seed: "random-isolation" })
3572 yield* Arbitrary.checkEffect(arbitrary, (value) =>
3573 Effect.gen(function*() {
3574 effectfulInputs.push(value)
3575 yield* Random.next
3576 yield* Random.next
3577 return true
3578 }), { runs: 20, seed: "random-isolation" })
3579
3580 assert.deepStrictEqual(effectfulInputs, pureInputs)
3581 }))
3582
3583 it.effect("grows size across successful runs without advancing on discards", () =>
3584 Effect.gen(function*() {
3585 type Node = null | { readonly next: Node }
3586 const Node: Schema.Codec<Node> = Schema.Union([
3587 Schema.Null.check(Schema.makeFilter(() => false, { expected: "discard" })),
3588 Schema.Null,
3589 Schema.Struct({ next: Schema.suspend(() => Node) })
3590 ])
3591 const inputs: Array<Node> = []
3592 const result = yield* Arbitrary.checkEffect(Arbitrary.schema(Node), (input) => {
3593 inputs.push(input)
3594 return true
3595 }, {
3596 runs: 100,
3597 size: 1,
3598 seed: "progressive-size"
3599 })
3600
3601 assert.strictEqual(result._tag, "Passed")
3602 if (result._tag === "Passed") assert.isAbove(result.discards, 0)
3603 assert.isTrue(inputs.slice(0, 50).every((input) => input === null))
3604 assert.isTrue(inputs.slice(50).some((input) => input !== null))
3605 }))
3606
3607 it.effect("shrinks integers to the local failure boundary", () =>
3608 Effect.gen(function*() {
3609 const result = yield* Arbitrary.checkEffect(
3610 Arbitrary.schema(Schema.Int.check(Schema.isBetween({ minimum: 0, maximum: 100 }))),
3611 (value) => value < 10,
3612 { runs: 100, seed: 0, size: 10 }
3613 )
3614
3615 assert.strictEqual(result._tag, "Falsified")
3616 if (result._tag === "Falsified") {
3617 assert.strictEqual(result.shrunkInput, 10)
3618 }
3619 }))
3620
3621 it.effect("shrinks bounded positive BigInts toward the minimum", () =>
3622 Effect.gen(function*() {
3623 const result = yield* Arbitrary.checkEffect(
3624 Arbitrary.schema(Schema.BigInt.check(Schema.isBetweenBigInt({ minimum: 100n, maximum: 1_000n }))),
3625 () => false,
3626 { runs: 1, seed: 21 }
3627 )
3628
3629 assert.strictEqual(result._tag, "Falsified")
3630 if (result._tag === "Falsified") {
3631 assert.isTrue(result.initialInput > 100n)
3632 assert.strictEqual(result.shrunkInput, 100n)
3633 }
3634 }))
3635
3636 it.effect("shrinks bounded BigInts to the local failure boundary", () =>
3637 Effect.gen(function*() {
3638 const result = yield* Arbitrary.checkEffect(
3639 Arbitrary.schema(Schema.BigInt.check(Schema.isBetweenBigInt({ minimum: 100n, maximum: 1_000n }))),
3640 (value) => value < 700n,
3641 { runs: 1, seed: 839 }
3642 )
3643
3644 assert.strictEqual(result._tag, "Falsified")
3645 if (result._tag === "Falsified") {
3646 assert.strictEqual(result.initialInput, 775n)
3647 assert.strictEqual(result.shrunkInput, 700n)
3648 }
3649 }))
3650
3651 it.effect("selects the closest-to-zero BigInt shrink target", () =>
3652 Effect.gen(function*() {
3653 const cases = [
3654 { minimum: -1_000n, maximum: -100n, target: -100n },
3655 { minimum: -1_000n, maximum: 1_000n, target: 0n }
3656 ] as const
3657 for (const { maximum, minimum, target } of cases) {
3658 const result = yield* Arbitrary.checkEffect(
3659 Arbitrary.schema(Schema.BigInt.check(Schema.isBetweenBigInt({ minimum, maximum }))),
3660 () => false,
3661 { runs: 1, seed: 21 }
3662 )
3663
3664 assert.strictEqual(result._tag, "Falsified")
3665 if (result._tag === "Falsified") {
3666 assert.notStrictEqual(result.initialInput, target)
3667 assert.strictEqual(result.shrunkInput, target)
3668 }
3669 }
3670 }))
3671
3672 it.effect("shrinks Numbers to the local representable failure boundary", () =>
3673 Effect.gen(function*() {
3674 const result = yield* Arbitrary.checkEffect(
3675 Arbitrary.schema(Schema.Number.check(Schema.isBetween({ minimum: 2, maximum: 4 }))),
3676 (value) => value < 3,
3677 { runs: 100, seed: 1, size: 10 }
3678 )
3679
3680 assert.strictEqual(result._tag, "Falsified")
3681 if (result._tag === "Falsified") {
3682 assert.isAtLeast(result.initialInput, 3)
3683 assert.strictEqual(result.shrunkInput, 3)
3684 }
3685 }))
3686
3687 it.effect("shrinks NaN through the ordinary Number target", () =>
3688 Effect.gen(function*() {
3689 const result = yield* Arbitrary.checkEffect(Arbitrary.schema(Schema.Number), () => false, {
3690 runs: 1,
3691 seed: 231,
3692 size: 10
3693 })
3694
3695 assert.strictEqual(result._tag, "Falsified")
3696 if (result._tag === "Falsified") {
3697 assert.isTrue(Number.isNaN(result.initialInput))
3698 assert.strictEqual(result.shrunkInput, 0)
3699 }
3700 }))
3701
3702 it.effect("shrinks recursive unions toward a finite base branch", () =>
3703 Effect.gen(function*() {
3704 type Node = null | { readonly next: Node }
3705 const Node: Schema.Codec<Node> = Schema.Union([
3706 Schema.Null,
3707 Schema.Struct({ next: Schema.suspend(() => Node) })
3708 ])
3709 const result = yield* Arbitrary.checkEffect(Arbitrary.schema(Node), () => false, {
3710 runs: 1,
3711 seed: 1,
3712 size: 5
3713 })
3714
3715 assert.strictEqual(result._tag, "Falsified")
3716 if (result._tag === "Falsified") {
3717 assert.notStrictEqual(result.initialInput, null)
3718 assert.strictEqual(result.shrunkInput, null)
3719
3720 const replayed = yield* Arbitrary.checkEffect(Arbitrary.schema(Node), () => false, { replay: result.replay })
3721 assert.strictEqual(replayed._tag, "Falsified")
3722 if (replayed._tag === "Falsified") {
3723 assert.deepStrictEqual(replayed.initialInput, result.initialInput)
3724 assert.strictEqual(replayed.shrunkInput, null)
3725 }
3726 }
3727 }))
3728
3729 it.effect("isolates Union fallback shrinking from later tuple members", () =>
3730 Effect.gen(function*() {
3731 type Node = string | { readonly next: Node }
3732 const Node: Schema.Codec<Node> = Schema.Union([
3733 Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(1)),
3734 Schema.Struct({ next: Schema.suspend(() => Node) })
3735 ])
3736 const trace = <A>(schema: Schema.Schema<A>) => {
3737 const evaluated: Array<A> = []
3738 return Arbitrary.checkEffect(Arbitrary.schema(schema), (value) => {
3739 evaluated.push(value)
3740 return false
3741 }, { runs: 1, maxShrinks: 1, seed: 1, size: 5 }).pipe(Effect.as(evaluated))
3742 }
3743
3744 const withConstantSuffix = yield* trace(Schema.Tuple([Node, Schema.Null]))
3745 const withRandomSuffix = yield* trace(Schema.Tuple([
3746 Node,
3747 Schema.String.check(Schema.isMinLength(8), Schema.isMaxLength(8))
3748 ]))
3749
3750 assert.deepStrictEqual(withConstantSuffix[0][0], withRandomSuffix[0][0])
3751 assert.strictEqual(withConstantSuffix[1][0], withRandomSuffix[1][0])
3752 }))
3753
3754 it.effect("shrinks and replays the complete falsification", () =>
3755 Effect.gen(function*() {
3756 const arbitrary = Arbitrary.schema(
3757 Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 100 }))
3758 )
3759 const first = yield* Arbitrary.checkEffect(arbitrary, (value) => value < 0, {
3760 runs: 1,
3761 seed: "replay",
3762 size: 10
3763 })
3764 assert.strictEqual(first._tag, "Falsified")
3765 if (first._tag !== "Falsified") return
3766 assert.strictEqual(first.runs, 1)
3767 assert.strictEqual(typeof first.replay, "string")
3768
3769 const replay = `${first.replay}`
3770 const replayed = yield* Arbitrary.checkEffect(arbitrary, (value) => value < 0, { replay })
3771 assert.strictEqual(replayed._tag, "Falsified")
3772 if (replayed._tag !== "Falsified") return
3773 assert.strictEqual(replayed.runs, 1)
3774 assert.strictEqual(first.shrunkInput, 1)
3775 assert.strictEqual(replayed.initialInput, first.initialInput)
3776 assert.strictEqual(replayed.shrunkInput, first.shrunkInput)
3777 assert.deepStrictEqual(replayed.failure, first.failure)
3778 assert.strictEqual(replayed.shrinks, first.shrinks)
3779 }))
3780
3781 it.effect("preserves a returned-false failure while shrinking", () =>
3782 Effect.gen(function*() {
3783 const evaluated: Array<string> = []
3784 const arbitrary = Arbitrary.schema(Schema.Literals(["root", "error", "false"]), {
3785 shrink: (value) => value === "root" ? ["error", "false"] as const : []
3786 })
3787 const result = yield* Arbitrary.checkEffect(arbitrary, (value) => {
3788 evaluated.push(value)
3789 return value === "error" ? Effect.fail("different failure") : false
3790 }, {
3791 runs: 1,
3792 maxShrinks: 2,
3793 seed: 0
3794 })
3795
3796 assert.strictEqual(result._tag, "Falsified")
3797 if (result._tag === "Falsified") {
3798 assert.strictEqual(result.initialInput, "root")
3799 assert.strictEqual(result.shrunkInput, "false")
3800 assert.deepStrictEqual(result.failure, { _tag: "ReturnedFalse" })
3801 assert.strictEqual(result.shrinks, 1)
3802 assert.deepStrictEqual(evaluated, ["root", "error", "false"])
3803 }
3804 }))
3805
3806 it.effect("preserves a typed-error failure while shrinking and replaying", () =>
3807 Effect.gen(function*() {
3808 const evaluated: Array<string> = []
3809 const arbitrary = Arbitrary.schema(Schema.Literals(["root", "false", "error"]), {
3810 shrink: (value) => value === "root" ? ["false", "error"] as const : []
3811 })
3812 const property = (value: "root" | "false" | "error") => {
3813 evaluated.push(value)
3814 return value === "false" ? false : Effect.fail(value)
3815 }
3816 const result = yield* Arbitrary.checkEffect(arbitrary, property, {
3817 runs: 1,
3818 maxShrinks: 2,
3819 seed: 0
3820 })
3821
3822 assert.strictEqual(result._tag, "Falsified")
3823 if (result._tag === "Falsified") {
3824 assert.strictEqual(result.initialInput, "root")
3825 assert.strictEqual(result.shrunkInput, "error")
3826 assert.deepStrictEqual(result.failure, { _tag: "PropertyError", error: "error" })
3827 assert.strictEqual(result.shrinks, 1)
3828 assert.deepStrictEqual(evaluated, ["root", "false", "error"])
3829
3830 evaluated.length = 0
3831 const replayed = yield* Arbitrary.checkEffect(arbitrary, property, { replay: result.replay })
3832 assert.deepStrictEqual(replayed, result)
3833 assert.deepStrictEqual(evaluated, ["root", "error"])
3834
3835 const mismatch = yield* Arbitrary.checkEffect(
3836 arbitrary,
3837 (value) => value === "root" ? Effect.fail(value) : false,
3838 { replay: result.replay }
3839 )
3840 assert.deepStrictEqual(mismatch, { _tag: "ReplayMismatch", reason: "ShrinkPassed" })
3841 }
3842 }))
3843
3844 it.effect("rejects replay when the recorded root failure class changes", () =>
3845 Effect.gen(function*() {
3846 const arbitrary = Arbitrary.Constant("root")
3847 const returnedFalse = yield* Arbitrary.checkEffect(arbitrary, () => false, { runs: 1, seed: 0 })
3848 const propertyError = yield* Arbitrary.checkEffect(arbitrary, () => Effect.fail("error"), {
3849 runs: 1,
3850 seed: 0
3851 })
3852
3853 assert.strictEqual(returnedFalse._tag, "Falsified")
3854 assert.strictEqual(propertyError._tag, "Falsified")
3855 if (returnedFalse._tag === "Falsified" && propertyError._tag === "Falsified") {
3856 const replayedAsError = yield* Arbitrary.checkEffect(arbitrary, () => Effect.fail("error"), {
3857 replay: returnedFalse.replay
3858 })
3859 const replayedAsFalse = yield* Arbitrary.checkEffect(arbitrary, () => false, {
3860 replay: propertyError.replay
3861 })
3862
3863 assert.deepStrictEqual(replayedAsError, { _tag: "ReplayMismatch", reason: "ShrinkPassed" })
3864 assert.deepStrictEqual(replayedAsFalse, { _tag: "ReplayMismatch", reason: "ShrinkPassed" })
3865 }
3866 }))
3867
3868 it.effect("counts a different failure class against maxShrinks", () =>
3869 Effect.gen(function*() {
3870 const evaluated: Array<string> = []
3871 const arbitrary = Arbitrary.schema(Schema.Literals(["root", "error", "false"]), {
3872 shrink: (value) => value === "root" ? ["error", "false"] as const : []
3873 })
3874 const result = yield* Arbitrary.checkEffect(arbitrary, (value) => {
3875 evaluated.push(value)
3876 return value === "error" ? Effect.fail("different failure") : false
3877 }, {
3878 runs: 1,
3879 maxShrinks: 1,
3880 seed: 0
3881 })
3882
3883 assert.strictEqual(result._tag, "Falsified")
3884 if (result._tag === "Falsified") {
3885 assert.strictEqual(result.shrunkInput, "root")
3886 assert.deepStrictEqual(result.failure, { _tag: "ReturnedFalse" })
3887 assert.strictEqual(result.shrinks, 0)
3888 assert.deepStrictEqual(evaluated, ["root", "error"])
3889 }
3890 }))
3891
3892 it.effect("counts every tested shrink candidate against maxShrinks", () =>
3893 Effect.gen(function*() {
3894 let evaluations = 0
3895 const result = yield* Arbitrary.checkEffect(
3896 Arbitrary.schema(Schema.Int.check(Schema.isBetween({ minimum: 0, maximum: 100 }))),
3897 () => {
3898 evaluations++
3899 return evaluations !== 1
3900 },
3901 { runs: 1, seed: 0, size: 10, maxShrinks: 1 }
3902 )
3903
3904 assert.strictEqual(result._tag, "Falsified")
3905 assert.strictEqual(evaluations, 2)
3906 if (result._tag === "Falsified") {
3907 assert.strictEqual(result.shrinks, 0)
3908 }
3909 }))
3910
3911 it.effect("counts rejected shrink candidates once and preserves the best shrunk input", () =>
3912 Effect.gen(function*() {
3913 type Make = (
3914 source: Arbitrary.Arbitrary<number>,
3915 visit: (value: number) => void
3916 ) => Arbitrary.Arbitrary<number>
3917 const allowed = (value: number) => value === 8 || value === 5 || value === 4
3918 const cases: ReadonlyArray<readonly [name: string, make: Make]> = [
3919 ["filter", (source, visit) =>
3920 Arbitrary.filter(source, (value) => {
3921 visit(value)
3922 return allowed(value)
3923 })],
3924 ["filterMap", (source, visit) =>
3925 Arbitrary.filterMap(source, (value) => {
3926 visit(value)
3927 return allowed(value) ? Result.succeed(value) : Result.fail(value)
3928 })],
3929 ["flatMap", (source, visit) =>
3930 Arbitrary.flatMap(source, (value) => {
3931 visit(value)
3932 return Arbitrary.filter(Arbitrary.schema(Schema.Literal(value)), () => allowed(value))
3933 })]
3934 ]
3935
3936 for (const [name, make] of cases) {
3937 const visited: Array<number> = []
3938 const propertyEvaluations: Array<number> = []
3939 const source = Arbitrary.schema(Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 8 })))
3940 const arbitrary = make(source, (value) => visited.push(value))
3941 const result = yield* Arbitrary.checkEffect(arbitrary, (value) => {
3942 propertyEvaluations.push(value)
3943 return false
3944 }, { runs: 1, seed: 47, size: 10, maxShrinks: 3 })
3945
3946 assert.strictEqual(result._tag, "Falsified", name)
3947 assert.deepStrictEqual(visited, [8, 1, 5, 3], name)
3948 assert.deepStrictEqual(propertyEvaluations, [8, 5], name)
3949 if (result._tag !== "Falsified") continue
3950 assert.strictEqual(result.initialInput, 8, name)
3951 assert.strictEqual(result.shrunkInput, 5, name)
3952 assert.strictEqual(result.shrinks, 1, name)
3953
3954 visited.length = 0
3955 propertyEvaluations.length = 0
3956 const replayed = yield* Arbitrary.checkEffect(arbitrary, (value) => {
3957 propertyEvaluations.push(value)
3958 return false
3959 }, { replay: result.replay, maxShrinks: 0 })
3960 assert.strictEqual(replayed._tag, "Falsified", name)
3961 assert.deepStrictEqual(visited, [8, 1, 5], name)
3962 assert.deepStrictEqual(propertyEvaluations, [8, 5], name)
3963 if (replayed._tag === "Falsified") {
3964 assert.strictEqual(replayed.initialInput, result.initialInput, name)
3965 assert.strictEqual(replayed.shrunkInput, result.shrunkInput, name)
3966 assert.deepStrictEqual(replayed.failure, result.failure, name)
3967 assert.strictEqual(replayed.shrinks, result.shrinks, name)
3968 }
3969 }
3970 }))
3971
3972 it.effect("does not count a rejected candidate once per nested filter", () =>
3973 Effect.gen(function*() {
3974 const innerEvaluations: Array<number> = []
3975 const outerEvaluations: Array<number> = []
3976 const propertyEvaluations: Array<number> = []
3977 const arbitrary = Arbitrary.schema(
3978 Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 8 }))
3979 ).pipe(
3980 Arbitrary.filter((value) => {
3981 innerEvaluations.push(value)
3982 return value === 8 || value === 5
3983 }),
3984 Arbitrary.filter((value) => {
3985 outerEvaluations.push(value)
3986 return true
3987 })
3988 )
3989 const result = yield* Arbitrary.checkEffect(arbitrary, (value) => {
3990 propertyEvaluations.push(value)
3991 return false
3992 }, { runs: 1, seed: 47, size: 10, maxShrinks: 1 })
3993
3994 assert.strictEqual(result._tag, "Falsified")
3995 assert.deepStrictEqual(innerEvaluations, [8, 1])
3996 assert.deepStrictEqual(outerEvaluations, [8])
3997 assert.deepStrictEqual(propertyEvaluations, [8])
3998 if (result._tag === "Falsified") {
3999 assert.strictEqual(result.shrunkInput, 8)
4000 assert.strictEqual(result.shrinks, 0)
4001 }
4002 }))
4003
4004 it.effect("lazily materializes structural shrinks and replays them", () =>
4005 Effect.gen(function*() {
4006 const arbitrary = Arbitrary.schema(
4007 Schema.Array(Schema.Literal("value")).check(Schema.isMaxLength(3))
4008 )
4009 const first = yield* Arbitrary.checkEffect(arbitrary, () => false, { runs: 1, seed: 1, size: 3 })
4010 assert.strictEqual(first._tag, "Falsified")
4011 if (first._tag !== "Falsified") return
4012
4013 const replayed = yield* Arbitrary.checkEffect(arbitrary, () => false, { replay: first.replay })
4014 assert.strictEqual(replayed._tag, "Falsified")
4015 if (replayed._tag !== "Falsified") return
4016 assert.deepStrictEqual(first.initialInput, ["value"])
4017 assert.deepStrictEqual(first.shrunkInput, [])
4018 assert.strictEqual(first.shrinks, 1)
4019 assert.deepStrictEqual(replayed.initialInput, first.initialInput)
4020 assert.deepStrictEqual(replayed.shrunkInput, first.shrunkInput)
4021 assert.strictEqual(replayed.shrinks, first.shrinks)
4022 }))
4023
4024 it.effect("traverses a thousand lazy child shrinks without overflowing", () =>
4025 Effect.gen(function*() {
4026 const count = 1_000
4027 const schema = Schema.Array(Schema.Int).check(Schema.isMinLength(count), Schema.isMaxLength(count))
4028 let evaluations = 0
4029 const result = yield* Arbitrary.checkEffect(Arbitrary.schema(schema), () => {
4030 evaluations++
4031 return evaluations !== 1
4032 }, { runs: 1, seed: "wide-shrink", size: count, maxShrinks: count })
4033
4034 assert.strictEqual(result._tag, "Falsified")
4035 assert.strictEqual(evaluations, count + 1)
4036 }))
4037
4038 it.effect("propagates interruption while shrinking", () =>
4039 Effect.gen(function*() {
4040 const shrinking = yield* Deferred.make<void>()
4041 let evaluations = 0
4042 const fiber = yield* Effect.forkChild(Arbitrary.checkEffect(
4043 Arbitrary.schema(Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 1_000 }))),
4044 () => {
4045 evaluations++
4046 return evaluations === 1
4047 ? false
4048 : Deferred.succeed(shrinking, undefined).pipe(Effect.andThen(Effect.never))
4049 },
4050 { runs: 1, seed: 139, size: 10 }
4051 ))
4052
4053 yield* Deferred.await(shrinking)
4054 yield* Fiber.interrupt(fiber)
4055 const exit = yield* Fiber.await(fiber)
4056
4057 assert.strictEqual(evaluations, 2)
4058 assert.isTrue(Exit.hasInterrupts(exit))
4059 }))
4060
4061 it.effect("preserves typed property failures", () =>
4062 Effect.gen(function*() {
4063 const result = yield* Arbitrary.checkEffect(
4064 Arbitrary.schema(Schema.Literal("value")),
4065 () => Effect.fail("property failure"),
4066 { runs: 1, seed: "typed-failure" }
4067 )
4068
4069 assert.strictEqual(result._tag, "Falsified")
4070 if (result._tag === "Falsified") {
4071 assert.deepStrictEqual(result.failure, { _tag: "PropertyError", error: "property failure" })
4072 }
4073 }))
4074
4075 it.effect("requires an explicit true result from pure and Effectful properties", () =>
4076 Effect.gen(function*() {
4077 const arbitrary = Arbitrary.schema(Schema.Literal("value"))
4078 const pureProperty = (() => 1) as unknown as () => boolean
4079 const effectfulProperty = (() => Effect.succeed("yes")) as unknown as () => Effect.Effect<boolean>
4080
4081 const pure = yield* Arbitrary.checkEffect(arbitrary, pureProperty, { runs: 1, seed: "pure-truthy" })
4082 const effectful = yield* Arbitrary.checkEffect(arbitrary, effectfulProperty, {
4083 runs: 1,
4084 seed: "effectful-truthy"
4085 })
4086
4087 assert.strictEqual(pure._tag, "Falsified")
4088 assert.strictEqual(effectful._tag, "Falsified")
4089 if (pure._tag === "Falsified") assert.deepStrictEqual(pure.failure, { _tag: "ReturnedFalse" })
4090 if (effectful._tag === "Falsified") assert.deepStrictEqual(effectful.failure, { _tag: "ReturnedFalse" })
4091 }))
4092
4093 it.effect("does not turn synchronous property defects into a property result", () =>
4094 Effect.gen(function*() {
4095 const exit = yield* Effect.exit(
4096 Arbitrary.checkEffect(Arbitrary.schema(Schema.Literal("value")), () => {
4097 throw new Error("property defect")
4098 }, { runs: 1, seed: "property-defect" })
4099 )
4100
4101 assert.isTrue(Exit.hasDies(exit))
4102 }))
4103
4104 it.effect("does not turn interruption into a property result", () =>
4105 Effect.gen(function*() {
4106 const exit = yield* Effect.exit(
4107 Arbitrary.checkEffect(Arbitrary.schema(Schema.Literal("value")), () => Effect.interrupt, {
4108 runs: 1,
4109 seed: "interruption"
4110 })
4111 )
4112
4113 assert.isTrue(Exit.hasInterrupts(exit))
4114 }))
4115 })
4116})