diff options
Diffstat (limited to 'tests/unit/files_test.ts')
-rw-r--r-- | tests/unit/files_test.ts | 1095 |
1 files changed, 1095 insertions, 0 deletions
diff --git a/tests/unit/files_test.ts b/tests/unit/files_test.ts new file mode 100644 index 000000000..c29092963 --- /dev/null +++ b/tests/unit/files_test.ts @@ -0,0 +1,1095 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +// deno-lint-ignore-file no-deprecated-deno-api + +import { + assert, + assertEquals, + assertRejects, + assertThrows, +} from "./test_util.ts"; +import { copy } from "@test_util/std/streams/copy.ts"; + +Deno.test(function filesStdioFileDescriptors() { + assertEquals(Deno.stdin.rid, 0); + assertEquals(Deno.stdout.rid, 1); + assertEquals(Deno.stderr.rid, 2); +}); + +Deno.test({ permissions: { read: true } }, async function filesCopyToStdout() { + const filename = "tests/testdata/assets/fixture.json"; + using file = await Deno.open(filename); + assert(file instanceof Deno.File); + assert(file instanceof Deno.FsFile); + assert(file.rid > 2); + const bytesWritten = await copy(file, Deno.stdout); + const fileSize = Deno.statSync(filename).size; + assertEquals(bytesWritten, fileSize); +}); + +Deno.test({ permissions: { read: true } }, async function filesIter() { + const filename = "tests/testdata/assets/hello.txt"; + using file = await Deno.open(filename); + + let totalSize = 0; + for await (const buf of Deno.iter(file)) { + totalSize += buf.byteLength; + } + + assertEquals(totalSize, 12); +}); + +Deno.test( + { permissions: { read: true } }, + async function filesIterCustomBufSize() { + const filename = "tests/testdata/assets/hello.txt"; + using file = await Deno.open(filename); + + let totalSize = 0; + let iterations = 0; + for await (const buf of Deno.iter(file, { bufSize: 6 })) { + totalSize += buf.byteLength; + iterations += 1; + } + + assertEquals(totalSize, 12); + assertEquals(iterations, 2); + }, +); + +Deno.test({ permissions: { read: true } }, function filesIterSync() { + const filename = "tests/testdata/assets/hello.txt"; + using file = Deno.openSync(filename); + + let totalSize = 0; + for (const buf of Deno.iterSync(file)) { + totalSize += buf.byteLength; + } + + assertEquals(totalSize, 12); +}); + +Deno.test( + { permissions: { read: true } }, + function filesIterSyncCustomBufSize() { + const filename = "tests/testdata/assets/hello.txt"; + using file = Deno.openSync(filename); + + let totalSize = 0; + let iterations = 0; + for (const buf of Deno.iterSync(file, { bufSize: 6 })) { + totalSize += buf.byteLength; + iterations += 1; + } + + assertEquals(totalSize, 12); + assertEquals(iterations, 2); + }, +); + +Deno.test(async function readerIter() { + // ref: https://github.com/denoland/deno/issues/2330 + const encoder = new TextEncoder(); + + class TestReader implements Deno.Reader { + #offset = 0; + #buf: Uint8Array; + + constructor(s: string) { + this.#buf = new Uint8Array(encoder.encode(s)); + } + + read(p: Uint8Array): Promise<number | null> { + const n = Math.min(p.byteLength, this.#buf.byteLength - this.#offset); + p.set(this.#buf.slice(this.#offset, this.#offset + n)); + this.#offset += n; + + if (n === 0) { + return Promise.resolve(null); + } + + return Promise.resolve(n); + } + } + + const reader = new TestReader("hello world!"); + + let totalSize = 0; + for await (const buf of Deno.iter(reader)) { + totalSize += buf.byteLength; + } + + assertEquals(totalSize, 12); +}); + +Deno.test(async function readerIterSync() { + // ref: https://github.com/denoland/deno/issues/2330 + const encoder = new TextEncoder(); + + class TestReader implements Deno.ReaderSync { + #offset = 0; + #buf: Uint8Array; + + constructor(s: string) { + this.#buf = new Uint8Array(encoder.encode(s)); + } + + readSync(p: Uint8Array): number | null { + const n = Math.min(p.byteLength, this.#buf.byteLength - this.#offset); + p.set(this.#buf.slice(this.#offset, this.#offset + n)); + this.#offset += n; + + if (n === 0) { + return null; + } + + return n; + } + } + + const reader = new TestReader("hello world!"); + + let totalSize = 0; + for await (const buf of Deno.iterSync(reader)) { + totalSize += buf.byteLength; + } + + assertEquals(totalSize, 12); +}); + +Deno.test( + { + permissions: { read: true, write: true }, + }, + function openSyncMode() { + const path = Deno.makeTempDirSync() + "/test_openSync.txt"; + using _file = Deno.openSync(path, { + write: true, + createNew: true, + mode: 0o626, + }); + const pathInfo = Deno.statSync(path); + if (Deno.build.os !== "windows") { + assertEquals(pathInfo.mode! & 0o777, 0o626 & ~Deno.umask()); + } + }, +); + +Deno.test( + { + permissions: { read: true, write: true }, + }, + async function openMode() { + const path = (await Deno.makeTempDir()) + "/test_open.txt"; + using _file = await Deno.open(path, { + write: true, + createNew: true, + mode: 0o626, + }); + const pathInfo = Deno.statSync(path); + if (Deno.build.os !== "windows") { + assertEquals(pathInfo.mode! & 0o777, 0o626 & ~Deno.umask()); + } + }, +); + +Deno.test( + { + permissions: { read: true, write: true }, + }, + function openSyncUrl() { + const tempDir = Deno.makeTempDirSync(); + const fileUrl = new URL( + `file://${ + Deno.build.os === "windows" ? "/" : "" + }${tempDir}/test_open.txt`, + ); + using _file = Deno.openSync(fileUrl, { + write: true, + createNew: true, + mode: 0o626, + }); + const pathInfo = Deno.statSync(fileUrl); + if (Deno.build.os !== "windows") { + assertEquals(pathInfo.mode! & 0o777, 0o626 & ~Deno.umask()); + } + + Deno.removeSync(tempDir, { recursive: true }); + }, +); + +Deno.test( + { + permissions: { read: true, write: true }, + }, + async function openUrl() { + const tempDir = await Deno.makeTempDir(); + const fileUrl = new URL( + `file://${ + Deno.build.os === "windows" ? "/" : "" + }${tempDir}/test_open.txt`, + ); + using _file = await Deno.open(fileUrl, { + write: true, + createNew: true, + mode: 0o626, + }); + const pathInfo = Deno.statSync(fileUrl); + if (Deno.build.os !== "windows") { + assertEquals(pathInfo.mode! & 0o777, 0o626 & ~Deno.umask()); + } + + Deno.removeSync(tempDir, { recursive: true }); + }, +); + +Deno.test( + { permissions: { write: false } }, + async function writePermFailure() { + const filename = "tests/hello.txt"; + const openOptions: Deno.OpenOptions[] = [{ write: true }, { append: true }]; + for (const options of openOptions) { + await assertRejects(async () => { + await Deno.open(filename, options); + }, Deno.errors.PermissionDenied); + } + }, +); + +Deno.test(async function openOptions() { + const filename = "tests/testdata/assets/fixture.json"; + await assertRejects( + async () => { + await Deno.open(filename, { write: false }); + }, + Error, + "OpenOptions requires at least one option to be true", + ); + + await assertRejects( + async () => { + await Deno.open(filename, { truncate: true, write: false }); + }, + Error, + "'truncate' option requires 'write' option", + ); + + await assertRejects( + async () => { + await Deno.open(filename, { create: true, write: false }); + }, + Error, + "'create' or 'createNew' options require 'write' or 'append' option", + ); + + await assertRejects( + async () => { + await Deno.open(filename, { createNew: true, append: false }); + }, + Error, + "'create' or 'createNew' options require 'write' or 'append' option", + ); +}); + +Deno.test({ permissions: { read: false } }, async function readPermFailure() { + await assertRejects(async () => { + await Deno.open("package.json", { read: true }); + }, Deno.errors.PermissionDenied); +}); + +Deno.test( + { permissions: { write: true } }, + async function writeNullBufferFailure() { + const tempDir = Deno.makeTempDirSync(); + const filename = tempDir + "hello.txt"; + const w = { + write: true, + truncate: true, + create: true, + }; + using file = await Deno.open(filename, w); + + // writing null should throw an error + await assertRejects( + async () => { + // deno-lint-ignore no-explicit-any + await file.write(null as any); + }, + ); // TODO(bartlomieju): Check error kind when dispatch_minimal pipes errors properly + await Deno.remove(tempDir, { recursive: true }); + }, +); + +Deno.test( + { permissions: { write: true, read: true } }, + async function readNullBufferFailure() { + const tempDir = Deno.makeTempDirSync(); + const filename = tempDir + "hello.txt"; + using file = await Deno.open(filename, { + read: true, + write: true, + truncate: true, + create: true, + }); + + // reading into an empty buffer should return 0 immediately + const bytesRead = await file.read(new Uint8Array(0)); + assert(bytesRead === 0); + + // reading file into null buffer should throw an error + await assertRejects(async () => { + // deno-lint-ignore no-explicit-any + await file.read(null as any); + }, TypeError); + // TODO(bartlomieju): Check error kind when dispatch_minimal pipes errors properly + + await Deno.remove(tempDir, { recursive: true }); + }, +); + +Deno.test( + { permissions: { write: false, read: false } }, + async function readWritePermFailure() { + const filename = "tests/hello.txt"; + await assertRejects(async () => { + await Deno.open(filename, { read: true }); + }, Deno.errors.PermissionDenied); + }, +); + +Deno.test( + { permissions: { write: true, read: true } }, + async function openNotFound() { + await assertRejects( + async () => { + await Deno.open("bad_file_name"); + }, + Deno.errors.NotFound, + `open 'bad_file_name'`, + ); + }, +); + +Deno.test( + { permissions: { write: true, read: true } }, + function openSyncNotFound() { + assertThrows( + () => { + Deno.openSync("bad_file_name"); + }, + Deno.errors.NotFound, + `open 'bad_file_name'`, + ); + }, +); + +Deno.test( + { permissions: { read: true, write: true } }, + async function createFile() { + const tempDir = await Deno.makeTempDir(); + const filename = tempDir + "/test.txt"; + const f = await Deno.create(filename); + let fileInfo = Deno.statSync(filename); + assert(fileInfo.isFile); + assert(fileInfo.size === 0); + const enc = new TextEncoder(); + const data = enc.encode("Hello"); + await f.write(data); + fileInfo = Deno.statSync(filename); + assert(fileInfo.size === 5); + f.close(); + + // TODO(bartlomieju): test different modes + await Deno.remove(tempDir, { recursive: true }); + }, +); + +Deno.test( + { permissions: { read: true, write: true } }, + async function createFileWithUrl() { + const tempDir = await Deno.makeTempDir(); + const fileUrl = new URL( + `file://${Deno.build.os === "windows" ? "/" : ""}${tempDir}/test.txt`, + ); + const f = await Deno.create(fileUrl); + let fileInfo = Deno.statSync(fileUrl); + assert(fileInfo.isFile); + assert(fileInfo.size === 0); + const enc = new TextEncoder(); + const data = enc.encode("Hello"); + await f.write(data); + fileInfo = Deno.statSync(fileUrl); + assert(fileInfo.size === 5); + f.close(); + + await Deno.remove(tempDir, { recursive: true }); + }, +); + +Deno.test( + { permissions: { read: true, write: true } }, + async function createSyncFile() { + const tempDir = await Deno.makeTempDir(); + const filename = tempDir + "/test.txt"; + const f = Deno.createSync(filename); + let fileInfo = Deno.statSync(filename); + assert(fileInfo.isFile); + assert(fileInfo.size === 0); + const enc = new TextEncoder(); + const data = enc.encode("Hello"); + await f.write(data); + fileInfo = Deno.statSync(filename); + assert(fileInfo.size === 5); + f.close(); + + // TODO(bartlomieju): test different modes + await Deno.remove(tempDir, { recursive: true }); + }, +); + +Deno.test( + { permissions: { read: true, write: true } }, + async function createSyncFileWithUrl() { + const tempDir = await Deno.makeTempDir(); + const fileUrl = new URL( + `file://${Deno.build.os === "windows" ? "/" : ""}${tempDir}/test.txt`, + ); + const f = Deno.createSync(fileUrl); + let fileInfo = Deno.statSync(fileUrl); + assert(fileInfo.isFile); + assert(fileInfo.size === 0); + const enc = new TextEncoder(); + const data = enc.encode("Hello"); + await f.write(data); + fileInfo = Deno.statSync(fileUrl); + assert(fileInfo.size === 5); + f.close(); + + await Deno.remove(tempDir, { recursive: true }); + }, +); + +Deno.test( + { permissions: { read: true, write: true } }, + async function openModeWrite() { + const tempDir = Deno.makeTempDirSync(); + const encoder = new TextEncoder(); + const filename = tempDir + "hello.txt"; + const data = encoder.encode("Hello world!\n"); + let file = await Deno.open(filename, { + create: true, + write: true, + truncate: true, + }); + // assert file was created + let fileInfo = Deno.statSync(filename); + assert(fileInfo.isFile); + assertEquals(fileInfo.size, 0); + // write some data + await file.write(data); + fileInfo = Deno.statSync(filename); + assertEquals(fileInfo.size, 13); + // assert we can't read from file + let thrown = false; + try { + const buf = new Uint8Array(20); + await file.read(buf); + } catch (_e) { + thrown = true; + } finally { + assert(thrown, "'w' mode shouldn't allow to read file"); + } + file.close(); + // assert that existing file is truncated on open + file = await Deno.open(filename, { + write: true, + truncate: true, + }); + file.close(); + const fileSize = Deno.statSync(filename).size; + assertEquals(fileSize, 0); + await Deno.remove(tempDir, { recursive: true }); + }, +); + +Deno.test( + { permissions: { read: true, write: true } }, + async function openModeWriteRead() { + const tempDir = Deno.makeTempDirSync(); + const encoder = new TextEncoder(); + const filename = tempDir + "hello.txt"; + const data = encoder.encode("Hello world!\n"); + + using file = await Deno.open(filename, { + write: true, + truncate: true, + create: true, + read: true, + }); + const seekPosition = 0; + // assert file was created + let fileInfo = Deno.statSync(filename); + assert(fileInfo.isFile); + assertEquals(fileInfo.size, 0); + // write some data + await file.write(data); + fileInfo = Deno.statSync(filename); + assertEquals(fileInfo.size, 13); + + const buf = new Uint8Array(20); + // seeking from beginning of a file + const cursorPosition = await file.seek(seekPosition, Deno.SeekMode.Start); + assertEquals(seekPosition, cursorPosition); + const result = await file.read(buf); + assertEquals(result, 13); + + await Deno.remove(tempDir, { recursive: true }); + }, +); + +Deno.test({ permissions: { read: true } }, async function seekStart() { + const filename = "tests/testdata/assets/hello.txt"; + using file = await Deno.open(filename); + const seekPosition = 6; + // Deliberately move 1 step forward + await file.read(new Uint8Array(1)); // "H" + // Skipping "Hello " + // seeking from beginning of a file plus seekPosition + const cursorPosition = await file.seek(seekPosition, Deno.SeekMode.Start); + assertEquals(seekPosition, cursorPosition); + const buf = new Uint8Array(6); + await file.read(buf); + const decoded = new TextDecoder().decode(buf); + assertEquals(decoded, "world!"); +}); + +Deno.test({ permissions: { read: true } }, async function seekStartBigInt() { + const filename = "tests/testdata/assets/hello.txt"; + using file = await Deno.open(filename); + const seekPosition = 6n; + // Deliberately move 1 step forward + await file.read(new Uint8Array(1)); // "H" + // Skipping "Hello " + // seeking from beginning of a file plus seekPosition + const cursorPosition = await file.seek(seekPosition, Deno.SeekMode.Start); + assertEquals(seekPosition, BigInt(cursorPosition)); + const buf = new Uint8Array(6); + await file.read(buf); + const decoded = new TextDecoder().decode(buf); + assertEquals(decoded, "world!"); +}); + +Deno.test({ permissions: { read: true } }, function seekSyncStart() { + const filename = "tests/testdata/assets/hello.txt"; + using file = Deno.openSync(filename); + const seekPosition = 6; + // Deliberately move 1 step forward + file.readSync(new Uint8Array(1)); // "H" + // Skipping "Hello " + // seeking from beginning of a file plus seekPosition + const cursorPosition = file.seekSync(seekPosition, Deno.SeekMode.Start); + assertEquals(seekPosition, cursorPosition); + const buf = new Uint8Array(6); + file.readSync(buf); + const decoded = new TextDecoder().decode(buf); + assertEquals(decoded, "world!"); +}); + +Deno.test({ permissions: { read: true } }, async function seekCurrent() { + const filename = "tests/testdata/assets/hello.txt"; + using file = await Deno.open(filename); + // Deliberately move 1 step forward + await file.read(new Uint8Array(1)); // "H" + // Skipping "ello " + const seekPosition = 5; + // seekPosition is relative to current cursor position after read + const cursorPosition = await file.seek(seekPosition, Deno.SeekMode.Current); + assertEquals(seekPosition + 1, cursorPosition); + const buf = new Uint8Array(6); + await file.read(buf); + const decoded = new TextDecoder().decode(buf); + assertEquals(decoded, "world!"); +}); + +Deno.test({ permissions: { read: true } }, function seekSyncCurrent() { + const filename = "tests/testdata/assets/hello.txt"; + using file = Deno.openSync(filename); + // Deliberately move 1 step forward + file.readSync(new Uint8Array(1)); // "H" + // Skipping "ello " + const seekPosition = 5; + // seekPosition is relative to current cursor position after read + const cursorPosition = file.seekSync(seekPosition, Deno.SeekMode.Current); + assertEquals(seekPosition + 1, cursorPosition); + const buf = new Uint8Array(6); + file.readSync(buf); + const decoded = new TextDecoder().decode(buf); + assertEquals(decoded, "world!"); +}); + +Deno.test({ permissions: { read: true } }, async function seekEnd() { + const filename = "tests/testdata/assets/hello.txt"; + using file = await Deno.open(filename); + const seekPosition = -6; + // seek from end of file that has 12 chars, 12 - 6 = 6 + const cursorPosition = await file.seek(seekPosition, Deno.SeekMode.End); + assertEquals(6, cursorPosition); + const buf = new Uint8Array(6); + await file.read(buf); + const decoded = new TextDecoder().decode(buf); + assertEquals(decoded, "world!"); +}); + +Deno.test({ permissions: { read: true } }, function seekSyncEnd() { + const filename = "tests/testdata/assets/hello.txt"; + using file = Deno.openSync(filename); + const seekPosition = -6; + // seek from end of file that has 12 chars, 12 - 6 = 6 + const cursorPosition = file.seekSync(seekPosition, Deno.SeekMode.End); + assertEquals(6, cursorPosition); + const buf = new Uint8Array(6); + file.readSync(buf); + const decoded = new TextDecoder().decode(buf); + assertEquals(decoded, "world!"); +}); + +Deno.test({ permissions: { read: true } }, async function seekMode() { + const filename = "tests/testdata/assets/hello.txt"; + using file = await Deno.open(filename); + await assertRejects( + async () => { + await file.seek(1, -1 as unknown as Deno.SeekMode); + }, + TypeError, + "Invalid seek mode", + ); + + // We should still be able to read the file + // since it is still open. + const buf = new Uint8Array(1); + await file.read(buf); // "H" + assertEquals(new TextDecoder().decode(buf), "H"); +}); + +Deno.test( + { permissions: { read: true, write: true } }, + function fileTruncateSyncSuccess() { + const filename = Deno.makeTempDirSync() + "/test_fileTruncateSync.txt"; + using file = Deno.openSync(filename, { + create: true, + read: true, + write: true, + }); + + file.truncateSync(20); + assertEquals(Deno.readFileSync(filename).byteLength, 20); + file.truncateSync(5); + assertEquals(Deno.readFileSync(filename).byteLength, 5); + file.truncateSync(-5); + assertEquals(Deno.readFileSync(filename).byteLength, 0); + + Deno.removeSync(filename); + }, +); + +Deno.test( + { permissions: { read: true, write: true } }, + async function fileTruncateSuccess() { + const filename = Deno.makeTempDirSync() + "/test_fileTruncate.txt"; + using file = await Deno.open(filename, { + create: true, + read: true, + write: true, + }); + + await file.truncate(20); + assertEquals((await Deno.readFile(filename)).byteLength, 20); + await file.truncate(5); + assertEquals((await Deno.readFile(filename)).byteLength, 5); + await file.truncate(-5); + assertEquals((await Deno.readFile(filename)).byteLength, 0); + + await Deno.remove(filename); + }, +); + +Deno.test({ permissions: { read: true } }, function fileStatSyncSuccess() { + using file = Deno.openSync("README.md"); + const fileInfo = file.statSync(); + assert(fileInfo.isFile); + assert(!fileInfo.isSymlink); + assert(!fileInfo.isDirectory); + assert(fileInfo.size); + assert(fileInfo.atime); + assert(fileInfo.mtime); + // The `birthtime` field is not available on Linux before kernel version 4.11. + assert(fileInfo.birthtime || Deno.build.os === "linux"); +}); + +Deno.test(async function fileStatSuccess() { + using file = await Deno.open("README.md"); + const fileInfo = await file.stat(); + assert(fileInfo.isFile); + assert(!fileInfo.isSymlink); + assert(!fileInfo.isDirectory); + assert(fileInfo.size); + assert(fileInfo.atime); + assert(fileInfo.mtime); + // The `birthtime` field is not available on Linux before kernel version 4.11. + assert(fileInfo.birthtime || Deno.build.os === "linux"); +}); + +Deno.test({ permissions: { read: true } }, async function readableStream() { + const filename = "tests/testdata/assets/hello.txt"; + const file = await Deno.open(filename); + assert(file.readable instanceof ReadableStream); + const chunks = []; + for await (const chunk of file.readable) { + chunks.push(chunk); + } + assertEquals(chunks.length, 1); + assertEquals(chunks[0].byteLength, 12); +}); + +Deno.test( + { permissions: { read: true } }, + async function readableStreamTextEncoderPipe() { + const filename = "tests/testdata/assets/hello.txt"; + const file = await Deno.open(filename); + const readable = file.readable.pipeThrough(new TextDecoderStream()); + const chunks = []; + for await (const chunk of readable) { + chunks.push(chunk); + } + assertEquals(chunks.length, 1); + assertEquals(chunks[0].length, 12); + }, +); + +Deno.test( + { permissions: { read: true, write: true } }, + async function writableStream() { + const path = await Deno.makeTempFile(); + const file = await Deno.open(path, { write: true }); + assert(file.writable instanceof WritableStream); + const readable = new ReadableStream({ + start(controller) { + controller.enqueue(new TextEncoder().encode("hello ")); + controller.enqueue(new TextEncoder().encode("world!")); + controller.close(); + }, + }); + await readable.pipeTo(file.writable); + const res = await Deno.readTextFile(path); + assertEquals(res, "hello world!"); + }, +); + +Deno.test( + { permissions: { read: true, write: true } }, + async function readTextFileNonUtf8() { + const path = await Deno.makeTempFile(); + using file = await Deno.open(path, { write: true }); + await file.write(new TextEncoder().encode("hello ")); + await file.write(new Uint8Array([0xC0])); + + const res = await Deno.readTextFile(path); + const resSync = Deno.readTextFileSync(path); + assertEquals(res, resSync); + assertEquals(res, "hello \uFFFD"); + }, +); + +Deno.test( + { permissions: { read: true } }, + async function fsFileExplicitResourceManagement() { + let file2: Deno.FsFile; + + { + using file = await Deno.open("tests/testdata/assets/hello.txt"); + file2 = file; + + const stat = file.statSync(); + assert(stat.isFile); + } + + assertThrows(() => file2.statSync(), Deno.errors.BadResource); + }, +); + +Deno.test( + { permissions: { read: true } }, + async function fsFileExplicitResourceManagementManualClose() { + using file = await Deno.open("tests/testdata/assets/hello.txt"); + file.close(); + assertThrows(() => file.statSync(), Deno.errors.BadResource); // definitely closed + // calling [Symbol.dispose] after manual close is a no-op + }, +); + +Deno.test( + { permissions: { read: true, write: true } }, + function fsFileDatasyncSyncSuccess() { + const filename = Deno.makeTempDirSync() + "/test_fdatasyncSync.txt"; + const file = Deno.openSync(filename, { + read: true, + write: true, + create: true, + }); + const data = new Uint8Array(64); + file.writeSync(data); + file.syncDataSync(); + assertEquals(Deno.readFileSync(filename), data); + file.close(); + Deno.removeSync(filename); + }, +); + +Deno.test( + { permissions: { read: true, write: true } }, + async function fsFileDatasyncSuccess() { + const filename = (await Deno.makeTempDir()) + "/test_fdatasync.txt"; + const file = await Deno.open(filename, { + read: true, + write: true, + create: true, + }); + const data = new Uint8Array(64); + await file.write(data); + await file.syncData(); + assertEquals(await Deno.readFile(filename), data); + file.close(); + await Deno.remove(filename); + }, +); + +Deno.test( + { permissions: { read: true, write: true } }, + function fsFileSyncSyncSuccess() { + const filename = Deno.makeTempDirSync() + "/test_fsyncSync.txt"; + const file = Deno.openSync(filename, { + read: true, + write: true, + create: true, + }); + const size = 64; + file.truncateSync(size); + file.syncSync(); + assertEquals(file.statSync().size, size); + file.close(); + Deno.removeSync(filename); + }, +); + +Deno.test( + { permissions: { read: true, write: true } }, + async function fsFileSyncSuccess() { + const filename = (await Deno.makeTempDir()) + "/test_fsync.txt"; + const file = await Deno.open(filename, { + read: true, + write: true, + create: true, + }); + const size = 64; + await file.truncate(size); + await file.sync(); + assertEquals((await file.stat()).size, size); + file.close(); + await Deno.remove(filename); + }, +); + +Deno.test( + { permissions: { read: true, run: true, hrtime: true } }, + async function fsFileLockFileSync() { + await runFlockTests({ sync: true }); + }, +); + +Deno.test( + { permissions: { read: true, run: true, hrtime: true } }, + async function fsFileLockFileAsync() { + await runFlockTests({ sync: false }); + }, +); + +async function runFlockTests(opts: { sync: boolean }) { + assertEquals( + await checkFirstBlocksSecond({ + firstExclusive: true, + secondExclusive: false, + sync: opts.sync, + }), + true, + "exclusive blocks shared", + ); + assertEquals( + await checkFirstBlocksSecond({ + firstExclusive: false, + secondExclusive: true, + sync: opts.sync, + }), + true, + "shared blocks exclusive", + ); + assertEquals( + await checkFirstBlocksSecond({ + firstExclusive: true, + secondExclusive: true, + sync: opts.sync, + }), + true, + "exclusive blocks exclusive", + ); + assertEquals( + await checkFirstBlocksSecond({ + firstExclusive: false, + secondExclusive: false, + sync: opts.sync, + // need to wait for both to enter the lock to prevent the case where the + // first process enters and exits the lock before the second even enters + waitBothEnteredLock: true, + }), + false, + "shared does not block shared", + ); +} + +async function checkFirstBlocksSecond(opts: { + firstExclusive: boolean; + secondExclusive: boolean; + sync: boolean; + waitBothEnteredLock?: boolean; +}) { + const firstProcess = runFlockTestProcess({ + exclusive: opts.firstExclusive, + sync: opts.sync, + }); + const secondProcess = runFlockTestProcess({ + exclusive: opts.secondExclusive, + sync: opts.sync, + }); + try { + const sleep = (time: number) => new Promise((r) => setTimeout(r, time)); + + await Promise.all([ + firstProcess.waitStartup(), + secondProcess.waitStartup(), + ]); + + await firstProcess.enterLock(); + await firstProcess.waitEnterLock(); + + await secondProcess.enterLock(); + await sleep(100); + + if (!opts.waitBothEnteredLock) { + await firstProcess.exitLock(); + } + + await secondProcess.waitEnterLock(); + + if (opts.waitBothEnteredLock) { + await firstProcess.exitLock(); + } + + await secondProcess.exitLock(); + + // collect the final output + const firstPsTimes = await firstProcess.getTimes(); + const secondPsTimes = await secondProcess.getTimes(); + return firstPsTimes.exitTime < secondPsTimes.enterTime; + } finally { + await firstProcess.close(); + await secondProcess.close(); + } +} + +function runFlockTestProcess(opts: { exclusive: boolean; sync: boolean }) { + const path = "tests/testdata/assets/lock_target.txt"; + const scriptText = ` + const file = Deno.openSync("${path}"); + + // ready signal + Deno.stdout.writeSync(new Uint8Array(1)); + // wait for enter lock signal + Deno.stdin.readSync(new Uint8Array(1)); + + // entering signal + Deno.stdout.writeSync(new Uint8Array(1)); + // lock and record the entry time + ${ + opts.sync + ? `file.lockSync(${opts.exclusive ? "true" : "false"});` + : `await file.lock(${opts.exclusive ? "true" : "false"});` + } + const enterTime = new Date().getTime(); + // entered signal + Deno.stdout.writeSync(new Uint8Array(1)); + + // wait for exit lock signal + Deno.stdin.readSync(new Uint8Array(1)); + + // record the exit time and wait a little bit before releasing + // the lock so that the enter time of the next process doesn't + // occur at the same time as this exit time + const exitTime = new Date().getTime(); + await new Promise(resolve => setTimeout(resolve, 100)); + + // release the lock + ${opts.sync ? "file.unlockSync();" : "await file.unlock();"} + + // exited signal + Deno.stdout.writeSync(new Uint8Array(1)); + + // output the enter and exit time + console.log(JSON.stringify({ enterTime, exitTime })); +`; + + const process = new Deno.Command(Deno.execPath(), { + args: ["eval", "--unstable", scriptText], + stdin: "piped", + stdout: "piped", + stderr: "null", + }).spawn(); + + const waitSignal = async () => { + const reader = process.stdout.getReader({ mode: "byob" }); + await reader.read(new Uint8Array(1)); + reader.releaseLock(); + }; + const signal = async () => { + const writer = process.stdin.getWriter(); + await writer.write(new Uint8Array(1)); + writer.releaseLock(); + }; + + return { + async waitStartup() { + await waitSignal(); + }, + async enterLock() { + await signal(); + await waitSignal(); // entering signal + }, + async waitEnterLock() { + await waitSignal(); + }, + async exitLock() { + await signal(); + await waitSignal(); + }, + getTimes: async () => { + const { stdout } = await process.output(); + const text = new TextDecoder().decode(stdout); + return JSON.parse(text) as { + enterTime: number; + exitTime: number; + }; + }, + close: async () => { + await process.status; + await process.stdin.close(); + }, + }; +} |