| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169 |
- import { describe, expect } from "bun:test"
- import { Effect } from "effect"
- import { Failed, NotFound, WrongKind, type Files } from "../../src/environment/index"
- import { it } from "./effect"
- export interface EnvironmentHarness {
- readonly files: Files
- readonly root: string
- readonly symlink?: (target: string, path: string) => Effect.Effect<void, Failed>
- readonly dispose?: Effect.Effect<void>
- }
- export const environmentConformance = <E>(
- name: string,
- makeHarness: () => Effect.Effect<EnvironmentHarness, E>,
- skip = false,
- ) => {
- const check = <A, E2>(title: string, body: (harness: EnvironmentHarness) => Effect.Effect<A, E2>) =>
- it.live(title, () =>
- Effect.gen(function* () {
- const harness = yield* Effect.acquireRelease(makeHarness(), (harness) =>
- Effect.gen(function* () {
- yield* Effect.ignore(harness.files.remove(harness.root))
- if (harness.dispose) yield* harness.dispose
- }),
- )
- yield* harness.files.mkdir(harness.root)
- return yield* body(harness)
- }),
- )
- const bytes = (value: string) => new TextEncoder().encode(value)
- const text = (value: Uint8Array) => new TextDecoder().decode(value)
- const suite = skip ? describe.skip : describe
- suite(name, () => {
- check("writes, stats, and reads a file with its info", (harness) =>
- Effect.gen(function* () {
- const target = `${harness.root}/hello.txt`
- yield* harness.files.write(target, bytes("hello"))
- const result = yield* harness.files.read(target)
- expect(text(result.bytes)).toBe("hello")
- expect(result.info.type).toBe("file")
- expect(result.info.size).toBe(5)
- expect(yield* harness.files.stat(target)).toEqual(result.info)
- }),
- )
- check("reports missing paths", (harness) =>
- Effect.gen(function* () {
- const target = `${harness.root}/missing`
- expect(yield* Effect.flip(harness.files.read(target))).toBeInstanceOf(NotFound)
- expect(yield* Effect.flip(harness.files.stat(target))).toBeInstanceOf(NotFound)
- expect(yield* Effect.flip(harness.files.list(target))).toBeInstanceOf(NotFound)
- expect(yield* Effect.flip(harness.files.move(target, `${harness.root}/other`))).toBeInstanceOf(NotFound)
- }),
- )
- check("reports the actual kind", (harness) =>
- Effect.gen(function* () {
- const directory = `${harness.root}/directory`
- const file = `${harness.root}/file`
- yield* harness.files.mkdir(directory)
- yield* harness.files.write(file, bytes("data"))
- const readError = yield* Effect.flip(harness.files.read(directory))
- const listError = yield* Effect.flip(harness.files.list(file))
- expect(readError).toBeInstanceOf(WrongKind)
- expect((readError as WrongKind).actual).toBe("directory")
- expect(listError).toBeInstanceOf(WrongKind)
- expect((listError as WrongKind).actual).toBe("file")
- }),
- )
- check("write creates parent directories", (harness) =>
- Effect.gen(function* () {
- const target = `${harness.root}/one/two/file`
- yield* harness.files.write(target, bytes("nested"))
- yield* harness.files.write(`${harness.root}/empty`, new Uint8Array())
- expect((yield* harness.files.stat(`${harness.root}/one/two`)).type).toBe("directory")
- expect(yield* harness.files.stat(`${harness.root}/empty`)).toMatchObject({ type: "file", size: 0 })
- expect(text((yield* harness.files.read(target)).bytes)).toBe("nested")
- }),
- )
- check("reads byte ranges", (harness) =>
- Effect.gen(function* () {
- const target = `${harness.root}/range`
- yield* harness.files.write(target, bytes("0123456789"))
- expect(text((yield* harness.files.read(target, { offset: 2, length: 4 })).bytes)).toBe("2345")
- expect(text((yield* harness.files.read(target, { offset: 8, length: 8 })).bytes)).toBe("89")
- expect(text((yield* harness.files.read(target, { offset: 20, length: 4 })).bytes)).toBe("")
- }),
- )
- check("lists immediate entries with their kinds", (harness) =>
- Effect.gen(function* () {
- yield* harness.files.write(`${harness.root}/file name`, bytes("data"))
- yield* harness.files.mkdir(`${harness.root}/directory`)
- yield* harness.files.write(`${harness.root}/directory/nested`, bytes("nested"))
- const entries = yield* harness.files.list(harness.root)
- expect(entries.toSorted((a, b) => a.name.localeCompare(b.name))).toEqual([
- { name: "directory", type: "directory" },
- { name: "file name", type: "file" },
- ])
- }),
- )
- check("preserves symlink metadata while following symlinks for content", (harness) =>
- Effect.gen(function* () {
- if (!harness.symlink) return
- yield* harness.files.write(`${harness.root}/target`, bytes("target"))
- yield* harness.files.write(`${harness.root}/target-dir/file`, bytes("through link"))
- yield* harness.symlink("../target", `${harness.root}/target-dir/entry-link`)
- yield* harness.symlink("target", `${harness.root}/link`)
- yield* harness.symlink("target-dir", `${harness.root}/link-dir`)
- yield* harness.symlink("missing", `${harness.root}/dangling-link`)
- expect((yield* harness.files.stat(`${harness.root}/link`)).type).toBe("symlink")
- expect(yield* harness.files.list(harness.root)).toContainEqual({ name: "link", type: "symlink" })
- expect(text((yield* harness.files.read(`${harness.root}/link-dir/file`)).bytes)).toBe("through link")
- expect(
- (yield* harness.files.list(`${harness.root}/link-dir`)).toSorted((a, b) => a.name.localeCompare(b.name)),
- ).toEqual([
- { name: "entry-link", type: "symlink" },
- { name: "file", type: "file" },
- ])
- const fileError = yield* Effect.flip(harness.files.list(`${harness.root}/link`))
- expect(fileError).toBeInstanceOf(WrongKind)
- expect((fileError as WrongKind).actual).toBe("file")
- expect(yield* Effect.flip(harness.files.list(`${harness.root}/dangling-link`))).toBeInstanceOf(NotFound)
- }),
- )
- check("follows symlinks when reading", (harness) =>
- Effect.gen(function* () {
- if (!harness.symlink) return
- yield* harness.files.write(`${harness.root}/target`, bytes("target content"))
- yield* harness.files.mkdir(`${harness.root}/directory`)
- yield* harness.symlink("target", `${harness.root}/file-link`)
- yield* harness.symlink("directory", `${harness.root}/directory-link`)
- yield* harness.symlink("missing", `${harness.root}/dangling-link`)
- const result = yield* harness.files.read(`${harness.root}/file-link`)
- expect(text(result.bytes)).toBe("target content")
- expect(result.info.type).toBe("file")
- expect(result.info.size).toBe(bytes("target content").length)
- const directoryError = yield* Effect.flip(harness.files.read(`${harness.root}/directory-link`))
- expect(directoryError).toBeInstanceOf(WrongKind)
- expect((directoryError as WrongKind).actual).toBe("directory")
- expect(yield* Effect.flip(harness.files.read(`${harness.root}/dangling-link`))).toBeInstanceOf(NotFound)
- }),
- )
- check("moves files and removes trees idempotently", (harness) =>
- Effect.gen(function* () {
- const source = `${harness.root}/source/file`
- const destination = `${harness.root}/destination`
- yield* harness.files.write(source, bytes("moved"))
- yield* harness.files.move(source, destination)
- expect(text((yield* harness.files.read(destination)).bytes)).toBe("moved")
- expect(yield* Effect.flip(harness.files.stat(source))).toBeInstanceOf(NotFound)
- yield* harness.files.remove(`${harness.root}/source`)
- yield* harness.files.remove(`${harness.root}/source`)
- expect(yield* Effect.flip(harness.files.stat(`${harness.root}/source`))).toBeInstanceOf(NotFound)
- }),
- )
- })
- }
|