| 1 | import { assert, describe, it } from "@effect/vitest" |
| 2 | import * as testAssert from "@effect/vitest/utils" |
| 3 | import { |
| 4 | Cause, |
| 5 | Clock, |
| 6 | Context, |
| 7 | Deferred, |
| 8 | Effect, |
| 9 | Exit, |
| 10 | Fiber, |
| 11 | Layer, |
| 12 | Ref, |
| 13 | Result, |
| 14 | Schema, |
| 15 | SchemaGetter, |
| 16 | SchemaIssue |
| 17 | } from "effect" |
| 18 | import * as SchemaTransformation from "effect/SchemaTransformation" |
| 19 | import { TestClock, TestSchema } from "effect/testing" |
| 20 | import { AssertionError } from "node:assert" |
| 21 | |
| 22 | describe("TestSchema", () => { |
| 23 | describe("ast.fields.equals", () => { |
| 24 | const equals = TestSchema.Asserts.ast.fields.equals |
| 25 | |
| 26 | it("compares string and symbol fields by AST", () => { |
| 27 | const key = Symbol("field") |
| 28 | const fields = () => ({ field: Schema.Literal("value"), [key]: Schema.Literal("value") }) |
| 29 | const left = fields() |
| 30 | const right = fields() |
| 31 | assert.notStrictEqual(left[key], right[key]) |
| 32 | equals(left, right) |
| 33 | }) |
| 34 | |
| 35 | it("rejects missing or structurally different symbol fields", () => { |
| 36 | const key = Symbol("field") |
| 37 | assert.throws(() => equals({ [key]: Schema.Literal("left") }, { [key]: Schema.Literal("right") })) |
| 38 | assert.throws(() => equals({ [key]: Schema.Literal("value") }, {})) |
| 39 | assert.throws(() => |
| 40 | equals( |
| 41 | { [key]: Schema.Literal("value").annotate({ title: "Field" }) }, |
| 42 | { [key]: Schema.Literal("value") } |
| 43 | ) |
| 44 | ) |
| 45 | assert.throws(() => |
| 46 | equals({ [key]: Schema.optionalKey(Schema.Literal("value")) }, { [key]: Schema.Literal("value") }) |
| 47 | ) |
| 48 | }) |
| 49 | |
| 50 | it("preserves symbol identity even with equal descriptions", () => { |
| 51 | const left: symbol = Symbol("field") |
| 52 | const right: symbol = Symbol("field") |
| 53 | const schema = Schema.Literal("value") |
| 54 | assert.notStrictEqual(left, right) |
| 55 | assert.throws(() => equals({ [left]: schema }, { [right]: schema })) |
| 56 | }) |
| 57 | |
| 58 | it("preserves own __proto__ and constructor fields", () => { |
| 59 | const fields = () => ({ ["__proto__"]: Schema.Literal("proto"), constructor: Schema.Literal("ctor") }) |
| 60 | const left = fields() |
| 61 | assert.isTrue(Object.hasOwn(left, "__proto__")) |
| 62 | assert.isTrue(Object.hasOwn(left, "constructor")) |
| 63 | equals(left, fields()) |
| 64 | assert.throws(() => equals(left, { constructor: Schema.Literal("ctor") })) |
| 65 | assert.throws(() => equals(left, { ["__proto__"]: Schema.Literal("other"), constructor: Schema.Literal("ctor") })) |
| 66 | }) |
| 67 | |
| 68 | it("uses the Struct own-key contract for non-enumerable fields", () => { |
| 69 | const key = Symbol("hidden") |
| 70 | const fields = (value: string): Schema.Struct.Fields => |
| 71 | Object.defineProperties({}, { |
| 72 | hidden: { value: Schema.Literal(value) }, |
| 73 | [key]: { value: Schema.Literal(value) } |
| 74 | }) |
| 75 | const left = fields("value") |
| 76 | const right = fields("value") |
| 77 | assert.deepStrictEqual(Object.keys(left), []) |
| 78 | assert.deepStrictEqual(Schema.Struct(left).ast.propertySignatures.map((field) => field.name), ["hidden", key]) |
| 79 | equals(left, right) |
| 80 | equals(left, { hidden: Schema.Literal("value"), [key]: Schema.Literal("value") }) |
| 81 | assert.throws(() => equals(left, fields("other"))) |
| 82 | }) |
| 83 | }) |
| 84 | |
| 85 | it("decoding", async () => { |
| 86 | const schema = Schema.FiniteFromString.check(Schema.isGreaterThan(0)) |
| 87 | const asserts = new TestSchema.Asserts(schema) |
| 88 | const decoding = asserts.decoding() |
| 89 | await decoding.succeed("1", 1) |
| 90 | await decoding.fail("-1", `Expected a value greater than 0`) |
| 91 | await decoding.fail("a", `Expected a finite number`) |
| 92 | }) |
| 93 | |
| 94 | it("decoding.provide", async () => { |
| 95 | class Service extends Context.Service<Service, { fallback: Effect.Effect<string> }>()("Service") {} |
| 96 | |
| 97 | const schema = Schema.String.pipe( |
| 98 | Schema.decode({ |
| 99 | decode: SchemaGetter.checkEffect((s) => |
| 100 | Effect.gen(function*() { |
| 101 | yield* Service |
| 102 | if (s.length === 0) { |
| 103 | return new SchemaIssue.InvalidValue({ |
| 104 | message: "input should not be empty string" |
| 105 | }) |
| 106 | } |
| 107 | }) |
| 108 | ), |
| 109 | encode: SchemaGetter.passthrough() |
| 110 | }) |
| 111 | ) |
| 112 | const asserts = new TestSchema.Asserts(schema) |
| 113 | |
| 114 | const decoding = asserts.decoding().provide(Service, { fallback: Effect.succeed("b") }) |
| 115 | await decoding.succeed("a") |
| 116 | await decoding.fail("", "input should not be empty string") |
| 117 | }) |
| 118 | |
| 119 | it("encoding", async () => { |
| 120 | const schema = Schema.FiniteFromString.check(Schema.isGreaterThan(0)) |
| 121 | const asserts = new TestSchema.Asserts(schema) |
| 122 | const encoding = asserts.encoding() |
| 123 | await encoding.succeed(1, "1") |
| 124 | await encoding.fail(-1, `Expected a value greater than 0`) |
| 125 | }) |
| 126 | |
| 127 | it("encoding.provide", async () => { |
| 128 | class Service extends Context.Service<Service, { fallback: Effect.Effect<string> }>()("Service") {} |
| 129 | |
| 130 | const schema = Schema.String.pipe( |
| 131 | Schema.decode({ |
| 132 | decode: SchemaGetter.passthrough(), |
| 133 | encode: SchemaGetter.checkEffect((s) => |
| 134 | Effect.gen(function*() { |
| 135 | yield* Service |
| 136 | if (s.length === 0) { |
| 137 | return new SchemaIssue.InvalidValue({ |
| 138 | message: "input should not be empty string" |
| 139 | }) |
| 140 | } |
| 141 | }) |
| 142 | ) |
| 143 | }) |
| 144 | ) |
| 145 | const asserts = new TestSchema.Asserts(schema) |
| 146 | |
| 147 | const encoding = asserts.encoding().provide(Service, { fallback: Effect.succeed("b") }) |
| 148 | await encoding.succeed("a") |
| 149 | await encoding.fail("", "input should not be empty string") |
| 150 | }) |
| 151 | |
| 152 | it("verifyRoundTrip", async () => { |
| 153 | const schema = Schema.FiniteFromString.check(Schema.isGreaterThan(0)) |
| 154 | const asserts = new TestSchema.Asserts(schema) |
| 155 | await asserts.verifyRoundTrip({ runs: 20, seed: "lossless" }) |
| 156 | }) |
| 157 | |
| 158 | it("verifyRoundTrip reports a shrunk input and replay", async () => { |
| 159 | const schema = Schema.Number.pipe( |
| 160 | Schema.decodeTo( |
| 161 | Schema.Number, |
| 162 | SchemaTransformation.transform({ decode: (value) => value, encode: () => 0 }) |
| 163 | ) |
| 164 | ) |
| 165 | const asserts = new TestSchema.Asserts(schema) |
| 166 | |
| 167 | await testAssert.throwsAsync( |
| 168 | () => asserts.verifyRoundTrip({ runs: 20, seed: "lossy" }), |
| 169 | (error) => { |
| 170 | assert.instanceOf(error, Error) |
| 171 | assert.match(error.message, /Property falsified/) |
| 172 | assert.match(error.message, /Shrunk input:/) |
| 173 | assert.match(error.message, /Replay:/) |
| 174 | } |
| 175 | ) |
| 176 | }) |
| 177 | |
| 178 | it("verifyGeneration bounds residual discards", () => { |
| 179 | const schema = Schema.Null.check(Schema.makeFilter(() => false)) |
| 180 | const asserts = new TestSchema.Asserts(schema) |
| 181 | |
| 182 | assert.throws( |
| 183 | () => asserts.arbitrary().verifyGeneration({ runs: 1, maxDiscards: 2, seed: "exhausted" }), |
| 184 | /Property exhausted after 0 run\(s\) and 3 discard\(s\)/ |
| 185 | ) |
| 186 | }) |
| 187 | |
| 188 | for (const method of ["decoding", "encoding", "make"] as const) { |
| 189 | describe(`${method} Effect assertions`, () => { |
| 190 | it.effect("defers parsing and reruns it on every execution", () => |
| 191 | Effect.gen(function*() { |
| 192 | let calls = 0 |
| 193 | const schema = Schema.String.check(Schema.makeFilter((value) => { |
| 194 | calls++ |
| 195 | return value === "ok" || "Expected ok" |
| 196 | })) |
| 197 | const assertions = new TestSchema.Asserts(schema)[method]() |
| 198 | const success = assertions.succeedEffect("ok") |
| 199 | const failure = assertions.failEffect("bad", "Expected ok") |
| 200 | assert.strictEqual(calls, 0) |
| 201 | |
| 202 | yield* success |
| 203 | yield* success |
| 204 | yield* failure |
| 205 | assert.strictEqual(calls, 3) |
| 206 | })) |
| 207 | |
| 208 | it.effect("preserves explicit undefined and reports mismatches as defects", () => |
| 209 | Effect.gen(function*() { |
| 210 | const assertions = new TestSchema.Asserts(Schema.Unknown)[method]() |
| 211 | yield* assertions.succeedEffect("value") |
| 212 | yield* assertions.succeedEffect(undefined) |
| 213 | const exit = yield* Effect.exit(assertions.succeedEffect("value", undefined)) |
| 214 | assert.isTrue(Exit.isFailure(exit)) |
| 215 | if (Exit.isFailure(exit)) { |
| 216 | assert.isFalse(Cause.hasFails(exit.cause)) |
| 217 | const error = Cause.squash(exit.cause) |
| 218 | assert.instanceOf(error, AssertionError) |
| 219 | assert.deepStrictEqual(error.actual, Result.succeed("value")) |
| 220 | assert.deepStrictEqual(error.expected, Result.succeed(undefined)) |
| 221 | } |
| 222 | |
| 223 | const strings = new TestSchema.Asserts( |
| 224 | Schema.String.check(Schema.makeFilter((value) => value.length > 0 || "Expected nonempty string")) |
| 225 | )[method]() |
| 226 | for ( |
| 227 | const effect of [ |
| 228 | strings.succeedEffect(""), |
| 229 | strings.failEffect("valid", "Expected string"), |
| 230 | strings.failEffect(123, "wrong message") |
| 231 | ] |
| 232 | ) { |
| 233 | const exit = yield* Effect.exit(effect) |
| 234 | assert.isTrue(Exit.isFailure(exit)) |
| 235 | if (Exit.isFailure(exit)) { |
| 236 | assert.isFalse(Cause.hasFails(exit.cause)) |
| 237 | assert.instanceOf(Cause.squash(exit.cause), AssertionError) |
| 238 | } |
| 239 | } |
| 240 | })) |
| 241 | |
| 242 | it("preserves Promise assertion errors and explicit undefined", async () => { |
| 243 | const assertions = new TestSchema.Asserts(Schema.Unknown)[method]() |
| 244 | await assertions.succeed("value") |
| 245 | await testAssert.throwsAsync(() => assertions.succeed("value", undefined), (error) => { |
| 246 | assert.instanceOf(error, AssertionError) |
| 247 | assert.deepStrictEqual(error.actual, Result.succeed("value")) |
| 248 | assert.deepStrictEqual(error.expected, Result.succeed(undefined)) |
| 249 | assert.strictEqual(error.operator, "deepStrictEqual") |
| 250 | }) |
| 251 | await testAssert.throwsAsync(() => assertions.fail("value", "wrong message"), (error) => { |
| 252 | assert.instanceOf(error, AssertionError) |
| 253 | }) |
| 254 | }) |
| 255 | |
| 256 | it.effect("forwards parsing options", () => |
| 257 | Effect.gen(function*() { |
| 258 | const schema = Schema.String.check( |
| 259 | Schema.makeFilter((_, __, options) => options.errors === "all" || "Expected all errors") |
| 260 | ) |
| 261 | const asserts = new TestSchema.Asserts(schema) |
| 262 | yield* asserts[method]({ parseOptions: { errors: "all" } }).succeedEffect("value") |
| 263 | yield* asserts[method]().failEffect("value", "Expected all errors") |
| 264 | })) |
| 265 | |
| 266 | it.effect("preserves thrown schema defects in success and failure assertions", () => |
| 267 | Effect.gen(function*() { |
| 268 | const defect = new Error("schema defect") |
| 269 | const schema = Schema.String.check(Schema.makeFilter(() => { |
| 270 | throw defect |
| 271 | })) |
| 272 | const assertions = new TestSchema.Asserts(schema)[method]() |
| 273 | const success = assertions.succeedEffect("value") |
| 274 | const failure = assertions.failEffect("value", "schema defect") |
| 275 | |
| 276 | assert.deepStrictEqual(yield* Effect.exit(success), Exit.die(defect)) |
| 277 | assert.deepStrictEqual(yield* Effect.exit(failure), Exit.die(defect)) |
| 278 | })) |
| 279 | }) |
| 280 | } |
| 281 | |
| 282 | it.effect("asserts decoded and encoded transformations", () => |
| 283 | Effect.gen(function*() { |
| 284 | const asserts = new TestSchema.Asserts(Schema.NumberFromString) |
| 285 | yield* asserts.decoding().succeedEffect("42", 42) |
| 286 | yield* asserts.encoding().succeedEffect(42, "42") |
| 287 | yield* asserts.decoding().failEffect(null, "Expected string") |
| 288 | yield* asserts.encoding().failEffect(null, "Expected number") |
| 289 | })) |
| 290 | |
| 291 | it.effect("make uses constructor defaults from the test environment and honors disableChecks", () => |
| 292 | Effect.gen(function*() { |
| 293 | const schema = Schema.Struct({ |
| 294 | time: Schema.Number.pipe(Schema.withConstructorDefault(Clock.currentTimeMillis)) |
| 295 | }) |
| 296 | const make = new TestSchema.Asserts(schema).make() |
| 297 | const assertion = make.succeedEffect({}, { time: 123 }) |
| 298 | yield* TestClock.setTime(123) |
| 299 | yield* assertion |
| 300 | |
| 301 | const checked = new TestSchema.Asserts(Schema.Number.check(Schema.isGreaterThan(0))) |
| 302 | yield* checked.make().failEffect(-1, "Expected a value greater than 0") |
| 303 | yield* checked.make({ disableChecks: true }).succeedEffect(-1) |
| 304 | })) |
| 305 | |
| 306 | for (const method of ["decoding", "encoding"] as const) { |
| 307 | it.effect(`${method} preserves validation failures and defective finalizers`, () => |
| 308 | Effect.gen(function*() { |
| 309 | const defect = new Error("cleanup failed") |
| 310 | const finalize = Effect.ensuring(Effect.die(defect)) |
| 311 | const schema = method === "decoding" |
| 312 | ? Schema.String.pipe(Schema.middlewareDecoding(finalize)) |
| 313 | : Schema.String.pipe(Schema.middlewareEncoding(finalize)) |
| 314 | const assertions = new TestSchema.Asserts(schema)[method]() |
| 315 | |
| 316 | for ( |
| 317 | const effect of [ |
| 318 | assertions.succeedEffect(123, "value"), |
| 319 | assertions.failEffect(123, "Expected string") |
| 320 | ] |
| 321 | ) { |
| 322 | const exit = yield* Effect.exit(effect) |
| 323 | assert.isTrue(Exit.isFailure(exit)) |
| 324 | if (Exit.isFailure(exit)) { |
| 325 | assert.deepStrictEqual(exit.cause.reasons.map((reason) => reason._tag), ["Die", "Die"]) |
| 326 | const [validation, cleanup] = exit.cause.reasons |
| 327 | assert.isTrue(Cause.isDieReason(validation) && SchemaIssue.isIssue(validation.defect)) |
| 328 | assert.isTrue(Cause.isDieReason(cleanup) && cleanup.defect === defect) |
| 329 | } |
| 330 | } |
| 331 | |
| 332 | const asserts = new TestSchema.Asserts(schema) |
| 333 | yield* Effect.promise(() => |
| 334 | testAssert.throwsAsync( |
| 335 | () => |
| 336 | method === "decoding" |
| 337 | ? asserts.decoding().fail(123, "Expected string") |
| 338 | : asserts.encoding().fail(123, "Expected string"), |
| 339 | (error) => { |
| 340 | assert.isTrue(SchemaIssue.isIssue(error)) |
| 341 | } |
| 342 | ) |
| 343 | ) |
| 344 | })) |
| 345 | |
| 346 | it.effect(`${method} preserves interruption alongside annotated validation failures`, () => |
| 347 | Effect.gen(function*() { |
| 348 | class Metadata extends Context.Service<Metadata, string>()("Metadata") {} |
| 349 | const annotations = Context.make(Metadata, "validation") |
| 350 | const issue = new SchemaIssue.InvalidValue({ message: "schema issue" }) |
| 351 | const interrupt = Cause.makeInterruptReason(123) |
| 352 | const failure = Effect.failCause(Cause.fromReasons([ |
| 353 | Cause.makeFailReason(issue).annotate(annotations), |
| 354 | interrupt |
| 355 | ])) |
| 356 | const schema = method === "decoding" |
| 357 | ? Schema.String.pipe(Schema.middlewareDecoding(() => failure)) |
| 358 | : Schema.String.pipe(Schema.middlewareEncoding(() => failure)) |
| 359 | const assertions = new TestSchema.Asserts(schema)[method]() |
| 360 | |
| 361 | for ( |
| 362 | const effect of [ |
| 363 | assertions.succeedEffect("value"), |
| 364 | assertions.failEffect("value", "schema issue") |
| 365 | ] |
| 366 | ) { |
| 367 | const exit = yield* Effect.exit(effect) |
| 368 | assert.isTrue(Exit.isFailure(exit)) |
| 369 | if (Exit.isFailure(exit)) { |
| 370 | assert.deepStrictEqual(exit.cause.reasons.map((reason) => reason._tag), ["Die", "Interrupt"]) |
| 371 | const [validation, interruption] = exit.cause.reasons |
| 372 | assert.isTrue(Cause.isDieReason(validation) && SchemaIssue.isIssue(validation.defect)) |
| 373 | if (Cause.isDieReason(validation) && SchemaIssue.isIssue(validation.defect)) { |
| 374 | assert.strictEqual(SchemaIssue.defaultFormatter(validation.defect), "schema issue") |
| 375 | } |
| 376 | assert.strictEqual(Context.getOrUndefined(Cause.reasonAnnotations(validation), Metadata), "validation") |
| 377 | assert.deepStrictEqual(interruption, interrupt) |
| 378 | } |
| 379 | } |
| 380 | })) |
| 381 | |
| 382 | it.effect(`round trips preserve mixed causes from ${method}`, () => |
| 383 | Effect.gen(function*() { |
| 384 | const issue = new SchemaIssue.InvalidValue({ message: "schema issue" }) |
| 385 | const defect = new Error("cleanup failed") |
| 386 | const failure = Effect.fail(issue).pipe(Effect.ensuring(Effect.die(defect))) |
| 387 | const schema = method === "decoding" |
| 388 | ? Schema.String.pipe(Schema.middlewareDecoding(() => failure)) |
| 389 | : Schema.String.pipe(Schema.middlewareEncoding(() => failure)) |
| 390 | |
| 391 | const exit = yield* Effect.exit(new TestSchema.Asserts(schema).verifyRoundTripEffect({ runs: 1, seed: 1 })) |
| 392 | assert.isTrue(Exit.isFailure(exit)) |
| 393 | if (Exit.isFailure(exit)) { |
| 394 | assert.deepStrictEqual(exit.cause.reasons.map((reason) => reason._tag), ["Die", "Die"]) |
| 395 | const [validation, cleanup] = exit.cause.reasons |
| 396 | assert.isTrue(Cause.isDieReason(validation) && SchemaIssue.isIssue(validation.defect)) |
| 397 | if (Cause.isDieReason(validation) && SchemaIssue.isIssue(validation.defect)) { |
| 398 | assert.strictEqual(SchemaIssue.defaultFormatter(validation.defect), "schema issue") |
| 399 | } |
| 400 | assert.isTrue(Cause.isDieReason(cleanup) && cleanup.defect === defect) |
| 401 | } |
| 402 | })) |
| 403 | |
| 404 | it.effect(`${method} inherits layer services and supports provide`, () => |
| 405 | Effect.gen(function*() { |
| 406 | class Expected extends Context.Service<Expected, string>()("Expected") {} |
| 407 | const check = SchemaGetter.checkEffect((value: string) => |
| 408 | Effect.map(Expected, (expected) => |
| 409 | value === expected ? undefined : new SchemaIssue.InvalidValue({ message: "Unexpected value" })) |
| 410 | ) |
| 411 | const schema = Schema.String.pipe(Schema.decode({ decode: check, encode: check })) |
| 412 | const assertions = new TestSchema.Asserts(schema)[method]() |
| 413 | |
| 414 | yield* Effect.gen(function*() { |
| 415 | yield* assertions.succeedEffect("provided") |
| 416 | yield* assertions.failEffect("wrong", "Unexpected value") |
| 417 | }).pipe(Effect.provide(Layer.succeed(Expected, "provided"))) |
| 418 | |
| 419 | yield* assertions.provide(Expected, "local").succeedEffect("local") |
| 420 | yield* assertions.provide(Expected, "local").failEffect("wrong", "Unexpected value") |
| 421 | })) |
| 422 | |
| 423 | it.effect(`${method} reads the test clock`, () => |
| 424 | Effect.gen(function*() { |
| 425 | const check = SchemaGetter.checkEffect((value: number) => |
| 426 | Effect.map(Clock.currentTimeMillis, (now) => |
| 427 | value === now ? undefined : new SchemaIssue.InvalidValue({ message: "Unexpected time" })) |
| 428 | ) |
| 429 | const schema = Schema.Number.pipe(Schema.decode({ decode: check, encode: check })) |
| 430 | const assertions = new TestSchema.Asserts(schema)[method]() |
| 431 | yield* TestClock.setTime(123) |
| 432 | yield* assertions.succeedEffect(123) |
| 433 | yield* assertions.failEffect(0, "Unexpected time") |
| 434 | })) |
| 435 | } |
| 436 | |
| 437 | it.effect("make preserves mixed causes from constructor defaults", () => |
| 438 | Effect.gen(function*() { |
| 439 | const issue = new SchemaIssue.InvalidValue({ message: "schema issue" }) |
| 440 | const defect = new Error("cleanup failed") |
| 441 | const schema = Schema.Struct({ |
| 442 | value: Schema.String.pipe( |
| 443 | Schema.withConstructorDefault(Effect.fail(issue).pipe(Effect.ensuring(Effect.die(defect)))) |
| 444 | ) |
| 445 | }) |
| 446 | const assertions = new TestSchema.Asserts(schema).make() |
| 447 | |
| 448 | const exit = yield* Effect.exit(assertions.failEffect({}, "schema issue")) |
| 449 | assert.isTrue(Exit.isFailure(exit)) |
| 450 | if (Exit.isFailure(exit)) { |
| 451 | assert.deepStrictEqual(exit.cause.reasons.map((reason) => reason._tag), ["Die", "Die"]) |
| 452 | const [validation, cleanup] = exit.cause.reasons |
| 453 | assert.isTrue(Cause.isDieReason(validation) && SchemaIssue.isIssue(validation.defect)) |
| 454 | if (Cause.isDieReason(validation) && SchemaIssue.isIssue(validation.defect)) { |
| 455 | assert.include(SchemaIssue.defaultFormatter(validation.defect), "schema issue") |
| 456 | } |
| 457 | assert.isTrue(Cause.isDieReason(cleanup) && cleanup.defect === defect) |
| 458 | } |
| 459 | })) |
| 460 | |
| 461 | for (const method of ["decoding", "encoding", "roundTrip"] as const) { |
| 462 | it.effect(`${method} propagates interruption and runs finalizers`, () => |
| 463 | Effect.gen(function*() { |
| 464 | const started = yield* Deferred.make<void>() |
| 465 | const finalized = yield* Ref.make(false) |
| 466 | const check = SchemaGetter.checkEffect<string>(() => |
| 467 | Effect.andThen(Deferred.succeed(started, undefined), Effect.never).pipe( |
| 468 | Effect.ensuring(Ref.set(finalized, true)) |
| 469 | ) |
| 470 | ) |
| 471 | const schema = Schema.String.pipe(Schema.decode({ decode: check, encode: check })) |
| 472 | const asserts = new TestSchema.Asserts(schema) |
| 473 | const effect = method === "roundTrip" |
| 474 | ? asserts.verifyRoundTripEffect({ runs: 1, seed: 1 }) |
| 475 | : asserts[method]().failEffect("value", "should not pass") |
| 476 | const fiber = yield* Effect.forkChild(effect) |
| 477 | yield* Deferred.await(started) |
| 478 | yield* Fiber.interrupt(fiber) |
| 479 | assert.isTrue(yield* Ref.get(finalized)) |
| 480 | const exit = yield* Fiber.await(fiber) |
| 481 | assert.isTrue(Exit.isFailure(exit)) |
| 482 | if (Exit.isFailure(exit)) { |
| 483 | assert.isTrue(Cause.hasInterruptsOnly(exit.cause)) |
| 484 | } |
| 485 | })) |
| 486 | } |
| 487 | |
| 488 | it.effect("round trips use both decoding and encoding services lazily", () => |
| 489 | Effect.gen(function*() { |
| 490 | class Decoding extends Context.Service<Decoding, Ref.Ref<number>>()("Decoding") {} |
| 491 | class Encoding extends Context.Service<Encoding, Ref.Ref<number>>()("Encoding") {} |
| 492 | const schema = Schema.String.pipe(Schema.decode({ |
| 493 | decode: SchemaGetter.checkEffect(() => |
| 494 | Effect.flatMap(Decoding, (ref) => Ref.update(ref, (n) => n + 1)).pipe(Effect.as(true)) |
| 495 | ), |
| 496 | encode: SchemaGetter.checkEffect(() => |
| 497 | Effect.flatMap(Encoding, (ref) => Ref.update(ref, (n) => n + 1)).pipe(Effect.as(true)) |
| 498 | ) |
| 499 | })) |
| 500 | const decoding = yield* Ref.make(0) |
| 501 | const encoding = yield* Ref.make(0) |
| 502 | const assertion = new TestSchema.Asserts(schema).verifyRoundTripEffect({ runs: 3, seed: 1 }) |
| 503 | assert.strictEqual(yield* Ref.get(decoding), 0) |
| 504 | assert.strictEqual(yield* Ref.get(encoding), 0) |
| 505 | yield* assertion.pipe(Effect.provide(Layer.mergeAll( |
| 506 | Layer.succeed(Decoding, decoding), |
| 507 | Layer.succeed(Encoding, encoding) |
| 508 | ))) |
| 509 | assert.strictEqual(yield* Ref.get(decoding), 3) |
| 510 | assert.strictEqual(yield* Ref.get(encoding), 3) |
| 511 | })) |
| 512 | |
| 513 | it.effect("round-trip assertion defects report shrunk inputs and replay", () => |
| 514 | Effect.gen(function*() { |
| 515 | const schema = Schema.Number.pipe(Schema.decodeTo( |
| 516 | Schema.Number, |
| 517 | SchemaTransformation.transform({ decode: (value) => value, encode: () => 0 }) |
| 518 | )) |
| 519 | const exit = yield* Effect.exit( |
| 520 | new TestSchema.Asserts(schema).verifyRoundTripEffect({ runs: 20, seed: "lossy" }) |
| 521 | ) |
| 522 | assert.isTrue(Exit.isFailure(exit)) |
| 523 | if (Exit.isFailure(exit)) { |
| 524 | const error = Cause.squash(exit.cause) |
| 525 | assert.instanceOf(error, AssertionError) |
| 526 | assert.match(error.message, /Property falsified/) |
| 527 | assert.match(error.message, /Shrunk input:/) |
| 528 | assert.match(error.message, /Replay:/) |
| 529 | } |
| 530 | })) |
| 531 | |
| 532 | it.effect("round trips preserve schema defects", () => |
| 533 | Effect.gen(function*() { |
| 534 | const defect = new Error("round-trip defect") |
| 535 | const schema = Schema.String.pipe(Schema.decode({ |
| 536 | decode: SchemaGetter.passthrough(), |
| 537 | encode: SchemaGetter.checkEffect(() => Effect.die(defect)) |
| 538 | })) |
| 539 | const assertion = new TestSchema.Asserts(schema).verifyRoundTripEffect({ runs: 1, seed: 1 }) |
| 540 | assert.deepStrictEqual(yield* Effect.exit(assertion), Exit.die(defect)) |
| 541 | })) |
| 542 | }) |