Skip to content

Commit

Permalink
refactor vitest (#3122)
Browse files Browse the repository at this point in the history
  • Loading branch information
sukovanej committed Jun 29, 2024
1 parent 07be551 commit 489d20a
Show file tree
Hide file tree
Showing 7 changed files with 238 additions and 161 deletions.
30 changes: 30 additions & 0 deletions .changeset/lucky-moose-hear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
"@effect/vitest": minor
---

Refactor `@effect/vitest` package.

- Clear separation of the public API and internals.
- Fix type of `scoped`, `live`, `scopedLive` and `effect` objects. Make sure `skip` and `only` are available.
- Add `each` method to `scoped`, `live`, `scopedLive` and `effect` objects.

Example usage

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

it.scoped.skip(
"test skipped",
() =>
Effect.acquireRelease(
Effect.die("skipped anyway"),
() => Effect.void
)
)

it.effect.each([1, 2, 3])(
"effect each %s",
(n) => Effect.sync(() => expect(n).toEqual(n))
)
```
1 change: 1 addition & 0 deletions packages/effect/test/utils/effect-vitest-link
1 change: 0 additions & 1 deletion packages/effect/test/utils/extend.ts

This file was deleted.

1 change: 1 addition & 0 deletions packages/effect/test/utils/extend.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./effect-vitest-link/index.js"
2 changes: 1 addition & 1 deletion packages/vitest/docgen.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "../../node_modules/@effect/docgen/schema.json",
"exclude": [
"src/internal/**/*.ts"
"src/internal.ts"
],
"examplesCompilerOptions": {
"noEmit": true,
Expand Down
203 changes: 44 additions & 159 deletions packages/vitest/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,206 +1,91 @@
/**
* @since 1.0.0
*/
import type { Tester, TesterContext } from "@vitest/expect"
import * as Cause from "effect/Cause"
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 { pipe } from "effect/Function"
import * as Layer from "effect/Layer"
import * as Logger from "effect/Logger"
import * as Schedule from "effect/Schedule"
import type * as Duration from "effect/Duration"
import type * as Effect from "effect/Effect"
import type * as Scope from "effect/Scope"
import * as TestEnvironment from "effect/TestContext"
import type * as TestServices from "effect/TestServices"
import * as Utils from "effect/Utils"
import type { TestAPI } from "vitest"
import * as V from "vitest"
import * as internal from "./internal.js"

const runTest = <E, A>(effect: Effect.Effect<A, E>) =>
Effect.gen(function*() {
const exit: Exit.Exit<A, E> = yield* Effect.exit(effect)
if (Exit.isSuccess(exit)) {
return () => {}
} else {
const errors = Cause.prettyErrors(exit.cause)
for (let i = 1; i < errors.length; i++) {
yield* Effect.logError(errors[i])
}
return () => {
throw errors[0]
}
}
}).pipe(Effect.runPromise).then((f) => f())
/**
* @since 1.0.0
*/
export type API = V.TestAPI<{}>

/**
* @since 1.0.0
*/
export type API = TestAPI<{}>
export namespace Vitest {
/**
* @since 1.0.0
*/
export interface TestFunction<A, E, R, TestArgs extends Array<any>> {
(...args: TestArgs): Effect.Effect<A, E, R>
}

const TestEnv = TestEnvironment.TestContext.pipe(
Layer.provide(Logger.remove(Logger.defaultLogger))
)
/**
* @since 1.0.0
*/
export interface Test<R> {
<A, E>(
name: string,
self: TestFunction<A, E, R, [V.TaskContext<V.Test<{}>> & V.TestContext]>,
timeout?: number | V.TestOptions
): void
}

/** @internal */
function customTester(this: TesterContext, a: unknown, b: unknown, customTesters: Array<Tester>) {
if (!Equal.isEqual(a) || !Equal.isEqual(b)) {
return undefined
/**
* @since 1.0.0
*/
export interface Tester<R> extends Vitest.Test<R> {
skip: Vitest.Test<R>
only: Vitest.Test<R>
each: <T>(
cases: ReadonlyArray<T>
) => <A, E>(name: string, self: TestFunction<A, E, R, Array<T>>, timeout?: number | V.TestOptions) => void
}
return Utils.structuralRegion(
() => Equal.equals(a, b),
(x, y) => this.equals(x, y, customTesters.filter((t) => t !== customTester))
)
}

/**
* @since 1.0.0
*/
export const addEqualityTesters = () => {
V.expect.addEqualityTesters([customTester])
}
export const addEqualityTesters: () => void = internal.addEqualityTesters

/**
* @since 1.0.0
*/
export const effect = (() => {
const f = <E, A>(
name: string,
self: (ctx: V.TaskContext<V.Test<{}>> & V.TestContext) => Effect.Effect<A, E, TestServices.TestServices>,
timeout: number | V.TestOptions = 5_000
) =>
it(
name,
(c) =>
pipe(
Effect.suspend(() => self(c)),
Effect.provide(TestEnv),
runTest
),
timeout
)
return Object.assign(f, {
skip: <E, A>(
name: string,
self: (ctx: V.TaskContext<V.Test<{}>> & V.TestContext) => Effect.Effect<A, E, TestServices.TestServices>,
timeout = 5_000
) =>
it.skip(
name,
(c) =>
pipe(
Effect.suspend(() => self(c)),
Effect.provide(TestEnv),
runTest
),
timeout
),
only: <E, A>(
name: string,
self: (ctx: V.TaskContext<V.Test<{}>> & V.TestContext) => Effect.Effect<A, E, TestServices.TestServices>,
timeout = 5_000
) =>
it.only(
name,
(c) =>
pipe(
Effect.suspend(() => self(c)),
Effect.provide(TestEnv),
runTest
),
timeout
)
})
})()
export const effect: Vitest.Tester<TestServices.TestServices> = internal.effect

/**
* @since 1.0.0
*/
export const live = <E, A>(
name: string,
self: (ctx: V.TaskContext<V.Test<{}>> & V.TestContext) => Effect.Effect<A, E>,
timeout = 5_000
) =>
it(
name,
(c) =>
pipe(
Effect.suspend(() => self(c)),
runTest
),
timeout
)
export const scoped: Vitest.Tester<TestServices.TestServices | Scope.Scope> = internal.scoped

/**
* @since 1.0.0
*/
export const flakyTest = <A, E, R>(
self: Effect.Effect<A, E, R>,
timeout: Duration.DurationInput = Duration.seconds(30)
) =>
pipe(
Effect.catchAllDefect(self, Effect.fail),
Effect.retry(
pipe(
Schedule.recurs(10),
Schedule.compose(Schedule.elapsed),
Schedule.whileOutput(Duration.lessThanOrEqualTo(timeout))
)
),
Effect.orDie
)
export const live: Vitest.Tester<never> = internal.live

/**
* @since 1.0.0
*/
export const scoped = <E, A>(
name: string,
self: (
ctx: V.TaskContext<V.Test<{}>> & V.TestContext
) => Effect.Effect<A, E, Scope.Scope | TestServices.TestServices>,
timeout = 5_000
) =>
it(
name,
(c) =>
pipe(
Effect.suspend(() => self(c)),
Effect.scoped,
Effect.provide(TestEnv),
runTest
),
timeout
)
export const scopedLive: Vitest.Tester<Scope.Scope> = internal.scopedLive

/**
* @since 1.0.0
*/
export const scopedLive = <E, A>(
name: string,
self: (ctx: V.TaskContext<V.Test<{}>> & V.TestContext) => Effect.Effect<A, E, Scope.Scope>,
timeout = 5_000
) =>
it(
name,
(c) =>
pipe(
Effect.suspend(() => self(c)),
Effect.scoped,
runTest
),
timeout
)
export const flakyTest: <A, E, R>(
self: Effect.Effect<A, E, R>,
timeout?: Duration.DurationInput
) => Effect.Effect<A, never, R> = internal.flakyTest

/** @ignored */
const methods = { effect, live, flakyTest, scoped, scopedLive } as const

/**
* @since 1.0.0
*/
export const it: API & typeof methods = Object.assign(
V.it,
methods
)
export const it: API & typeof methods = Object.assign(V.it, methods)

/**
* @since 1.0.0
Expand Down
Loading

0 comments on commit 489d20a

Please sign in to comment.