From 08c9511accfbbfd53c34434d284728c6537c043a Mon Sep 17 00:00:00 2001 From: Georgijs <48869301+gvilums@users.noreply.github.com> Date: Fri, 26 Jan 2024 20:07:33 -0800 Subject: [PATCH] [windows] nodefs (#8509) * 100 passing fs tests * 111 fs tests passing * 114 passing fs tests * 115 passing (TODO: fix path normalization for windows ntCreateFile * all fs tests passing * [autofix.ci] apply automated fixes * make windows path norm smarter, fix tests * [autofix.ci] apply automated fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- src/async/windows_event_loop.zig | 35 +++------ src/bun.js/node/node_fs.zig | 21 +++--- src/string_immutable.zig | 5 -- src/sys.zig | 103 ++++++++++++++++++--------- src/sys_uv.zig | 8 ++- src/windows_c.zig | 2 +- test/harness.ts | 10 +++ test/js/node/fs/fs-stream.winlink.js | 1 + test/js/node/fs/fs.test.ts | 93 +++++++++++++----------- 9 files changed, 157 insertions(+), 121 deletions(-) create mode 120000 test/js/node/fs/fs-stream.winlink.js diff --git a/src/async/windows_event_loop.zig b/src/async/windows_event_loop.zig index 400c9c779c..cd2d038204 100644 --- a/src/async/windows_event_loop.zig +++ b/src/async/windows_event_loop.zig @@ -8,9 +8,6 @@ const uv = bun.windows.libuv; pub const Loop = uv.Loop; pub const KeepAlive = struct { - // handle.init zeroes the memory - handle: uv.uv_async_t = undefined, - status: Status = .inactive, const log = Output.scoped(.KeepAlive, false); @@ -18,11 +15,6 @@ pub const KeepAlive = struct { const Status = enum { active, inactive, done }; pub inline fn isActive(this: KeepAlive) bool { - if (comptime Environment.allow_assert) { - if (this.status == .active) { - std.debug.assert(this.handle.isActive()); - } - } return this.status == .active; } @@ -30,7 +22,6 @@ pub const KeepAlive = struct { pub fn disable(this: *KeepAlive) void { if (this.status == .active) { this.unref(JSC.VirtualMachine.get()); - this.handle.close(null); } this.status = .done; @@ -38,12 +29,11 @@ pub const KeepAlive = struct { /// Only intended to be used from EventLoop.Pollable pub fn deactivate(this: *KeepAlive, loop: *Loop) void { - _ = loop; if (this.status != .active) return; this.status = .inactive; - this.handle.close(null); + loop.inc(); } /// Only intended to be used from EventLoop.Pollable @@ -52,7 +42,7 @@ pub const KeepAlive = struct { return; this.status = .active; - this.handle.init(loop, null); + loop.inc(); } pub fn init() KeepAlive { @@ -61,42 +51,39 @@ pub const KeepAlive = struct { /// Prevent a poll from keeping the process alive. pub fn unref(this: *KeepAlive, event_loop_ctx_: anytype) void { - _ = event_loop_ctx_; - // const event_loop_ctx = JSC.AbstractVM(event_loop_ctx_); + const event_loop_ctx = JSC.AbstractVM(event_loop_ctx_); if (this.status != .active) return; this.status = .inactive; - this.handle.unref(); + event_loop_ctx.platformEventLoop().dec(); } /// From another thread, Prevent a poll from keeping the process alive. pub fn unrefConcurrently(this: *KeepAlive, vm: *JSC.VirtualMachine) void { - _ = vm; + // _ = vm; if (this.status != .active) return; this.status = .inactive; // TODO: https://github.com/oven-sh/bun/pull/4410#discussion_r1317326194 - this.handle.unref(); + vm.event_loop_handle.?.dec(); } /// Prevent a poll from keeping the process alive on the next tick. pub fn unrefOnNextTick(this: *KeepAlive, vm: *JSC.VirtualMachine) void { - _ = vm; if (this.status != .active) return; this.status = .inactive; - this.handle.unref(); + vm.event_loop_handle.?.dec(); } /// From another thread, prevent a poll from keeping the process alive on the next tick. pub fn unrefOnNextTickConcurrently(this: *KeepAlive, vm: *JSC.VirtualMachine) void { - _ = vm; if (this.status != .active) return; this.status = .inactive; // TODO: https://github.com/oven-sh/bun/pull/4410#discussion_r1317326194 - this.handle.unref(); + vm.event_loop_handle.?.dec(); } /// Allow a poll to keep the process alive. @@ -105,8 +92,7 @@ pub const KeepAlive = struct { if (this.status != .inactive) return; this.status = .active; - this.handle.init(event_loop_ctx.platformEventLoop(), null); - this.handle.ref(); + event_loop_ctx.platformEventLoop().inc(); } /// Allow a poll to keep the process alive. @@ -115,8 +101,7 @@ pub const KeepAlive = struct { return; this.status = .active; // TODO: https://github.com/oven-sh/bun/pull/4410#discussion_r1317326194 - this.handle.init(vm.event_loop_handle.?, null); - this.handle.ref(); + vm.event_loop_handle.?.inc(); } pub fn refConcurrentlyFromEventLoop(this: *KeepAlive, loop: *JSC.EventLoop) void { diff --git a/src/bun.js/node/node_fs.zig b/src/bun.js/node/node_fs.zig index 0b25295a86..2e505b8de4 100644 --- a/src/bun.js/node/node_fs.zig +++ b/src/bun.js/node/node_fs.zig @@ -21,8 +21,6 @@ const FDImpl = bun.FDImpl; const Syscall = if (Environment.isWindows) bun.sys.sys_uv else bun.sys; -const errno_enoent = if (Environment.isWindows) .UV_ENOENT else .NOENT; - const Constants = @import("./node_fs_constant.zig").Constants; const builtin = @import("builtin"); const os = @import("std").os; @@ -463,11 +461,6 @@ pub const AsyncReaddirRecursiveTask = struct { args: Arguments.Readdir, vm: *JSC.VirtualMachine, ) JSC.JSValue { - if (comptime Environment.isWindows) { - globalObject.throwTODO("fs.promises.readdir is not implemented on Windows yet"); - return .zero; - } - var task = AsyncReaddirRecursiveTask.new(.{ .promise = JSC.JSPromise.Strong.init(globalObject), .args = args, @@ -4275,7 +4268,7 @@ pub const NodeFS = struct { )) { .result => |result| Maybe(Return.Lstat){ .result = .{ .stats = Stats.init(result, args.big_int) } }, .err => |err| brk: { - if (!args.throw_if_no_entry and err.getErrno() == errno_enoent) { + if (!args.throw_if_no_entry and err.getErrno() == .NOENT) { return Maybe(Return.Lstat){ .result = .{ .not_found = {} } }; } break :brk Maybe(Return.Lstat){ .err = err }; @@ -4759,7 +4752,13 @@ pub const NodeFS = struct { comptime is_root: bool, ) Maybe(void) { const flags = os.O.DIRECTORY | os.O.RDONLY; - const fd = switch (Syscall.openat(if (comptime is_root) bun.toFD(std.fs.cwd().fd) else async_task.root_fd, basename, flags, 0)) { + + const atfd = if (comptime is_root) bun.toFD(std.fs.cwd().fd) else async_task.root_fd; + const fd = switch (switch (Environment.os) { + else => Syscall.openat(atfd, basename, flags, 0), + // windows bun.sys.open does not pass iterable=true, + .windows => bun.sys.openDirAtWindowsA(atfd, basename, true, false), + }) { .err => |err| { if (comptime !is_root) { switch (err.getErrno()) { @@ -4778,7 +4777,6 @@ pub const NodeFS = struct { .err = err.withPath(bun.path.joinZBuf(buf, &path_parts, .auto)), }; } - return .{ .err = err.withPath(args.path.slice()), }; @@ -5418,6 +5416,7 @@ pub const NodeFS = struct { return .{ .err = Syscall.Error{ .errno = errno, .syscall = .realpath, + .path = args.path.slice(), } }; // Seems like `rc` does not contain the errno? @@ -5668,7 +5667,7 @@ pub const NodeFS = struct { .result = .{ .stats = Stats.init(result, args.big_int) }, }, .err => |err| brk: { - if (!args.throw_if_no_entry and err.getErrno() == errno_enoent) { + if (!args.throw_if_no_entry and err.getErrno() == .NOENT) { return .{ .result = .{ .not_found = {} } }; } break :brk .{ .err = err }; diff --git a/src/string_immutable.zig b/src/string_immutable.zig index 3b55d41beb..5bc0efd5d9 100644 --- a/src/string_immutable.zig +++ b/src/string_immutable.zig @@ -1696,11 +1696,6 @@ pub fn toWDirPath(wbuf: []u16, utf8: []const u8) [:0]const u16 { pub fn assertIsValidWindowsPath(utf8: []const u8) void { if (Environment.allow_assert and Environment.isWindows) { - if (windowsPathIsPosixAbsolute(utf8)) { - std.debug.panic("Do not pass posix paths to windows APIs, was given '{s}' (missing a root like 'C:\\', see PosixToWinNormalizer for why this is an assertion)", .{ - utf8, - }); - } if (startsWith(utf8, ":/")) { std.debug.panic("Path passed to windows API '{s}' is almost certainly invalid. Where did the drive letter go?", .{ utf8, diff --git a/src/sys.zig b/src/sys.zig index 99c3274dca..3c1384b8d9 100644 --- a/src/sys.zig +++ b/src/sys.zig @@ -349,42 +349,68 @@ pub fn getErrno(rc: anytype) bun.C.E { }; } -// pub fn openOptionsFromFlagsWindows(flags: u32) windows.OpenFileOptions { -// const w = windows; -// const O = std.os.O; - -// var access_mask: w.ULONG = w.READ_CONTROL | w.FILE_WRITE_ATTRIBUTES | w.SYNCHRONIZE; -// if (flags & O.RDWR != 0) { -// access_mask |= w.GENERIC_READ | w.GENERIC_WRITE; -// } else if (flags & O.WRONLY != 0) { -// access_mask |= w.GENERIC_WRITE; -// } else { -// access_mask |= w.GENERIC_READ | w.GENERIC_WRITE; -// } - -// const filter: windows.OpenFileOptions.Filter = if (flags & O.DIRECTORY != 0) .dir_only else .file_only; -// const follow_symlinks: bool = flags & O.NOFOLLOW == 0; - -// const creation: w.ULONG = blk: { -// if (flags & O.CREAT != 0) { -// if (flags & O.EXCL != 0) { -// break :blk w.FILE_CREATE; -// } -// } -// break :blk w.FILE_OPEN; -// }; - -// return .{ -// .access_mask = access_mask, -// .io_mode = .blocking, -// .creation = creation, -// .filter = filter, -// .follow_symlinks = follow_symlinks, -// }; -// } const O = std.os.O; const w = std.os.windows; +fn normalizePathWindows( + dir_fd: bun.FileDescriptor, + path: []const u8, + buf: *bun.WPathBuffer, +) Maybe([:0]const u16) { + const slash = bun.strings.charIsAnySlash; + const ok = brk: { + var slash_or_start = true; + for (0..path.len) |i| { + // if we're starting with a dot or just saw a slash and now a dot + if (slash_or_start and path[i] == '.') { + // just '.' or ending on '/.' + if (i + 1 == path.len) break :brk false; + // starting with './' or containing '/./' + if (slash(path[i + 1])) break :brk false; + if (path.len > i + 2) { + // starting with '../'' or containing '/../' + if (path[i + 1] == '.' and slash(path[i + 2])) break :brk false; + } + } + // two slashes in a row + if (slash_or_start and slash(path[i])) break :brk false; + slash_or_start = slash(path[i]); + } + break :brk true; + }; + if (ok) { + // no need to normalize, proceed normally + return .{ + .result = bun.strings.toNTPath(buf, path), + }; + } + var buf1: bun.PathBuffer = undefined; + var buf2: bun.PathBuffer = undefined; + if (std.fs.path.isAbsoluteWindows(path)) { + const norm = bun.path.normalizeStringWindows(path, &buf1, false, false); + return .{ + .result = bun.strings.toNTPath(buf, norm), + }; + } + + const base_fd = if (dir_fd == bun.invalid_fd) + std.fs.cwd().fd + else + dir_fd.cast(); + + const base_path = w.GetFinalPathNameByHandle(base_fd, w.GetFinalPathNameByHandleFormat{}, buf) catch { + return .{ .err = .{ + .errno = @intFromEnum(bun.C.E.BADFD), + .syscall = .open, + } }; + }; + const base_count = bun.simdutf.convert.utf16.to.utf8.le(base_path, &buf1); + const norm = bun.path.joinAbsStringBuf(buf1[0..base_count], &buf2, &[_][]const u8{path}, .windows); + return .{ + .result = bun.strings.toNTPath(buf, norm), + }; +} + pub fn openDirAtWindows( dirFd: bun.FileDescriptor, path: [:0]const u16, @@ -432,7 +458,7 @@ pub fn openDirAtWindows( ); if (comptime Environment.allow_assert) { - log("NtCreateFile({d}, {s}) = {s} (dir) = {d}", .{ dirFd, bun.fmt.fmtUTF16(path), @tagName(rc), @intFromPtr(fd) }); + log("NtCreateFile({d}, {s}, iterable = {}) = {s} (dir) = {d}", .{ dirFd, bun.fmt.fmtUTF16(path), iterable, @tagName(rc), @intFromPtr(fd) }); } switch (windows.Win32Error.fromNTStatus(rc)) { @@ -468,8 +494,15 @@ pub noinline fn openDirAtWindowsA( no_follow: bool, ) Maybe(bun.FileDescriptor) { var wbuf: bun.WPathBuffer = undefined; - return openDirAtWindows(dirFd, bun.strings.toNTDir(&wbuf, path), iterable, no_follow); + + const norm = switch (normalizePathWindows(dirFd, path, &wbuf)) { + .err => |err| return .{ .err = err }, + .result => |norm| norm, + }; + + return openDirAtWindows(dirFd, norm, iterable, no_follow); } + pub fn openatWindows(dir: bun.FileDescriptor, path: []const u16, flags: bun.Mode) Maybe(bun.FileDescriptor) { const nonblock = flags & O.NONBLOCK != 0; const overwrite = flags & O.WRONLY != 0 and flags & O.APPEND == 0; diff --git a/src/sys_uv.zig b/src/sys_uv.zig index 0343c8b9a9..696302b6c5 100644 --- a/src/sys_uv.zig +++ b/src/sys_uv.zig @@ -37,12 +37,18 @@ pub const mkdirOSPath = bun.sys.mkdirOSPath; // Note: `req = undefined; req.deinit()` has a saftey-check in a debug build -pub fn open(file_path: [:0]const u8, c_flags: bun.Mode, perm: bun.Mode) Maybe(bun.FileDescriptor) { +pub fn open(file_path: [:0]const u8, c_flags: bun.Mode, _perm: bun.Mode) Maybe(bun.FileDescriptor) { var req: uv.fs_t = uv.fs_t.uninitialized; defer req.deinit(); const flags = uv.O.fromStd(c_flags); + var perm = _perm; + if (perm == 0) { + // Set a sensible default, otherwise on windows the file will be unuseable + perm = 0o644; + } + const rc = uv.uv_fs_open(uv.Loop.get(), &req, file_path.ptr, flags, perm, null); log("uv open({s}, {d}, {d}) = {d}", .{ file_path, flags, perm, rc.int() }); return if (rc.errno()) |errno| diff --git a/src/windows_c.zig b/src/windows_c.zig index 1b80ecd40d..96eeec53e7 100644 --- a/src/windows_c.zig +++ b/src/windows_c.zig @@ -765,7 +765,7 @@ pub const SystemErrno = enum(u16) { Win32Error.WSAENETUNREACH => SystemErrno.ENETUNREACH, Win32Error.WSAENOBUFS => SystemErrno.ENOBUFS, Win32Error.BAD_PATHNAME => SystemErrno.ENOENT, - Win32Error.DIRECTORY => SystemErrno.ENOENT, + Win32Error.DIRECTORY => SystemErrno.ENOTDIR, Win32Error.ENVVAR_NOT_FOUND => SystemErrno.ENOENT, Win32Error.FILE_NOT_FOUND => SystemErrno.ENOENT, Win32Error.INVALID_NAME => SystemErrno.ENOENT, diff --git a/test/harness.ts b/test/harness.ts index eedf572312..c011f28932 100644 --- a/test/harness.ts +++ b/test/harness.ts @@ -1,6 +1,7 @@ import { gc as bunGC, unsafe, which } from "bun"; import { expect } from "bun:test"; import { platform } from "os"; +import { openSync, closeSync } from "node:fs"; export const bunEnv: NodeJS.ProcessEnv = { ...process.env, @@ -258,6 +259,15 @@ export function ospath(path: string) { return path; } +export function getMaxFD(): number { + if (process.platform === "win32") { + return 0; + } + const maxFD = openSync("/dev/null", "r"); + closeSync(maxFD); + return maxFD; +} + // This is extremely frowned upon but I think it's easier to deal with than // remembering to do this manually everywhere declare global { diff --git a/test/js/node/fs/fs-stream.winlink.js b/test/js/node/fs/fs-stream.winlink.js new file mode 120000 index 0000000000..612c620e39 --- /dev/null +++ b/test/js/node/fs/fs-stream.winlink.js @@ -0,0 +1 @@ +./fs-stream.js \ No newline at end of file diff --git a/test/js/node/fs/fs.test.ts b/test/js/node/fs/fs.test.ts index 977e73acc5..41daf2136c 100644 --- a/test/js/node/fs/fs.test.ts +++ b/test/js/node/fs/fs.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from "bun:test"; import { dirname, resolve, relative } from "node:path"; import { promisify } from "node:util"; -import { bunEnv, bunExe, gc } from "harness"; +import { bunEnv, bunExe, gc, getMaxFD } from "harness"; import { isAscii } from "node:buffer"; import fs, { closeSync, @@ -48,7 +48,7 @@ import { join } from "node:path"; import { ReadStream as ReadStream_, WriteStream as WriteStream_ } from "./export-from.js"; import { ReadStream as ReadStreamStar_, WriteStream as WriteStreamStar_ } from "./export-star-from.js"; -import { SystemError, spawnSync } from "bun"; +import { SystemError, pathToFileURL, spawnSync } from "bun"; const Buffer = globalThis.Buffer || Uint8Array; @@ -582,9 +582,10 @@ describe("promises.readFile", async () => { }); it("promises.readFile - UTF16 file path", async () => { - const dest = `/tmp/superduperduperdupduperdupersuperduperduperduperduperduperdupersuperduperduperduperduperduperdupersuperduperduperdupe-Bun-👍-${Date.now()}-${ + const filename = `superduperduperdupduperdupersuperduperduperduperduperduperdupersuperduperduperduperduperduperdupersuperduperduperdupe-Bun-👍-${Date.now()}-${ (Math.random() * 1024000) | 0 }.txt`; + const dest = join(tmpdir(), filename); await fs.promises.copyFile(import.meta.path, dest); const expected = readFileSync(import.meta.path, "utf-8"); Bun.gc(true); @@ -595,9 +596,10 @@ it("promises.readFile - UTF16 file path", async () => { }); it("promises.readFile - atomized file path", async () => { - const destInput = `/tmp/superduperduperdupduperdupersuperduperduperduperduperduperdupersuperduperduperduperduperduperdupersuperduperduperdupe-Bun-👍-${Date.now()}-${ + const filename = `superduperduperdupduperdupersuperduperduperduperduperduperdupersuperduperduperduperduperduperdupersuperduperduperdupe-Bun-👍-${Date.now()}-${ (Math.random() * 1024000) | 0 }.txt`; + const destInput = join(tmpdir(), filename); // Force it to become an atomized string by making it a property access const dest: string = ( { @@ -727,14 +729,14 @@ it("mkdtempSync() non-exist dir #2568", () => { try { expect(mkdtempSync("/tmp/hello/world")).toBeFalsy(); } catch (err: any) { - expect(err?.errno).toBe(process.platform === "win32" ? -4058 : -2); + expect(err?.errno).toBe(-2); } }); it("mkdtemp() non-exist dir #2568", done => { mkdtemp("/tmp/hello/world", (err, folder) => { try { - expect(err?.errno).toBe(process.platform === "win32" ? -4058 : -2); + expect(err?.errno).toBe(-2); expect(folder).toBeUndefined(); done(); } catch (e) { @@ -785,7 +787,9 @@ it("readdirSync throws when given a path that doesn't exist", () => { readdirSync(import.meta.path + "/does-not-exist/really"); throw new Error("should not get here"); } catch (exception: any) { - expect(exception.name).toBe("ENOTDIR"); + // the correct error to return in this case is actually ENOENT (which we do on windows), + // but on posix we return ENOTDIR + expect(exception.name).toMatch(/ENOTDIR|ENOENT/); } }); @@ -907,7 +911,7 @@ it("readvSync", () => { }); it("preadv", () => { - var fd = openSync(`${tmpdir()}/preadv.txt`, "w"); + var fd = openSync(join(tmpdir(), "preadv.txt"), "w"); fs.ftruncateSync(fd, 0); const buf = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); @@ -991,17 +995,18 @@ describe("readFileSync", () => { gc(); const outpath = join(tmpdir(), "read file sync with space characters " + Math.random().toString(32) + " .txt"); await Bun.write(outpath, Bun.file(Bun.fileURLToPath(new URL("./readFileSync.txt", import.meta.url)))); - const text = readFileSync(new URL(outpath, import.meta.url), "utf8"); + // on windows constructing a file url from an absolute path containing a drive letter will not add the "file:///" prefix + // node.js has the same behavior, not sure what makes the most sense here + const url = isWindows ? new URL("file:///" + outpath) : new URL(outpath, import.meta.url); + const text = readFileSync(url, "utf8"); gc(); expect(text).toBe("File read successfully"); }); - it("works with special files in the filesystem", () => { - { - const text = readFileSync("/dev/null", "utf8"); - gc(); - expect(text).toBe(""); - } + it.skipIf(isWindows)("works with special files in the filesystem", () => { + const text = readFileSync("/dev/null", "utf8"); + gc(); + expect(text).toBe(""); if (process.platform === "linux") { const text = readFileSync("/proc/filesystems"); @@ -1062,7 +1067,7 @@ describe("writeFileSync", () => { const path = `${tmpdir()}/${Date.now()}.writeFileSyncWithMode.txt`; writeFileSync(path, "bun", { mode: 33188 }); const stat = fs.statSync(path); - expect(stat.mode).toBe(process.platform === "win32" ? 33206 : 33188); + expect(stat.mode).toBe(isWindows ? 33206 : 33188); }); it("returning Buffer works", () => { const buffer = new Buffer([ @@ -1102,7 +1107,7 @@ function triggerDOMJIT(target: fs.Stats, fn: (..._: any[]) => any, result: any) describe("lstat", () => { it("file metadata is correct", () => { - const fileStats = lstatSync(new URL("./fs-stream.js", import.meta.url).toString().slice("file://".length - 1)); + const fileStats = lstatSync(join(import.meta.dir, "fs-stream.js")); expect(fileStats.isSymbolicLink()).toBe(false); expect(fileStats.isFile()).toBe(true); expect(fileStats.isDirectory()).toBe(false); @@ -1113,7 +1118,8 @@ describe("lstat", () => { }); it("folder metadata is correct", () => { - const fileStats = lstatSync(new URL("../../../../test", import.meta.url).toString().slice("file://".length - 1)); + const path = join(import.meta.dir, "../../../../test"); + const fileStats = lstatSync(path); expect(fileStats.isSymbolicLink()).toBe(false); expect(fileStats.isFile()).toBe(false); expect(fileStats.isDirectory()).toBe(true); @@ -1124,7 +1130,9 @@ describe("lstat", () => { }); it("symlink metadata is correct", () => { - const linkStats = lstatSync(new URL("./fs-stream.link.js", import.meta.url).toString().slice("file://".length - 1)); + const link = join(tmpdir(), `fs-stream.link${Math.random().toString(32)}.js`); + symlinkSync(join(import.meta.dir, "fs-stream.js"), link); + const linkStats = lstatSync(link); expect(linkStats.isSymbolicLink()).toBe(true); expect(linkStats.isFile()).toBe(false); expect(linkStats.isDirectory()).toBe(false); @@ -1189,7 +1197,7 @@ it("realpath async", async () => { describe("stat", () => { it("file metadata is correct", () => { - const fileStats = statSync(new URL("./fs-stream.js", import.meta.url).toString().slice("file://".length - 1)); + const fileStats = statSync(join(import.meta.dir, "fs-stream.js")); expect(fileStats.isSymbolicLink()).toBe(false); expect(fileStats.isFile()).toBe(true); expect(fileStats.isDirectory()).toBe(false); @@ -1200,7 +1208,8 @@ describe("stat", () => { }); it("folder metadata is correct", () => { - const fileStats = statSync(new URL("../../../../test", import.meta.url).toString().slice("file://".length - 1)); + const path = join(import.meta.dir, "../../../../test"); + const fileStats = statSync(path); expect(fileStats.isSymbolicLink()).toBe(false); expect(fileStats.isFile()).toBe(false); expect(fileStats.isDirectory()).toBe(true); @@ -2002,22 +2011,22 @@ describe("fs/promises", () => { }, 100000); for (let withFileTypes of [false, true] as const) { + const iterCount = 100; const doIt = async () => { - const maxFD = openSync("/dev/null", "r"); - closeSync(maxFD); + const maxFD = getMaxFD(); const full = resolve(import.meta.dir, "../"); - const pending = new Array(100); - for (let i = 0; i < 100; i++) { + const pending = new Array(iterCount); + for (let i = 0; i < iterCount; i++) { pending[i] = promises.readdir(full, { recursive: true, withFileTypes }); } const results = await Promise.all(pending); - for (let i = 0; i < 100; i++) { + for (let i = 0; i < iterCount; i++) { results[i].sort(); } expect(results[0].length).toBeGreaterThan(0); - for (let i = 1; i < 100; i++) { + for (let i = 1; i < iterCount; i++) { expect(results[i]).toEqual(results[0]); } @@ -2025,29 +2034,28 @@ describe("fs/promises", () => { expect(results[0]).toContain(relative(full, import.meta.path)); } - const newMaxFD = openSync("/dev/null", "r"); - closeSync(newMaxFD); + const newMaxFD = getMaxFD(); expect(maxFD).toBe(newMaxFD); // assert we do not leak file descriptors }; const fail = async () => { - const maxFD = openSync("/dev/null", "r"); - closeSync(maxFD); + const notfound = isWindows ? "C:\\notfound\\for\\sure" : "/notfound/for/sure"; - const pending = new Array(100); - for (let i = 0; i < 100; i++) { - pending[i] = promises.readdir("/notfound/i/dont/exist/for/sure/" + i, { recursive: true, withFileTypes }); + const maxFD = getMaxFD(); + + const pending = new Array(iterCount); + for (let i = 0; i < iterCount; i++) { + pending[i] = promises.readdir(join(notfound, i), { recursive: true, withFileTypes }); } const results = await Promise.allSettled(pending); - for (let i = 0; i < 100; i++) { + for (let i = 0; i < iterCount; i++) { expect(results[i].status).toBe("rejected"); expect(results[i].reason!.code).toBe("ENOENT"); - expect(results[i].reason!.path).toBe("/notfound/i/dont/exist/for/sure/" + i); + expect(results[i].reason!.path).toBe(join(notfound, i)); } - const newMaxFD = openSync("/dev/null", "r"); - closeSync(newMaxFD); + const newMaxFD = getMaxFD(); expect(maxFD).toBe(newMaxFD); // assert we do not leak file descriptors }; @@ -2325,7 +2333,7 @@ describe("utimesSync", () => { expect(finalStats.atime).toEqual(prevAccessTime); }); - it("works after 2038", () => { + it.skipIf(isWindows)("works after 2038", () => { const tmp = join(tmpdir(), "utimesSync-test-file-" + Math.random().toString(36).slice(2)); writeFileSync(tmp, "test"); const prevStats = fs.statSync(tmp); @@ -2345,7 +2353,6 @@ describe("utimesSync", () => { fs.utimesSync(tmp, newAccessTime, newModifiedTime); const newStats = fs.statSync(tmp); - console.log(newStats); expect(newStats.mtime).toEqual(newModifiedTime); expect(newStats.atime).toEqual(newAccessTime); @@ -2640,8 +2647,8 @@ it("new Stats", () => { }); it("BigIntStats", () => { - const withoutBigInt = statSync(__filename, { bigint: false }); - const withBigInt = statSync(__filename, { bigint: true }); + const withoutBigInt = statSync(import.meta.path, { bigint: false }); + const withBigInt = statSync(import.meta.path, { bigint: true }); expect(withoutBigInt.isFile() === withBigInt.isFile()).toBe(true); expect(withoutBigInt.isDirectory() === withBigInt.isDirectory()).toBe(true); @@ -2695,7 +2702,7 @@ it("test syscall errno, issue#4198", () => { { "darwin": "Operation not permitted", "linux": "Is a directory", - "windows": "lol", + "win32": "Operation not permitted", } as any )[process.platform], );