aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/lib/tinyio/test.zig
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-09-10 21:51:57 +0300
committerGitHub <noreply@github.com>2026-09-10 21:51:57 +0300
commit425c86179791cd32e2d4ce0ae7f269940b4b0663 (patch)
treee52c4383a6d69216cbe2a32638bf90841d7e2189 /vendor/libghostty-vt/src/lib/tinyio/test.zig
parent90e947a6e26e02f10ec7f1694c4eb5b78a86406b (diff)
chore: upgrade libghostty and preserve terminal behavior (#3906)
Diffstat (limited to 'vendor/libghostty-vt/src/lib/tinyio/test.zig')
-rw-r--r--vendor/libghostty-vt/src/lib/tinyio/test.zig836
1 files changed, 836 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/lib/tinyio/test.zig b/vendor/libghostty-vt/src/lib/tinyio/test.zig
new file mode 100644
index 00000000..c6b4d96f
--- /dev/null
+++ b/vendor/libghostty-vt/src/lib/tinyio/test.zig
@@ -0,0 +1,836 @@
+//! Tests for TinyIo that only touch its public surface, so they run on
+//! every supported platform. Platform-specific tests live next to the
+//! platform arm they exercise.
+
+const std = @import("std");
+const builtin = @import("builtin");
+const posix = std.posix;
+const Io = std.Io;
+const File = Io.File;
+const Dir = Io.Dir;
+const Threaded = std.Io.Threaded;
+const TinyIo = @import("../TinyIo.zig");
+const supported = TinyIo.supported;
+
+const is_windows = builtin.os.tag == .windows;
+const windows = std.os.windows;
+const os_windows = @import("../../os/windows.zig");
+const ntdll = os_windows.exp.ntdll;
+const kernel32 = os_windows.exp.kernel32;
+const HANDLE = windows.HANDLE;
+
+/// Reads until every buffer is full or the stream ends. POSIX readv fills
+/// all buffers in one call; NT reads one buffer per call.
+fn readStreamingAll(file: File, io_: Io, buffers: []const []u8) !usize {
+ var remaining: [8][]u8 = undefined;
+ std.debug.assert(buffers.len <= remaining.len);
+ @memcpy(remaining[0..buffers.len], buffers);
+ var list: [][]u8 = remaining[0..buffers.len];
+
+ var total: usize = 0;
+ while (list.len > 0) {
+ var n = file.readStreaming(io_, list) catch |err| switch (err) {
+ error.EndOfStream => break,
+ else => return err,
+ };
+ if (n == 0) break;
+ total += n;
+ while (list.len > 0 and n >= list[0].len) {
+ n -= list[0].len;
+ list = list[1..];
+ }
+ if (list.len > 0) list[0] = list[0][n..];
+ }
+ return total;
+}
+
+test "read a file through File.Reader" {
+ if (comptime !supported) return error.SkipZigTest;
+ const tio: TinyIo = .init;
+ const test_io = tio.io();
+ const testing = std.testing;
+
+ var tmp_dir = testing.tmpDir(.{});
+ defer tmp_dir.cleanup();
+
+ const contents = "hello minimal test_io\n" ** 100;
+ try tmp_dir.dir.writeFile(testing.io, .{
+ .sub_path = "test.txt",
+ .data = contents,
+ });
+
+ // Open through our Io. The Dir handle is a plain fd, so it is usable
+ // across Io implementations.
+ const dir: Dir = .{ .handle = tmp_dir.dir.handle };
+ var file = try dir.openFile(test_io, "test.txt", .{});
+ defer file.close(test_io);
+
+ // Stat through our Io.
+ const stat = try file.stat(test_io);
+ try testing.expectEqual(@as(u64, contents.len), stat.size);
+ try testing.expectEqual(File.Kind.file, stat.kind);
+
+ // fileLength.
+ try testing.expectEqual(@as(u64, contents.len), try file.length(test_io));
+
+ // Read it all back through File.Reader (exercises positional reads
+ // and streaming fallbacks).
+ var buf: [64]u8 = undefined;
+ var reader = file.reader(test_io, &buf);
+ var list: std.ArrayList(u8) = .empty;
+ defer list.deinit(testing.allocator);
+ try reader.interface.appendRemaining(testing.allocator, &list, .unlimited);
+ try testing.expectEqualStrings(contents, list.items);
+}
+
+test "seek" {
+ if (comptime !supported) return error.SkipZigTest;
+ const tio: TinyIo = .init;
+ const test_io = tio.io();
+ const testing = std.testing;
+
+ var tmp_dir = testing.tmpDir(.{});
+ defer tmp_dir.cleanup();
+
+ try tmp_dir.dir.writeFile(testing.io, .{
+ .sub_path = "seek.txt",
+ .data = "0123456789",
+ });
+
+ const dir: Dir = .{ .handle = tmp_dir.dir.handle };
+ var file = try dir.openFile(test_io, "seek.txt", .{});
+ defer file.close(test_io);
+
+ // Exercise our seek and streaming read implementations directly at
+ // the vtable level; the higher-level File.Reader seek plumbing has
+ // its own buffering behaviors that are independent of the Io
+ // implementation.
+ var out: [4]u8 = undefined;
+ var slices = [_][]u8{&out};
+
+ try test_io.vtable.fileSeekTo(test_io.userdata, file, 6);
+ var n = try file.readStreaming(test_io, &slices);
+ try testing.expectEqualStrings("6789", out[0..n]);
+
+ // Seek back relative and re-read.
+ try test_io.vtable.fileSeekBy(test_io.userdata, file, -8);
+ n = try file.readStreaming(test_io, &slices);
+ try testing.expectEqualStrings("2345", out[0..n]);
+
+ // Positional reads are independent of the seek position.
+ var pslices = [_][]u8{&out};
+ n = try test_io.vtable.fileReadPositional(test_io.userdata, file, &pslices, 1);
+ try testing.expectEqualStrings("1234", out[0..n]);
+}
+
+test "realPath and deleteFile" {
+ if (comptime !supported) return error.SkipZigTest;
+ const tio: TinyIo = .init;
+ const test_io = tio.io();
+ const testing = std.testing;
+
+ // Only platforms with a real implementation.
+ switch (builtin.os.tag) {
+ .macos, .ios, .linux, .freebsd, .windows => {},
+ else => return error.SkipZigTest,
+ }
+
+ var tmp_dir = testing.tmpDir(.{});
+ defer tmp_dir.cleanup();
+
+ try tmp_dir.dir.writeFile(testing.io, .{
+ .sub_path = "real.txt",
+ .data = "x",
+ });
+
+ const dir: Dir = .{ .handle = tmp_dir.dir.handle };
+ var file = try dir.openFile(test_io, "real.txt", .{});
+ var path_buf: [std.fs.max_path_bytes]u8 = undefined;
+ const path = path_buf[0..try file.realPath(test_io, &path_buf)];
+ try testing.expect(std.mem.endsWith(u8, path, "real.txt"));
+ file.close(test_io);
+
+ // dirRealPathFile via absolute path from cwd.
+ var path_buf2: [std.fs.max_path_bytes]u8 = undefined;
+ const path2 = path_buf2[0..try Dir.cwd().realPathFile(test_io, path, &path_buf2)];
+ try testing.expectEqualStrings(path, path2);
+
+ // Delete it through our Io, verify it is gone.
+ try dir.deleteFile(test_io, "real.txt");
+ try testing.expectError(error.FileNotFound, dir.openFile(test_io, "real.txt", .{}));
+}
+
+test "openFile of a directory returns IsDir" {
+ if (comptime !supported) return error.SkipZigTest;
+ const tio: TinyIo = .init;
+ const test_io = tio.io();
+ const testing = std.testing;
+
+ var tmp_dir = testing.tmpDir(.{});
+ defer tmp_dir.cleanup();
+
+ try tmp_dir.dir.createDir(testing.io, "sub", .default_dir);
+ const dir: Dir = .{ .handle = tmp_dir.dir.handle };
+ try testing.expectError(error.IsDir, dir.openFile(test_io, "sub", .{
+ .allow_directory = false,
+ }));
+}
+
+test "Io.Mutex through TinyIo" {
+ if (comptime !supported) return error.SkipZigTest;
+ const tio: TinyIo = .init;
+ const test_io = tio.io();
+ // Contended and uncontended lock/unlock; exercises the futex ops.
+ var mutex: Io.Mutex = .init;
+ mutex.lockUncancelable(test_io);
+ mutex.unlock(test_io);
+
+ // Wake with no waiters must be a no-op.
+ var word: u32 = 0;
+ test_io.vtable.futexWake(test_io.userdata, &word, 1);
+
+ // Wait with a non-matching expected value must return immediately.
+ test_io.vtable.futexWaitUncancelable(test_io.userdata, &word, 1);
+}
+
+test "randomSecure fills with fresh entropy" {
+ if (comptime !supported) return error.SkipZigTest;
+ const tio: TinyIo = .init;
+ const test_io = tio.io();
+ const testing = std.testing;
+
+ var a: [32]u8 = @splat(0);
+ var b: [32]u8 = @splat(0);
+ try test_io.randomSecure(&a);
+ try test_io.randomSecure(&b);
+
+ // Non-zero and non-repeating. A zero fill is what `random` does
+ // without a source, which would make every one-time password the
+ // same; identical draws would mean the same thing.
+ try testing.expect(!std.mem.allEqual(u8, &a, 0));
+ try testing.expect(!std.mem.allEqual(u8, &b, 0));
+ try testing.expect(!std.mem.eql(u8, &a, &b));
+
+ // Zero-length is a no-op.
+ try test_io.randomSecure(a[0..0]);
+}
+
+test "unused operations fail gracefully" {
+ if (comptime !supported) return error.SkipZigTest;
+ const tio: TinyIo = .init;
+ const test_io = tio.io();
+ const testing = std.testing;
+
+ var tmp_dir = testing.tmpDir(.{});
+ defer tmp_dir.cleanup();
+
+ const dir: Dir = .{ .handle = tmp_dir.dir.handle };
+ try testing.expectError(
+ error.NoSpaceLeft,
+ dir.createFile(test_io, "nope.txt", .{}),
+ );
+}
+
+test "openFile edge cases" {
+ if (comptime !supported) return error.SkipZigTest;
+ const tio: TinyIo = .init;
+ const test_io = tio.io();
+ const testing = std.testing;
+
+ var tmp_dir = testing.tmpDir(.{});
+ defer tmp_dir.cleanup();
+ try tmp_dir.dir.writeFile(testing.io, .{
+ .sub_path = "edge.txt",
+ .data = "edge",
+ });
+ const dir: Dir = .{ .handle = tmp_dir.dir.handle };
+
+ // File locking is unimplemented and must be reported, not ignored.
+ try testing.expectError(error.FileLocksUnsupported, dir.openFile(
+ test_io,
+ "edge.txt",
+ .{ .lock = .shared },
+ ));
+
+ // NUL bytes never form a valid path.
+ try testing.expectError(error.BadPathName, dir.openFile(
+ test_io,
+ "bad\x00path",
+ .{},
+ ));
+
+ // Paths that can't fit in PATH_MAX must not be silently truncated.
+ const long_name = "a" ** (std.fs.max_path_bytes + 1);
+ try testing.expectError(error.NameTooLong, dir.openFile(
+ test_io,
+ long_name,
+ .{},
+ ));
+
+ // A path component that is a file, not a directory. NT reports this
+ // as the path not existing (Threaded does too) rather than ENOTDIR.
+ try testing.expectError(
+ if (comptime is_windows) error.FileNotFound else error.NotDir,
+ dir.openFile(test_io, "edge.txt/child", .{}),
+ );
+
+ // Directories may be opened when allowed (the default) and stat
+ // reports their kind.
+ try tmp_dir.dir.createDir(testing.io, "subdir", .default_dir);
+ var dir_file = try dir.openFile(test_io, "subdir", .{});
+ const dir_stat = try dir_file.stat(test_io);
+ try testing.expectEqual(File.Kind.directory, dir_stat.kind);
+ dir_file.close(test_io);
+
+ // Write modes translate to the right ACCMODE flags; TinyIo can't
+ // write but opening for write must succeed.
+ var wfile = try dir.openFile(test_io, "edge.txt", .{ .mode = .write_only });
+ wfile.close(test_io);
+ var rwfile = try dir.openFile(test_io, "edge.txt", .{ .mode = .read_write });
+ rwfile.close(test_io);
+}
+
+test "openFile symlink handling" {
+ if (comptime !supported) return error.SkipZigTest;
+ const tio: TinyIo = .init;
+ const test_io = tio.io();
+ const testing = std.testing;
+
+ // Platforms where we know both symlink creation (via the testing Io)
+ // and O_NOFOLLOW behave as expected.
+ switch (builtin.os.tag) {
+ .macos, .ios, .linux, .freebsd => {},
+ else => return error.SkipZigTest,
+ }
+
+ var tmp_dir = testing.tmpDir(.{});
+ defer tmp_dir.cleanup();
+ try tmp_dir.dir.writeFile(testing.io, .{
+ .sub_path = "target.txt",
+ .data = "target",
+ });
+ try tmp_dir.dir.symLink(testing.io, "target.txt", "link.txt", .{});
+ const dir: Dir = .{ .handle = tmp_dir.dir.handle };
+
+ // Following symlinks (the default) opens the target...
+ var file = try dir.openFile(test_io, "link.txt", .{});
+ try testing.expectEqual(@as(u64, "target".len), try file.length(test_io));
+
+ // ...and realPath resolves through the link to the target. This is
+ // the property the Kitty graphics path validation relies on.
+ var path_buf: [std.fs.max_path_bytes]u8 = undefined;
+ const path = path_buf[0..try file.realPath(test_io, &path_buf)];
+ try testing.expect(std.mem.endsWith(u8, path, "target.txt"));
+ file.close(test_io);
+
+ // Refusing to follow symlinks fails with SymLinkLoop.
+ try testing.expectError(error.SymLinkLoop, dir.openFile(
+ test_io,
+ "link.txt",
+ .{ .follow_symlinks = false },
+ ));
+}
+
+test "positional reads at and beyond EOF return zero" {
+ if (comptime !supported) return error.SkipZigTest;
+ const tio: TinyIo = .init;
+ const test_io = tio.io();
+ const testing = std.testing;
+
+ var tmp_dir = testing.tmpDir(.{});
+ defer tmp_dir.cleanup();
+ try tmp_dir.dir.writeFile(testing.io, .{
+ .sub_path = "eof.txt",
+ .data = "0123456789",
+ });
+ const dir: Dir = .{ .handle = tmp_dir.dir.handle };
+ var file = try dir.openFile(test_io, "eof.txt", .{});
+ defer file.close(test_io);
+
+ var out: [4]u8 = undefined;
+ var slices = [_][]u8{&out};
+
+ // At EOF and past EOF: the vtable contract is "returns 0 if reading
+ // at or past the end".
+ try testing.expectEqual(@as(usize, 0), try test_io.vtable.fileReadPositional(
+ test_io.userdata,
+ file,
+ &slices,
+ 10,
+ ));
+ try testing.expectEqual(@as(usize, 0), try test_io.vtable.fileReadPositional(
+ test_io.userdata,
+ file,
+ &slices,
+ 9999,
+ ));
+
+ // No buffers and only-empty buffers read nothing without a syscall.
+ try testing.expectEqual(@as(usize, 0), try test_io.vtable.fileReadPositional(
+ test_io.userdata,
+ file,
+ &.{},
+ 0,
+ ));
+ var empty = [_][]u8{ &.{}, &.{} };
+ try testing.expectEqual(@as(usize, 0), try test_io.vtable.fileReadPositional(
+ test_io.userdata,
+ file,
+ &empty,
+ 0,
+ ));
+}
+
+test "vectored reads scatter across buffers" {
+ if (comptime !supported) return error.SkipZigTest;
+ const tio: TinyIo = .init;
+ const test_io = tio.io();
+ const testing = std.testing;
+
+ var tmp_dir = testing.tmpDir(.{});
+ defer tmp_dir.cleanup();
+ try tmp_dir.dir.writeFile(testing.io, .{
+ .sub_path = "vec.txt",
+ .data = "0123456789abcdef",
+ });
+ const dir: Dir = .{ .handle = tmp_dir.dir.handle };
+ var file = try dir.openFile(test_io, "vec.txt", .{});
+ defer file.close(test_io);
+
+ // Scatter a positional read across multiple buffers, with empty
+ // buffers interleaved (they must be skipped).
+ var a: [4]u8 = undefined;
+ var b: [2]u8 = undefined;
+ var c: [6]u8 = undefined;
+ var slices = [_][]u8{ &a, &.{}, &b, &c };
+ const n = try test_io.vtable.fileReadPositional(
+ test_io.userdata,
+ file,
+ &slices,
+ 0,
+ );
+ try testing.expectEqual(@as(usize, 12), n);
+ try testing.expectEqualStrings("0123", &a);
+ try testing.expectEqualStrings("45", &b);
+ try testing.expectEqualStrings("6789ab", &c);
+
+ // More buffers than max_iovecs_len: reads are truncated to the
+ // first max_iovecs_len non-empty buffers (partial reads are allowed
+ // by the vtable contract; callers retry).
+ comptime std.debug.assert(Threaded.max_iovecs_len < 16);
+ var bytes: [16][1]u8 = undefined;
+ var many: [16][]u8 = undefined;
+ for (&many, &bytes) |*s, *byte| s.* = byte;
+ const n2 = try test_io.vtable.fileReadPositional(
+ test_io.userdata,
+ file,
+ &many,
+ 0,
+ );
+ try testing.expectEqual(@as(usize, Threaded.max_iovecs_len), n2);
+ for (bytes[0..n2], "0123456789abcdef"[0..n2]) |got, want| {
+ try testing.expectEqual(want, got[0]);
+ }
+}
+
+test "streaming reads: EndOfStream and scatter" {
+ if (comptime !supported) return error.SkipZigTest;
+ const tio: TinyIo = .init;
+ const test_io = tio.io();
+ const testing = std.testing;
+
+ var tmp_dir = testing.tmpDir(.{});
+ defer tmp_dir.cleanup();
+ try tmp_dir.dir.writeFile(testing.io, .{
+ .sub_path = "stream.txt",
+ .data = "streaming!",
+ });
+ const dir: Dir = .{ .handle = tmp_dir.dir.handle };
+ var file = try dir.openFile(test_io, "stream.txt", .{});
+ defer file.close(test_io);
+
+ // Scatter a streaming read (one readv on POSIX, one NtReadFile per
+ // buffer on Windows).
+ var a: [6]u8 = undefined;
+ var b: [4]u8 = undefined;
+ var slices = [_][]u8{ &a, &b };
+ try testing.expectEqual(@as(usize, 10), try readStreamingAll(file, test_io, &slices));
+ try testing.expectEqualStrings("stream", &a);
+ try testing.expectEqualStrings("ing!", &b);
+
+ // Reading again at EOF is a stream end, not a zero-length success.
+ try testing.expectError(error.EndOfStream, file.readStreaming(test_io, &slices));
+
+ // Empty destinations read nothing.
+ try testing.expectEqual(@as(usize, 0), try file.readStreaming(test_io, &.{}));
+
+ // Seeking beyond EOF is legal; the next streaming read hits EOF.
+ try test_io.vtable.fileSeekTo(test_io.userdata, file, 9999);
+ try testing.expectError(error.EndOfStream, file.readStreaming(test_io, &slices));
+
+ // And seeking back to zero re-reads from the start.
+ try test_io.vtable.fileSeekTo(test_io.userdata, file, 0);
+ try testing.expectEqual(@as(usize, 10), try readStreamingAll(file, test_io, &slices));
+ try testing.expectEqualStrings("stream", &a);
+}
+
+test "pipes: streaming works; positional and seek are Unseekable or ignored" {
+ if (comptime !supported) return error.SkipZigTest;
+ const tio: TinyIo = .init;
+ const test_io = tio.io();
+ const testing = std.testing;
+
+ const msg = "through the pipe";
+ const read_end: File = if (comptime is_windows) pipe: {
+ var read_h: HANDLE = undefined;
+ var write_h: HANDLE = undefined;
+ if (!kernel32.CreatePipe(&read_h, &write_h, null, 0).toBool()) {
+ return error.SkipZigTest;
+ }
+ defer _ = ntdll.NtClose(write_h);
+ // Twice, so a second read after the streaming one has data.
+ for (0..2) |_| {
+ var iosb: os_windows.IO_STATUS_BLOCK = undefined;
+ try testing.expectEqual(.SUCCESS, windows.ntdll.NtWriteFile(
+ write_h,
+ null,
+ null,
+ null,
+ &iosb,
+ msg.ptr,
+ msg.len,
+ null,
+ null,
+ ));
+ }
+ break :pipe .{ .handle = read_h, .flags = .{ .nonblocking = false } };
+ } else pipe: {
+ var fds: [2]posix.fd_t = undefined;
+ switch (posix.errno(posix.system.pipe(&fds))) {
+ .SUCCESS => {},
+ else => return error.SkipZigTest,
+ }
+ defer _ = posix.system.close(fds[1]);
+ try testing.expectEqual(
+ @as(isize, msg.len),
+ @as(isize, @intCast(posix.system.write(fds[1], msg, msg.len))),
+ );
+ break :pipe .{ .handle = fds[0], .flags = .{ .nonblocking = false } };
+ };
+ defer read_end.close(test_io);
+
+ // Streaming reads work on unseekable files; this is the fallback
+ // File.Reader depends on when positional reads report Unseekable.
+ var buf: [msg.len]u8 = undefined;
+ var slices = [_][]u8{&buf};
+ try testing.expectEqual(@as(usize, msg.len), try read_end.readStreaming(test_io, &slices));
+ try testing.expectEqualStrings(msg, &buf);
+
+ if (comptime is_windows) {
+ // NT pipes accept a byte offset and a file position but ignore
+ // both: reads stay sequential and seeks succeed without effect.
+ // Threaded behaves the same way, so TinyIo mirrors it rather than
+ // spending a syscall per read to detect pipes.
+ @memset(&buf, 0);
+ try testing.expectEqual(@as(usize, msg.len), try test_io.vtable.fileReadPositional(
+ test_io.userdata,
+ read_end,
+ &slices,
+ 8,
+ ));
+ try testing.expectEqualStrings(msg, &buf);
+ try test_io.vtable.fileSeekTo(test_io.userdata, read_end, 0);
+ try test_io.vtable.fileSeekBy(test_io.userdata, read_end, 1);
+
+ // Drained with the writer closed is the end of the stream.
+ try testing.expectError(error.EndOfStream, read_end.readStreaming(test_io, &slices));
+ return;
+ }
+
+ // Positional reads and seeks must report Unseekable so callers can
+ // fall back to streaming.
+ try testing.expectError(error.Unseekable, test_io.vtable.fileReadPositional(
+ test_io.userdata,
+ read_end,
+ &slices,
+ 0,
+ ));
+ try testing.expectError(error.Unseekable, test_io.vtable.fileSeekTo(
+ test_io.userdata,
+ read_end,
+ 0,
+ ));
+ try testing.expectError(error.Unseekable, test_io.vtable.fileSeekBy(
+ test_io.userdata,
+ read_end,
+ 1,
+ ));
+}
+
+test "operate delegates non-read operations to failing stubs" {
+ if (comptime !supported) return error.SkipZigTest;
+ const tio: TinyIo = .init;
+ const test_io = tio.io();
+ const testing = std.testing;
+
+ var tmp_dir = testing.tmpDir(.{});
+ defer tmp_dir.cleanup();
+ try tmp_dir.dir.writeFile(testing.io, .{
+ .sub_path = "w.txt",
+ .data = "x",
+ });
+ const dir: Dir = .{ .handle = tmp_dir.dir.handle };
+ var file = try dir.openFile(test_io, "w.txt", .{ .mode = .write_only });
+ defer file.close(test_io);
+
+ const result = try test_io.vtable.operate(test_io.userdata, .{
+ .file_write_streaming = .{
+ .file = file,
+ .data = &.{"nope"},
+ },
+ });
+ try testing.expectError(error.InputOutput, result.file_write_streaming);
+}
+
+test "dirRealPathFile edge cases" {
+ if (comptime !supported) return error.SkipZigTest;
+ const tio: TinyIo = .init;
+ const test_io = tio.io();
+ const testing = std.testing;
+
+ switch (builtin.os.tag) {
+ .macos, .ios, .linux, .freebsd, .windows => {},
+ else => return error.SkipZigTest,
+ }
+
+ var tmp_dir = testing.tmpDir(.{});
+ defer tmp_dir.cleanup();
+ try tmp_dir.dir.writeFile(testing.io, .{
+ .sub_path = "real.txt",
+ .data = "x",
+ });
+ const dir: Dir = .{ .handle = tmp_dir.dir.handle };
+
+ // Resolve the canonical path through an open file for reference.
+ var file = try dir.openFile(test_io, "real.txt", .{});
+ var want_buf: [std.fs.max_path_bytes]u8 = undefined;
+ const want = want_buf[0..try file.realPath(test_io, &want_buf)];
+ file.close(test_io);
+
+ // A non-cwd directory handle exercises the open-then-resolve
+ // fallback branch rather than libc realpath.
+ var got_buf: [std.fs.max_path_bytes]u8 = undefined;
+ const got = got_buf[0..try test_io.vtable.dirRealPathFile(
+ test_io.userdata,
+ dir,
+ "real.txt",
+ &got_buf,
+ )];
+ try testing.expectEqualStrings(want, got);
+
+ // Missing paths report FileNotFound (libc realpath branch, via an
+ // absolute path anchored at cwd).
+ var missing_buf: [std.fs.max_path_bytes]u8 = undefined;
+ const missing = std.fmt.bufPrint(&missing_buf, "{s}.missing", .{want}) catch
+ return error.SkipZigTest;
+ var out_buf: [std.fs.max_path_bytes]u8 = undefined;
+ try testing.expectError(error.FileNotFound, Dir.cwd().realPathFile(
+ test_io,
+ missing,
+ &out_buf,
+ ));
+
+ // libc realpath requires a PATH_MAX-sized output buffer; smaller
+ // buffers must error rather than risk truncation.
+ var small_buf: [8]u8 = undefined;
+ try testing.expectError(error.NameTooLong, Dir.cwd().realPathFile(
+ test_io,
+ want,
+ &small_buf,
+ ));
+}
+
+test "deleteFile edge cases" {
+ if (comptime !supported) return error.SkipZigTest;
+ const tio: TinyIo = .init;
+ const test_io = tio.io();
+ const testing = std.testing;
+
+ var tmp_dir = testing.tmpDir(.{});
+ defer tmp_dir.cleanup();
+ const dir: Dir = .{ .handle = tmp_dir.dir.handle };
+
+ // Nonexistent files.
+ try testing.expectError(error.FileNotFound, dir.deleteFile(test_io, "missing.txt"));
+
+ // NUL bytes never form a valid POSIX path.
+ try testing.expectError(error.BadPathName, dir.deleteFile(test_io, "bad\x00path"));
+
+ // Deleting a directory reports IsDir. On BSD-derived systems
+ // (including macOS) unlink returns EPERM for directories, which
+ // exercises the stat-based disambiguation path.
+ try tmp_dir.dir.createDir(testing.io, "subdir", .default_dir);
+ try testing.expectError(error.IsDir, dir.deleteFile(test_io, "subdir"));
+
+ // Deleting a symlink removes the link, not its target. Creating one
+ // on Windows needs a privilege that plain users and CI lack.
+ try tmp_dir.dir.writeFile(testing.io, .{
+ .sub_path = "target.txt",
+ .data = "x",
+ });
+ tmp_dir.dir.symLink(testing.io, "target.txt", "link.txt", .{}) catch |err| switch (err) {
+ error.AccessDenied => if (comptime is_windows) return error.SkipZigTest else return err,
+ else => return err,
+ };
+ try dir.deleteFile(test_io, "link.txt");
+ var file = try dir.openFile(test_io, "target.txt", .{});
+ file.close(test_io);
+ try testing.expectError(error.FileNotFound, dir.openFile(test_io, "link.txt", .{}));
+}
+
+test "dirClose closes the descriptor" {
+ if (comptime !supported) return error.SkipZigTest;
+ const tio: TinyIo = .init;
+ const test_io = tio.io();
+ const testing = std.testing;
+
+ var tmp_dir = testing.tmpDir(.{});
+ defer tmp_dir.cleanup();
+ try tmp_dir.dir.createDir(testing.io, "subdir", .default_dir);
+ const opened = try tmp_dir.dir.openDir(testing.io, "subdir", .{});
+
+ const dir: Dir = .{ .handle = opened.handle };
+ test_io.vtable.dirClose(test_io.userdata, &.{dir});
+
+ if (comptime is_windows) {
+ // A query on a closed handle fails without raising anything.
+ var iosb: os_windows.IO_STATUS_BLOCK = undefined;
+ var info: os_windows.FILE_STANDARD_INFORMATION = undefined;
+ try testing.expectEqual(.INVALID_HANDLE, ntdll.NtQueryInformationFile(
+ dir.handle,
+ &iosb,
+ &info,
+ @sizeOf(os_windows.FILE_STANDARD_INFORMATION),
+ .Standard,
+ ));
+ return;
+ }
+
+ // Verify with a raw dup that the fd is gone. We check the errno
+ // directly rather than going through our Io so no error path prints
+ // "unexpected errno" diagnostics in debug test builds. `dup` rather
+ // than `fstat` because glibc has no LFS64 `fstat` symbol, so
+ // `fstat_sym` doesn't exist on Linux.
+ try testing.expectEqual(posix.E.BADF, posix.errno(posix.system.dup(dir.handle)));
+}
+
+test "cancel protection operations are benign" {
+ if (comptime !supported) return error.SkipZigTest;
+ const tio: TinyIo = .init;
+ const test_io = tio.io();
+ const testing = std.testing;
+
+ // There is no async, so cancelation can never be requested.
+ try test_io.vtable.checkCancel(test_io.userdata);
+
+ // Swap and restore roundtrip like generic std code does around
+ // uninterruptible sections.
+ const prev = test_io.vtable.swapCancelProtection(test_io.userdata, .blocked);
+ _ = test_io.vtable.swapCancelProtection(test_io.userdata, prev);
+ test_io.vtable.recancel(test_io.userdata);
+
+ _ = testing;
+}
+
+test "async runs inline; concurrency is unavailable" {
+ if (comptime !supported) return error.SkipZigTest;
+ const tio: TinyIo = .init;
+ const test_io = tio.io();
+ const testing = std.testing;
+
+ const S = struct {
+ fn work(x: *u32) u32 {
+ x.* += 1;
+ return x.*;
+ }
+ };
+
+ // The task must have executed synchronously, before await.
+ var state: u32 = 41;
+ var future = test_io.async(S.work, .{&state});
+ try testing.expectEqual(@as(u32, 42), state);
+ try testing.expectEqual(@as(u32, 42), future.await(test_io));
+
+ // Concurrency must be reported as unavailable, not silently run.
+ try testing.expectError(
+ error.ConcurrencyUnavailable,
+ test_io.concurrent(S.work, .{&state}),
+ );
+ try testing.expectEqual(@as(u32, 42), state);
+}
+
+test "futex timed waits return" {
+ if (comptime !supported) return error.SkipZigTest;
+ const tio: TinyIo = .init;
+ const test_io = tio.io();
+
+ // Nobody wakes this futex, so returning at all proves the timeout
+ // (or the spurious-wakeup contract) works.
+ var word: u32 = 1;
+ try test_io.vtable.futexWait(test_io.userdata, &word, 1, .{
+ .duration = .{ .raw = .fromNanoseconds(5 * std.time.ns_per_ms), .clock = .awake },
+ });
+
+ // Deadlines can't be resolved without a clock; they degrade to a
+ // short poll which must also return.
+ try test_io.vtable.futexWait(test_io.userdata, &word, 1, .{
+ .deadline = .{ .raw = .fromNanoseconds(1), .clock = .awake },
+ });
+
+ // A mismatched expected value returns immediately even with no
+ // timeout.
+ try test_io.vtable.futexWait(test_io.userdata, &word, 2, .none);
+
+ // Waking more than one waiter takes the wake-all path.
+ test_io.vtable.futexWake(test_io.userdata, &word, 2);
+ test_io.vtable.futexWake(test_io.userdata, &word, std.math.maxInt(u32));
+}
+
+test "Io.Mutex under real thread contention" {
+ if (comptime !supported) return error.SkipZigTest;
+ if (comptime builtin.single_threaded) return error.SkipZigTest;
+ const tio: TinyIo = .init;
+ const test_io = tio.io();
+ const testing = std.testing;
+
+ // Hammer a mutex from several threads so waiters actually park in
+ // futexWait and get released by futexWake.
+ const S = struct {
+ const iterations = 10_000;
+
+ fn worker(m: *Io.Mutex, io_: Io, counter: *u64) void {
+ for (0..iterations) |_| {
+ m.lockUncancelable(io_);
+ defer m.unlock(io_);
+ counter.* += 1;
+ }
+ }
+ };
+
+ var mutex: Io.Mutex = .init;
+ var counter: u64 = 0;
+
+ const thread_count = 4;
+ var threads: [thread_count]std.Thread = undefined;
+ var spawned: usize = 0;
+ defer for (threads[0..spawned]) |t| t.join();
+ for (&threads) |*t| {
+ t.* = std.Thread.spawn(.{}, S.worker, .{ &mutex, test_io, &counter }) catch
+ break;
+ spawned += 1;
+ }
+ for (threads[0..spawned]) |t| t.join();
+ spawned = 0;
+
+ try testing.expectEqual(@as(u64, S.iterations * thread_count), counter);
+}