Merge commit '36c66dd290d3ce6eb1ccd310d0c658d4a32bb8eb' as 'repos/effect'
This commit is contained in:
259
repos/effect/packages/vitest/src/index.ts
Normal file
259
repos/effect/packages/vitest/src/index.ts
Normal file
@@ -0,0 +1,259 @@
|
||||
/**
|
||||
* @since 4.0.0
|
||||
*/
|
||||
import type * as Duration from "effect/Duration"
|
||||
import type * as Effect from "effect/Effect"
|
||||
import type * as Layer from "effect/Layer"
|
||||
import type * as Schema from "effect/Schema"
|
||||
import type * as Scope from "effect/Scope"
|
||||
import type * as FC from "effect/testing/FastCheck"
|
||||
import * as V from "vitest"
|
||||
import * as internal from "./internal/internal.ts"
|
||||
|
||||
/**
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export * from "vitest"
|
||||
|
||||
/**
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export type API = V.TestAPI<{}>
|
||||
|
||||
/**
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export namespace Vitest {
|
||||
/**
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export interface TestFunction<A, E, R, TestArgs extends Array<any>> {
|
||||
(...args: TestArgs): Effect.Effect<A, E, R>
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export interface Test<R> {
|
||||
<A, E>(
|
||||
name: string,
|
||||
self: TestFunction<A, E, R, [V.TestContext]>,
|
||||
timeout?: number | V.TestOptions
|
||||
): void
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export type Arbitraries =
|
||||
| Array<Schema.Schema<any> | FC.Arbitrary<any>>
|
||||
| { [K in string]: Schema.Schema<any> | FC.Arbitrary<any> }
|
||||
|
||||
/**
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export interface Tester<R> extends Vitest.Test<R> {
|
||||
skip: Vitest.Test<R>
|
||||
skipIf: (condition: unknown) => Vitest.Test<R>
|
||||
runIf: (condition: unknown) => 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
|
||||
fails: Vitest.Test<R>
|
||||
|
||||
/**
|
||||
* @since 4.0.0
|
||||
*/
|
||||
prop: <const Arbs extends Arbitraries, A, E>(
|
||||
name: string,
|
||||
arbitraries: Arbs,
|
||||
self: TestFunction<
|
||||
A,
|
||||
E,
|
||||
R,
|
||||
[
|
||||
{
|
||||
[K in keyof Arbs]: Arbs[K] extends FC.Arbitrary<infer T> ? T
|
||||
: Arbs[K] extends Schema.Schema<infer T> ? T
|
||||
: never
|
||||
},
|
||||
V.TestContext
|
||||
]
|
||||
>,
|
||||
timeout?:
|
||||
| number
|
||||
| V.TestOptions & {
|
||||
fastCheck?: FC.Parameters<
|
||||
{
|
||||
[K in keyof Arbs]: Arbs[K] extends FC.Arbitrary<infer T> ? T : Arbs[K] extends Schema.Schema<infer T> ? T
|
||||
: never
|
||||
}
|
||||
>
|
||||
}
|
||||
) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export interface MethodsNonLive<R = never> extends API {
|
||||
readonly effect: Vitest.Tester<R | Scope.Scope>
|
||||
readonly flakyTest: <A, E, R2>(
|
||||
self: Effect.Effect<A, E, R2 | Scope.Scope>,
|
||||
timeout?: Duration.Input
|
||||
) => Effect.Effect<A, never, R2>
|
||||
readonly layer: <R2, E>(layer: Layer.Layer<R2, E, R>, options?: {
|
||||
readonly timeout?: Duration.Input
|
||||
}) => {
|
||||
(f: (it: Vitest.MethodsNonLive<R | R2>) => void): void
|
||||
(
|
||||
name: string,
|
||||
f: (it: Vitest.MethodsNonLive<R | R2>) => void
|
||||
): void
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 4.0.0
|
||||
*/
|
||||
readonly prop: <const Arbs extends Arbitraries>(
|
||||
name: string,
|
||||
arbitraries: Arbs,
|
||||
self: (
|
||||
properties: {
|
||||
[K in keyof Arbs]: Arbs[K] extends FC.Arbitrary<infer T> ? T : Arbs[K] extends Schema.Schema<infer T> ? T
|
||||
: never
|
||||
},
|
||||
ctx: V.TestContext
|
||||
) => void,
|
||||
timeout?:
|
||||
| number
|
||||
| V.TestOptions & {
|
||||
fastCheck?: FC.Parameters<
|
||||
{
|
||||
[K in keyof Arbs]: Arbs[K] extends FC.Arbitrary<infer T> ? T : Arbs[K] extends Schema.Schema<infer T> ? T
|
||||
: never
|
||||
}
|
||||
>
|
||||
}
|
||||
) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export interface Methods<R = never> extends MethodsNonLive<R> {
|
||||
readonly live: Vitest.Tester<Scope.Scope | R>
|
||||
readonly layer: <R2, E>(layer: Layer.Layer<R2, E, R>, options?: {
|
||||
readonly memoMap?: Layer.MemoMap
|
||||
readonly timeout?: Duration.Input
|
||||
readonly excludeTestServices?: boolean
|
||||
}) => {
|
||||
(f: (it: Vitest.MethodsNonLive<R | R2>) => void): void
|
||||
(
|
||||
name: string,
|
||||
f: (it: Vitest.MethodsNonLive<R | R2>) => void
|
||||
): void
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export const addEqualityTesters: () => void = internal.addEqualityTesters
|
||||
|
||||
/**
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export const effect: Vitest.Tester<Scope.Scope> = internal.effect
|
||||
|
||||
/**
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export const live: Vitest.Tester<Scope.Scope> = internal.live
|
||||
|
||||
/**
|
||||
* Share a `Layer` between multiple tests, optionally wrapping
|
||||
* the tests in a `describe` block if a name is provided.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* ```ts
|
||||
* import { expect, layer } from "@effect/vitest"
|
||||
* import { Effect, Layer, Context } from "effect"
|
||||
*
|
||||
* class Foo extends Context.Service("Foo")<Foo, "foo">() {
|
||||
* static Live = Layer.succeed(Foo, "foo")
|
||||
* }
|
||||
*
|
||||
* class Bar extends Context.Service("Bar")<Bar, "bar">() {
|
||||
* static Live = Layer.effect(
|
||||
* Bar,
|
||||
* Effect.map(Foo, () => "bar" as const)
|
||||
* )
|
||||
* }
|
||||
*
|
||||
* layer(Foo.Live)("layer", (it) => {
|
||||
* it.effect("adds context", () =>
|
||||
* Effect.gen(function*() {
|
||||
* const foo = yield* Foo
|
||||
* expect(foo).toEqual("foo")
|
||||
* }))
|
||||
*
|
||||
* it.layer(Bar.Live)("nested", (it) => {
|
||||
* it.effect("adds context", () =>
|
||||
* Effect.gen(function*() {
|
||||
* const foo = yield* Foo
|
||||
* const bar = yield* Bar
|
||||
* expect(foo).toEqual("foo")
|
||||
* expect(bar).toEqual("bar")
|
||||
* }))
|
||||
* })
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export const layer: <R, E>(
|
||||
layer_: Layer.Layer<R, E>,
|
||||
options?: {
|
||||
readonly memoMap?: Layer.MemoMap
|
||||
readonly timeout?: Duration.Input
|
||||
readonly excludeTestServices?: boolean
|
||||
}
|
||||
) => {
|
||||
(f: (it: Vitest.MethodsNonLive<R>) => void): void
|
||||
(name: string, f: (it: Vitest.MethodsNonLive<R>) => void): void
|
||||
} = internal.layer
|
||||
|
||||
/**
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export const flakyTest: <A, E, R>(
|
||||
self: Effect.Effect<A, E, R | Scope.Scope>,
|
||||
timeout?: Duration.Input
|
||||
) => Effect.Effect<A, never, R> = internal.flakyTest
|
||||
|
||||
/**
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export const prop: Vitest.Methods["prop"] = internal.prop
|
||||
|
||||
/**
|
||||
* @since 4.0.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export const it: Vitest.Methods = internal.makeMethods(V.it)
|
||||
|
||||
/**
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export const makeMethods: (it: V.TestAPI) => Vitest.Methods = internal.makeMethods
|
||||
|
||||
/**
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export const describeWrapped: (name: string, f: (it: Vitest.Methods) => void) => V.SuiteCollector =
|
||||
internal.describeWrapped
|
||||
371
repos/effect/packages/vitest/src/internal/internal.ts
Normal file
371
repos/effect/packages/vitest/src/internal/internal.ts
Normal file
@@ -0,0 +1,371 @@
|
||||
/**
|
||||
* @since 4.0.0
|
||||
*/
|
||||
|
||||
import { getCurrentSuite } from "@vitest/runner"
|
||||
import * as Cause from "effect/Cause"
|
||||
import * as Duration from "effect/Duration"
|
||||
import * as Effect from "effect/Effect"
|
||||
import * as Exit from "effect/Exit"
|
||||
import { flow, pipe } from "effect/Function"
|
||||
import * as Layer from "effect/Layer"
|
||||
import { isObject } from "effect/Predicate"
|
||||
import * as Schedule from "effect/Schedule"
|
||||
import * as Schema from "effect/Schema"
|
||||
import * as Scope from "effect/Scope"
|
||||
import * as fc from "effect/testing/FastCheck"
|
||||
import * as TestClock from "effect/testing/TestClock"
|
||||
import * as TestConsole from "effect/testing/TestConsole"
|
||||
import * as V from "vitest"
|
||||
import type * as Vitest from "../index.ts"
|
||||
|
||||
const runPromise: <E, A>(
|
||||
_: Effect.Effect<A, E, never>,
|
||||
ctx?: V.TestContext | undefined
|
||||
) => Promise<A> = Effect.fnUntraced(function*<E, A>(effect: Effect.Effect<A, E>, _ctx?: Vitest.TestContext) {
|
||||
const exit = yield* Effect.exit(effect)
|
||||
if (Exit.isFailure(exit)) {
|
||||
const errors = Cause.prettyErrors(exit.cause)
|
||||
for (let i = 0; i < errors.length; i++) {
|
||||
yield* Effect.logError(errors[i])
|
||||
}
|
||||
}
|
||||
return yield* exit
|
||||
}, (effect, _, ctx) => Effect.runPromise(effect, { signal: ctx?.signal }))
|
||||
|
||||
/** @internal */
|
||||
const runTest = (ctx?: Vitest.TestContext) => <E, A>(effect: Effect.Effect<A, E>) => runPromise(effect, ctx)
|
||||
|
||||
/** @internal */
|
||||
export type TestContext = TestConsole.TestConsole | TestClock.TestClock
|
||||
|
||||
const TestEnv = Layer.mergeAll(TestConsole.layer, TestClock.layer())
|
||||
|
||||
/** @internal */
|
||||
export const addEqualityTesters = () => {
|
||||
V.expect.addEqualityTesters([])
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
const testOptions = (timeout?: number | V.TestOptions) => typeof timeout === "number" ? { timeout } : timeout ?? {}
|
||||
|
||||
const hookTimeout = (timeout?: Duration.Input) =>
|
||||
timeout === undefined ? undefined : Duration.toMillis(Duration.fromInputUnsafe(timeout))
|
||||
|
||||
const makeItProxy = <Methods extends object>(
|
||||
it: V.TestAPI,
|
||||
overrides: Methods
|
||||
): Methods & V.TestAPI =>
|
||||
new Proxy(it as Methods & V.TestAPI, {
|
||||
apply(target, thisArg, argArray) {
|
||||
return Reflect.apply(target, thisArg, argArray)
|
||||
},
|
||||
get(target, property, receiver) {
|
||||
if (property in overrides) {
|
||||
return Reflect.get(overrides, property)
|
||||
}
|
||||
// do not bind: binding would strip vitest's static helpers (e.g. `describe.each`)
|
||||
return Reflect.get(target, property, receiver)
|
||||
}
|
||||
})
|
||||
|
||||
type CollectedTask = {
|
||||
readonly type: string
|
||||
readonly mode?: string
|
||||
readonly tasks?: ReadonlyArray<CollectedTask>
|
||||
}
|
||||
|
||||
const collectTasks = (tasks: ReadonlyArray<CollectedTask>, acc: Array<V.TestContext["task"]> = []) => {
|
||||
for (let i = 0; i < tasks.length; i++) {
|
||||
const task = tasks[i]
|
||||
if (task.type === "test" && task.mode !== "skip" && task.mode !== "todo") {
|
||||
acc.push(task as V.TestContext["task"])
|
||||
} else if (task.tasks !== undefined) {
|
||||
collectTasks(task.tasks, acc)
|
||||
}
|
||||
}
|
||||
return acc
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
const makeTester = <R>(
|
||||
mapEffect: <A, E>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E, never>,
|
||||
it: V.TestAPI = V.it
|
||||
): Vitest.Vitest.Tester<R> => {
|
||||
const run = <A, E, TestArgs extends Array<unknown>>(
|
||||
ctx: 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) =>
|
||||
it(name, testOptions(timeout), (ctx) => run(ctx, [ctx], self))
|
||||
|
||||
const skip: Vitest.Vitest.Tester<R>["only"] = (name, self, timeout) =>
|
||||
it.skip(name, testOptions(timeout), (ctx) => run(ctx, [ctx], self))
|
||||
|
||||
const skipIf: Vitest.Vitest.Tester<R>["skipIf"] = (condition) => (name, self, timeout) =>
|
||||
it.skipIf(condition)(name, testOptions(timeout), (ctx) => run(ctx, [ctx], self))
|
||||
|
||||
const runIf: Vitest.Vitest.Tester<R>["runIf"] = (condition) => (name, self, timeout) =>
|
||||
it.runIf(condition)(name, testOptions(timeout), (ctx) => run(ctx, [ctx], self))
|
||||
|
||||
const only: Vitest.Vitest.Tester<R>["only"] = (name, self, timeout) =>
|
||||
it.only(name, testOptions(timeout), (ctx) => run(ctx, [ctx], self))
|
||||
|
||||
const each: Vitest.Vitest.Tester<R>["each"] = (cases) => (name, self, timeout) =>
|
||||
it.for(cases)(
|
||||
name,
|
||||
testOptions(timeout),
|
||||
(args, ctx) => run(ctx, [args], self) as any
|
||||
)
|
||||
|
||||
const fails: Vitest.Vitest.Tester<R>["fails"] = (name, self, timeout) =>
|
||||
V.it.fails(name, testOptions(timeout), (ctx) => run(ctx, [ctx], self))
|
||||
|
||||
const prop: Vitest.Vitest.Tester<R>["prop"] = (name, arbitraries, self, timeout) => {
|
||||
if (Array.isArray(arbitraries)) {
|
||||
const arbs = arbitraries.map((arbitrary) => {
|
||||
if (Schema.isSchema(arbitrary)) {
|
||||
return Schema.toArbitrary(arbitrary)
|
||||
}
|
||||
return arbitrary as fc.Arbitrary<any>
|
||||
})
|
||||
return it(
|
||||
name,
|
||||
testOptions(timeout),
|
||||
(ctx) =>
|
||||
// @ts-ignore
|
||||
fc.assert(
|
||||
// @ts-ignore
|
||||
fc.asyncProperty(...arbs, (...as) => run(ctx, [as as any, ctx], self)),
|
||||
// @ts-ignore
|
||||
isObject(timeout) ? timeout?.fastCheck : {}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const arbs = fc.record(
|
||||
Object.keys(arbitraries).reduce(function(result, key) {
|
||||
const arb: any = arbitraries[key]
|
||||
result[key] = Schema.isSchema(arb) ? Schema.toArbitrary(arb) : arb
|
||||
return result
|
||||
}, {} as Record<string, fc.Arbitrary<any>>)
|
||||
)
|
||||
|
||||
return it(
|
||||
name,
|
||||
testOptions(timeout),
|
||||
(ctx) =>
|
||||
// @ts-ignore
|
||||
fc.assert(
|
||||
fc.asyncProperty(arbs, (...as) =>
|
||||
// @ts-ignore
|
||||
run(ctx, [as[0] as any, ctx], self)),
|
||||
// @ts-ignore
|
||||
isObject(timeout) ? timeout?.fastCheck : {}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return Object.assign(f, { skip, skipIf, runIf, only, each, fails, prop })
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export const prop: Vitest.Vitest.Methods["prop"] = (name, arbitraries, self, timeout) => {
|
||||
if (Array.isArray(arbitraries)) {
|
||||
const arbs = arbitraries.map((arbitrary) => {
|
||||
if (Schema.isSchema(arbitrary)) {
|
||||
throw new Error("Schemas are not supported yet")
|
||||
}
|
||||
return arbitrary
|
||||
})
|
||||
return V.it(
|
||||
name,
|
||||
testOptions(timeout),
|
||||
// @ts-ignore
|
||||
(ctx) => fc.assert(fc.property(...arbs, (...as) => self(as, ctx)), isObject(timeout) ? timeout?.fastCheck : {})
|
||||
)
|
||||
}
|
||||
|
||||
const arbs = fc.record(
|
||||
Object.keys(arbitraries).reduce(function(result, key) {
|
||||
const arb: any = arbitraries[key]
|
||||
if (Schema.isSchema(arb)) {
|
||||
throw new Error("Schemas are not supported yet")
|
||||
}
|
||||
result[key] = arb
|
||||
return result
|
||||
}, {} as Record<string, fc.Arbitrary<any>>)
|
||||
)
|
||||
|
||||
return V.it(
|
||||
name,
|
||||
testOptions(timeout),
|
||||
// @ts-ignore
|
||||
(ctx) => fc.assert(fc.property(arbs, (as) => self(as, ctx)), isObject(timeout) ? timeout?.fastCheck : {})
|
||||
)
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export const layer = <R, E>(
|
||||
layer_: Layer.Layer<R, E>,
|
||||
options?: {
|
||||
readonly memoMap?: Layer.MemoMap
|
||||
readonly timeout?: Duration.Input
|
||||
readonly excludeTestServices?: boolean
|
||||
}
|
||||
): {
|
||||
(f: (it: Vitest.Vitest.MethodsNonLive<R>) => void): void
|
||||
(
|
||||
name: string,
|
||||
f: (it: Vitest.Vitest.MethodsNonLive<R>) => void
|
||||
): void
|
||||
} =>
|
||||
(
|
||||
...args: [
|
||||
name: string,
|
||||
f: (
|
||||
it: Vitest.Vitest.MethodsNonLive<R>
|
||||
) => void
|
||||
] | [
|
||||
f: (it: Vitest.Vitest.MethodsNonLive<R>) => void
|
||||
]
|
||||
) => {
|
||||
const excludeTestServices = options?.excludeTestServices ?? false
|
||||
const withTestEnv = excludeTestServices
|
||||
? layer_ as Layer.Layer<R, E>
|
||||
: Layer.provideMerge(layer_, TestEnv)
|
||||
const memoMap = options?.memoMap ?? Effect.runSync(Layer.makeMemoMap)
|
||||
const scope = Effect.runSync(Scope.make())
|
||||
const contextEffect = Layer.buildWithMemoMap(withTestEnv, memoMap, scope).pipe(
|
||||
Effect.orDie,
|
||||
Effect.cached,
|
||||
Effect.runSync
|
||||
)
|
||||
let closed = false
|
||||
const closeScope = (ctx?: Vitest.TestContext) => {
|
||||
if (closed) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
closed = true
|
||||
return runPromise(Scope.close(scope, Exit.void), ctx)
|
||||
}
|
||||
|
||||
const makeIt = (it: V.TestAPI): Vitest.Vitest.MethodsNonLive<R> =>
|
||||
makeItProxy(it, {
|
||||
effect: makeTester<R | Scope.Scope>(
|
||||
(effect) =>
|
||||
Effect.flatMap(contextEffect, (context) =>
|
||||
effect.pipe(
|
||||
Effect.scoped,
|
||||
Effect.provide(context)
|
||||
)),
|
||||
it
|
||||
),
|
||||
prop,
|
||||
flakyTest,
|
||||
layer<R2, E2>(nestedLayer: Layer.Layer<R2, E2, R>, options?: {
|
||||
readonly timeout?: Duration.Input
|
||||
}) {
|
||||
return layer(Layer.provideMerge(nestedLayer, withTestEnv), {
|
||||
...options,
|
||||
memoMap: Layer.forkMemoMapUnsafe(memoMap),
|
||||
excludeTestServices
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
if (args.length === 1) {
|
||||
const currentSuite = getCurrentSuite()
|
||||
const previousTasks = new Set(currentSuite.tasks)
|
||||
|
||||
args[0](makeIt(V.it))
|
||||
|
||||
const blockTasks = collectTasks(
|
||||
currentSuite.tasks.filter((task) => !previousTasks.has(task)) as ReadonlyArray<CollectedTask>
|
||||
)
|
||||
if (blockTasks.length === 0) {
|
||||
V.afterAll(() => closeScope(), hookTimeout(options?.timeout))
|
||||
return
|
||||
}
|
||||
|
||||
const blockTaskSet = new Set(blockTasks)
|
||||
let remaining = blockTasks.length
|
||||
|
||||
V.beforeEach(
|
||||
(ctx) => {
|
||||
if (!blockTaskSet.has(ctx.task)) {
|
||||
return
|
||||
}
|
||||
ctx.onTestFinished(() => {
|
||||
remaining--
|
||||
if (remaining === 0) {
|
||||
return closeScope(ctx)
|
||||
}
|
||||
})
|
||||
return runPromise(Effect.asVoid(contextEffect), ctx)
|
||||
},
|
||||
hookTimeout(options?.timeout)
|
||||
)
|
||||
V.afterAll(() => closeScope(), hookTimeout(options?.timeout))
|
||||
return
|
||||
}
|
||||
|
||||
return V.describe(args[0], () => {
|
||||
V.beforeAll(
|
||||
() => runPromise(Effect.asVoid(contextEffect)),
|
||||
hookTimeout(options?.timeout)
|
||||
)
|
||||
V.afterAll(
|
||||
() => closeScope(),
|
||||
hookTimeout(options?.timeout)
|
||||
)
|
||||
return args[1](makeIt(V.it))
|
||||
})
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export const flakyTest = <A, E, R>(
|
||||
self: Effect.Effect<A, E, R | Scope.Scope>,
|
||||
timeout: Duration.Input = Duration.seconds(30)
|
||||
) =>
|
||||
pipe(
|
||||
self,
|
||||
Effect.scoped,
|
||||
Effect.sandbox,
|
||||
Effect.retry(
|
||||
pipe(
|
||||
Schedule.recurs(10),
|
||||
Schedule.while((_) =>
|
||||
Effect.succeed(Duration.isLessThanOrEqualTo(
|
||||
Duration.fromInputUnsafe(_.elapsed),
|
||||
Duration.fromInputUnsafe(timeout)
|
||||
))
|
||||
)
|
||||
)
|
||||
),
|
||||
Effect.orDie
|
||||
)
|
||||
|
||||
/** @internal */
|
||||
export const makeMethods = (it: V.TestAPI): Vitest.Vitest.Methods =>
|
||||
makeItProxy(it, {
|
||||
effect: makeTester<Scope.Scope>(flow(Effect.scoped, Effect.provide(TestEnv)), it),
|
||||
live: makeTester<Scope.Scope>(Effect.scoped, it),
|
||||
flakyTest,
|
||||
layer,
|
||||
prop
|
||||
})
|
||||
|
||||
/** @internal */
|
||||
export const {
|
||||
/** @internal */
|
||||
effect,
|
||||
/** @internal */
|
||||
live
|
||||
} = makeMethods(V.it)
|
||||
|
||||
/** @internal */
|
||||
export const describeWrapped = (name: string, f: (it: Vitest.Vitest.Methods) => void): V.SuiteCollector =>
|
||||
V.describe(name, (it) => f(makeMethods(it)))
|
||||
321
repos/effect/packages/vitest/src/utils.ts
Normal file
321
repos/effect/packages/vitest/src/utils.ts
Normal file
@@ -0,0 +1,321 @@
|
||||
/**
|
||||
* Provides assertion helpers used by `@effect/vitest` tests.
|
||||
*
|
||||
* This module defines small assertion functions built on Node's `assert`,
|
||||
* Vitest's instance checks, and Effect's equality support. The helpers cover
|
||||
* basic equality, thrown errors, defined and undefined values, strings, regular
|
||||
* expressions, class instances, `Option`, `Result`, and `Exit`. Most helpers are
|
||||
* synchronous; `throwsAsync` handles rejected promises.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*/
|
||||
import type * as Cause from "effect/Cause"
|
||||
import * as Equal from "effect/Equal"
|
||||
import * as Exit from "effect/Exit"
|
||||
import * as Option from "effect/Option"
|
||||
import * as Predicate from "effect/Predicate"
|
||||
import * as Result from "effect/Result"
|
||||
import * as assert from "node:assert"
|
||||
import { assert as vassert } from "vitest"
|
||||
|
||||
// ----------------------------
|
||||
// Primitives
|
||||
// ----------------------------
|
||||
|
||||
/**
|
||||
* Fails the current test with the provided error message.
|
||||
*
|
||||
* @category testing
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export function fail(message: string) {
|
||||
assert.fail(message)
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that `actual` is deeply strictly equal to `expected` using Node's `assert.deepStrictEqual`.
|
||||
*
|
||||
* @category testing
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export function deepStrictEqual<A>(actual: A, expected: A, message?: string, ..._: Array<never>) {
|
||||
assert.deepStrictEqual(actual, expected, message as string)
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that `actual` is not deeply strictly equal to `expected` using Node's `assert.notDeepStrictEqual`.
|
||||
*
|
||||
* @category testing
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export function notDeepStrictEqual<A>(actual: A, expected: A, message?: string, ..._: Array<never>) {
|
||||
assert.notDeepStrictEqual(actual, expected, message as string)
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that `actual` is strictly equal to `expected` using Node's `assert.strictEqual`.
|
||||
*
|
||||
* @category testing
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export function strictEqual<A>(actual: A, expected: A, message?: string, ..._: Array<never>) {
|
||||
assert.strictEqual(actual, expected, message as string)
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that `actual` is equal to `expected` using the `Equal.equals` trait.
|
||||
*
|
||||
* @category testing
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export function assertEquals<A>(actual: A, expected: A, message?: string, ..._: Array<never>) {
|
||||
if (!Equal.equals(actual, expected)) {
|
||||
deepStrictEqual(actual, expected, message) // show diff
|
||||
fail(message ?? "Expected values to be Equal.equals")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that `thunk` does not throw an error.
|
||||
*
|
||||
* @category testing
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export function doesNotThrow(thunk: () => void, message?: string, ..._: Array<never>) {
|
||||
assert.doesNotThrow(thunk, message)
|
||||
}
|
||||
|
||||
// ----------------------------
|
||||
// Derived
|
||||
// ----------------------------
|
||||
|
||||
/**
|
||||
* Asserts that `value` is an instance of `constructor`.
|
||||
*
|
||||
* @category testing
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export function assertInstanceOf<C extends abstract new(...args: any) => any>(
|
||||
value: unknown,
|
||||
constructor: C,
|
||||
message?: string,
|
||||
..._: Array<never>
|
||||
): asserts value is InstanceType<C> {
|
||||
vassert.instanceOf(value, constructor as any, message)
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that `self` is `true`.
|
||||
*
|
||||
* @category testing
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export function assertTrue(self: unknown, message?: string, ..._: Array<never>): asserts self {
|
||||
strictEqual(self, true, message)
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that `self` is `false`.
|
||||
*
|
||||
* @category testing
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export function assertFalse(self: boolean, message?: string, ..._: Array<never>) {
|
||||
strictEqual(self, false, message)
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that `actual` includes `expected`.
|
||||
*
|
||||
* @category testing
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export function assertInclude(actual: string | undefined, expected: string, ..._: Array<never>) {
|
||||
if (typeof expected === "string") {
|
||||
if (!actual?.includes(expected)) {
|
||||
fail(`Expected\n\n${actual}\n\nto include\n\n${expected}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that `actual` matches `regExp`.
|
||||
*
|
||||
* @category testing
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export function assertMatch(actual: string, regExp: RegExp, ..._: Array<never>) {
|
||||
if (!regExp.test(actual)) {
|
||||
fail(`Expected\n\n${actual}\n\nto match\n\n${regExp}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that `thunk` throws, optionally checking the thrown value against an expected `Error` or validation function.
|
||||
*
|
||||
* @category testing
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export function throws(thunk: () => void, error?: Error | ((u: unknown) => undefined), ..._: Array<never>) {
|
||||
try {
|
||||
thunk()
|
||||
fail("Expected to throw an error")
|
||||
} catch (e) {
|
||||
if (error !== undefined) {
|
||||
if (Predicate.isFunction(error)) {
|
||||
error(e)
|
||||
} else if (error) {
|
||||
deepStrictEqual(e, error)
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that `thunk` throws or returns a rejected promise, optionally checking the failure value against an expected `Error` or validation function.
|
||||
*
|
||||
* @category testing
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export async function throwsAsync(
|
||||
thunk: () => Promise<void>,
|
||||
error?: Error | ((u: unknown) => undefined),
|
||||
..._: Array<never>
|
||||
) {
|
||||
try {
|
||||
await thunk()
|
||||
fail("Expected to throw an error")
|
||||
} catch (e) {
|
||||
if (error !== undefined) {
|
||||
if (Predicate.isFunction(error)) {
|
||||
error(e)
|
||||
} else {
|
||||
deepStrictEqual(e, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------
|
||||
// Option
|
||||
// ----------------------------
|
||||
|
||||
/**
|
||||
* Asserts that `option` is `None`.
|
||||
*
|
||||
* @category testing
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export function assertNone<A>(option: Option.Option<A>, ..._: Array<never>): asserts option is Option.None<never> {
|
||||
deepStrictEqual(option, Option.none())
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that `a` is not `undefined`.
|
||||
*
|
||||
* @category testing
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export function assertDefined<A>(
|
||||
a: A | undefined,
|
||||
..._: Array<never>
|
||||
): asserts a is Exclude<A, undefined> {
|
||||
if (a === undefined) {
|
||||
fail("Expected value to be defined")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that `a` is `undefined`.
|
||||
*
|
||||
* @category testing
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export function assertUndefined<A>(
|
||||
a: A | undefined,
|
||||
..._: Array<never>
|
||||
): asserts a is undefined {
|
||||
if (a !== undefined) {
|
||||
fail("Expected value to be undefined")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that `option` is `Some` and contains a value equal to `expected`.
|
||||
*
|
||||
* @category testing
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export function assertSome<A>(
|
||||
option: Option.Option<A>,
|
||||
expected: A,
|
||||
..._: Array<never>
|
||||
): asserts option is Option.Some<A> {
|
||||
deepStrictEqual(option, Option.some(expected))
|
||||
}
|
||||
|
||||
// ----------------------------
|
||||
// Result
|
||||
// ----------------------------
|
||||
|
||||
/**
|
||||
* Asserts that `result` is `Success` and contains a value equal to `expected`.
|
||||
*
|
||||
* @category testing
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export function assertSuccess<A, E>(
|
||||
result: Result.Result<A, E>,
|
||||
expected: A,
|
||||
..._: Array<never>
|
||||
): asserts result is Result.Success<A, never> {
|
||||
deepStrictEqual(result, Result.succeed(expected))
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that `result` is `Failure` and contains an error equal to `expected`.
|
||||
*
|
||||
* @category testing
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export function assertFailure<A, E>(
|
||||
result: Result.Result<A, E>,
|
||||
expected: E,
|
||||
..._: Array<never>
|
||||
): asserts result is Result.Failure<never, E> {
|
||||
deepStrictEqual(result, Result.fail(expected))
|
||||
}
|
||||
|
||||
// ----------------------------
|
||||
// Exit
|
||||
// ----------------------------
|
||||
|
||||
/**
|
||||
* Asserts that `exit` is a failure with a cause equal to `expected`.
|
||||
*
|
||||
* @category testing
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export function assertExitFailure<A, E>(
|
||||
exit: Exit.Exit<A, E>,
|
||||
expected: Cause.Cause<E>,
|
||||
..._: Array<never>
|
||||
): asserts exit is Exit.Failure<never, E> {
|
||||
deepStrictEqual(exit, Exit.failCause(expected))
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that `exit` is a success with a value equal to `expected`.
|
||||
*
|
||||
* @category testing
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export function assertExitSuccess<A, E>(
|
||||
exit: Exit.Exit<A, E>,
|
||||
expected: A,
|
||||
..._: Array<never>
|
||||
): asserts exit is Exit.Success<A, never> {
|
||||
deepStrictEqual(exit, Exit.succeed(expected))
|
||||
}
|
||||
Reference in New Issue
Block a user