2026-04-12 02:28:05 +09:00
|
|
|
import { describe, it, expect, beforeEach, afterEach } from "bun:test"
|
|
|
|
|
import { readFileSync, writeFileSync, existsSync, rmSync, mkdirSync } from "fs"
|
|
|
|
|
import { join } from "path"
|
|
|
|
|
import { tmpdir } from "os"
|
|
|
|
|
import { writeFileAtomically } from "./write-file-atomically"
|
|
|
|
|
|
|
|
|
|
const testDir = join(tmpdir(), "write-file-atomically-test-" + Date.now())
|
|
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
mkdirSync(testDir, { recursive: true })
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
|
rmSync(testDir, { recursive: true, force: true })
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe("writeFileAtomically", () => {
|
|
|
|
|
it("writes content to a new file", () => {
|
|
|
|
|
// given
|
|
|
|
|
const filePath = join(testDir, "new-file.txt")
|
|
|
|
|
const content = "hello world"
|
|
|
|
|
|
|
|
|
|
// when
|
|
|
|
|
writeFileAtomically(filePath, content)
|
|
|
|
|
|
|
|
|
|
// then
|
|
|
|
|
expect(existsSync(filePath)).toBe(true)
|
|
|
|
|
expect(readFileSync(filePath, "utf-8")).toBe(content)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it("#given target file exists #when writeFileAtomically called #then overwrites successfully", () => {
|
|
|
|
|
// given
|
|
|
|
|
const filePath = join(testDir, "existing-file.txt")
|
|
|
|
|
const originalContent = "original content"
|
|
|
|
|
const newContent = "new content"
|
|
|
|
|
writeFileSync(filePath, originalContent, "utf-8")
|
|
|
|
|
|
|
|
|
|
// when
|
|
|
|
|
writeFileAtomically(filePath, newContent)
|
|
|
|
|
|
|
|
|
|
// then
|
|
|
|
|
expect(existsSync(filePath)).toBe(true)
|
|
|
|
|
expect(readFileSync(filePath, "utf-8")).toBe(newContent)
|
|
|
|
|
expect(existsSync(`${filePath}.tmp`)).toBe(false)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it("#given parent directory does not exist #when writeFileAtomically called #then throws", () => {
|
|
|
|
|
// given
|
|
|
|
|
const filePath = join(testDir, "nonexistent", "deep", "file.txt")
|
|
|
|
|
|
|
|
|
|
// when/then
|
|
|
|
|
expect(() => writeFileAtomically(filePath, "content")).toThrow()
|
|
|
|
|
})
|
2026-05-08 14:05:57 +09:00
|
|
|
|
|
|
|
|
it("#given fsync fails with EPERM (synced folder) #when writeFileAtomically called #then write succeeds", () => {
|
|
|
|
|
// given
|
|
|
|
|
const filePath = join(testDir, "synced-folder.txt")
|
|
|
|
|
const content = "content from a synced folder where fsync is rejected"
|
|
|
|
|
|
|
|
|
|
// when
|
|
|
|
|
writeFileAtomically(filePath, content, {
|
|
|
|
|
fsyncSync: () => {
|
|
|
|
|
const error = new Error("EPERM: operation not permitted, fsync") as NodeJS.ErrnoException
|
|
|
|
|
error.code = "EPERM"
|
|
|
|
|
throw error
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// then
|
|
|
|
|
expect(existsSync(filePath)).toBe(true)
|
|
|
|
|
expect(readFileSync(filePath, "utf-8")).toBe(content)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it("#given fsync fails with EIO (real I/O error) #when writeFileAtomically called #then propagates the error", () => {
|
|
|
|
|
// given
|
|
|
|
|
const filePath = join(testDir, "io-error.txt")
|
|
|
|
|
|
|
|
|
|
// when/then
|
|
|
|
|
expect(() =>
|
|
|
|
|
writeFileAtomically(filePath, "content", {
|
|
|
|
|
fsyncSync: () => {
|
|
|
|
|
const error = new Error("EIO: input/output error") as NodeJS.ErrnoException
|
|
|
|
|
error.code = "EIO"
|
|
|
|
|
throw error
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
).toThrow("EIO")
|
|
|
|
|
})
|
2026-04-12 02:28:05 +09:00
|
|
|
})
|