From bf149d047f4f768ced17d76c47623ca13c90f7e7 Mon Sep 17 00:00:00 2001 From: Max Dahlgren <83047501+maxedahlgren@users.noreply.github.com> Date: Mon, 20 Mar 2023 03:10:39 +1100 Subject: [PATCH] test(ext/node): port _fs tests from std/node (#18262) Co-authored-by: Yoshiya Hinosawa --- cli/tests/unit_node/_fs/_fs_access_test.ts | 4 +- .../unit_node/_fs/_fs_appendFile_test.ts | 259 ++++++++++++++++++ cli/tests/unit_node/_fs/_fs_chmod_test.ts | 121 ++++++++ cli/tests/unit_node/_fs/_fs_chown_test.ts | 72 +++++ cli/tests/unit_node/_fs/_fs_close_test.ts | 97 +++++++ cli/tests/unit_node/_fs/_fs_copy_test.ts | 52 ++++ cli/tests/unit_node/_fs/_fs_exists_test.ts | 65 +++++ cli/tests/unit_node/_fs/_fs_fdatasync_test.ts | 63 +++++ cli/tests/unit_node/_fs/_fs_fstat_test.ts | 83 ++++++ cli/tests/unit_node/_fs/_fs_fsync_test.ts | 61 +++++ cli/tests/unit_node/_fs/_fs_ftruncate_test.ts | 131 +++++++++ cli/tests/unit_node/_fs/_fs_futimes_test.ts | 112 ++++++++ cli/tests/unit_node/_fs/_fs_link_test.ts | 81 ++++++ cli/tests/unit_node/_fs/_fs_lstat_test.ts | 71 +++++ cli/tests/unit_node/_fs/_fs_mkdir_test.ts | 43 +++ cli/tests/unit_node/_fs/_fs_mkdtemp_test.ts | 86 ++++++ cli/tests/unit_node/_fs/_fs_opendir_test.ts | 146 ++++++++++ cli/tests/unit_node/_fs/_fs_readFile_test.ts | 121 ++++++++ cli/tests/unit_node/_fs/_fs_readdir_test.ts | 96 +++++++ cli/tests/unit_node/_fs/_fs_readlink_test.ts | 78 ++++++ cli/tests/unit_node/_fs/_fs_realpath_test.ts | 55 ++++ cli/tests/unit_node/_fs/_fs_rename_test.ts | 55 ++++ cli/tests/unit_node/_fs/_fs_rm_test.ts | 158 +++++++++++ cli/tests/unit_node/_fs/_fs_rmdir_test.ts | 104 +++++++ cli/tests/unit_node/_fs/_fs_stat_test.ts | 134 +++++++++ cli/tests/unit_node/_fs/_fs_symlink_test.ts | 111 ++++++++ cli/tests/unit_node/_fs/_fs_truncate_test.ts | 99 +++++++ cli/tests/unit_node/_fs/_fs_unlink_test.ts | 43 +++ cli/tests/unit_node/_fs/_fs_utimes_test.ts | 104 +++++++ cli/tests/unit_node/_fs/_fs_write_test.ts | 53 ++++ cli/tests/unit_node/_fs/testdata/hello.txt | 1 + cli/tests/unit_node/_test_utils.ts | 43 +++ 32 files changed, 2800 insertions(+), 2 deletions(-) create mode 100644 cli/tests/unit_node/_fs/_fs_appendFile_test.ts create mode 100644 cli/tests/unit_node/_fs/_fs_chmod_test.ts create mode 100644 cli/tests/unit_node/_fs/_fs_chown_test.ts create mode 100644 cli/tests/unit_node/_fs/_fs_close_test.ts create mode 100644 cli/tests/unit_node/_fs/_fs_copy_test.ts create mode 100644 cli/tests/unit_node/_fs/_fs_exists_test.ts create mode 100644 cli/tests/unit_node/_fs/_fs_fdatasync_test.ts create mode 100644 cli/tests/unit_node/_fs/_fs_fstat_test.ts create mode 100644 cli/tests/unit_node/_fs/_fs_fsync_test.ts create mode 100644 cli/tests/unit_node/_fs/_fs_ftruncate_test.ts create mode 100644 cli/tests/unit_node/_fs/_fs_futimes_test.ts create mode 100644 cli/tests/unit_node/_fs/_fs_link_test.ts create mode 100644 cli/tests/unit_node/_fs/_fs_lstat_test.ts create mode 100644 cli/tests/unit_node/_fs/_fs_mkdir_test.ts create mode 100644 cli/tests/unit_node/_fs/_fs_mkdtemp_test.ts create mode 100644 cli/tests/unit_node/_fs/_fs_opendir_test.ts create mode 100644 cli/tests/unit_node/_fs/_fs_readFile_test.ts create mode 100644 cli/tests/unit_node/_fs/_fs_readdir_test.ts create mode 100644 cli/tests/unit_node/_fs/_fs_readlink_test.ts create mode 100644 cli/tests/unit_node/_fs/_fs_realpath_test.ts create mode 100644 cli/tests/unit_node/_fs/_fs_rename_test.ts create mode 100644 cli/tests/unit_node/_fs/_fs_rm_test.ts create mode 100644 cli/tests/unit_node/_fs/_fs_rmdir_test.ts create mode 100644 cli/tests/unit_node/_fs/_fs_stat_test.ts create mode 100644 cli/tests/unit_node/_fs/_fs_symlink_test.ts create mode 100644 cli/tests/unit_node/_fs/_fs_truncate_test.ts create mode 100644 cli/tests/unit_node/_fs/_fs_unlink_test.ts create mode 100644 cli/tests/unit_node/_fs/_fs_utimes_test.ts create mode 100644 cli/tests/unit_node/_fs/_fs_write_test.ts create mode 100644 cli/tests/unit_node/_fs/testdata/hello.txt create mode 100644 cli/tests/unit_node/_test_utils.ts diff --git a/cli/tests/unit_node/_fs/_fs_access_test.ts b/cli/tests/unit_node/_fs/_fs_access_test.ts index c3d54f10e5..5fd2a5b31d 100644 --- a/cli/tests/unit_node/_fs/_fs_access_test.ts +++ b/cli/tests/unit_node/_fs/_fs_access_test.ts @@ -11,7 +11,7 @@ Deno.test( async () => { const file = await Deno.makeTempFile(); try { - Deno.chmod(file, 0o600); + await Deno.chmod(file, 0o600); await fs.promises.access(file, fs.constants.R_OK); await fs.promises.access(file, fs.constants.W_OK); await assertRejects(async () => { @@ -43,7 +43,7 @@ Deno.test( () => { const file = Deno.makeTempFileSync(); try { - Deno.chmod(file, 0o600); + Deno.chmodSync(file, 0o600); fs.accessSync(file, fs.constants.R_OK); fs.accessSync(file, fs.constants.W_OK); assertThrows(() => { diff --git a/cli/tests/unit_node/_fs/_fs_appendFile_test.ts b/cli/tests/unit_node/_fs/_fs_appendFile_test.ts new file mode 100644 index 0000000000..5741938f06 --- /dev/null +++ b/cli/tests/unit_node/_fs/_fs_appendFile_test.ts @@ -0,0 +1,259 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +import { + assertEquals, + assertThrows, + fail, +} from "../../../../test_util/std/testing/asserts.ts"; +import { appendFile, appendFileSync } from "node:fs"; +import { fromFileUrl } from "../../../../test_util/std/path/mod.ts"; +import { assertCallbackErrorUncaught } from "../_test_utils.ts"; + +const decoder = new TextDecoder("utf-8"); + +Deno.test({ + name: "No callback Fn results in Error", + fn() { + assertThrows( + () => { + // @ts-expect-error Argument of type 'string' is not assignable to parameter of type 'NoParamCallback' + appendFile("some/path", "some data", "utf8"); + }, + Error, + "The \"cb\" argument must be of type function. Received type string ('utf8')", + ); + }, +}); + +Deno.test({ + name: "Unsupported encoding results in error()", + fn() { + assertThrows( + () => { + // @ts-expect-error Type '"made-up-encoding"' is not assignable to type + appendFile("some/path", "some data", "made-up-encoding", () => {}); + }, + Error, + "The argument 'made-up-encoding' is invalid encoding. Received 'encoding'", + ); + assertThrows( + () => { + appendFile( + "some/path", + "some data", + // @ts-expect-error Type '"made-up-encoding"' is not assignable to type + { encoding: "made-up-encoding" }, + () => {}, + ); + }, + Error, + "The argument 'made-up-encoding' is invalid encoding. Received 'encoding'", + ); + assertThrows( + // @ts-expect-error Type '"made-up-encoding"' is not assignable to type + () => appendFileSync("some/path", "some data", "made-up-encoding"), + Error, + "The argument 'made-up-encoding' is invalid encoding. Received 'encoding'", + ); + assertThrows( + () => + appendFileSync("some/path", "some data", { + // @ts-expect-error Type '"made-up-encoding"' is not assignable to type + encoding: "made-up-encoding", + }), + Error, + "The argument 'made-up-encoding' is invalid encoding. Received 'encoding'", + ); + }, +}); + +Deno.test({ + name: "Async: Data is written to passed in rid", + async fn() { + const tempFile: string = await Deno.makeTempFile(); + const file: Deno.FsFile = await Deno.open(tempFile, { + create: true, + write: true, + read: true, + }); + await new Promise((resolve, reject) => { + appendFile(file.rid, "hello world", (err) => { + if (err) reject(); + else resolve(); + }); + }) + .then(async () => { + const data = await Deno.readFile(tempFile); + assertEquals(decoder.decode(data), "hello world"); + }, () => { + fail("No error expected"); + }) + .finally(async () => { + Deno.close(file.rid); + await Deno.remove(tempFile); + }); + }, +}); + +Deno.test({ + name: "Async: Data is written to passed in file path", + async fn() { + const openResourcesBeforeAppend: Deno.ResourceMap = Deno.resources(); + await new Promise((resolve, reject) => { + appendFile("_fs_appendFile_test_file.txt", "hello world", (err) => { + if (err) reject(err); + else resolve(); + }); + }) + .then(async () => { + assertEquals(Deno.resources(), openResourcesBeforeAppend); + const data = await Deno.readFile("_fs_appendFile_test_file.txt"); + assertEquals(decoder.decode(data), "hello world"); + }, (err) => { + fail("No error was expected: " + err); + }) + .finally(async () => { + await Deno.remove("_fs_appendFile_test_file.txt"); + }); + }, +}); + +Deno.test({ + name: "Async: Data is written to passed in URL", + async fn() { + const openResourcesBeforeAppend: Deno.ResourceMap = Deno.resources(); + const fileURL = new URL("_fs_appendFile_test_file.txt", import.meta.url); + await new Promise((resolve, reject) => { + appendFile(fileURL, "hello world", (err) => { + if (err) reject(err); + else resolve(); + }); + }) + .then(async () => { + assertEquals(Deno.resources(), openResourcesBeforeAppend); + const data = await Deno.readFile(fromFileUrl(fileURL)); + assertEquals(decoder.decode(data), "hello world"); + }, (err) => { + fail("No error was expected: " + err); + }) + .finally(async () => { + await Deno.remove(fromFileUrl(fileURL)); + }); + }, +}); + +Deno.test({ + name: + "Async: Callback is made with error if attempting to append data to an existing file with 'ax' flag", + async fn() { + const openResourcesBeforeAppend: Deno.ResourceMap = Deno.resources(); + const tempFile: string = await Deno.makeTempFile(); + await new Promise((resolve, reject) => { + appendFile(tempFile, "hello world", { flag: "ax" }, (err) => { + if (err) reject(err); + else resolve(); + }); + }) + .then(() => { + fail("Expected error to be thrown"); + }, () => { + assertEquals(Deno.resources(), openResourcesBeforeAppend); + }) + .finally(async () => { + await Deno.remove(tempFile); + }); + }, +}); + +Deno.test({ + name: "Sync: Data is written to passed in rid", + fn() { + const tempFile: string = Deno.makeTempFileSync(); + const file: Deno.FsFile = Deno.openSync(tempFile, { + create: true, + write: true, + read: true, + }); + appendFileSync(file.rid, "hello world"); + Deno.close(file.rid); + const data = Deno.readFileSync(tempFile); + assertEquals(decoder.decode(data), "hello world"); + Deno.removeSync(tempFile); + }, +}); + +Deno.test({ + name: "Sync: Data is written to passed in file path", + fn() { + const openResourcesBeforeAppend: Deno.ResourceMap = Deno.resources(); + appendFileSync("_fs_appendFile_test_file_sync.txt", "hello world"); + assertEquals(Deno.resources(), openResourcesBeforeAppend); + const data = Deno.readFileSync("_fs_appendFile_test_file_sync.txt"); + assertEquals(decoder.decode(data), "hello world"); + Deno.removeSync("_fs_appendFile_test_file_sync.txt"); + }, +}); + +Deno.test({ + name: + "Sync: error thrown if attempting to append data to an existing file with 'ax' flag", + fn() { + const openResourcesBeforeAppend: Deno.ResourceMap = Deno.resources(); + const tempFile: string = Deno.makeTempFileSync(); + assertThrows( + () => appendFileSync(tempFile, "hello world", { flag: "ax" }), + Error, + "", + ); + assertEquals(Deno.resources(), openResourcesBeforeAppend); + Deno.removeSync(tempFile); + }, +}); + +Deno.test({ + name: "Sync: Data is written in Uint8Array to passed in file path", + fn() { + const openResourcesBeforeAppend: Deno.ResourceMap = Deno.resources(); + const testData = new TextEncoder().encode("hello world"); + appendFileSync("_fs_appendFile_test_file_sync.txt", testData); + assertEquals(Deno.resources(), openResourcesBeforeAppend); + const data = Deno.readFileSync("_fs_appendFile_test_file_sync.txt"); + assertEquals(data, testData); + Deno.removeSync("_fs_appendFile_test_file_sync.txt"); + }, +}); + +Deno.test({ + name: "Async: Data is written in Uint8Array to passed in file path", + async fn() { + const openResourcesBeforeAppend: Deno.ResourceMap = Deno.resources(); + const testData = new TextEncoder().encode("hello world"); + await new Promise((resolve, reject) => { + appendFile("_fs_appendFile_test_file.txt", testData, (err) => { + if (err) reject(err); + else resolve(); + }); + }) + .then(async () => { + assertEquals(Deno.resources(), openResourcesBeforeAppend); + const data = await Deno.readFile("_fs_appendFile_test_file.txt"); + assertEquals(data, testData); + }, (err) => { + fail("No error was expected: " + err); + }) + .finally(async () => { + await Deno.remove("_fs_appendFile_test_file.txt"); + }); + }, +}); + +Deno.test("[std/node/fs] appendFile callback isn't called twice if error is thrown", async () => { + const tempFile = await Deno.makeTempFile(); + const importUrl = new URL("node:fs", import.meta.url); + await assertCallbackErrorUncaught({ + prelude: `import { appendFile } from ${JSON.stringify(importUrl)}`, + invocation: `appendFile(${JSON.stringify(tempFile)}, "hello world", `, + async cleanup() { + await Deno.remove(tempFile); + }, + }); +}); diff --git a/cli/tests/unit_node/_fs/_fs_chmod_test.ts b/cli/tests/unit_node/_fs/_fs_chmod_test.ts new file mode 100644 index 0000000000..11f01d1ad6 --- /dev/null +++ b/cli/tests/unit_node/_fs/_fs_chmod_test.ts @@ -0,0 +1,121 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +import { + assert, + assertRejects, + assertThrows, + fail, +} from "../../../../test_util/std/testing/asserts.ts"; +import { assertCallbackErrorUncaught } from "../_test_utils.ts"; +import { chmod, chmodSync } from "node:fs"; + +Deno.test({ + name: "ASYNC: Permissions are changed (non-Windows)", + ignore: Deno.build.os === "windows", + async fn() { + const tempFile: string = await Deno.makeTempFile(); + const originalFileMode: number | null = (await Deno.lstat(tempFile)).mode; + await new Promise((resolve, reject) => { + chmod(tempFile, 0o777, (err) => { + if (err) reject(err); + else resolve(); + }); + }) + .then(() => { + const newFileMode: number | null = Deno.lstatSync(tempFile).mode; + assert(newFileMode && originalFileMode); + assert(newFileMode === 33279 && newFileMode > originalFileMode); + }, (error) => { + fail(error); + }) + .finally(() => { + Deno.removeSync(tempFile); + }); + }, +}); + +Deno.test({ + name: "ASYNC: don't throw NotSupportedError (Windows)", + ignore: Deno.build.os !== "windows", + async fn() { + const tempFile: string = await Deno.makeTempFile(); + await new Promise((resolve, reject) => { + chmod(tempFile, 0o777, (err) => { + if (err) reject(err); + else resolve(); + }); + }).finally(() => { + Deno.removeSync(tempFile); + }); + }, +}); + +Deno.test({ + name: "ASYNC: don't swallow NotFoundError (Windows)", + ignore: Deno.build.os !== "windows", + async fn() { + await assertRejects(async () => { + await new Promise((resolve, reject) => { + chmod("./__non_existent_file__", 0o777, (err) => { + if (err) reject(err); + else resolve(); + }); + }); + }); + }, +}); + +Deno.test({ + name: "SYNC: Permissions are changed (non-Windows)", + ignore: Deno.build.os === "windows", + fn() { + const tempFile: string = Deno.makeTempFileSync(); + try { + const originalFileMode: number | null = Deno.lstatSync(tempFile).mode; + chmodSync(tempFile, "777"); + + const newFileMode: number | null = Deno.lstatSync(tempFile).mode; + assert(newFileMode && originalFileMode); + assert(newFileMode === 33279 && newFileMode > originalFileMode); + } finally { + Deno.removeSync(tempFile); + } + }, +}); + +Deno.test({ + name: "SYNC: don't throw NotSupportedError (Windows)", + ignore: Deno.build.os !== "windows", + fn() { + const tempFile: string = Deno.makeTempFileSync(); + try { + chmodSync(tempFile, "777"); + } finally { + Deno.removeSync(tempFile); + } + }, +}); + +Deno.test({ + name: "SYNC: don't swallow NotFoundError (Windows)", + ignore: Deno.build.os !== "windows", + fn() { + assertThrows(() => { + chmodSync("./__non_existent_file__", "777"); + }); + }, +}); + +Deno.test({ + name: "[std/node/fs] chmod callback isn't called twice if error is thrown", + async fn() { + const tempFile = await Deno.makeTempFile(); + const importUrl = new URL("node:fs", import.meta.url); + await assertCallbackErrorUncaught({ + prelude: `import { chmod } from ${JSON.stringify(importUrl)}`, + invocation: `chmod(${JSON.stringify(tempFile)}, 0o777, `, + async cleanup() { + await Deno.remove(tempFile); + }, + }); + }, +}); diff --git a/cli/tests/unit_node/_fs/_fs_chown_test.ts b/cli/tests/unit_node/_fs/_fs_chown_test.ts new file mode 100644 index 0000000000..2174d49d71 --- /dev/null +++ b/cli/tests/unit_node/_fs/_fs_chown_test.ts @@ -0,0 +1,72 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +import { + assertEquals, + fail, +} from "../../../../test_util/std/testing/asserts.ts"; +import { assertCallbackErrorUncaught } from "../_test_utils.ts"; +import { chown, chownSync } from "node:fs"; + +// chown is difficult to test. Best we can do is set the existing user id/group +// id again +const ignore = Deno.build.os === "windows"; + +Deno.test({ + ignore, + name: "ASYNC: setting existing uid/gid works as expected (non-Windows)", + async fn() { + const tempFile: string = await Deno.makeTempFile(); + const originalUserId: number | null = (await Deno.lstat(tempFile)).uid; + const originalGroupId: number | null = (await Deno.lstat(tempFile)).gid; + await new Promise((resolve, reject) => { + chown(tempFile, originalUserId!, originalGroupId!, (err) => { + if (err) reject(err); + else resolve(); + }); + }) + .then(() => { + const newUserId: number | null = Deno.lstatSync(tempFile).uid; + const newGroupId: number | null = Deno.lstatSync(tempFile).gid; + assertEquals(newUserId, originalUserId); + assertEquals(newGroupId, originalGroupId); + }, () => { + fail(); + }) + .finally(() => { + Deno.removeSync(tempFile); + }); + }, +}); + +Deno.test({ + ignore, + name: "SYNC: setting existing uid/gid works as expected (non-Windows)", + fn() { + const tempFile: string = Deno.makeTempFileSync(); + const originalUserId: number | null = Deno.lstatSync(tempFile).uid; + const originalGroupId: number | null = Deno.lstatSync(tempFile).gid; + chownSync(tempFile, originalUserId!, originalGroupId!); + + const newUserId: number | null = Deno.lstatSync(tempFile).uid; + const newGroupId: number | null = Deno.lstatSync(tempFile).gid; + assertEquals(newUserId, originalUserId); + assertEquals(newGroupId, originalGroupId); + Deno.removeSync(tempFile); + }, +}); + +Deno.test({ + name: "[std/node/fs] chown callback isn't called twice if error is thrown", + ignore: Deno.build.os === "windows", + async fn() { + const tempFile = await Deno.makeTempFile(); + const { uid, gid } = await Deno.lstat(tempFile); + const importUrl = new URL("node:fs", import.meta.url); + await assertCallbackErrorUncaught({ + prelude: `import { chown } from ${JSON.stringify(importUrl)}`, + invocation: `chown(${JSON.stringify(tempFile)}, ${uid}, ${gid}, `, + async cleanup() { + await Deno.remove(tempFile); + }, + }); + }, +}); diff --git a/cli/tests/unit_node/_fs/_fs_close_test.ts b/cli/tests/unit_node/_fs/_fs_close_test.ts new file mode 100644 index 0000000000..4d0743dbd7 --- /dev/null +++ b/cli/tests/unit_node/_fs/_fs_close_test.ts @@ -0,0 +1,97 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +import { + assert, + assertThrows, + fail, +} from "../../../../test_util/std/testing/asserts.ts"; +import { assertCallbackErrorUncaught } from "../_test_utils.ts"; +import { close, closeSync } from "node:fs"; + +Deno.test({ + name: "ASYNC: File is closed", + async fn() { + const tempFile: string = await Deno.makeTempFile(); + const file: Deno.FsFile = await Deno.open(tempFile); + + assert(Deno.resources()[file.rid]); + await new Promise((resolve, reject) => { + close(file.rid, (err) => { + if (err !== null) reject(); + else resolve(); + }); + }) + .then(() => { + assert(!Deno.resources()[file.rid]); + }, () => { + fail("No error expected"); + }) + .finally(async () => { + await Deno.remove(tempFile); + }); + }, +}); + +Deno.test({ + name: "ASYNC: Invalid fd", + fn() { + assertThrows(() => { + close(-1, (_err) => {}); + }, RangeError); + }, +}); + +Deno.test({ + name: "close callback should be asynchronous", + async fn() { + const tempFile: string = Deno.makeTempFileSync(); + const file: Deno.FsFile = Deno.openSync(tempFile); + + let foo: string; + const promise = new Promise((resolve) => { + close(file.rid, () => { + assert(foo === "bar"); + resolve(); + }); + foo = "bar"; + }); + + await promise; + Deno.removeSync(tempFile); + }, +}); + +Deno.test({ + name: "SYNC: File is closed", + fn() { + const tempFile: string = Deno.makeTempFileSync(); + const file: Deno.FsFile = Deno.openSync(tempFile); + + assert(Deno.resources()[file.rid]); + closeSync(file.rid); + assert(!Deno.resources()[file.rid]); + Deno.removeSync(tempFile); + }, +}); + +Deno.test({ + name: "SYNC: Invalid fd", + fn() { + assertThrows(() => closeSync(-1)); + }, +}); + +Deno.test("[std/node/fs] close callback isn't called twice if error is thrown", async () => { + const tempFile = await Deno.makeTempFile(); + const importUrl = new URL("node:fs", import.meta.url); + await assertCallbackErrorUncaught({ + prelude: ` + import { close } from ${JSON.stringify(importUrl)}; + + const file = await Deno.open(${JSON.stringify(tempFile)}); + `, + invocation: "close(file.rid, ", + async cleanup() { + await Deno.remove(tempFile); + }, + }); +}); diff --git a/cli/tests/unit_node/_fs/_fs_copy_test.ts b/cli/tests/unit_node/_fs/_fs_copy_test.ts new file mode 100644 index 0000000000..b08e8d7f3d --- /dev/null +++ b/cli/tests/unit_node/_fs/_fs_copy_test.ts @@ -0,0 +1,52 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +import * as path from "../../../../test_util/std/path/mod.ts"; +import { assert } from "../../../../test_util/std/testing/asserts.ts"; +import { assertCallbackErrorUncaught } from "../_test_utils.ts"; +import { copyFile, copyFileSync, existsSync } from "node:fs"; + +const destFile = "./destination.txt"; + +Deno.test({ + name: "[std/node/fs] copy file", + fn: async () => { + const sourceFile = Deno.makeTempFileSync(); + const err = await new Promise((resolve) => { + copyFile(sourceFile, destFile, (err?: Error | null) => resolve(err)); + }); + assert(!err); + assert(existsSync(destFile)); + Deno.removeSync(sourceFile); + Deno.removeSync(destFile); + }, +}); + +Deno.test({ + name: "[std/node/fs] copy file sync", + fn: () => { + const sourceFile = Deno.makeTempFileSync(); + copyFileSync(sourceFile, destFile); + assert(existsSync(destFile)); + Deno.removeSync(sourceFile); + Deno.removeSync(destFile); + }, +}); + +Deno.test("[std/node/fs] copyFile callback isn't called twice if error is thrown", async () => { + // The correct behaviour is not to catch any errors thrown, + // but that means there'll be an uncaught error and the test will fail. + // So the only way to test this is to spawn a subprocess, and succeed if it has a non-zero exit code. + // (assertRejects won't work because there's no way to catch the error.) + const tempDir = await Deno.makeTempDir(); + const tempFile1 = path.join(tempDir, "file1.txt"); + const tempFile2 = path.join(tempDir, "file2.txt"); + await Deno.writeTextFile(tempFile1, "hello world"); + const importUrl = new URL("node:fs", import.meta.url); + await assertCallbackErrorUncaught({ + prelude: `import { copyFile } from ${JSON.stringify(importUrl)}`, + invocation: `copyFile(${JSON.stringify(tempFile1)}, + ${JSON.stringify(tempFile2)}, `, + async cleanup() { + await Deno.remove(tempDir, { recursive: true }); + }, + }); +}); diff --git a/cli/tests/unit_node/_fs/_fs_exists_test.ts b/cli/tests/unit_node/_fs/_fs_exists_test.ts new file mode 100644 index 0000000000..9af5b32081 --- /dev/null +++ b/cli/tests/unit_node/_fs/_fs_exists_test.ts @@ -0,0 +1,65 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +import { + assert, + assertEquals, + assertStringIncludes, +} from "../../../../test_util/std/testing/asserts.ts"; +import { exists, existsSync } from "node:fs"; +import { promisify } from "node:util"; + +Deno.test("[std/node/fs] exists", async function () { + const availableFile = await new Promise((resolve) => { + const tmpFilePath = Deno.makeTempFileSync(); + exists(tmpFilePath, (exists: boolean) => { + Deno.removeSync(tmpFilePath); + resolve(exists); + }); + }); + const notAvailableFile = await new Promise((resolve) => { + exists("./notAvailable.txt", (exists: boolean) => resolve(exists)); + }); + assertEquals(availableFile, true); + assertEquals(notAvailableFile, false); +}); + +Deno.test("[std/node/fs] existsSync", function () { + const tmpFilePath = Deno.makeTempFileSync(); + assertEquals(existsSync(tmpFilePath), true); + Deno.removeSync(tmpFilePath); + assertEquals(existsSync("./notAvailable.txt"), false); +}); + +Deno.test("[std/node/fs] promisify(exists)", async () => { + const tmpFilePath = await Deno.makeTempFile(); + try { + const existsPromisified = promisify(exists); + assert(await existsPromisified(tmpFilePath)); + assert(!await existsPromisified("./notAvailable.txt")); + } finally { + await Deno.remove(tmpFilePath); + } +}); + +Deno.test("[std/node/fs] exists callback isn't called twice if error is thrown", async () => { + // This doesn't use `assertCallbackErrorUncaught()` because `exists()` doesn't return a standard node callback, which is what it expects. + const tempFile = await Deno.makeTempFile(); + const importUrl = new URL("node:fs", import.meta.url); + const command = new Deno.Command(Deno.execPath(), { + args: [ + "eval", + "--no-check", + ` + import { exists } from ${JSON.stringify(importUrl)}; + + exists(${JSON.stringify(tempFile)}, (exists) => { + // If the bug is present and the callback is called again with false (meaning an error occurred), + // don't throw another error, so if the subprocess fails we know it had the correct behaviour. + if (exists) throw new Error("success"); + });`, + ], + }); + const { success, stderr } = await command.output(); + await Deno.remove(tempFile); + assert(!success); + assertStringIncludes(new TextDecoder().decode(stderr), "Error: success"); +}); diff --git a/cli/tests/unit_node/_fs/_fs_fdatasync_test.ts b/cli/tests/unit_node/_fs/_fs_fdatasync_test.ts new file mode 100644 index 0000000000..d7ccae927f --- /dev/null +++ b/cli/tests/unit_node/_fs/_fs_fdatasync_test.ts @@ -0,0 +1,63 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +import { + assertEquals, + fail, +} from "../../../../test_util/std/testing/asserts.ts"; +import { fdatasync, fdatasyncSync } from "node:fs"; + +Deno.test({ + name: + "ASYNC: flush any pending data operations of the given file stream to disk", + async fn() { + const file: string = await Deno.makeTempFile(); + const { rid } = await Deno.open(file, { + read: true, + write: true, + create: true, + }); + const data = new Uint8Array(64); + await Deno.write(rid, data); + + await new Promise((resolve, reject) => { + fdatasync(rid, (err: Error | null) => { + if (err !== null) reject(); + else resolve(); + }); + }) + .then( + async () => { + assertEquals(await Deno.readFile(file), data); + }, + () => { + fail("No error expected"); + }, + ) + .finally(async () => { + Deno.close(rid); + await Deno.remove(file); + }); + }, +}); + +Deno.test({ + name: + "SYNC: flush any pending data operations of the given file stream to disk.", + fn() { + const file: string = Deno.makeTempFileSync(); + const { rid } = Deno.openSync(file, { + read: true, + write: true, + create: true, + }); + const data = new Uint8Array(64); + Deno.writeSync(rid, data); + + try { + fdatasyncSync(rid); + assertEquals(Deno.readFileSync(file), data); + } finally { + Deno.close(rid); + Deno.removeSync(file); + } + }, +}); diff --git a/cli/tests/unit_node/_fs/_fs_fstat_test.ts b/cli/tests/unit_node/_fs/_fs_fstat_test.ts new file mode 100644 index 0000000000..70c3db254a --- /dev/null +++ b/cli/tests/unit_node/_fs/_fs_fstat_test.ts @@ -0,0 +1,83 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +import { fstat, fstatSync } from "node:fs"; +import { fail } from "../../../../test_util/std/testing/asserts.ts"; +import { assertStats, assertStatsBigInt } from "./_fs_stat_test.ts"; +import type { BigIntStats, Stats } from "node:fs"; + +Deno.test({ + name: "ASYNC: get a file Stats", + async fn() { + const file = await Deno.makeTempFile(); + const { rid } = await Deno.open(file); + + await new Promise((resolve, reject) => { + fstat(rid, (err: Error | null, stat: Stats) => { + if (err) reject(err); + resolve(stat); + }); + }) + .then( + (stat) => { + assertStats(stat, Deno.fstatSync(rid)); + }, + () => fail(), + ) + .finally(() => { + Deno.removeSync(file); + Deno.close(rid); + }); + }, +}); + +Deno.test({ + name: "ASYNC: get a file BigInt Stats", + async fn() { + const file = await Deno.makeTempFile(); + const { rid } = await Deno.open(file); + + await new Promise((resolve, reject) => { + fstat(rid, { bigint: true }, (err: Error | null, stat: BigIntStats) => { + if (err) reject(err); + resolve(stat); + }); + }) + .then( + (stat) => assertStatsBigInt(stat, Deno.fstatSync(rid)), + () => fail(), + ) + .finally(() => { + Deno.removeSync(file); + Deno.close(rid); + }); + }, +}); + +Deno.test({ + name: "SYNC: get a file Stats", + fn() { + const file = Deno.makeTempFileSync(); + const { rid } = Deno.openSync(file); + + try { + assertStats(fstatSync(rid), Deno.fstatSync(rid)); + } finally { + Deno.removeSync(file); + Deno.close(rid); + } + }, +}); + +Deno.test({ + name: "SYNC: get a file BigInt Stats", + fn() { + const file = Deno.makeTempFileSync(); + const { rid } = Deno.openSync(file); + + try { + assertStatsBigInt(fstatSync(rid, { bigint: true }), Deno.fstatSync(rid)); + } finally { + Deno.removeSync(file); + Deno.close(rid); + } + }, +}); diff --git a/cli/tests/unit_node/_fs/_fs_fsync_test.ts b/cli/tests/unit_node/_fs/_fs_fsync_test.ts new file mode 100644 index 0000000000..586c7d7e6c --- /dev/null +++ b/cli/tests/unit_node/_fs/_fs_fsync_test.ts @@ -0,0 +1,61 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +import { + assertEquals, + fail, +} from "../../../../test_util/std/testing/asserts.ts"; +import { fsync, fsyncSync } from "node:fs"; + +Deno.test({ + name: "ASYNC: flush any pending data of the given file stream to disk", + async fn() { + const file: string = await Deno.makeTempFile(); + const { rid } = await Deno.open(file, { + read: true, + write: true, + create: true, + }); + const size = 64; + await Deno.ftruncate(rid, size); + + await new Promise((resolve, reject) => { + fsync(rid, (err: Error | null) => { + if (err !== null) reject(); + else resolve(); + }); + }) + .then( + async () => { + assertEquals((await Deno.stat(file)).size, size); + }, + () => { + fail("No error expected"); + }, + ) + .finally(async () => { + await Deno.remove(file); + Deno.close(rid); + }); + }, +}); + +Deno.test({ + name: "SYNC: flush any pending data the given file stream to disk", + fn() { + const file: string = Deno.makeTempFileSync(); + const { rid } = Deno.openSync(file, { + read: true, + write: true, + create: true, + }); + const size = 64; + Deno.ftruncateSync(rid, size); + + try { + fsyncSync(rid); + assertEquals(Deno.statSync(file).size, size); + } finally { + Deno.removeSync(file); + Deno.close(rid); + } + }, +}); diff --git a/cli/tests/unit_node/_fs/_fs_ftruncate_test.ts b/cli/tests/unit_node/_fs/_fs_ftruncate_test.ts new file mode 100644 index 0000000000..46787bec13 --- /dev/null +++ b/cli/tests/unit_node/_fs/_fs_ftruncate_test.ts @@ -0,0 +1,131 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +import { + assertEquals, + assertThrows, + fail, +} from "../../../../test_util/std/testing/asserts.ts"; +import { ftruncate, ftruncateSync } from "node:fs"; + +Deno.test({ + name: "ASYNC: no callback function results in Error", + fn() { + assertThrows( + () => { + // @ts-expect-error Argument of type 'number' is not assignable to parameter of type 'NoParamCallback' + ftruncate(123, 0); + }, + Error, + "No callback function supplied", + ); + }, +}); + +Deno.test({ + name: "ASYNC: truncate entire file contents", + async fn() { + const file: string = Deno.makeTempFileSync(); + await Deno.writeFile(file, new TextEncoder().encode("hello world")); + const { rid } = await Deno.open(file, { + read: true, + write: true, + create: true, + }); + + await new Promise((resolve, reject) => { + ftruncate(rid, (err: Error | null) => { + if (err !== null) reject(); + else resolve(); + }); + }) + .then( + () => { + const fileInfo: Deno.FileInfo = Deno.lstatSync(file); + assertEquals(fileInfo.size, 0); + }, + () => { + fail("No error expected"); + }, + ) + .finally(() => { + Deno.removeSync(file); + Deno.close(rid); + }); + }, +}); + +Deno.test({ + name: "ASYNC: truncate file to a size of precisely len bytes", + async fn() { + const file: string = Deno.makeTempFileSync(); + await Deno.writeFile(file, new TextEncoder().encode("hello world")); + const { rid } = await Deno.open(file, { + read: true, + write: true, + create: true, + }); + + await new Promise((resolve, reject) => { + ftruncate(rid, 3, (err: Error | null) => { + if (err !== null) reject(); + else resolve(); + }); + }) + .then( + () => { + const fileInfo: Deno.FileInfo = Deno.lstatSync(file); + assertEquals(fileInfo.size, 3); + }, + () => { + fail("No error expected"); + }, + ) + .finally(() => { + Deno.removeSync(file); + Deno.close(rid); + }); + }, +}); + +Deno.test({ + name: "SYNC: truncate entire file contents", + fn() { + const file: string = Deno.makeTempFileSync(); + Deno.writeFileSync(file, new TextEncoder().encode("hello world")); + const { rid } = Deno.openSync(file, { + read: true, + write: true, + create: true, + }); + + try { + ftruncateSync(rid); + const fileInfo: Deno.FileInfo = Deno.lstatSync(file); + assertEquals(fileInfo.size, 0); + } finally { + Deno.removeSync(file); + Deno.close(rid); + } + }, +}); + +Deno.test({ + name: "SYNC: truncate file to a size of precisely len bytes", + fn() { + const file: string = Deno.makeTempFileSync(); + Deno.writeFileSync(file, new TextEncoder().encode("hello world")); + const { rid } = Deno.openSync(file, { + read: true, + write: true, + create: true, + }); + + try { + ftruncateSync(rid, 3); + const fileInfo: Deno.FileInfo = Deno.lstatSync(file); + assertEquals(fileInfo.size, 3); + } finally { + Deno.removeSync(file); + Deno.close(rid); + } + }, +}); diff --git a/cli/tests/unit_node/_fs/_fs_futimes_test.ts b/cli/tests/unit_node/_fs/_fs_futimes_test.ts new file mode 100644 index 0000000000..c5b9012a53 --- /dev/null +++ b/cli/tests/unit_node/_fs/_fs_futimes_test.ts @@ -0,0 +1,112 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +import { + assertEquals, + assertThrows, + fail, +} from "../../../../test_util/std/testing/asserts.ts"; +import { futimes, futimesSync } from "node:fs"; + +const randomDate = new Date(Date.now() + 1000); + +Deno.test({ + name: + "ASYNC: change the file system timestamps of the object referenced by path", + async fn() { + const file: string = Deno.makeTempFileSync(); + const { rid } = await Deno.open(file, { create: true, write: true }); + + await new Promise((resolve, reject) => { + futimes(rid, randomDate, randomDate, (err: Error | null) => { + if (err !== null) reject(); + else resolve(); + }); + }) + .then( + () => { + const fileInfo: Deno.FileInfo = Deno.lstatSync(file); + assertEquals(fileInfo.mtime, randomDate); + assertEquals(fileInfo.atime, randomDate); + }, + () => { + fail("No error expected"); + }, + ) + .finally(() => { + Deno.removeSync(file); + Deno.close(rid); + }); + }, +}); + +Deno.test({ + name: "ASYNC: should throw error if atime is infinity", + fn() { + assertThrows( + () => { + futimes(123, Infinity, 0, (_err: Error | null) => {}); + }, + Error, + "invalid atime, must not be infinity or NaN", + ); + }, +}); + +Deno.test({ + name: "ASYNC: should throw error if atime is NaN", + fn() { + assertThrows( + () => { + futimes(123, "some string", 0, (_err: Error | null) => {}); + }, + Error, + "invalid atime, must not be infinity or NaN", + ); + }, +}); + +Deno.test({ + name: + "SYNC: change the file system timestamps of the object referenced by path", + fn() { + const file: string = Deno.makeTempFileSync(); + const { rid } = Deno.openSync(file, { create: true, write: true }); + + try { + futimesSync(rid, randomDate, randomDate); + + const fileInfo: Deno.FileInfo = Deno.lstatSync(file); + + assertEquals(fileInfo.mtime, randomDate); + assertEquals(fileInfo.atime, randomDate); + } finally { + Deno.removeSync(file); + Deno.close(rid); + } + }, +}); + +Deno.test({ + name: "SYNC: should throw error if atime is NaN", + fn() { + assertThrows( + () => { + futimesSync(123, "some string", 0); + }, + Error, + "invalid atime, must not be infinity or NaN", + ); + }, +}); + +Deno.test({ + name: "SYNC: should throw error if atime is Infinity", + fn() { + assertThrows( + () => { + futimesSync(123, Infinity, 0); + }, + Error, + "invalid atime, must not be infinity or NaN", + ); + }, +}); diff --git a/cli/tests/unit_node/_fs/_fs_link_test.ts b/cli/tests/unit_node/_fs/_fs_link_test.ts new file mode 100644 index 0000000000..a96457eb06 --- /dev/null +++ b/cli/tests/unit_node/_fs/_fs_link_test.ts @@ -0,0 +1,81 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +import * as path from "../../../../test_util/std/path/mod.ts"; +import { + assert, + assertEquals, + fail, +} from "../../../../test_util/std/testing/asserts.ts"; +import { assertCallbackErrorUncaught } from "../_test_utils.ts"; +import { link, linkSync } from "node:fs"; + +Deno.test({ + name: "ASYNC: hard linking files works as expected", + async fn() { + const tempFile: string = await Deno.makeTempFile(); + const linkedFile: string = tempFile + ".link"; + await new Promise((res, rej) => { + link(tempFile, linkedFile, (err) => { + if (err) rej(err); + else res(); + }); + }) + .then(() => { + assertEquals(Deno.statSync(tempFile), Deno.statSync(linkedFile)); + }, () => { + fail("Expected to succeed"); + }) + .finally(() => { + Deno.removeSync(tempFile); + Deno.removeSync(linkedFile); + }); + }, +}); + +Deno.test({ + name: "ASYNC: hard linking files passes error to callback", + async fn() { + let failed = false; + await new Promise((res, rej) => { + link("no-such-file", "no-such-file", (err) => { + if (err) rej(err); + else res(); + }); + }) + .then(() => { + fail("Expected to succeed"); + }, (err) => { + assert(err); + failed = true; + }); + assert(failed); + }, +}); + +Deno.test({ + name: "SYNC: hard linking files works as expected", + fn() { + const tempFile: string = Deno.makeTempFileSync(); + const linkedFile: string = tempFile + ".link"; + linkSync(tempFile, linkedFile); + + assertEquals(Deno.statSync(tempFile), Deno.statSync(linkedFile)); + Deno.removeSync(tempFile); + Deno.removeSync(linkedFile); + }, +}); + +Deno.test("[std/node/fs] link callback isn't called twice if error is thrown", async () => { + const tempDir = await Deno.makeTempDir(); + const tempFile = path.join(tempDir, "file.txt"); + const linkFile = path.join(tempDir, "link.txt"); + await Deno.writeTextFile(tempFile, "hello world"); + const importUrl = new URL("node:fs", import.meta.url); + await assertCallbackErrorUncaught({ + prelude: `import { link } from ${JSON.stringify(importUrl)}`, + invocation: `link(${JSON.stringify(tempFile)}, + ${JSON.stringify(linkFile)}, `, + async cleanup() { + await Deno.remove(tempDir, { recursive: true }); + }, + }); +}); diff --git a/cli/tests/unit_node/_fs/_fs_lstat_test.ts b/cli/tests/unit_node/_fs/_fs_lstat_test.ts new file mode 100644 index 0000000000..cd11279898 --- /dev/null +++ b/cli/tests/unit_node/_fs/_fs_lstat_test.ts @@ -0,0 +1,71 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +import { lstat, lstatSync } from "node:fs"; +import { fail } from "../../../../test_util/std/testing/asserts.ts"; +import { assertCallbackErrorUncaught } from "../_test_utils.ts"; +import { assertStats, assertStatsBigInt } from "./_fs_stat_test.ts"; +import type { BigIntStats, Stats } from "node:fs"; + +Deno.test({ + name: "ASYNC: get a file Stats (lstat)", + async fn() { + const file = Deno.makeTempFileSync(); + await new Promise((resolve, reject) => { + lstat(file, (err, stat) => { + if (err) reject(err); + resolve(stat); + }); + }) + .then((stat) => { + assertStats(stat, Deno.lstatSync(file)); + }, () => fail()) + .finally(() => { + Deno.removeSync(file); + }); + }, +}); + +Deno.test({ + name: "SYNC: get a file Stats (lstat)", + fn() { + const file = Deno.makeTempFileSync(); + assertStats(lstatSync(file), Deno.lstatSync(file)); + }, +}); + +Deno.test({ + name: "ASYNC: get a file BigInt Stats (lstat)", + async fn() { + const file = Deno.makeTempFileSync(); + await new Promise((resolve, reject) => { + lstat(file, { bigint: true }, (err, stat) => { + if (err) reject(err); + resolve(stat); + }); + }) + .then( + (stat) => assertStatsBigInt(stat, Deno.lstatSync(file)), + () => fail(), + ) + .finally(() => Deno.removeSync(file)); + }, +}); + +Deno.test({ + name: "SYNC: BigInt Stats (lstat)", + fn() { + const file = Deno.makeTempFileSync(); + assertStatsBigInt(lstatSync(file, { bigint: true }), Deno.lstatSync(file)); + }, +}); + +Deno.test("[std/node/fs] lstat callback isn't called twice if error is thrown", async () => { + const tempFile = await Deno.makeTempFile(); + const importUrl = new URL("node:fs", import.meta.url); + await assertCallbackErrorUncaught({ + prelude: `import { lstat } from ${JSON.stringify(importUrl)}`, + invocation: `lstat(${JSON.stringify(tempFile)}, `, + async cleanup() { + await Deno.remove(tempFile); + }, + }); +}); diff --git a/cli/tests/unit_node/_fs/_fs_mkdir_test.ts b/cli/tests/unit_node/_fs/_fs_mkdir_test.ts new file mode 100644 index 0000000000..0deae9276d --- /dev/null +++ b/cli/tests/unit_node/_fs/_fs_mkdir_test.ts @@ -0,0 +1,43 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +import * as path from "../../../../test_util/std/path/mod.ts"; +import { assert } from "../../../../test_util/std/testing/asserts.ts"; +import { assertCallbackErrorUncaught } from "../_test_utils.ts"; +import { existsSync, mkdir, mkdirSync } from "node:fs"; + +const tmpDir = "./tmpdir"; + +Deno.test({ + name: "[node/fs] mkdir", + fn: async () => { + const result = await new Promise((resolve) => { + mkdir(tmpDir, (err) => { + err && resolve(false); + resolve(existsSync(tmpDir)); + Deno.removeSync(tmpDir); + }); + }); + assert(result); + }, +}); + +Deno.test({ + name: "[node/fs] mkdirSync", + fn: () => { + mkdirSync(tmpDir); + assert(existsSync(tmpDir)); + Deno.removeSync(tmpDir); + }, +}); + +Deno.test("[std/node/fs] mkdir callback isn't called twice if error is thrown", async () => { + const tempDir = await Deno.makeTempDir(); + const subdir = path.join(tempDir, "subdir"); + const importUrl = new URL("node:fs", import.meta.url); + await assertCallbackErrorUncaught({ + prelude: `import { mkdir } from ${JSON.stringify(importUrl)}`, + invocation: `mkdir(${JSON.stringify(subdir)}, `, + async cleanup() { + await Deno.remove(tempDir, { recursive: true }); + }, + }); +}); diff --git a/cli/tests/unit_node/_fs/_fs_mkdtemp_test.ts b/cli/tests/unit_node/_fs/_fs_mkdtemp_test.ts new file mode 100644 index 0000000000..998128596d --- /dev/null +++ b/cli/tests/unit_node/_fs/_fs_mkdtemp_test.ts @@ -0,0 +1,86 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +import { + assert, + assertRejects, + assertThrows, +} from "../../../../test_util/std/testing/asserts.ts"; +import { EncodingOption, existsSync, mkdtemp, mkdtempSync } from "node:fs"; +import { env } from "node:process"; +import { promisify } from "node:util"; + +const prefix = Deno.build.os === "windows" + ? env.TEMP + "\\" + : (env.TMPDIR || "/tmp") + "/"; +const doesNotExists = "/does/not/exists/"; +const options: EncodingOption = { encoding: "ascii" }; +const badOptions = { encoding: "bogus" }; + +const mkdtempP = promisify(mkdtemp); + +Deno.test({ + name: "[node/fs] mkdtemp", + fn: async () => { + const directory = await mkdtempP(prefix); + assert(existsSync(directory)); + Deno.removeSync(directory); + }, +}); + +Deno.test({ + name: "[node/fs] mkdtemp (does not exists)", + fn: async () => { + await assertRejects(() => mkdtempP(doesNotExists)); + }, +}); + +Deno.test({ + name: "[node/fs] mkdtemp (with options)", + fn: async () => { + const directory = await mkdtempP(prefix, options); + assert(existsSync(directory)); + Deno.removeSync(directory); + }, +}); + +Deno.test({ + name: "[node/fs] mkdtemp (with bad options)", + fn: async () => { + // @ts-expect-error No overload matches this call + await assertRejects(() => mkdtempP(prefix, badOptions)); + }, +}); + +Deno.test({ + name: "[node/fs] mkdtempSync", + fn: () => { + const directory = mkdtempSync(prefix); + const dirExists = existsSync(directory); + Deno.removeSync(directory); + assert(dirExists); + }, +}); + +Deno.test({ + name: "[node/fs] mkdtempSync (does not exists)", + fn: () => { + assertThrows(() => mkdtempSync(doesNotExists)); + }, +}); + +Deno.test({ + name: "[node/fs] mkdtempSync (with options)", + fn: () => { + const directory = mkdtempSync(prefix, options); + const dirExists = existsSync(directory); + Deno.removeSync(directory); + assert(dirExists); + }, +}); + +Deno.test({ + name: "[node/fs] mkdtempSync (with bad options)", + fn: () => { + // @ts-expect-error No overload matches this call + assertThrows(() => mkdtempSync(prefix, badOptions)); + }, +}); diff --git a/cli/tests/unit_node/_fs/_fs_opendir_test.ts b/cli/tests/unit_node/_fs/_fs_opendir_test.ts new file mode 100644 index 0000000000..196bea7778 --- /dev/null +++ b/cli/tests/unit_node/_fs/_fs_opendir_test.ts @@ -0,0 +1,146 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +import { + assert, + assertEquals, + assertFalse, + assertInstanceOf, + assertThrows, +} from "../../../../test_util/std/testing/asserts.ts"; +import { opendir, opendirSync } from "node:fs"; +import { Buffer } from "node:buffer"; +import { assertCallbackErrorUncaught } from "../_test_utils.ts"; + +Deno.test("[node/fs] opendir()", async (t) => { + const path = await Deno.makeTempDir(); + const file = await Deno.makeTempFile(); + + await t.step( + "fails if encoding is invalid", + () => + opendir( + path, + // @ts-expect-error Type '"invalid-encoding"' is not assignable to type 'BufferEncoding | undefined' + { encoding: "invalid-encoding" }, + (err) => assertInstanceOf(err, TypeError), + ), + ); + + await t.step( + "fails if bufferSize is invalid", + () => + opendir( + path, + { bufferSize: -1 }, + (err) => assertInstanceOf(err, RangeError), + ), + ); + + await t.step( + "fails if directory does not exist", + () => + opendir( + "directory-that-does-not-exist", + (err) => assertInstanceOf(err, Error), + ), + ); + + await t.step( + "fails if not a directory", + () => + opendir( + file, + (err) => assertInstanceOf(err, Error), + ), + ); + + await t.step( + "passes if path is a string", + () => + opendir( + path, + (err, dir) => { + assertEquals(err, null); + assert(dir); + }, + ), + ); + + await t.step( + "passes if path is a Buffer", + () => + opendir( + Buffer.from(path), + (err, dir) => { + assertFalse(err); + assert(dir); + }, + ), + ); + + await t.step( + "passes if path is a URL", + () => + opendir( + new URL(`file://` + path), + (err, dir) => { + assertFalse(err); + assert(dir); + }, + ), + ); + + await t.step("passes if callback isn't called twice", async () => { + const importUrl = new URL("node:fs", import.meta.url); + await assertCallbackErrorUncaught({ + prelude: `import { opendir } from ${JSON.stringify(importUrl)}`, + invocation: `opendir(${JSON.stringify(path)}, `, + }); + }); + + await Deno.remove(path); + await Deno.remove(file); +}); + +Deno.test("[node/fs] opendirSync()", async (t) => { + const path = await Deno.makeTempDir(); + const file = await Deno.makeTempFile(); + + await t.step("fails if encoding is invalid", () => { + assertThrows( + // @ts-expect-error Type '"invalid-encoding"' is not assignable to type 'BufferEncoding | undefined' + () => opendirSync(path, { encoding: "invalid-encoding" }), + TypeError, + ); + }); + + await t.step("fails if bufferSize is invalid", () => { + assertThrows( + () => opendirSync(path, { bufferSize: -1 }), + RangeError, + ); + }); + + await t.step("fails if directory does not exist", () => { + assertThrows(() => opendirSync("directory-that-does-not-exist")); + }); + + await t.step("fails if not a directory", () => { + assertThrows(() => opendirSync(file)); + }); + + await t.step("passes if path is a string", () => { + assert(opendirSync(path)); + }); + + await t.step("passes if path is a Buffer", () => { + assert(opendirSync(Buffer.from(path))); + }); + + await t.step("passes if path is a URL", () => { + assert(opendirSync(new URL(`file://` + path))); + }); + + await Deno.remove(path); + await Deno.remove(file); +}); diff --git a/cli/tests/unit_node/_fs/_fs_readFile_test.ts b/cli/tests/unit_node/_fs/_fs_readFile_test.ts new file mode 100644 index 0000000000..8fa9e7b010 --- /dev/null +++ b/cli/tests/unit_node/_fs/_fs_readFile_test.ts @@ -0,0 +1,121 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +import { assertCallbackErrorUncaught } from "../_test_utils.ts"; +import { readFile, readFileSync } from "node:fs"; +import * as path from "../../../../test_util/std/path/mod.ts"; +import { + assert, + assertEquals, +} from "../../../../test_util/std/testing/asserts.ts"; + +const moduleDir = path.dirname(path.fromFileUrl(import.meta.url)); +const testData = path.resolve(moduleDir, "testdata", "hello.txt"); + +Deno.test("readFileSuccess", async function () { + const data = await new Promise((res, rej) => { + readFile(testData, (err, data) => { + if (err) { + rej(err); + } + res(data); + }); + }); + + assert(data instanceof Uint8Array); + assertEquals(new TextDecoder().decode(data as Uint8Array), "hello world"); +}); + +Deno.test("readFileEncodeUtf8Success", async function () { + const data = await new Promise((res, rej) => { + readFile(testData, { encoding: "utf8" }, (err, data) => { + if (err) { + rej(err); + } + res(data); + }); + }); + assertEquals(typeof data, "string"); + assertEquals(data as string, "hello world"); +}); + +Deno.test("readFileEncodeHexSuccess", async function () { + const data = await new Promise((res, rej) => { + readFile(testData, { encoding: "hex" }, (err, data) => { + if (err) { + rej(err); + } + res(data); + }); + }); + + assertEquals(typeof data, "string"); + assertEquals(data as string, "68656c6c6f20776f726c64"); +}); + +Deno.test("readFileEncodeBase64Success", async function () { + const data = await new Promise((res, rej) => { + readFile(testData, { encoding: "base64" }, (err, data) => { + if (err) { + rej(err); + } + res(data); + }); + }); + assertEquals(typeof data, "string"); + assertEquals(data as string, "aGVsbG8gd29ybGQ="); +}); + +Deno.test("readFileEncodingAsString", async function () { + const data = await new Promise((res, rej) => { + readFile(testData, "utf8", (err, data) => { + if (err) { + rej(err); + } + res(data); + }); + }); + + assertEquals(typeof data, "string"); + assertEquals(data as string, "hello world"); +}); + +Deno.test("readFileSyncSuccess", function () { + const data = readFileSync(testData); + assert(data instanceof Uint8Array); + assertEquals(new TextDecoder().decode(data as Uint8Array), "hello world"); +}); + +Deno.test("readFileEncodeUtf8Success", function () { + const data = readFileSync(testData, { encoding: "utf8" }); + assertEquals(typeof data, "string"); + assertEquals(data as string, "hello world"); +}); + +Deno.test("readFileEncodeHexSuccess", function () { + const data = readFileSync(testData, { encoding: "hex" }); + assertEquals(typeof data, "string"); + assertEquals(data as string, "68656c6c6f20776f726c64"); +}); + +Deno.test("readFileEncodeBase64Success", function () { + const data = readFileSync(testData, { encoding: "base64" }); + assertEquals(typeof data, "string"); + assertEquals(data as string, "aGVsbG8gd29ybGQ="); +}); + +Deno.test("readFileEncodeAsString", function () { + const data = readFileSync(testData, "utf8"); + assertEquals(typeof data, "string"); + assertEquals(data as string, "hello world"); +}); + +Deno.test("[std/node/fs] readFile callback isn't called twice if error is thrown", async () => { + const tempFile = await Deno.makeTempFile(); + const importUrl = new URL("node:fs", import.meta.url); + await assertCallbackErrorUncaught({ + prelude: `import { readFile } from ${JSON.stringify(importUrl)}`, + invocation: `readFile(${JSON.stringify(tempFile)}, `, + async cleanup() { + await Deno.remove(tempFile); + }, + }); +}); diff --git a/cli/tests/unit_node/_fs/_fs_readdir_test.ts b/cli/tests/unit_node/_fs/_fs_readdir_test.ts new file mode 100644 index 0000000000..bb1df079f9 --- /dev/null +++ b/cli/tests/unit_node/_fs/_fs_readdir_test.ts @@ -0,0 +1,96 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +import { + assertEquals, + assertNotEquals, + fail, +} from "../../../../test_util/std/testing/asserts.ts"; +import { assertCallbackErrorUncaught } from "../_test_utils.ts"; +import { readdir, readdirSync } from "node:fs"; +import { join } from "../../../../test_util/std/path/mod.ts"; + +Deno.test({ + name: "ASYNC: reading empty directory", + async fn() { + const dir = Deno.makeTempDirSync(); + await new Promise((resolve, reject) => { + readdir(dir, (err, files) => { + if (err) reject(err); + resolve(files); + }); + }) + .then((files) => assertEquals(files, []), () => fail()) + .finally(() => Deno.removeSync(dir)); + }, +}); + +function assertEqualsArrayAnyOrder(actual: T[], expected: T[]) { + assertEquals(actual.length, expected.length); + for (const item of expected) { + const index = actual.indexOf(item); + assertNotEquals(index, -1); + expected = expected.splice(index, 1); + } +} + +Deno.test({ + name: "ASYNC: reading non-empty directory", + async fn() { + const dir = Deno.makeTempDirSync(); + Deno.writeTextFileSync(join(dir, "file1.txt"), "hi"); + Deno.writeTextFileSync(join(dir, "file2.txt"), "hi"); + Deno.mkdirSync(join(dir, "some_dir")); + await new Promise((resolve, reject) => { + readdir(dir, (err, files) => { + if (err) reject(err); + resolve(files); + }); + }) + .then( + (files) => + assertEqualsArrayAnyOrder( + files, + ["file1.txt", "some_dir", "file2.txt"], + ), + () => fail(), + ) + .finally(() => Deno.removeSync(dir, { recursive: true })); + }, +}); + +Deno.test({ + name: "SYNC: reading empty the directory", + fn() { + const dir = Deno.makeTempDirSync(); + assertEquals(readdirSync(dir), []); + }, +}); + +Deno.test({ + name: "SYNC: reading non-empty directory", + fn() { + const dir = Deno.makeTempDirSync(); + Deno.writeTextFileSync(join(dir, "file1.txt"), "hi"); + Deno.writeTextFileSync(join(dir, "file2.txt"), "hi"); + Deno.mkdirSync(join(dir, "some_dir")); + assertEqualsArrayAnyOrder( + readdirSync(dir), + ["file1.txt", "some_dir", "file2.txt"], + ); + }, +}); + +Deno.test("[std/node/fs] readdir callback isn't called twice if error is thrown", async () => { + // The correct behaviour is not to catch any errors thrown, + // but that means there'll be an uncaught error and the test will fail. + // So the only way to test this is to spawn a subprocess, and succeed if it has a non-zero exit code. + // (assertRejects won't work because there's no way to catch the error.) + const tempDir = await Deno.makeTempDir(); + const importUrl = new URL("node:fs", import.meta.url); + await assertCallbackErrorUncaught({ + prelude: `import { readdir } from ${JSON.stringify(importUrl)}`, + invocation: `readdir(${JSON.stringify(tempDir)}, `, + async cleanup() { + await Deno.remove(tempDir); + }, + }); +}); diff --git a/cli/tests/unit_node/_fs/_fs_readlink_test.ts b/cli/tests/unit_node/_fs/_fs_readlink_test.ts new file mode 100644 index 0000000000..7f0d3cef83 --- /dev/null +++ b/cli/tests/unit_node/_fs/_fs_readlink_test.ts @@ -0,0 +1,78 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +import { assertCallbackErrorUncaught } from "../_test_utils.ts"; +import { readlink, readlinkSync } from "node:fs"; +import { + assert, + assertEquals, +} from "../../../../test_util/std/testing/asserts.ts"; +import * as path from "../../../../test_util/std/path/mod.ts"; + +const testDir = Deno.makeTempDirSync(); +const oldname = path.join(testDir, "oldname"); +const newname = path.join(testDir, "newname"); + +if (Deno.build.os === "windows") { + Deno.symlinkSync(oldname, newname, { type: "file" }); +} else { + Deno.symlinkSync(oldname, newname); +} + +Deno.test({ + name: "readlinkSuccess", + async fn() { + const data = await new Promise((res, rej) => { + readlink(newname, (err, data) => { + if (err) { + rej(err); + } + res(data); + }); + }); + + assertEquals(typeof data, "string"); + assertEquals(data as string, oldname); + }, +}); + +Deno.test({ + name: "readlinkEncodeBufferSuccess", + async fn() { + const data = await new Promise((res, rej) => { + readlink(newname, { encoding: "buffer" }, (err, data) => { + if (err) { + rej(err); + } + res(data); + }); + }); + + assert(data instanceof Uint8Array); + assertEquals(new TextDecoder().decode(data as Uint8Array), oldname); + }, +}); + +Deno.test({ + name: "readlinkSyncSuccess", + fn() { + const data = readlinkSync(newname); + assertEquals(typeof data, "string"); + assertEquals(data as string, oldname); + }, +}); + +Deno.test({ + name: "readlinkEncodeBufferSuccess", + fn() { + const data = readlinkSync(newname, { encoding: "buffer" }); + assert(data instanceof Uint8Array); + assertEquals(new TextDecoder().decode(data as Uint8Array), oldname); + }, +}); + +Deno.test("[std/node/fs] readlink callback isn't called twice if error is thrown", async () => { + const importUrl = new URL("node:fs", import.meta.url); + await assertCallbackErrorUncaught({ + prelude: `import { readlink } from ${JSON.stringify(importUrl)}`, + invocation: `readlink(${JSON.stringify(newname)}, `, + }); +}); diff --git a/cli/tests/unit_node/_fs/_fs_realpath_test.ts b/cli/tests/unit_node/_fs/_fs_realpath_test.ts new file mode 100644 index 0000000000..871ebe9836 --- /dev/null +++ b/cli/tests/unit_node/_fs/_fs_realpath_test.ts @@ -0,0 +1,55 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +import * as path from "../../../../test_util/std/path/mod.ts"; +import { assertEquals } from "../../../../test_util/std/testing/asserts.ts"; +import { assertCallbackErrorUncaught } from "../_test_utils.ts"; +import { realpath, realpathSync } from "node:fs"; + +Deno.test("realpath", async function () { + const tempFile = await Deno.makeTempFile(); + const tempFileAlias = tempFile + ".alias"; + await Deno.symlink(tempFile, tempFileAlias); + const realPath = await new Promise((resolve, reject) => { + realpath(tempFile, (err, path) => { + if (err) { + reject(err); + return; + } + resolve(path); + }); + }); + const realSymLinkPath = await new Promise((resolve, reject) => { + realpath(tempFileAlias, (err, path) => { + if (err) { + reject(err); + return; + } + resolve(path); + }); + }); + assertEquals(realPath, realSymLinkPath); +}); + +Deno.test("realpathSync", function () { + const tempFile = Deno.makeTempFileSync(); + const tempFileAlias = tempFile + ".alias"; + Deno.symlinkSync(tempFile, tempFileAlias); + const realPath = realpathSync(tempFile); + const realSymLinkPath = realpathSync(tempFileAlias); + assertEquals(realPath, realSymLinkPath); +}); + +Deno.test("[std/node/fs] realpath callback isn't called twice if error is thrown", async () => { + const tempDir = await Deno.makeTempDir(); + const tempFile = path.join(tempDir, "file.txt"); + const linkFile = path.join(tempDir, "link.txt"); + await Deno.writeTextFile(tempFile, "hello world"); + await Deno.symlink(tempFile, linkFile, { type: "file" }); + const importUrl = new URL("node:fs", import.meta.url); + await assertCallbackErrorUncaught({ + prelude: `import { realpath } from ${JSON.stringify(importUrl)}`, + invocation: `realpath(${JSON.stringify(`${tempDir}/link.txt`)}, `, + async cleanup() { + await Deno.remove(tempDir, { recursive: true }); + }, + }); +}); diff --git a/cli/tests/unit_node/_fs/_fs_rename_test.ts b/cli/tests/unit_node/_fs/_fs_rename_test.ts new file mode 100644 index 0000000000..cefd7f61ad --- /dev/null +++ b/cli/tests/unit_node/_fs/_fs_rename_test.ts @@ -0,0 +1,55 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +import { + assertEquals, + fail, +} from "../../../../test_util/std/testing/asserts.ts"; +import { assertCallbackErrorUncaught } from "../_test_utils.ts"; +import { rename, renameSync } from "node:fs"; +import { existsSync } from "node:fs"; +import { join, parse } from "../../../../test_util/std/path/mod.ts"; + +Deno.test({ + name: "ASYNC: renaming a file", + async fn() { + const file = Deno.makeTempFileSync(); + const newPath = join(parse(file).dir, `${parse(file).base}_renamed`); + await new Promise((resolve, reject) => { + rename(file, newPath, (err) => { + if (err) reject(err); + resolve(); + }); + }) + .then(() => { + assertEquals(existsSync(newPath), true); + assertEquals(existsSync(file), false); + }, () => fail()) + .finally(() => { + if (existsSync(file)) Deno.removeSync(file); + if (existsSync(newPath)) Deno.removeSync(newPath); + }); + }, +}); + +Deno.test({ + name: "SYNC: renaming a file", + fn() { + const file = Deno.makeTempFileSync(); + const newPath = join(parse(file).dir, `${parse(file).base}_renamed`); + renameSync(file, newPath); + assertEquals(existsSync(newPath), true); + assertEquals(existsSync(file), false); + }, +}); + +Deno.test("[std/node/fs] rename callback isn't called twice if error is thrown", async () => { + const tempFile = await Deno.makeTempFile(); + const importUrl = new URL("node:fs", import.meta.url); + await assertCallbackErrorUncaught({ + prelude: `import { rename } from ${JSON.stringify(importUrl)}`, + invocation: `rename(${JSON.stringify(tempFile)}, + ${JSON.stringify(`${tempFile}.newname`)}, `, + async cleanup() { + await Deno.remove(`${tempFile}.newname`); + }, + }); +}); diff --git a/cli/tests/unit_node/_fs/_fs_rm_test.ts b/cli/tests/unit_node/_fs/_fs_rm_test.ts new file mode 100644 index 0000000000..e196ee08a9 --- /dev/null +++ b/cli/tests/unit_node/_fs/_fs_rm_test.ts @@ -0,0 +1,158 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +import { + assertEquals, + assertRejects, + assertThrows, + fail, +} from "../../../../test_util/std/testing/asserts.ts"; +import { rm, rmSync } from "node:fs"; +import { closeSync, existsSync } from "node:fs"; +import { join } from "../../../../test_util/std/path/mod.ts"; + +Deno.test({ + name: "ASYNC: removing empty folder", + async fn() { + const dir = Deno.makeTempDirSync(); + await new Promise((resolve, reject) => { + rm(dir, { recursive: true }, (err) => { + if (err) reject(err); + resolve(); + }); + }) + .then(() => assertEquals(existsSync(dir), false), () => fail()) + .finally(() => { + if (existsSync(dir)) Deno.removeSync(dir); + }); + }, +}); + +function closeRes(before: Deno.ResourceMap, after: Deno.ResourceMap) { + for (const key in after) { + if (!before[key]) { + try { + closeSync(Number(key)); + } catch (error) { + return error; + } + } + } +} + +Deno.test({ + name: "ASYNC: removing non-empty folder", + async fn() { + const rBefore = Deno.resources(); + const dir = Deno.makeTempDirSync(); + Deno.createSync(join(dir, "file1.txt")); + Deno.createSync(join(dir, "file2.txt")); + Deno.mkdirSync(join(dir, "some_dir")); + Deno.createSync(join(dir, "some_dir", "file.txt")); + await new Promise((resolve, reject) => { + rm(dir, { recursive: true }, (err) => { + if (err) reject(err); + resolve(); + }); + }) + .then(() => assertEquals(existsSync(dir), false), () => fail()) + .finally(() => { + if (existsSync(dir)) Deno.removeSync(dir, { recursive: true }); + const rAfter = Deno.resources(); + closeRes(rBefore, rAfter); + }); + }, + ignore: Deno.build.os === "windows", +}); + +Deno.test({ + name: "ASYNC: removing a file", + async fn() { + const file = Deno.makeTempFileSync(); + await new Promise((resolve, reject) => { + rm(file, (err) => { + if (err) reject(err); + resolve(); + }); + }); + + assertEquals(existsSync(file), false); + }, +}); + +Deno.test({ + name: "ASYNC: remove should fail if target does not exist", + async fn() { + const removePromise = new Promise((resolve, reject) => { + rm("/path/to/noexist.text", (err) => { + if (err) reject(err); + resolve(); + }); + }); + await assertRejects(() => removePromise, Error); + }, +}); + +Deno.test({ + name: + "ASYNC: remove should not fail if target does not exist and force option is true", + async fn() { + await new Promise((resolve, reject) => { + rm("/path/to/noexist.text", { force: true }, (err) => { + if (err) reject(err); + resolve(); + }); + }); + }, +}); + +Deno.test({ + name: "SYNC: removing empty folder", + fn() { + const dir = Deno.makeTempDirSync(); + rmSync(dir, { recursive: true }); + assertEquals(existsSync(dir), false); + }, +}); + +Deno.test({ + name: "SYNC: removing non-empty folder", + fn() { + const rBefore = Deno.resources(); + const dir = Deno.makeTempDirSync(); + Deno.createSync(join(dir, "file1.txt")); + Deno.createSync(join(dir, "file2.txt")); + Deno.mkdirSync(join(dir, "some_dir")); + Deno.createSync(join(dir, "some_dir", "file.txt")); + rmSync(dir, { recursive: true }); + assertEquals(existsSync(dir), false); + // closing resources + const rAfter = Deno.resources(); + closeRes(rBefore, rAfter); + }, + ignore: Deno.build.os === "windows", +}); + +Deno.test({ + name: "SYNC: removing a file", + fn() { + const file = Deno.makeTempFileSync(); + + rmSync(file); + + assertEquals(existsSync(file), false); + }, +}); + +Deno.test({ + name: "SYNC: remove should fail if target does not exist", + fn() { + assertThrows(() => rmSync("/path/to/noexist.text"), Error); + }, +}); + +Deno.test({ + name: + "SYNC: remove should not fail if target does not exist and force option is true", + fn() { + rmSync("/path/to/noexist.text", { force: true }); + }, +}); diff --git a/cli/tests/unit_node/_fs/_fs_rmdir_test.ts b/cli/tests/unit_node/_fs/_fs_rmdir_test.ts new file mode 100644 index 0000000000..85aaa2ec37 --- /dev/null +++ b/cli/tests/unit_node/_fs/_fs_rmdir_test.ts @@ -0,0 +1,104 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +import { + assertEquals, + fail, +} from "../../../../test_util/std/testing/asserts.ts"; +import { rmdir, rmdirSync } from "node:fs"; +import { closeSync } from "node:fs"; +import { existsSync } from "node:fs"; +import { join } from "../../../../test_util/std/path/mod.ts"; +import { assertCallbackErrorUncaught } from "../_test_utils.ts"; + +Deno.test({ + name: "ASYNC: removing empty folder", + async fn() { + const dir = Deno.makeTempDirSync(); + await new Promise((resolve, reject) => { + rmdir(dir, (err) => { + if (err) reject(err); + resolve(); + }); + }) + .then(() => assertEquals(existsSync(dir), false), () => fail()) + .finally(() => { + if (existsSync(dir)) Deno.removeSync(dir); + }); + }, +}); + +Deno.test({ + name: "SYNC: removing empty folder", + fn() { + const dir = Deno.makeTempDirSync(); + rmdirSync(dir); + assertEquals(existsSync(dir), false); + }, +}); + +function closeRes(before: Deno.ResourceMap, after: Deno.ResourceMap) { + for (const key in after) { + if (!before[key]) { + try { + closeSync(Number(key)); + } catch (error) { + return error; + } + } + } +} + +Deno.test({ + name: "ASYNC: removing non-empty folder", + async fn() { + const rBefore = Deno.resources(); + const dir = Deno.makeTempDirSync(); + Deno.createSync(join(dir, "file1.txt")); + Deno.createSync(join(dir, "file2.txt")); + Deno.mkdirSync(join(dir, "some_dir")); + Deno.createSync(join(dir, "some_dir", "file.txt")); + await new Promise((resolve, reject) => { + rmdir(dir, { recursive: true }, (err) => { + if (err) reject(err); + resolve(); + }); + }) + .then(() => assertEquals(existsSync(dir), false), () => fail()) + .finally(() => { + if (existsSync(dir)) Deno.removeSync(dir, { recursive: true }); + const rAfter = Deno.resources(); + closeRes(rBefore, rAfter); + }); + }, + ignore: Deno.build.os === "windows", +}); + +Deno.test({ + name: "SYNC: removing non-empty folder", + fn() { + const rBefore = Deno.resources(); + const dir = Deno.makeTempDirSync(); + Deno.createSync(join(dir, "file1.txt")); + Deno.createSync(join(dir, "file2.txt")); + Deno.mkdirSync(join(dir, "some_dir")); + Deno.createSync(join(dir, "some_dir", "file.txt")); + rmdirSync(dir, { recursive: true }); + assertEquals(existsSync(dir), false); + // closing resources + const rAfter = Deno.resources(); + closeRes(rBefore, rAfter); + }, + ignore: Deno.build.os === "windows", +}); + +Deno.test("[std/node/fs] rmdir callback isn't called twice if error is thrown", async () => { + // The correct behaviour is not to catch any errors thrown, + // but that means there'll be an uncaught error and the test will fail. + // So the only way to test this is to spawn a subprocess, and succeed if it has a non-zero exit code. + // (assertRejects won't work because there's no way to catch the error.) + const tempDir = await Deno.makeTempDir(); + const importUrl = new URL("node:fs", import.meta.url); + await assertCallbackErrorUncaught({ + prelude: `import { rmdir } from ${JSON.stringify(importUrl)}`, + invocation: `rmdir(${JSON.stringify(tempDir)}, `, + }); +}); diff --git a/cli/tests/unit_node/_fs/_fs_stat_test.ts b/cli/tests/unit_node/_fs/_fs_stat_test.ts new file mode 100644 index 0000000000..b9e33c5074 --- /dev/null +++ b/cli/tests/unit_node/_fs/_fs_stat_test.ts @@ -0,0 +1,134 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +import { assertCallbackErrorUncaught } from "../_test_utils.ts"; +import { BigIntStats, stat, Stats, statSync } from "node:fs"; +import { + assertEquals, + fail, +} from "../../../../test_util/std/testing/asserts.ts"; + +export function assertStats(actual: Stats, expected: Deno.FileInfo) { + assertEquals(actual.dev, expected.dev); + assertEquals(actual.gid, expected.gid); + assertEquals(actual.size, expected.size); + assertEquals(actual.blksize, expected.blksize); + assertEquals(actual.blocks, expected.blocks); + assertEquals(actual.ino, expected.ino); + assertEquals(actual.gid, expected.gid); + assertEquals(actual.mode, expected.mode); + assertEquals(actual.nlink, expected.nlink); + assertEquals(actual.rdev, expected.rdev); + assertEquals(actual.uid, expected.uid); + assertEquals(actual.atime?.getTime(), expected.atime?.getTime()); + assertEquals(actual.mtime?.getTime(), expected.mtime?.getTime()); + assertEquals(actual.birthtime?.getTime(), expected.birthtime?.getTime()); + assertEquals(actual.atimeMs ?? undefined, expected.atime?.getTime()); + assertEquals(actual.mtimeMs ?? undefined, expected.mtime?.getTime()); + assertEquals(actual.birthtimeMs ?? undefined, expected.birthtime?.getTime()); + assertEquals(actual.isFile(), expected.isFile); + assertEquals(actual.isDirectory(), expected.isDirectory); + assertEquals(actual.isSymbolicLink(), expected.isSymlink); +} + +function toBigInt(num?: number | null) { + if (num === undefined || num === null) return null; + return BigInt(num); +} + +export function assertStatsBigInt( + actual: BigIntStats, + expected: Deno.FileInfo, +) { + assertEquals(actual.dev, toBigInt(expected.dev)); + assertEquals(actual.gid, toBigInt(expected.gid)); + assertEquals(actual.size, toBigInt(expected.size)); + assertEquals(actual.blksize, toBigInt(expected.blksize)); + assertEquals(actual.blocks, toBigInt(expected.blocks)); + assertEquals(actual.ino, toBigInt(expected.ino)); + assertEquals(actual.gid, toBigInt(expected.gid)); + assertEquals(actual.mode, toBigInt(expected.mode)); + assertEquals(actual.nlink, toBigInt(expected.nlink)); + assertEquals(actual.rdev, toBigInt(expected.rdev)); + assertEquals(actual.uid, toBigInt(expected.uid)); + assertEquals(actual.atime?.getTime(), expected.atime?.getTime()); + assertEquals(actual.mtime?.getTime(), expected.mtime?.getTime()); + assertEquals(actual.birthtime?.getTime(), expected.birthtime?.getTime()); + assertEquals( + actual.atimeMs === null ? undefined : Number(actual.atimeMs), + expected.atime?.getTime(), + ); + assertEquals( + actual.mtimeMs === null ? undefined : Number(actual.mtimeMs), + expected.mtime?.getTime(), + ); + assertEquals( + actual.birthtimeMs === null ? undefined : Number(actual.birthtimeMs), + expected.birthtime?.getTime(), + ); + assertEquals(actual.atimeNs === null, actual.atime === null); + assertEquals(actual.mtimeNs === null, actual.mtime === null); + assertEquals(actual.birthtimeNs === null, actual.birthtime === null); + assertEquals(actual.isFile(), expected.isFile); + assertEquals(actual.isDirectory(), expected.isDirectory); + assertEquals(actual.isSymbolicLink(), expected.isSymlink); +} + +Deno.test({ + name: "ASYNC: get a file Stats", + async fn() { + const file = Deno.makeTempFileSync(); + await new Promise((resolve, reject) => { + stat(file, (err, stat) => { + if (err) reject(err); + resolve(stat); + }); + }) + .then((stat) => assertStats(stat, Deno.statSync(file)), () => fail()) + .finally(() => Deno.removeSync(file)); + }, +}); + +Deno.test({ + name: "SYNC: get a file Stats", + fn() { + const file = Deno.makeTempFileSync(); + assertStats(statSync(file), Deno.statSync(file)); + }, +}); + +Deno.test({ + name: "ASYNC: get a file BigInt Stats", + async fn() { + const file = Deno.makeTempFileSync(); + await new Promise((resolve, reject) => { + stat(file, { bigint: true }, (err, stat) => { + if (err) reject(err); + resolve(stat); + }); + }) + .then( + (stat) => assertStatsBigInt(stat, Deno.statSync(file)), + () => fail(), + ) + .finally(() => Deno.removeSync(file)); + }, +}); + +Deno.test({ + name: "SYNC: get a file BigInt Stats", + fn() { + const file = Deno.makeTempFileSync(); + assertStatsBigInt(statSync(file, { bigint: true }), Deno.statSync(file)); + }, +}); + +Deno.test("[std/node/fs] stat callback isn't called twice if error is thrown", async () => { + const tempFile = await Deno.makeTempFile(); + const importUrl = new URL("node:fs", import.meta.url); + await assertCallbackErrorUncaught({ + prelude: `import { stat } from ${JSON.stringify(importUrl)}`, + invocation: `stat(${JSON.stringify(tempFile)}, `, + async cleanup() { + await Deno.remove(tempFile); + }, + }); +}); diff --git a/cli/tests/unit_node/_fs/_fs_symlink_test.ts b/cli/tests/unit_node/_fs/_fs_symlink_test.ts new file mode 100644 index 0000000000..72747ebfaa --- /dev/null +++ b/cli/tests/unit_node/_fs/_fs_symlink_test.ts @@ -0,0 +1,111 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +import { + assert, + assertThrows, + fail, +} from "../../../../test_util/std/testing/asserts.ts"; +import { symlink, symlinkSync } from "node:fs"; + +Deno.test({ + name: "ASYNC: no callback function results in Error", + fn() { + assertThrows( + () => { + // @ts-expect-error Argument of type 'string' is not assignable to parameter of type 'NoParamCallback' + symlink("some/path", "some/other/path", "dir"); + }, + Error, + "No callback function supplied", + ); + }, +}); + +Deno.test({ + name: "ASYNC: create symlink point to a file", + async fn() { + const file: string = Deno.makeTempFileSync(); + const linkedFile: string = file + ".link"; + + await new Promise((resolve, reject) => { + symlink(file, linkedFile, (err: Error | null) => { + if (err !== null) reject(); + else resolve(); + }); + }) + .then( + () => { + const stat = Deno.lstatSync(linkedFile); + assert(stat.isSymlink); + }, + () => { + fail("Expected to succeed"); + }, + ) + .finally(() => { + Deno.removeSync(file); + Deno.removeSync(linkedFile); + }); + }, +}); + +Deno.test({ + name: "ASYNC: create symlink point to a dir", + async fn() { + const dir: string = Deno.makeTempDirSync(); + const linkedDir: string = dir + ".link"; + + await new Promise((resolve, reject) => { + symlink(dir, linkedDir, (err: Error | null) => { + if (err !== null) reject(); + else resolve(); + }); + }) + .then( + () => { + const stat = Deno.lstatSync(linkedDir); + assert(stat.isSymlink); + }, + () => { + fail("Expected to succeed"); + }, + ) + .finally(() => { + Deno.removeSync(dir); + Deno.removeSync(linkedDir); + }); + }, +}); + +Deno.test({ + name: "SYNC: create symlink point to a file", + fn() { + const file: string = Deno.makeTempFileSync(); + const linkedFile: string = file + ".link"; + + try { + symlinkSync(file, linkedFile); + const stat = Deno.lstatSync(linkedFile); + assert(stat.isSymlink); + } finally { + Deno.removeSync(file); + Deno.removeSync(linkedFile); + } + }, +}); + +Deno.test({ + name: "SYNC: create symlink point to a dir", + fn() { + const dir: string = Deno.makeTempDirSync(); + const linkedDir: string = dir + ".link"; + + try { + symlinkSync(dir, linkedDir); + const stat = Deno.lstatSync(linkedDir); + assert(stat.isSymlink); + } finally { + Deno.removeSync(dir); + Deno.removeSync(linkedDir); + } + }, +}); diff --git a/cli/tests/unit_node/_fs/_fs_truncate_test.ts b/cli/tests/unit_node/_fs/_fs_truncate_test.ts new file mode 100644 index 0000000000..3aff3fac27 --- /dev/null +++ b/cli/tests/unit_node/_fs/_fs_truncate_test.ts @@ -0,0 +1,99 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +import { + assertEquals, + assertThrows, + fail, +} from "../../../../test_util/std/testing/asserts.ts"; +import { truncate, truncateSync } from "node:fs"; + +Deno.test({ + name: "ASYNC: no callback function results in Error", + fn() { + assertThrows( + () => { + // @ts-expect-error Argument of type 'number' is not assignable to parameter of type 'NoParamCallback' + truncate("some/path", 0); + }, + Error, + "No callback function supplied", + ); + }, +}); + +Deno.test({ + name: "ASYNC: truncate entire file contents", + async fn() { + const file: string = Deno.makeTempFileSync(); + await Deno.writeFile(file, new TextEncoder().encode("hello world")); + + await new Promise((resolve, reject) => { + truncate(file, (err: Error | null) => { + if (err !== null) reject(); + else resolve(); + }); + }) + .then( + () => { + const fileInfo: Deno.FileInfo = Deno.lstatSync(file); + assertEquals(fileInfo.size, 0); + }, + () => { + fail("No error expected"); + }, + ) + .finally(() => Deno.removeSync(file)); + }, +}); + +Deno.test({ + name: "ASYNC: truncate file to a size of precisely len bytes", + async fn() { + const file: string = Deno.makeTempFileSync(); + await Deno.writeFile(file, new TextEncoder().encode("hello world")); + + await new Promise((resolve, reject) => { + truncate(file, 3, (err: Error | null) => { + if (err !== null) reject(); + else resolve(); + }); + }) + .then( + () => { + const fileInfo: Deno.FileInfo = Deno.lstatSync(file); + assertEquals(fileInfo.size, 3); + }, + () => { + fail("No error expected"); + }, + ) + .finally(() => Deno.removeSync(file)); + }, +}); + +Deno.test({ + name: "SYNC: truncate entire file contents", + fn() { + const file: string = Deno.makeTempFileSync(); + try { + truncateSync(file); + const fileInfo: Deno.FileInfo = Deno.lstatSync(file); + assertEquals(fileInfo.size, 0); + } finally { + Deno.removeSync(file); + } + }, +}); + +Deno.test({ + name: "SYNC: truncate file to a size of precisely len bytes", + fn() { + const file: string = Deno.makeTempFileSync(); + try { + truncateSync(file, 3); + const fileInfo: Deno.FileInfo = Deno.lstatSync(file); + assertEquals(fileInfo.size, 3); + } finally { + Deno.removeSync(file); + } + }, +}); diff --git a/cli/tests/unit_node/_fs/_fs_unlink_test.ts b/cli/tests/unit_node/_fs/_fs_unlink_test.ts new file mode 100644 index 0000000000..a33534fbb6 --- /dev/null +++ b/cli/tests/unit_node/_fs/_fs_unlink_test.ts @@ -0,0 +1,43 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +import { + assertEquals, + fail, +} from "../../../../test_util/std/testing/asserts.ts"; +import { existsSync } from "node:fs"; +import { assertCallbackErrorUncaught } from "../_test_utils.ts"; +import { unlink, unlinkSync } from "node:fs"; + +Deno.test({ + name: "ASYNC: deleting a file", + async fn() { + const file = Deno.makeTempFileSync(); + await new Promise((resolve, reject) => { + unlink(file, (err) => { + if (err) reject(err); + resolve(); + }); + }) + .then(() => assertEquals(existsSync(file), false), () => fail()) + .finally(() => { + if (existsSync(file)) Deno.removeSync(file); + }); + }, +}); + +Deno.test({ + name: "SYNC: Test deleting a file", + fn() { + const file = Deno.makeTempFileSync(); + unlinkSync(file); + assertEquals(existsSync(file), false); + }, +}); + +Deno.test("[std/node/fs] unlink callback isn't called twice if error is thrown", async () => { + const tempFile = await Deno.makeTempFile(); + const importUrl = new URL("node:fs", import.meta.url); + await assertCallbackErrorUncaught({ + prelude: `import { unlink } from ${JSON.stringify(importUrl)}`, + invocation: `unlink(${JSON.stringify(tempFile)}, `, + }); +}); diff --git a/cli/tests/unit_node/_fs/_fs_utimes_test.ts b/cli/tests/unit_node/_fs/_fs_utimes_test.ts new file mode 100644 index 0000000000..170c0d73d8 --- /dev/null +++ b/cli/tests/unit_node/_fs/_fs_utimes_test.ts @@ -0,0 +1,104 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +import { + assertEquals, + assertThrows, + fail, +} from "../../../../test_util/std/testing/asserts.ts"; +import { utimes, utimesSync } from "node:fs"; + +const randomDate = new Date(Date.now() + 1000); + +Deno.test({ + name: + "ASYNC: change the file system timestamps of the object referenced by path", + async fn() { + const file: string = Deno.makeTempFileSync(); + + await new Promise((resolve, reject) => { + utimes(file, randomDate, randomDate, (err: Error | null) => { + if (err !== null) reject(); + else resolve(); + }); + }) + .then( + () => { + const fileInfo: Deno.FileInfo = Deno.lstatSync(file); + assertEquals(fileInfo.mtime, randomDate); + assertEquals(fileInfo.mtime, randomDate); + }, + () => { + fail("No error expected"); + }, + ) + .finally(() => Deno.removeSync(file)); + }, +}); + +Deno.test({ + name: "ASYNC: should throw error if atime is infinity", + fn() { + assertThrows( + () => { + utimes("some/path", Infinity, 0, (_err: Error | null) => {}); + }, + Error, + "invalid atime, must not be infinity or NaN", + ); + }, +}); + +Deno.test({ + name: "ASYNC: should throw error if atime is NaN", + fn() { + assertThrows( + () => { + utimes("some/path", "some string", 0, (_err: Error | null) => {}); + }, + Error, + "invalid atime, must not be infinity or NaN", + ); + }, +}); + +Deno.test({ + name: + "SYNC: change the file system timestamps of the object referenced by path", + fn() { + const file: string = Deno.makeTempFileSync(); + try { + utimesSync(file, randomDate, randomDate); + + const fileInfo: Deno.FileInfo = Deno.lstatSync(file); + + assertEquals(fileInfo.mtime, randomDate); + } finally { + Deno.removeSync(file); + } + }, +}); + +Deno.test({ + name: "SYNC: should throw error if atime is NaN", + fn() { + assertThrows( + () => { + utimesSync("some/path", "some string", 0); + }, + Error, + "invalid atime, must not be infinity or NaN", + ); + }, +}); + +Deno.test({ + name: "SYNC: should throw error if atime is Infinity", + fn() { + assertThrows( + () => { + utimesSync("some/path", Infinity, 0); + }, + Error, + "invalid atime, must not be infinity or NaN", + ); + }, +}); diff --git a/cli/tests/unit_node/_fs/_fs_write_test.ts b/cli/tests/unit_node/_fs/_fs_write_test.ts new file mode 100644 index 0000000000..7baa81b7df --- /dev/null +++ b/cli/tests/unit_node/_fs/_fs_write_test.ts @@ -0,0 +1,53 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +import { write, writeSync } from "node:fs"; +import { assertEquals } from "../../../../test_util/std/testing/asserts.ts"; +import { Buffer } from "node:buffer"; + +const decoder = new TextDecoder("utf-8"); + +Deno.test({ + name: "Data is written to the file with the correct length", + async fn() { + const tempFile: string = await Deno.makeTempFile(); + const file: Deno.FsFile = await Deno.open(tempFile, { + create: true, + write: true, + read: true, + }); + const buffer = Buffer.from("hello world"); + const bytesWrite = await new Promise((resolve, reject) => { + write(file.rid, buffer, 0, 5, (err: unknown, nwritten: number) => { + if (err) return reject(err); + resolve(nwritten); + }); + }); + Deno.close(file.rid); + + const data = await Deno.readFile(tempFile); + await Deno.remove(tempFile); + + assertEquals(bytesWrite, 5); + assertEquals(decoder.decode(data), "hello"); + }, +}); + +Deno.test({ + name: "Data is written synchronously to the file with the correct length", + fn() { + const tempFile: string = Deno.makeTempFileSync(); + const file: Deno.FsFile = Deno.openSync(tempFile, { + create: true, + write: true, + read: true, + }); + const buffer = Buffer.from("hello world"); + const bytesWrite = writeSync(file.rid, buffer, 0, 5); + Deno.close(file.rid); + + const data = Deno.readFileSync(tempFile); + Deno.removeSync(tempFile); + + assertEquals(bytesWrite, 5); + assertEquals(decoder.decode(data), "hello"); + }, +}); diff --git a/cli/tests/unit_node/_fs/testdata/hello.txt b/cli/tests/unit_node/_fs/testdata/hello.txt new file mode 100644 index 0000000000..95d09f2b10 --- /dev/null +++ b/cli/tests/unit_node/_fs/testdata/hello.txt @@ -0,0 +1 @@ +hello world \ No newline at end of file diff --git a/cli/tests/unit_node/_test_utils.ts b/cli/tests/unit_node/_test_utils.ts new file mode 100644 index 0000000000..3220677198 --- /dev/null +++ b/cli/tests/unit_node/_test_utils.ts @@ -0,0 +1,43 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +import { + assert, + assertStringIncludes, +} from "../../../test_util/std/testing/asserts.ts"; + +/** Asserts that an error thrown in a callback will not be wrongly caught. */ +export async function assertCallbackErrorUncaught( + { prelude, invocation, cleanup }: { + /** Any code which needs to run before the actual invocation (notably, any import statements). */ + prelude?: string; + /** + * The start of the invocation of the function, e.g. `open("foo.txt", `. + * The callback will be added after it. + */ + invocation: string; + /** Called after the subprocess is finished but before running the assertions, e.g. to clean up created files. */ + cleanup?: () => Promise | void; + }, +) { + // Since the error has to be uncaught, and that will kill the Deno process, + // the only way to test this is to spawn a subprocess. + const p = new Deno.Command(Deno.execPath(), { + args: [ + "eval", + "--unstable", + `${prelude ?? ""} + + ${invocation}(err) => { + // If the bug is present and the callback is called again with an error, + // don't throw another error, so if the subprocess fails we know it had the correct behaviour. + if (!err) throw new Error("success"); + });`, + ], + stderr: "piped", + }); + const { stderr, success } = await p.output(); + const error = new TextDecoder().decode(stderr); + await cleanup?.(); + assert(!success); + assertStringIncludes(error, "Error: success"); +}