diff options
author | 2023-08-30 18:30:06 -0700 | |
---|---|---|
committer | 2023-08-30 18:30:06 -0700 | |
commit | 0a5d2a8195fbbaab7ff1f40ad54ba94726bcc104 (patch) | |
tree | 218b52f72b1170c8595800dcda34adc312adf08b /test | |
parent | 89f24e66fff37eab4b984847b73be0b69dbcd0a8 (diff) | |
download | bun-0a5d2a8195fbbaab7ff1f40ad54ba94726bcc104.tar.gz bun-0a5d2a8195fbbaab7ff1f40ad54ba94726bcc104.tar.zst bun-0a5d2a8195fbbaab7ff1f40ad54ba94726bcc104.zip |
feat(node:fs): add `cp`/`cpSync`/`promises.cp` + async `copyFile` (#4340)
* half working disaster code
* this
* async copyFile
* .
* its failing symlink tests
* asdfg
* asdf
* hmm
* okay i think ti works
* small edits
* fix test on linux
* i hate atomics / atomics hate me back <3
* add a message in the builtins bundler that 0.8 is needed. it breaks on older versions lol.
* fixed
* rebase
Diffstat (limited to 'test')
-rw-r--r-- | test/harness.ts | 3 | ||||
-rw-r--r-- | test/js/node/fs/cp.test.ts | 255 | ||||
-rw-r--r-- | test/js/node/fs/fs.test.ts | 6 |
3 files changed, 261 insertions, 3 deletions
diff --git a/test/harness.ts b/test/harness.ts index af8a13a75..f6c0f0e98 100644 --- a/test/harness.ts +++ b/test/harness.ts @@ -91,12 +91,13 @@ export function tempDirWithFiles(basename: string, files: Record<string, string const dir = fs.mkdtempSync(path.join(fs.realpathSync(tmpdir()), basename + "_")); for (const [name, contents] of Object.entries(files)) { if (typeof contents === "object") { - fs.mkdirSync(path.join(dir, name)); for (const [_name, _contents] of Object.entries(contents)) { + fs.mkdirSync(path.dirname(path.join(dir, name, _name)), { recursive: true }); fs.writeFileSync(path.join(dir, name, _name), _contents); } continue; } + fs.mkdirSync(path.dirname(path.join(dir, name)), { recursive: true }); fs.writeFileSync(path.join(dir, name), contents); } return dir; diff --git a/test/js/node/fs/cp.test.ts b/test/js/node/fs/cp.test.ts new file mode 100644 index 000000000..37da58843 --- /dev/null +++ b/test/js/node/fs/cp.test.ts @@ -0,0 +1,255 @@ +import fs from "fs"; +import { describe, test, expect, jest } from "bun:test"; +import { tempDirWithFiles } from "harness"; + +const impls = [ + // ["cpSync", fs.cpSync], + ["cp", fs.promises.cp], +] as const; + +for (const [name, copy] of impls) { + async function copyShouldThrow(...args: Parameters<typeof copy>) { + 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", + }); + + await copyShouldThrow(basename + "/from", basename + "/result"); + }); + + 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", + }); + + await copyShouldThrow(basename + "/from/a.txt", basename + "/result/a.txt", { force: false, errorOnExist: true }); + + 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(basename + "/from", basename + "/result", { + filter, + recursive: true, + }); + + process.chdir(prev); + + expect(filter.mock.calls.sort((a, b) => a[0].localeCompare(b[0]))).toEqual([ + [basename + "/from", basename + "/result"], + [basename + "/from/a.txt", basename + "/result/a.txt"], + [basename + "/from/b.txt", basename + "/result/b.txt"], + ]); + }); + }); +} diff --git a/test/js/node/fs/fs.test.ts b/test/js/node/fs/fs.test.ts index a3522b486..cdd14bb01 100644 --- a/test/js/node/fs/fs.test.ts +++ b/test/js/node/fs/fs.test.ts @@ -1307,7 +1307,8 @@ describe("fs.WriteStream", () => { }); it("should use fd if provided", () => { - const path = join(tmpdir(), "/not-used.txt"); + const path = join(tmpdir(), `not-used-${Date.now()}.txt`); + expect(existsSync(path)).toBe(false); // @ts-ignore-next-line const ws = new WriteStream_(path, { fd: 2, @@ -1419,7 +1420,8 @@ describe("fs.ReadStream", () => { }); it("should use fd if provided", () => { - const path = join(tmpdir(), "not-used.txt"); + const path = join(tmpdir(), `not-used-${Date.now()}.txt`); + expect(existsSync(path)).toBe(false); // @ts-ignore-next-line const ws = new ReadStream_(path, { fd: 0, |