Skip to content

Commit

Permalink
interrupt effect when test finishes (#3416)
Browse files Browse the repository at this point in the history
  • Loading branch information
sukovanej authored Aug 7, 2024
1 parent 056b710 commit 8cc1517
Show file tree
Hide file tree
Showing 3 changed files with 82 additions and 11 deletions.
32 changes: 32 additions & 0 deletions .changeset/wild-sheep-pay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
"@effect/vitest": patch
---

Interrupt an effect when a test finishes. This ensures allocated resources
will be correctly released even if the test times out.

```ts
import { it } from "@effect/vitest"
import { Console, Effect, Layer } from "effect"

class Database extends Effect.Tag("Database")<Database, {}>() {
static readonly test = Layer.scoped(
Database,
Effect.acquireRelease(
Effect.as(Console.log("database setup"), Database.of({})),
() => Console.log("database teardown")
)
)
}

it.live(
"testing with closable resources",
() =>
Effect.gen(function* () {
const database = yield* Database
// performing some time consuming operations
yield* Effect.sleep("500 millis")
}).pipe(Effect.provide(Database.test)),
{ timeout: 100 }
)
```
42 changes: 31 additions & 11 deletions packages/vitest/src/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ import * as Duration from "effect/Duration"
import * as Effect from "effect/Effect"
import * as Equal from "effect/Equal"
import * as Exit from "effect/Exit"
import * as Fiber from "effect/Fiber"
import { flow, identity, pipe } from "effect/Function"
import * as Layer from "effect/Layer"
import * as Logger from "effect/Logger"
import * as Runtime from "effect/Runtime"
import * as Schedule from "effect/Schedule"
import type * as Scope from "effect/Scope"
import * as TestEnvironment from "effect/TestContext"
Expand All @@ -19,9 +21,19 @@ import * as V from "vitest"
import type * as Vitest from "./index.js"

/** @internal */
const runTest = <E, A>(effect: Effect.Effect<A, E>) =>
const runTest = (ctx: Vitest.TaskContext) => <E, A>(effect: Effect.Effect<A, E>) =>
Effect.gen(function*() {
const exit: Exit.Exit<A, E> = yield* Effect.exit(effect)
const exitFiber = yield* Effect.fork(Effect.exit(effect))
const runtime = yield* Effect.runtime()

ctx.onTestFinished(() =>
Fiber.interrupt(exitFiber).pipe(
Effect.asVoid,
Runtime.runPromise(runtime)
)
)

const exit = yield* Fiber.join(exitFiber)
if (Exit.isSuccess(exit)) {
return () => {}
} else {
Expand Down Expand Up @@ -60,20 +72,28 @@ export const addEqualityTesters = () => {
const makeTester = <R>(
mapEffect: <A, E>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E, never>
): Vitest.Vitest.Tester<R> => {
const run =
<A, E, TestArgs extends Array<any>>(self: Vitest.Vitest.TestFunction<A, E, R, TestArgs>) => (...args: TestArgs) =>
pipe(Effect.suspend(() => self(...args)), mapEffect, runTest)
const run = <A, E, TestArgs extends Array<unknown>>(
ctx: V.TaskContext<V.Test<object>> & V.TestContext & object,
args: TestArgs,
self: Vitest.Vitest.TestFunction<A, E, R, TestArgs>
) => pipe(Effect.suspend(() => self(...args)), mapEffect, runTest(ctx))

const f: Vitest.Vitest.Test<R> = (name, self, timeout) => V.it(name, run(self), timeout)
const f: Vitest.Vitest.Test<R> = (name, self, timeout) => V.it(name, (ctx) => run(ctx, [ctx], self), timeout)

const skip: Vitest.Vitest.Tester<R>["only"] = (name, self, timeout) => V.it.skip(name, run(self), timeout)
const skip: Vitest.Vitest.Tester<R>["only"] = (name, self, timeout) =>
V.it.skip(name, (ctx) => run(ctx, [ctx], self), timeout)
const skipIf: Vitest.Vitest.Tester<R>["skipIf"] = (condition) => (name, self, timeout) =>
V.it.skipIf(condition)(name, run(self), timeout)
V.it.skipIf(condition)(name, (ctx) => run(ctx, [ctx], self), timeout)
const runIf: Vitest.Vitest.Tester<R>["runIf"] = (condition) => (name, self, timeout) =>
V.it.runIf(condition)(name, run(self), timeout)
const only: Vitest.Vitest.Tester<R>["only"] = (name, self, timeout) => V.it.only(name, run(self), timeout)
V.it.runIf(condition)(name, (ctx) => run(ctx, [ctx], self), timeout)
const only: Vitest.Vitest.Tester<R>["only"] = (name, self, timeout) =>
V.it.only(name, (ctx) => run(ctx, [ctx], self), timeout)
const each: Vitest.Vitest.Tester<R>["each"] = (cases) => (name, self, timeout) =>
V.it.each(cases)(name, run(self), timeout)
V.it.for(cases)(
name,
typeof timeout === "number" ? { timeout } : timeout ?? {},
(args, ctx) => run(ctx, [args], self)
)

return Object.assign(f, { skip, skipIf, runIf, only, each })
}
Expand Down
19 changes: 19 additions & 0 deletions packages/vitest/test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,22 @@ it.effect.skipIf(false)("effect skipIf (false)", () => Effect.sync(() => expect(

it.effect.runIf(true)("effect runIf (true)", () => Effect.sync(() => expect(1).toEqual(1)))
it.effect.runIf(false)("effect runIf (false)", () => Effect.die("not run anyway"))

// The following test is expected to fail because it simulates a test timeout.
// Be aware that eventual "failure" of the test is only logged out.
it.scopedLive("interrupts on timeout", (ctx) =>
Effect.gen(function*() {
let acquired = false

ctx.onTestFailed(() => {
if (acquired) {
console.error("'effect is interrupted on timeout' @effect/vitest test failed")
}
})

yield* Effect.acquireRelease(
Effect.sync(() => acquired = true),
() => Effect.sync(() => acquired = false)
)
yield* Effect.sleep(1000)
}), { timeout: 100, fails: true })

0 comments on commit 8cc1517

Please sign in to comment.