import { describe, expect, jest, test } from "bun:test"; import fs from "fs"; import { tempDirWithFiles } from "harness"; import { join } from "path"; const impls = [ ["cpSync", fs.cpSync], ["cp", fs.promises.cp], ] as const; for (const [name, copy] of impls) { async function copyShouldThrow(...args: Parameters) { try { await (copy as any)(...args); } catch (e: any) { if (e?.code?.toUpperCase() === "TODO") { throw new Error("Expected " + name + "() to throw non TODO error"); } return e; } throw new Error("Expected " + name + "() to throw"); } function assertContent(path: string, content: string) { expect(fs.readFileSync(path, "utf8")).toBe(content); } describe("fs." + name, () => { test("single file", async () => { const basename = tempDirWithFiles("cp", { "from/a.txt": "a", }); await copy(basename + "/from/a.txt", basename + "/to.txt"); expect(fs.readFileSync(basename + "/to.txt", "utf8")).toBe("a"); }); test("refuse to copy directory with 'recursive: false'", async () => { const basename = tempDirWithFiles("cp", { "from/a.txt": "a", }); const e = await copyShouldThrow(basename + "/from", basename + "/result"); expect(e.code).toBe("EISDIR"); expect(e.path).toBe(join(basename, "from")); }); test("recursive directory structure - no destination", async () => { const basename = tempDirWithFiles("cp", { "from/a.txt": "a", "from/b/e.txt": "e", "from/c.txt": "c", "from/w/y/x/z.txt": "z", }); await copy(basename + "/from", basename + "/result", { recursive: true }); assertContent(basename + "/result/a.txt", "a"); assertContent(basename + "/result/b/e.txt", "e"); assertContent(basename + "/result/c.txt", "c"); assertContent(basename + "/result/w/y/x/z.txt", "z"); }); test("recursive directory structure - overwrite existing files by default", async () => { const basename = tempDirWithFiles("cp", { "from/a.txt": "a", "from/b/e.txt": "e", "from/c.txt": "c", "from/w/y/x/z.txt": "z", "result/a.txt": "fail", "result/w/y/x/z.txt": "lose", "result/w/y/v.txt": "keep this", }); await copy(basename + "/from", basename + "/result", { recursive: true }); assertContent(basename + "/result/a.txt", "a"); assertContent(basename + "/result/b/e.txt", "e"); assertContent(basename + "/result/c.txt", "c"); assertContent(basename + "/result/w/y/x/z.txt", "z"); assertContent(basename + "/result/w/y/v.txt", "keep this"); }); test("recursive directory structure - 'force: false' does not overwrite existing files", async () => { const basename = tempDirWithFiles("cp", { "from/a.txt": "lose", "from/b/e.txt": "e", "from/c.txt": "c", "from/w/y/x/z.txt": "lose", "result/a.txt": "win", "result/w/y/x/z.txt": "win", "result/w/y/v.txt": "keep this", }); await copy(basename + "/from", basename + "/result", { recursive: true, force: false }); assertContent(basename + "/result/a.txt", "win"); assertContent(basename + "/result/b/e.txt", "e"); assertContent(basename + "/result/c.txt", "c"); assertContent(basename + "/result/w/y/x/z.txt", "win"); assertContent(basename + "/result/w/y/v.txt", "keep this"); }); test("'force: false' on a single file doesn't override", async () => { const basename = tempDirWithFiles("cp", { "from/a.txt": "lose", "result/a.txt": "win", }); await copy(basename + "/from/a.txt", basename + "/result/a.txt", { force: false }); assertContent(basename + "/result/a.txt", "win"); }); test("'force: true' on a single file does override", async () => { const basename = tempDirWithFiles("cp", { "from/a.txt": "win", "result/a.txt": "lose", }); await copy(basename + "/from/a.txt", basename + "/result/a.txt", { force: true }); assertContent(basename + "/result/a.txt", "win"); }); test("'force: false' + 'errorOnExist: true' can throw", async () => { const basename = tempDirWithFiles("cp", { "from/a.txt": "lose", "result/a.txt": "win", }); const e = await copyShouldThrow(basename + "/from/a.txt", basename + "/result/a.txt", { force: false, errorOnExist: true, }); expect(e.code).toBe("EEXIST"); expect(e.path).toBe(join(basename, "result", "a.txt")); assertContent(basename + "/result/a.txt", "win"); }); test("symlinks - single file", async () => { const basename = tempDirWithFiles("cp", { "from/a.txt": "a", }); fs.symlinkSync(basename + "/from/a.txt", basename + "/from/a_symlink.txt"); await copy(basename + "/from/a_symlink.txt", basename + "/result.txt"); await copy(basename + "/from/a_symlink.txt", basename + "/result2.txt", { recursive: false }); const stats = fs.lstatSync(basename + "/result.txt"); expect(stats.isSymbolicLink()).toBe(true); expect(fs.readFileSync(basename + "/result.txt", "utf8")).toBe("a"); const stats2 = fs.lstatSync(basename + "/result2.txt"); expect(stats2.isSymbolicLink()).toBe(true); expect(fs.readFileSync(basename + "/result2.txt", "utf8")).toBe("a"); }); test("symlinks - single file recursive", async () => { const basename = tempDirWithFiles("cp", { "from/a.txt": "a", }); fs.symlinkSync(basename + "/from/a.txt", basename + "/from/a_symlink.txt"); await copy(basename + "/from/a_symlink.txt", basename + "/result.txt", { recursive: true }); const stats = fs.lstatSync(basename + "/result.txt"); expect(stats.isSymbolicLink()).toBe(true); expect(fs.readFileSync(basename + "/result.txt", "utf8")).toBe("a"); }); test("symlinks - directory recursive", async () => { const basename = tempDirWithFiles("cp", { "from/a.txt": "a", "from/b.txt": "b", "from/dir/c.txt": "c", }); fs.symlinkSync(basename + "/from/a.txt", basename + "/from/a_symlink.txt"); fs.symlinkSync(basename + "/from/dir", basename + "/from/dir_symlink"); await copy(basename + "/from", basename + "/result", { recursive: true }); const statsFile = fs.lstatSync(basename + "/result/a_symlink.txt"); expect(statsFile.isSymbolicLink()).toBe(true); expect(fs.readFileSync(basename + "/result/a_symlink.txt", "utf8")).toBe("a"); const statsDir = fs.lstatSync(basename + "/result/dir_symlink"); expect(statsDir.isSymbolicLink()).toBe(true); expect(fs.readdirSync(basename + "/result/dir_symlink")).toEqual(["c.txt"]); }); test("symlinks - directory recursive 2", async () => { const basename = tempDirWithFiles("cp", { "from/a.txt": "a", "from/b.txt": "b", "from/dir/c.txt": "c", }); fs.symlinkSync(basename + "/from/a.txt", basename + "/from/a_symlink.txt"); fs.symlinkSync(basename + "/from/dir", basename + "/from/dir_symlink"); fs.mkdirSync(basename + "/result"); await copy(basename + "/from", basename + "/result", { recursive: true }); const statsFile = fs.lstatSync(basename + "/result/a_symlink.txt"); expect(statsFile.isSymbolicLink()).toBe(true); expect(fs.readFileSync(basename + "/result/a_symlink.txt", "utf8")).toBe("a"); const statsDir = fs.lstatSync(basename + "/result/dir_symlink"); expect(statsDir.isSymbolicLink()).toBe(true); expect(fs.readdirSync(basename + "/result/dir_symlink")).toEqual(["c.txt"]); }); test("filter - works", async () => { const basename = tempDirWithFiles("cp", { "from/a.txt": "a", "from/b.txt": "b", }); await copy(basename + "/from", basename + "/result", { filter: (src: string) => { return src.endsWith("/from") || src.includes("a.txt"); }, recursive: true, }); expect(fs.existsSync(basename + "/result/a.txt")).toBe(true); expect(fs.existsSync(basename + "/result/b.txt")).toBe(false); }); test("filter - paths given are correct and relative", async () => { const basename = tempDirWithFiles("cp", { "from/a.txt": "a", "from/b.txt": "b", }); const filter = jest.fn((src: string) => true); let prev = process.cwd(); process.chdir(basename); await copy(join(basename, "from"), join(basename, "result"), { filter, recursive: true, }); process.chdir(prev); expect(filter.mock.calls.sort((a, b) => a[0].localeCompare(b[0]))).toEqual([ [join(basename, "from"), join(basename, "result")], [join(basename, "from", "a.txt"), join(basename, "result", "a.txt")], [join(basename, "from", "b.txt"), join(basename, "result", "b.txt")], ]); }); test("trailing slash", async () => { const basename = tempDirWithFiles("cp", { "from/a.txt": "a", "from/b.txt": "b", }); await copy(basename + "/from/", basename + "/result/", { recursive: true }); assertContent(basename + "/result/a.txt", "a"); assertContent(basename + "/result/b.txt", "b"); }); test("copy directory will ensure directory exists", async () => { const basename = tempDirWithFiles("cp", { "from/a.txt": "a", "from/b.txt": "b", }); fs.mkdirSync(basename + "/result/"); await copy(basename + "/from/", basename + "/hello/world/", { recursive: true }); assertContent(basename + "/hello/world/a.txt", "a"); assertContent(basename + "/hello/world/b.txt", "b"); }); test("relative paths for directories", async () => { const basename = tempDirWithFiles("cp", { "from/a.txt": "a", "from/b.txt": "b", "from/a.dir": { "c.txt": "c" }, }); const filter = jest.fn((src: string) => true); let prev = process.cwd(); process.chdir(basename); await copy("from", "result", { recursive: true, }); process.chdir(prev); assertContent(basename + "/result/a.dir/c.txt", "c"); }); test.if(process.platform === "win32")("should not throw EBUSY when copying the same file on windows", async () => { const basename = tempDirWithFiles("cp", { "hey": "hi", }); await copy(basename + "/hey", basename + "/hey"); }); }); } test("cp with missing callback throws", () => { expect(() => { // @ts-expect-error fs.cp("a", "b" as any); }).toThrow(/"cb"/); });