Merge commit '36c66dd290d3ce6eb1ccd310d0c658d4a32bb8eb' as 'repos/effect'
This commit is contained in:
239
repos/effect/packages/vitest/test/index.test.ts
Normal file
239
repos/effect/packages/vitest/test/index.test.ts
Normal file
@@ -0,0 +1,239 @@
|
||||
import { afterAll, assert, describe, expect, it, layer } from "@effect/vitest"
|
||||
import { Clock, Context, Duration, Effect, Fiber, Layer, Schema } from "effect"
|
||||
import { FastCheck, TestClock } from "effect/testing"
|
||||
|
||||
it.effect(
|
||||
"effect",
|
||||
() => Effect.acquireRelease(Effect.sync(() => expect(1).toEqual(1)), () => Effect.void)
|
||||
)
|
||||
it.live(
|
||||
"live",
|
||||
() => Effect.acquireRelease(Effect.sync(() => expect(1).toEqual(1)), () => Effect.void)
|
||||
)
|
||||
|
||||
// each
|
||||
|
||||
it.effect.each([1, 2, 3])(
|
||||
"effect each %s",
|
||||
(n) => Effect.acquireRelease(Effect.sync(() => expect(n).toEqual(n)), () => Effect.void)
|
||||
)
|
||||
it.live.each([1, 2, 3])(
|
||||
"live each %s",
|
||||
(n) => Effect.acquireRelease(Effect.sync(() => expect(n).toEqual(n)), () => Effect.void)
|
||||
)
|
||||
|
||||
// skip
|
||||
|
||||
it.live.skip(
|
||||
"live skipped",
|
||||
() => Effect.die("skipped anyway")
|
||||
)
|
||||
it.effect.skip(
|
||||
"effect skipped",
|
||||
() => Effect.die("skipped anyway")
|
||||
)
|
||||
|
||||
// skipIf
|
||||
|
||||
it.effect.skipIf(true)("effect skipIf (true)", () => Effect.die("skipped anyway"))
|
||||
it.effect.skipIf(false)("effect skipIf (false)", () => Effect.sync(() => expect(1).toEqual(1)))
|
||||
|
||||
// runIf
|
||||
|
||||
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"))
|
||||
|
||||
// chained helpers
|
||||
|
||||
it.describe.each(["foo", "bar"] as const)("describe.each %s", (text) => {
|
||||
it.effect("runs an Effect test", () =>
|
||||
Effect.sync(() => {
|
||||
assert.include(["foo", "bar"], text)
|
||||
}))
|
||||
})
|
||||
|
||||
it.skip.each([1])("skip.each %s", () => assert.fail("skipped 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.live.fails("interrupts on timeout", (ctx) =>
|
||||
Effect.gen(function*() {
|
||||
let acquired = false
|
||||
|
||||
ctx.onTestFailed(() => {
|
||||
if (acquired) {
|
||||
// oxlint-disable-next-line no-console
|
||||
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)
|
||||
}), 1)
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
class Sleeper extends Context.Service<Sleeper, {
|
||||
readonly sleep: (ms: number) => Effect.Effect<void>
|
||||
}>()("Sleeper") {
|
||||
static readonly layer = Layer.effect(Sleeper)(
|
||||
Effect.gen(function*() {
|
||||
const clock = yield* Clock.Clock
|
||||
|
||||
return {
|
||||
sleep: (ms: number) => clock.sleep(Duration.millis(ms))
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
describe("layer", () => {
|
||||
layer(Foo.Live)((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")
|
||||
}))
|
||||
})
|
||||
|
||||
it.layer(Bar.Live)((it) => {
|
||||
it.effect("without name", () =>
|
||||
Effect.gen(function*() {
|
||||
const foo = yield* Foo
|
||||
const bar = yield* Bar
|
||||
expect(foo).toEqual("foo")
|
||||
expect(bar).toEqual("bar")
|
||||
}))
|
||||
})
|
||||
|
||||
describe("release", () => {
|
||||
let released = false
|
||||
afterAll(() => {
|
||||
expect(released).toEqual(true)
|
||||
})
|
||||
|
||||
class Scoped extends Context.Service<Scoped, "scoped">()("Scoped") {
|
||||
static Live = Layer.effect(Scoped)(
|
||||
Effect.acquireRelease(
|
||||
Effect.succeed("scoped" as const),
|
||||
() => Effect.sync(() => released = true)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
it.layer(Scoped.Live)((it) => {
|
||||
it.effect("adds context", () =>
|
||||
Effect.gen(function*() {
|
||||
const foo = yield* Foo
|
||||
const scoped = yield* Scoped
|
||||
expect(foo).toEqual("foo")
|
||||
expect(scoped).toEqual("scoped")
|
||||
}))
|
||||
})
|
||||
|
||||
it.effect.prop(
|
||||
"adds context",
|
||||
[realNumber],
|
||||
([num]) =>
|
||||
Effect.gen(function*() {
|
||||
const foo = yield* Foo
|
||||
expect(foo).toEqual("foo")
|
||||
return num === num
|
||||
}),
|
||||
{ fastCheck: { numRuns: 200 } }
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
layer(Sleeper.layer)("test services", (it) => {
|
||||
it.effect("TestClock", () =>
|
||||
Effect.gen(function*() {
|
||||
const sleeper = yield* Sleeper
|
||||
const fiber = yield* Effect.forkChild(sleeper.sleep(100_000))
|
||||
yield* Effect.yieldNow
|
||||
yield* TestClock.adjust(100_000)
|
||||
yield* Fiber.join(fiber)
|
||||
}))
|
||||
})
|
||||
|
||||
layer(Foo.Live)("with a name", (it) => {
|
||||
describe("with a nested describe", () => {
|
||||
it.effect("adds context", () =>
|
||||
Effect.gen(function*() {
|
||||
const foo = yield* Foo
|
||||
expect(foo).toEqual("foo")
|
||||
}))
|
||||
})
|
||||
it.effect("adds context", () =>
|
||||
Effect.gen(function*() {
|
||||
const foo = yield* Foo
|
||||
expect(foo).toEqual("foo")
|
||||
}))
|
||||
})
|
||||
|
||||
layer(Sleeper.layer, { excludeTestServices: true })("live services", (it) => {
|
||||
it.effect("Clock", () =>
|
||||
Effect.gen(function*() {
|
||||
const sleeper = yield* Sleeper
|
||||
yield* sleeper.sleep(1)
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
// property testing
|
||||
|
||||
const realNumber = FastCheck.float({ noNaN: true, noDefaultInfinity: true })
|
||||
|
||||
it.prop("symmetry", [realNumber, FastCheck.integer()], ([a, b]) => a + b === b + a)
|
||||
|
||||
it.prop(
|
||||
"symmetry with object",
|
||||
{ a: realNumber, b: FastCheck.integer() },
|
||||
({ a, b }) => a + b === b + a
|
||||
)
|
||||
|
||||
it.live.prop(
|
||||
"schema with object",
|
||||
{ value: Schema.Int },
|
||||
({ value }) => Effect.sync(() => assert.isTrue(Number.isInteger(value)))
|
||||
)
|
||||
|
||||
it.effect.prop("symmetry", [realNumber, FastCheck.integer()], ([a, b]) =>
|
||||
Effect.gen(function*() {
|
||||
yield* Effect.void
|
||||
assert.isTrue(a + b === b + a)
|
||||
}))
|
||||
|
||||
it.effect.prop("symmetry with object", { a: realNumber, b: FastCheck.integer() }, ({ a, b }) =>
|
||||
Effect.gen(function*() {
|
||||
yield* Effect.void
|
||||
assert.strictEqual(a + b, b + a)
|
||||
}))
|
||||
|
||||
it.effect.prop(
|
||||
"should detect the substring",
|
||||
{ a: FastCheck.string(), b: FastCheck.string(), c: FastCheck.string() },
|
||||
({ a, b, c }) =>
|
||||
Effect.gen(function*() {
|
||||
yield* Effect.scope
|
||||
assert.include(a + b + c, b)
|
||||
})
|
||||
)
|
||||
105
repos/effect/packages/vitest/test/isolation.test.ts
Normal file
105
repos/effect/packages/vitest/test/isolation.test.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { afterAll, assert, describe, it } from "@effect/vitest"
|
||||
import { Context, Effect, Layer, Ref } from "effect"
|
||||
|
||||
interface StateShape {
|
||||
readonly id: number
|
||||
readonly todos: Ref.Ref<Array<string>>
|
||||
readonly migrated: Ref.Ref<boolean>
|
||||
}
|
||||
|
||||
class State extends Context.Service<State, StateShape>()("State") {}
|
||||
|
||||
class TodoService extends Context.Service<TodoService, {
|
||||
readonly stateId: Effect.Effect<number>
|
||||
readonly migrated: Effect.Effect<boolean>
|
||||
readonly add: (title: string) => Effect.Effect<void>
|
||||
readonly list: Effect.Effect<Array<string>>
|
||||
}>()("TodoService") {}
|
||||
|
||||
describe("top-level it.layer isolation", () => {
|
||||
let nextId = 0
|
||||
const observedStateIds: Array<number> = []
|
||||
|
||||
const baseLayer = Layer.effect(State)(
|
||||
Effect.gen(function*() {
|
||||
const id = ++nextId
|
||||
const todos = yield* Ref.make<Array<string>>([])
|
||||
const migrated = yield* Ref.make(false)
|
||||
return { id, todos, migrated }
|
||||
})
|
||||
)
|
||||
|
||||
const migrationLayer = Layer.effectDiscard(
|
||||
Effect.gen(function*() {
|
||||
const state = yield* State
|
||||
yield* Ref.set(state.migrated, true)
|
||||
})
|
||||
)
|
||||
|
||||
const migratedLayer = Layer.merge(
|
||||
baseLayer,
|
||||
migrationLayer.pipe(Layer.provide(baseLayer))
|
||||
)
|
||||
|
||||
const inMemoryLayer = Layer.effect(TodoService)(
|
||||
Effect.gen(function*() {
|
||||
const state = yield* State
|
||||
return {
|
||||
stateId: Effect.succeed(state.id),
|
||||
migrated: Ref.get(state.migrated),
|
||||
add: (title: string) => Ref.update(state.todos, (todos) => [...todos, title]),
|
||||
list: Ref.get(state.todos)
|
||||
} as const
|
||||
})
|
||||
).pipe(Layer.provide(migratedLayer))
|
||||
|
||||
it.layer(inMemoryLayer)((it) => {
|
||||
it.effect("first block mutates isolated state", () =>
|
||||
Effect.gen(function*() {
|
||||
const service = yield* TodoService
|
||||
const stateId = yield* service.stateId
|
||||
const migrated = yield* service.migrated
|
||||
|
||||
observedStateIds.push(stateId)
|
||||
yield* service.add("write tests")
|
||||
|
||||
assert.isTrue(migrated)
|
||||
assert.deepStrictEqual(yield* service.list, ["write tests"])
|
||||
}))
|
||||
})
|
||||
|
||||
it.layer(inMemoryLayer)((it) => {
|
||||
it.effect("second block starts fresh", () =>
|
||||
Effect.gen(function*() {
|
||||
const service = yield* TodoService
|
||||
const stateId = yield* service.stateId
|
||||
const migrated = yield* service.migrated
|
||||
|
||||
observedStateIds.push(stateId)
|
||||
|
||||
assert.isTrue(migrated)
|
||||
assert.deepStrictEqual(yield* service.list, [])
|
||||
|
||||
yield* service.add("ship feature")
|
||||
assert.deepStrictEqual(yield* service.list, ["ship feature"])
|
||||
}))
|
||||
})
|
||||
|
||||
it.layer(inMemoryLayer)((it) => {
|
||||
it.effect("third block also starts fresh", () =>
|
||||
Effect.gen(function*() {
|
||||
const service = yield* TodoService
|
||||
const stateId = yield* service.stateId
|
||||
const migrated = yield* service.migrated
|
||||
|
||||
observedStateIds.push(stateId)
|
||||
|
||||
assert.isTrue(migrated)
|
||||
assert.deepStrictEqual(yield* service.list, [])
|
||||
}))
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
assert.deepStrictEqual(observedStateIds, [1, 2, 3])
|
||||
})
|
||||
})
|
||||
212
repos/effect/packages/vitest/test/nested-isolation.test.ts
Normal file
212
repos/effect/packages/vitest/test/nested-isolation.test.ts
Normal file
@@ -0,0 +1,212 @@
|
||||
import { afterAll, assert, beforeAll, describe, expect, layer } from "@effect/vitest"
|
||||
import { Context, Effect, Layer, Ref } from "effect"
|
||||
|
||||
describe("nested sibling layers", () => {
|
||||
let nextChildId = 0
|
||||
let firstChildId = -1
|
||||
let secondChildId = -1
|
||||
const releasedChildIds: Array<number> = []
|
||||
|
||||
class Parent extends Context.Service<Parent, "parent">()("Parent") {
|
||||
static readonly Live = Layer.succeed(Parent)("parent")
|
||||
}
|
||||
|
||||
class Child extends Context.Service<Child, { readonly id: number }>()("Child") {
|
||||
static readonly Live = Layer.effect(Child)(
|
||||
Effect.flatMap(Parent, () => {
|
||||
const id = ++nextChildId
|
||||
return Effect.acquireRelease(
|
||||
Effect.succeed({ id }),
|
||||
() =>
|
||||
Effect.sync(() => {
|
||||
releasedChildIds.push(id)
|
||||
})
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
layer(Parent.Live)("parent", (it) => {
|
||||
it.layer(Child.Live)("first sibling", (it) => {
|
||||
it.effect("allocates child", () =>
|
||||
Effect.gen(function*() {
|
||||
const child = yield* Child
|
||||
firstChildId = child.id
|
||||
|
||||
assert.strictEqual(child.id, 1)
|
||||
assert.deepStrictEqual(releasedChildIds, [])
|
||||
}))
|
||||
})
|
||||
|
||||
it.layer(Child.Live)("second sibling", (it) => {
|
||||
beforeAll(() => {
|
||||
expect(releasedChildIds).toEqual([firstChildId])
|
||||
})
|
||||
|
||||
it.effect("allocates a fresh child", () =>
|
||||
Effect.gen(function*() {
|
||||
const child = yield* Child
|
||||
secondChildId = child.id
|
||||
|
||||
assert.strictEqual(child.id, 2)
|
||||
assert.isTrue(child.id !== firstChildId)
|
||||
assert.deepStrictEqual(releasedChildIds, [firstChildId])
|
||||
}))
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
expect(firstChildId).toEqual(1)
|
||||
expect(secondChildId).toEqual(2)
|
||||
expect(releasedChildIds).toEqual([1, 2])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe.concurrent("nested sibling layers in concurrent suites", () => {
|
||||
let nextSharedId = 0
|
||||
let firstSharedId: number | undefined
|
||||
let secondSharedId: number | undefined
|
||||
const releasedSharedIds: Array<number> = []
|
||||
|
||||
class Parent extends Context.Service<Parent, "parent">()("ConcurrentParent") {
|
||||
static readonly Live = Layer.succeed(Parent)("parent")
|
||||
}
|
||||
|
||||
class SharedChild extends Context.Service<SharedChild, { readonly id: number }>()("SharedChild") {
|
||||
static readonly Live = Layer.effect(SharedChild)(
|
||||
Effect.flatMap(Parent, () =>
|
||||
Effect.gen(function*() {
|
||||
yield* Effect.promise(() => new Promise<void>((resolve) => setTimeout(resolve, 50)))
|
||||
|
||||
const id = ++nextSharedId
|
||||
return yield* Effect.acquireRelease(
|
||||
Effect.succeed({ id }),
|
||||
() =>
|
||||
Effect.sync(() => {
|
||||
releasedSharedIds.push(id)
|
||||
})
|
||||
)
|
||||
}))
|
||||
)
|
||||
}
|
||||
|
||||
layer(Parent.Live)("parent", (it) => {
|
||||
describe.concurrent("concurrent siblings", () => {
|
||||
it.layer(SharedChild.Live)("first sibling", (it) => {
|
||||
it.effect("captures shared child", () =>
|
||||
Effect.gen(function*() {
|
||||
const child = yield* SharedChild
|
||||
firstSharedId = child.id
|
||||
assert.isTrue(child.id === 1 || child.id === 2)
|
||||
}))
|
||||
})
|
||||
|
||||
it.layer(SharedChild.Live)("second sibling", (it) => {
|
||||
it.effect("allocates an isolated child", () =>
|
||||
Effect.gen(function*() {
|
||||
const child = yield* SharedChild
|
||||
secondSharedId = child.id
|
||||
assert.isTrue(child.id === 1 || child.id === 2)
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
expect(firstSharedId).not.toEqual(secondSharedId)
|
||||
expect(nextSharedId).toEqual(2)
|
||||
expect([...releasedSharedIds].sort((a, b) => a - b)).toEqual([1, 2])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("nested sibling isolation with provided state graph", () => {
|
||||
interface StateShape {
|
||||
readonly id: number
|
||||
readonly todos: Ref.Ref<Array<string>>
|
||||
readonly migrated: Ref.Ref<boolean>
|
||||
}
|
||||
|
||||
class Parent extends Context.Service<Parent, "parent">()("ProvidedParent") {
|
||||
static readonly Live = Layer.succeed(Parent)("parent")
|
||||
}
|
||||
|
||||
class State extends Context.Service<State, StateShape>()("ProvidedState") {}
|
||||
|
||||
class TodoService extends Context.Service<TodoService, {
|
||||
readonly stateId: Effect.Effect<number>
|
||||
readonly migrated: Effect.Effect<boolean>
|
||||
readonly add: (title: string) => Effect.Effect<void>
|
||||
readonly list: Effect.Effect<Array<string>>
|
||||
}>()("ProvidedTodoService") {}
|
||||
|
||||
let nextId = 0
|
||||
let firstStateId = -1
|
||||
let secondStateId = -1
|
||||
|
||||
const baseLayer = Layer.effect(State)(
|
||||
Effect.gen(function*() {
|
||||
const id = ++nextId
|
||||
const todos = yield* Ref.make<Array<string>>([])
|
||||
const migrated = yield* Ref.make(false)
|
||||
return { id, todos, migrated }
|
||||
})
|
||||
)
|
||||
|
||||
const migrationLayer = Layer.effectDiscard(
|
||||
Effect.gen(function*() {
|
||||
const state = yield* State
|
||||
yield* Ref.set(state.migrated, true)
|
||||
})
|
||||
)
|
||||
|
||||
const migratedLayer = Layer.merge(
|
||||
baseLayer,
|
||||
migrationLayer.pipe(Layer.provide(baseLayer))
|
||||
)
|
||||
|
||||
const inMemoryLayer = Layer.effect(TodoService)(
|
||||
Effect.gen(function*() {
|
||||
const state = yield* State
|
||||
return {
|
||||
stateId: Effect.succeed(state.id),
|
||||
migrated: Ref.get(state.migrated),
|
||||
add: (title: string) => Ref.update(state.todos, (todos) => [...todos, title]),
|
||||
list: Ref.get(state.todos)
|
||||
} as const
|
||||
})
|
||||
).pipe(Layer.provide(migratedLayer))
|
||||
|
||||
layer(Parent.Live)("parent", (it) => {
|
||||
it.layer(inMemoryLayer)("first sibling", (it) => {
|
||||
it.effect("mutates isolated provided state", () =>
|
||||
Effect.gen(function*() {
|
||||
const service = yield* TodoService
|
||||
firstStateId = yield* service.stateId
|
||||
|
||||
assert.isTrue(yield* service.migrated)
|
||||
yield* service.add("write tests")
|
||||
assert.deepStrictEqual(yield* service.list, ["write tests"])
|
||||
}))
|
||||
})
|
||||
|
||||
it.layer(inMemoryLayer)("second sibling", (it) => {
|
||||
it.effect("starts fresh with a new provided state", () =>
|
||||
Effect.gen(function*() {
|
||||
const service = yield* TodoService
|
||||
secondStateId = yield* service.stateId
|
||||
|
||||
assert.isTrue(yield* service.migrated)
|
||||
assert.deepStrictEqual(yield* service.list, [])
|
||||
|
||||
yield* service.add("ship feature")
|
||||
assert.deepStrictEqual(yield* service.list, ["ship feature"])
|
||||
}))
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
expect(firstStateId).toEqual(1)
|
||||
expect(secondStateId).toEqual(2)
|
||||
expect(nextId).toEqual(2)
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user