diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-09-10 21:51:57 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-09-10 21:51:57 +0300 |
| commit | 425c86179791cd32e2d4ce0ae7f269940b4b0663 (patch) | |
| tree | e52c4383a6d69216cbe2a32638bf90841d7e2189 /vendor/libghostty-vt/src/lib/tinyio/test.zig | |
| parent | 90e947a6e26e02f10ec7f1694c4eb5b78a86406b (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.zig | 836 |
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); +} |
