zig/lib/std/posix/test.zig
Pat Tullmann 138d30bb47 wasi: fix wasm-wasi-musl constants
Zig's copy of the `SYMLINK_{NO,}FOLLOW` constants from wasi-musl was
wrong, as were the `IFIFO` and `IFSOCK` file type flags.  Fix these up,
and add comments pointing to exactly where they come from (as the
wasi-musl source has lots of unused, different definitions of these
constants).

Add tests for the Zig convention that WASM preopen 3 is the current
working directory.   This is true for WASM with or without libc.

Enable several fs and posix tests that are now passing (not necessarily
because of this change) on wasm targets.

Fixes #20890.
2025-02-09 09:08:11 +01:00

1339 lines
44 KiB
Zig

const std = @import("../std.zig");
const posix = std.posix;
const testing = std.testing;
const expect = testing.expect;
const expectEqual = testing.expectEqual;
const expectError = testing.expectError;
const io = std.io;
const fs = std.fs;
const mem = std.mem;
const elf = std.elf;
const File = std.fs.File;
const Thread = std.Thread;
const linux = std.os.linux;
const a = std.testing.allocator;
const builtin = @import("builtin");
const AtomicRmwOp = std.builtin.AtomicRmwOp;
const AtomicOrder = std.builtin.AtomicOrder;
const native_os = builtin.target.os.tag;
const tmpDir = std.testing.tmpDir;
const Dir = std.fs.Dir;
const ArenaAllocator = std.heap.ArenaAllocator;
// https://github.com/ziglang/zig/issues/20288
test "WTF-8 to WTF-16 conversion buffer overflows" {
if (native_os != .windows) return error.SkipZigTest;
const input_wtf8 = "\u{10FFFF}" ** 16385;
try expectError(error.NameTooLong, posix.chdir(input_wtf8));
try expectError(error.NameTooLong, posix.chdirZ(input_wtf8));
}
test "check WASI CWD" {
if (native_os == .wasi) {
if (std.options.wasiCwd() != 3) {
@panic("WASI code that uses cwd (like this test) needs a preopen for cwd (add '--dir=.' to wasmtime)");
}
if (!builtin.link_libc) {
// WASI without-libc hardcodes fd 3 as the FDCWD token so it can be passed directly to WASI calls
try expectEqual(3, posix.AT.FDCWD);
}
}
}
test "chdir smoke test" {
if (native_os == .wasi) return error.SkipZigTest;
if (true) {
// https://github.com/ziglang/zig/issues/14968
return error.SkipZigTest;
}
// Get current working directory path
var old_cwd_buf: [fs.max_path_bytes]u8 = undefined;
const old_cwd = try posix.getcwd(old_cwd_buf[0..]);
{
// Firstly, changing to itself should have no effect
try posix.chdir(old_cwd);
var new_cwd_buf: [fs.max_path_bytes]u8 = undefined;
const new_cwd = try posix.getcwd(new_cwd_buf[0..]);
try expect(mem.eql(u8, old_cwd, new_cwd));
}
// Next, change current working directory to one level above
if (native_os != .wasi) { // WASI does not support navigating outside of Preopens
const parent = fs.path.dirname(old_cwd) orelse unreachable; // old_cwd should be absolute
try posix.chdir(parent);
// Restore cwd because process may have other tests that do not tolerate chdir.
defer posix.chdir(old_cwd) catch unreachable;
var new_cwd_buf: [fs.max_path_bytes]u8 = undefined;
const new_cwd = try posix.getcwd(new_cwd_buf[0..]);
try expect(mem.eql(u8, parent, new_cwd));
}
// Next, change current working directory to a temp directory one level below
{
// Create a tmp directory
var tmp_dir_buf: [fs.max_path_bytes]u8 = undefined;
const tmp_dir_path = path: {
var allocator = std.heap.FixedBufferAllocator.init(&tmp_dir_buf);
break :path try fs.path.resolve(allocator.allocator(), &[_][]const u8{ old_cwd, "zig-test-tmp" });
};
var tmp_dir = try fs.cwd().makeOpenPath("zig-test-tmp", .{});
// Change current working directory to tmp directory
try posix.chdir("zig-test-tmp");
var new_cwd_buf: [fs.max_path_bytes]u8 = undefined;
const new_cwd = try posix.getcwd(new_cwd_buf[0..]);
// On Windows, fs.path.resolve returns an uppercase drive letter, but the drive letter returned by getcwd may be lowercase
var resolved_cwd_buf: [fs.max_path_bytes]u8 = undefined;
const resolved_cwd = path: {
var allocator = std.heap.FixedBufferAllocator.init(&resolved_cwd_buf);
break :path try fs.path.resolve(allocator.allocator(), &[_][]const u8{new_cwd});
};
try expect(mem.eql(u8, tmp_dir_path, resolved_cwd));
// Restore cwd because process may have other tests that do not tolerate chdir.
tmp_dir.close();
posix.chdir(old_cwd) catch unreachable;
try fs.cwd().deleteDir("zig-test-tmp");
}
}
test "open smoke test" {
if (native_os == .wasi) return error.SkipZigTest;
if (native_os == .windows) return error.SkipZigTest;
// TODO verify file attributes using `fstat`
var tmp = tmpDir(.{});
defer tmp.cleanup();
// Get base abs path
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const allocator = arena.allocator();
const base_path = blk: {
const relative_path = try fs.path.join(allocator, &[_][]const u8{ ".zig-cache", "tmp", tmp.sub_path[0..] });
break :blk try fs.realpathAlloc(allocator, relative_path);
};
var file_path: []u8 = undefined;
var fd: posix.fd_t = undefined;
const mode: posix.mode_t = if (native_os == .windows) 0 else 0o666;
// Create some file using `open`.
file_path = try fs.path.join(allocator, &[_][]const u8{ base_path, "some_file" });
fd = try posix.open(file_path, .{ .ACCMODE = .RDWR, .CREAT = true, .EXCL = true }, mode);
posix.close(fd);
// Try this again with the same flags. This op should fail with error.PathAlreadyExists.
file_path = try fs.path.join(allocator, &[_][]const u8{ base_path, "some_file" });
try expectError(error.PathAlreadyExists, posix.open(file_path, .{ .ACCMODE = .RDWR, .CREAT = true, .EXCL = true }, mode));
// Try opening without `EXCL` flag.
file_path = try fs.path.join(allocator, &[_][]const u8{ base_path, "some_file" });
fd = try posix.open(file_path, .{ .ACCMODE = .RDWR, .CREAT = true }, mode);
posix.close(fd);
// Try opening as a directory which should fail.
file_path = try fs.path.join(allocator, &[_][]const u8{ base_path, "some_file" });
try expectError(error.NotDir, posix.open(file_path, .{ .ACCMODE = .RDWR, .DIRECTORY = true }, mode));
// Create some directory
file_path = try fs.path.join(allocator, &[_][]const u8{ base_path, "some_dir" });
try posix.mkdir(file_path, mode);
// Open dir using `open`
file_path = try fs.path.join(allocator, &[_][]const u8{ base_path, "some_dir" });
fd = try posix.open(file_path, .{ .ACCMODE = .RDONLY, .DIRECTORY = true }, mode);
posix.close(fd);
// Try opening as file which should fail.
file_path = try fs.path.join(allocator, &[_][]const u8{ base_path, "some_dir" });
try expectError(error.IsDir, posix.open(file_path, .{ .ACCMODE = .RDWR }, mode));
}
test "openat smoke test" {
if (native_os == .windows) return error.SkipZigTest;
// TODO verify file attributes using `fstatat`
var tmp = tmpDir(.{});
defer tmp.cleanup();
var fd: posix.fd_t = undefined;
const mode: posix.mode_t = if (native_os == .windows) 0 else 0o666;
// Create some file using `openat`.
fd = try posix.openat(tmp.dir.fd, "some_file", CommonOpenFlags.lower(.{
.ACCMODE = .RDWR,
.CREAT = true,
.EXCL = true,
}), mode);
posix.close(fd);
// Try this again with the same flags. This op should fail with error.PathAlreadyExists.
try expectError(error.PathAlreadyExists, posix.openat(tmp.dir.fd, "some_file", CommonOpenFlags.lower(.{
.ACCMODE = .RDWR,
.CREAT = true,
.EXCL = true,
}), mode));
// Try opening without `EXCL` flag.
fd = try posix.openat(tmp.dir.fd, "some_file", CommonOpenFlags.lower(.{
.ACCMODE = .RDWR,
.CREAT = true,
}), mode);
posix.close(fd);
// Try opening as a directory which should fail.
try expectError(error.NotDir, posix.openat(tmp.dir.fd, "some_file", CommonOpenFlags.lower(.{
.ACCMODE = .RDWR,
.DIRECTORY = true,
}), mode));
// Create some directory
try posix.mkdirat(tmp.dir.fd, "some_dir", mode);
// Open dir using `open`
fd = try posix.openat(tmp.dir.fd, "some_dir", CommonOpenFlags.lower(.{
.ACCMODE = .RDONLY,
.DIRECTORY = true,
}), mode);
posix.close(fd);
// Try opening as file which should fail (skip on wasi+libc due to
// https://github.com/bytecodealliance/wasmtime/issues/9054)
if (native_os != .wasi or !builtin.link_libc) {
try expectError(error.IsDir, posix.openat(tmp.dir.fd, "some_dir", CommonOpenFlags.lower(.{
.ACCMODE = .RDWR,
}), mode));
}
}
test "symlink with relative paths" {
if (native_os == .wasi and builtin.link_libc) return error.SkipZigTest;
if (true) {
// https://github.com/ziglang/zig/issues/14968
return error.SkipZigTest;
}
const cwd = fs.cwd();
cwd.deleteFile("file.txt") catch {};
cwd.deleteFile("symlinked") catch {};
// First, try relative paths in cwd
try cwd.writeFile(.{ .sub_path = "file.txt", .data = "nonsense" });
if (native_os == .windows) {
std.os.windows.CreateSymbolicLink(
cwd.fd,
&[_]u16{ 's', 'y', 'm', 'l', 'i', 'n', 'k', 'e', 'd' },
&[_:0]u16{ 'f', 'i', 'l', 'e', '.', 't', 'x', 't' },
false,
) catch |err| switch (err) {
// Symlink requires admin privileges on windows, so this test can legitimately fail.
error.AccessDenied => {
try cwd.deleteFile("file.txt");
try cwd.deleteFile("symlinked");
return error.SkipZigTest;
},
else => return err,
};
} else {
try posix.symlink("file.txt", "symlinked");
}
var buffer: [fs.max_path_bytes]u8 = undefined;
const given = try posix.readlink("symlinked", buffer[0..]);
try expect(mem.eql(u8, "file.txt", given));
try cwd.deleteFile("file.txt");
try cwd.deleteFile("symlinked");
}
test "readlink on Windows" {
if (native_os != .windows) return error.SkipZigTest;
try testReadlink("C:\\ProgramData", "C:\\Users\\All Users");
try testReadlink("C:\\Users\\Default", "C:\\Users\\Default User");
try testReadlink("C:\\Users", "C:\\Documents and Settings");
}
fn testReadlink(target_path: []const u8, symlink_path: []const u8) !void {
var buffer: [fs.max_path_bytes]u8 = undefined;
const given = try posix.readlink(symlink_path, buffer[0..]);
try expect(mem.eql(u8, target_path, given));
}
test "link with relative paths" {
if (native_os == .wasi and builtin.link_libc) return error.SkipZigTest;
switch (native_os) {
.wasi, .linux, .solaris, .illumos => {},
else => return error.SkipZigTest,
}
if (true) {
// https://github.com/ziglang/zig/issues/14968
return error.SkipZigTest;
}
var cwd = fs.cwd();
cwd.deleteFile("example.txt") catch {};
cwd.deleteFile("new.txt") catch {};
try cwd.writeFile(.{ .sub_path = "example.txt", .data = "example" });
try posix.link("example.txt", "new.txt");
const efd = try cwd.openFile("example.txt", .{});
defer efd.close();
const nfd = try cwd.openFile("new.txt", .{});
defer nfd.close();
{
const estat = try posix.fstat(efd.handle);
const nstat = try posix.fstat(nfd.handle);
try testing.expectEqual(estat.ino, nstat.ino);
try testing.expectEqual(@as(@TypeOf(nstat.nlink), 2), nstat.nlink);
}
try posix.unlink("new.txt");
{
const estat = try posix.fstat(efd.handle);
try testing.expectEqual(@as(@TypeOf(estat.nlink), 1), estat.nlink);
}
try cwd.deleteFile("example.txt");
}
test "linkat with different directories" {
if (native_os == .wasi and builtin.link_libc) return error.SkipZigTest;
switch (native_os) {
.wasi, .linux, .solaris, .illumos => {},
else => return error.SkipZigTest,
}
if (true) {
// https://github.com/ziglang/zig/issues/14968
return error.SkipZigTest;
}
var cwd = fs.cwd();
var tmp = tmpDir(.{});
cwd.deleteFile("example.txt") catch {};
tmp.dir.deleteFile("new.txt") catch {};
try cwd.writeFile(.{ .sub_path = "example.txt", .data = "example" });
try posix.linkat(cwd.fd, "example.txt", tmp.dir.fd, "new.txt", 0);
const efd = try cwd.openFile("example.txt", .{});
defer efd.close();
const nfd = try tmp.dir.openFile("new.txt", .{});
{
defer nfd.close();
const estat = try posix.fstat(efd.handle);
const nstat = try posix.fstat(nfd.handle);
try testing.expectEqual(estat.ino, nstat.ino);
try testing.expectEqual(@as(@TypeOf(nstat.nlink), 2), nstat.nlink);
}
try posix.unlinkat(tmp.dir.fd, "new.txt", 0);
{
const estat = try posix.fstat(efd.handle);
try testing.expectEqual(@as(@TypeOf(estat.nlink), 1), estat.nlink);
}
try cwd.deleteFile("example.txt");
}
test "fstatat" {
if (builtin.cpu.arch == .riscv32 and builtin.os.tag == .linux and !builtin.link_libc) return error.SkipZigTest; // No `fstatat()`.
// enable when `fstat` and `fstatat` are implemented on Windows
if (native_os == .windows) return error.SkipZigTest;
var tmp = tmpDir(.{});
defer tmp.cleanup();
// create dummy file
const contents = "nonsense";
try tmp.dir.writeFile(.{ .sub_path = "file.txt", .data = contents });
// fetch file's info on the opened fd directly
const file = try tmp.dir.openFile("file.txt", .{});
const stat = try posix.fstat(file.handle);
defer file.close();
// now repeat but using `fstatat` instead
const statat = try posix.fstatat(tmp.dir.fd, "file.txt", posix.AT.SYMLINK_NOFOLLOW);
// s390x-linux does not have nanosecond precision for fstat(), but it does for fstatat(). As a
// result, comparing the two structures is doomed to fail.
if (builtin.cpu.arch == .s390x and builtin.os.tag == .linux) return error.SkipZigTest;
try expectEqual(stat, statat);
}
test "readlinkat" {
var tmp = tmpDir(.{});
defer tmp.cleanup();
// create file
try tmp.dir.writeFile(.{ .sub_path = "file.txt", .data = "nonsense" });
// create a symbolic link
if (native_os == .windows) {
std.os.windows.CreateSymbolicLink(
tmp.dir.fd,
&[_]u16{ 'l', 'i', 'n', 'k' },
&[_:0]u16{ 'f', 'i', 'l', 'e', '.', 't', 'x', 't' },
false,
) catch |err| switch (err) {
// Symlink requires admin privileges on windows, so this test can legitimately fail.
error.AccessDenied => return error.SkipZigTest,
else => return err,
};
} else {
try posix.symlinkat("file.txt", tmp.dir.fd, "link");
}
// read the link
var buffer: [fs.max_path_bytes]u8 = undefined;
const read_link = try posix.readlinkat(tmp.dir.fd, "link", buffer[0..]);
try expect(mem.eql(u8, "file.txt", read_link));
}
fn testThreadIdFn(thread_id: *Thread.Id) void {
thread_id.* = Thread.getCurrentId();
}
test "Thread.getCurrentId" {
if (builtin.single_threaded) return error.SkipZigTest;
var thread_current_id: Thread.Id = undefined;
const thread = try Thread.spawn(.{}, testThreadIdFn, .{&thread_current_id});
thread.join();
try expect(Thread.getCurrentId() != thread_current_id);
}
test "spawn threads" {
if (builtin.single_threaded) return error.SkipZigTest;
var shared_ctx: i32 = 1;
const thread1 = try Thread.spawn(.{}, start1, .{});
const thread2 = try Thread.spawn(.{}, start2, .{&shared_ctx});
const thread3 = try Thread.spawn(.{}, start2, .{&shared_ctx});
const thread4 = try Thread.spawn(.{}, start2, .{&shared_ctx});
thread1.join();
thread2.join();
thread3.join();
thread4.join();
try expect(shared_ctx == 4);
}
fn start1() u8 {
return 0;
}
fn start2(ctx: *i32) u8 {
_ = @atomicRmw(i32, ctx, AtomicRmwOp.Add, 1, AtomicOrder.seq_cst);
return 0;
}
test "cpu count" {
if (native_os == .wasi) return error.SkipZigTest;
const cpu_count = try Thread.getCpuCount();
try expect(cpu_count >= 1);
}
test "thread local storage" {
if (builtin.single_threaded) return error.SkipZigTest;
const thread1 = try Thread.spawn(.{}, testTls, .{});
const thread2 = try Thread.spawn(.{}, testTls, .{});
try testTls();
thread1.join();
thread2.join();
}
threadlocal var x: i32 = 1234;
fn testTls() !void {
if (x != 1234) return error.TlsBadStartValue;
x += 1;
if (x != 1235) return error.TlsBadEndValue;
}
test "getrandom" {
var buf_a: [50]u8 = undefined;
var buf_b: [50]u8 = undefined;
try posix.getrandom(&buf_a);
try posix.getrandom(&buf_b);
// If this test fails the chance is significantly higher that there is a bug than
// that two sets of 50 bytes were equal.
try expect(!mem.eql(u8, &buf_a, &buf_b));
}
test "getcwd" {
// at least call it so it gets compiled
var buf: [std.fs.max_path_bytes]u8 = undefined;
_ = posix.getcwd(&buf) catch undefined;
}
test "sigaltstack" {
if (native_os == .windows or native_os == .wasi) return error.SkipZigTest;
var st: posix.stack_t = undefined;
try posix.sigaltstack(null, &st);
// Setting a stack size less than MINSIGSTKSZ returns ENOMEM
st.flags = 0;
st.size = 1;
try testing.expectError(error.SizeTooSmall, posix.sigaltstack(&st, null));
}
// If the type is not available use void to avoid erroring out when `iter_fn` is
// analyzed
const have_dl_phdr_info = posix.system.dl_phdr_info != void;
const dl_phdr_info = if (have_dl_phdr_info) posix.dl_phdr_info else anyopaque;
const IterFnError = error{
MissingPtLoadSegment,
MissingLoad,
BadElfMagic,
FailedConsistencyCheck,
};
fn iter_fn(info: *dl_phdr_info, size: usize, counter: *usize) IterFnError!void {
_ = size;
// Count how many libraries are loaded
counter.* += @as(usize, 1);
// The image should contain at least a PT_LOAD segment
if (info.phnum < 1) return error.MissingPtLoadSegment;
// Quick & dirty validation of the phdr pointers, make sure we're not
// pointing to some random gibberish
var i: usize = 0;
var found_load = false;
while (i < info.phnum) : (i += 1) {
const phdr = info.phdr[i];
if (phdr.p_type != elf.PT_LOAD) continue;
const reloc_addr = info.addr + phdr.p_vaddr;
// Find the ELF header
const elf_header = @as(*elf.Ehdr, @ptrFromInt(reloc_addr - phdr.p_offset));
// Validate the magic
if (!mem.eql(u8, elf_header.e_ident[0..4], elf.MAGIC)) return error.BadElfMagic;
// Consistency check
if (elf_header.e_phnum != info.phnum) return error.FailedConsistencyCheck;
found_load = true;
break;
}
if (!found_load) return error.MissingLoad;
}
test "dl_iterate_phdr" {
if (builtin.object_format != .elf) return error.SkipZigTest;
var counter: usize = 0;
try posix.dl_iterate_phdr(&counter, IterFnError, iter_fn);
try expect(counter != 0);
}
test "gethostname" {
if (native_os == .windows or native_os == .wasi)
return error.SkipZigTest;
var buf: [posix.HOST_NAME_MAX]u8 = undefined;
const hostname = try posix.gethostname(&buf);
try expect(hostname.len != 0);
}
test "pipe" {
if (native_os == .windows or native_os == .wasi)
return error.SkipZigTest;
const fds = try posix.pipe();
try expect((try posix.write(fds[1], "hello")) == 5);
var buf: [16]u8 = undefined;
try expect((try posix.read(fds[0], buf[0..])) == 5);
try testing.expectEqualSlices(u8, buf[0..5], "hello");
posix.close(fds[1]);
posix.close(fds[0]);
}
test "argsAlloc" {
const args = try std.process.argsAlloc(std.testing.allocator);
std.process.argsFree(std.testing.allocator, args);
}
test "memfd_create" {
// memfd_create is only supported by linux and freebsd.
switch (native_os) {
.linux => {},
.freebsd => {
if (comptime builtin.os.version_range.semver.max.order(.{ .major = 13, .minor = 0, .patch = 0 }) == .lt)
return error.SkipZigTest;
},
else => return error.SkipZigTest,
}
const fd = try posix.memfd_create("test", 0);
defer posix.close(fd);
try expect((try posix.write(fd, "test")) == 4);
try posix.lseek_SET(fd, 0);
var buf: [10]u8 = undefined;
const bytes_read = try posix.read(fd, &buf);
try expect(bytes_read == 4);
try expect(mem.eql(u8, buf[0..4], "test"));
}
test "mmap" {
if (native_os == .windows or native_os == .wasi)
return error.SkipZigTest;
var tmp = tmpDir(.{});
defer tmp.cleanup();
// Simple mmap() call with non page-aligned size
{
const data = try posix.mmap(
null,
1234,
posix.PROT.READ | posix.PROT.WRITE,
.{ .TYPE = .PRIVATE, .ANONYMOUS = true },
-1,
0,
);
defer posix.munmap(data);
try testing.expectEqual(@as(usize, 1234), data.len);
// By definition the data returned by mmap is zero-filled
try testing.expect(mem.eql(u8, data, &[_]u8{0x00} ** 1234));
// Make sure the memory is writeable as requested
@memset(data, 0x55);
try testing.expect(mem.eql(u8, data, &[_]u8{0x55} ** 1234));
}
const test_out_file = "os_tmp_test";
// Must be a multiple of 4096 so that the test works with mmap2
const alloc_size = 8 * 4096;
// Create a file used for testing mmap() calls with a file descriptor
{
const file = try tmp.dir.createFile(test_out_file, .{});
defer file.close();
const stream = file.writer();
var i: u32 = 0;
while (i < alloc_size / @sizeOf(u32)) : (i += 1) {
try stream.writeInt(u32, i, .little);
}
}
// Map the whole file
{
const file = try tmp.dir.openFile(test_out_file, .{});
defer file.close();
const data = try posix.mmap(
null,
alloc_size,
posix.PROT.READ,
.{ .TYPE = .PRIVATE },
file.handle,
0,
);
defer posix.munmap(data);
var mem_stream = io.fixedBufferStream(data);
const stream = mem_stream.reader();
var i: u32 = 0;
while (i < alloc_size / @sizeOf(u32)) : (i += 1) {
try testing.expectEqual(i, try stream.readInt(u32, .little));
}
}
// Map the upper half of the file
{
const file = try tmp.dir.openFile(test_out_file, .{});
defer file.close();
const data = try posix.mmap(
null,
alloc_size / 2,
posix.PROT.READ,
.{ .TYPE = .PRIVATE },
file.handle,
alloc_size / 2,
);
defer posix.munmap(data);
var mem_stream = io.fixedBufferStream(data);
const stream = mem_stream.reader();
var i: u32 = alloc_size / 2 / @sizeOf(u32);
while (i < alloc_size / @sizeOf(u32)) : (i += 1) {
try testing.expectEqual(i, try stream.readInt(u32, .little));
}
}
try tmp.dir.deleteFile(test_out_file);
}
test "getenv" {
if (native_os == .wasi and !builtin.link_libc) {
// std.posix.getenv is not supported on WASI due to the need of allocation
return error.SkipZigTest;
}
if (native_os == .windows) {
try expect(std.process.getenvW(&[_:0]u16{ 'B', 'O', 'G', 'U', 'S', 0x11, 0x22, 0x33, 0x44, 0x55 }) == null);
} else {
try expect(posix.getenvZ("BOGUSDOESNOTEXISTENVVAR") == null);
}
}
test "fcntl" {
if (native_os == .windows or native_os == .wasi)
return error.SkipZigTest;
var tmp = tmpDir(.{});
defer tmp.cleanup();
const test_out_file = "os_tmp_test";
const file = try tmp.dir.createFile(test_out_file, .{});
defer {
file.close();
tmp.dir.deleteFile(test_out_file) catch {};
}
// Note: The test assumes createFile opens the file with CLOEXEC
{
const flags = try posix.fcntl(file.handle, posix.F.GETFD, 0);
try expect((flags & posix.FD_CLOEXEC) != 0);
}
{
_ = try posix.fcntl(file.handle, posix.F.SETFD, 0);
const flags = try posix.fcntl(file.handle, posix.F.GETFD, 0);
try expect((flags & posix.FD_CLOEXEC) == 0);
}
{
_ = try posix.fcntl(file.handle, posix.F.SETFD, posix.FD_CLOEXEC);
const flags = try posix.fcntl(file.handle, posix.F.GETFD, 0);
try expect((flags & posix.FD_CLOEXEC) != 0);
}
}
test "signalfd" {
switch (native_os) {
.linux, .solaris, .illumos => {},
else => return error.SkipZigTest,
}
_ = &posix.signalfd;
}
test "sync" {
if (native_os != .linux)
return error.SkipZigTest;
var tmp = tmpDir(.{});
defer tmp.cleanup();
const test_out_file = "os_tmp_test";
const file = try tmp.dir.createFile(test_out_file, .{});
defer {
file.close();
tmp.dir.deleteFile(test_out_file) catch {};
}
posix.sync();
try posix.syncfs(file.handle);
}
test "fsync" {
switch (native_os) {
.linux, .windows, .solaris, .illumos => {},
else => return error.SkipZigTest,
}
var tmp = tmpDir(.{});
defer tmp.cleanup();
const test_out_file = "os_tmp_test";
const file = try tmp.dir.createFile(test_out_file, .{});
defer {
file.close();
tmp.dir.deleteFile(test_out_file) catch {};
}
try posix.fsync(file.handle);
try posix.fdatasync(file.handle);
}
test "getrlimit and setrlimit" {
if (posix.system.rlimit_resource == void) return error.SkipZigTest;
inline for (@typeInfo(posix.rlimit_resource).@"enum".fields) |field| {
const resource: posix.rlimit_resource = @enumFromInt(field.value);
const limit = try posix.getrlimit(resource);
// XNU kernel does not support RLIMIT_STACK if a custom stack is active,
// which looks to always be the case. EINVAL is returned.
// See https://github.com/apple-oss-distributions/xnu/blob/5e3eaea39dcf651e66cb99ba7d70e32cc4a99587/bsd/kern/kern_resource.c#L1173
if (native_os.isDarwin() and resource == .STACK) {
continue;
}
// On 32 bit MIPS musl includes a fix which changes limits greater than -1UL/2 to RLIM_INFINITY.
// See http://git.musl-libc.org/cgit/musl/commit/src/misc/getrlimit.c?id=8258014fd1e34e942a549c88c7e022a00445c352
//
// This happens for example if RLIMIT_MEMLOCK is bigger than ~2GiB.
// In that case the following the limit would be RLIM_INFINITY and the following setrlimit fails with EPERM.
if (builtin.cpu.arch.isMIPS() and builtin.link_libc) {
if (limit.cur != linux.RLIM.INFINITY) {
try posix.setrlimit(resource, limit);
}
} else {
try posix.setrlimit(resource, limit);
}
}
}
test "shutdown socket" {
if (native_os == .wasi)
return error.SkipZigTest;
if (native_os == .windows) {
_ = try std.os.windows.WSAStartup(2, 2);
}
defer {
if (native_os == .windows) {
std.os.windows.WSACleanup() catch unreachable;
}
}
const sock = try posix.socket(posix.AF.INET, posix.SOCK.STREAM, 0);
posix.shutdown(sock, .both) catch |err| switch (err) {
error.SocketNotConnected => {},
else => |e| return e,
};
std.net.Stream.close(.{ .handle = sock });
}
test "sigaction" {
if (native_os == .wasi or native_os == .windows)
return error.SkipZigTest;
// https://github.com/ziglang/zig/issues/7427
if (native_os == .linux and builtin.target.cpu.arch == .x86)
return error.SkipZigTest;
// https://github.com/ziglang/zig/issues/15381
if (native_os == .macos and builtin.target.cpu.arch == .x86_64) {
return error.SkipZigTest;
}
const S = struct {
var handler_called_count: u32 = 0;
fn handler(sig: i32, info: *const posix.siginfo_t, ctx_ptr: ?*anyopaque) callconv(.c) void {
_ = ctx_ptr;
// Check that we received the correct signal.
switch (native_os) {
.netbsd => {
if (sig == posix.SIG.USR1 and sig == info.info.signo)
handler_called_count += 1;
},
else => {
if (sig == posix.SIG.USR1 and sig == info.signo)
handler_called_count += 1;
},
}
}
};
var sa: posix.Sigaction = .{
.handler = .{ .sigaction = &S.handler },
.mask = posix.empty_sigset,
.flags = posix.SA.SIGINFO | posix.SA.RESETHAND,
};
var old_sa: posix.Sigaction = undefined;
// Install the new signal handler.
posix.sigaction(posix.SIG.USR1, &sa, null);
// Check that we can read it back correctly.
posix.sigaction(posix.SIG.USR1, null, &old_sa);
try testing.expectEqual(&S.handler, old_sa.handler.sigaction.?);
try testing.expect((old_sa.flags & posix.SA.SIGINFO) != 0);
// Invoke the handler.
try posix.raise(posix.SIG.USR1);
try testing.expect(S.handler_called_count == 1);
// Check if passing RESETHAND correctly reset the handler to SIG_DFL
posix.sigaction(posix.SIG.USR1, null, &old_sa);
try testing.expectEqual(posix.SIG.DFL, old_sa.handler.handler);
// Reinstall the signal w/o RESETHAND and re-raise
sa.flags = posix.SA.SIGINFO;
posix.sigaction(posix.SIG.USR1, &sa, null);
try posix.raise(posix.SIG.USR1);
try testing.expect(S.handler_called_count == 2);
// Now set the signal to ignored
sa.handler = .{ .handler = posix.SIG.IGN };
sa.flags = 0;
posix.sigaction(posix.SIG.USR1, &sa, null);
// Re-raise to ensure handler is actually ignored
try posix.raise(posix.SIG.USR1);
try testing.expect(S.handler_called_count == 2);
// Ensure that ignored state is returned when querying
posix.sigaction(posix.SIG.USR1, null, &old_sa);
try testing.expectEqual(posix.SIG.IGN, old_sa.handler.handler.?);
}
test "dup & dup2" {
switch (native_os) {
.linux, .solaris, .illumos => {},
else => return error.SkipZigTest,
}
var tmp = tmpDir(.{});
defer tmp.cleanup();
{
var file = try tmp.dir.createFile("os_dup_test", .{});
defer file.close();
var duped = std.fs.File{ .handle = try posix.dup(file.handle) };
defer duped.close();
try duped.writeAll("dup");
// Tests aren't run in parallel so using the next fd shouldn't be an issue.
const new_fd = duped.handle + 1;
try posix.dup2(file.handle, new_fd);
var dup2ed = std.fs.File{ .handle = new_fd };
defer dup2ed.close();
try dup2ed.writeAll("dup2");
}
var file = try tmp.dir.openFile("os_dup_test", .{});
defer file.close();
var buf: [7]u8 = undefined;
try testing.expectEqualStrings("dupdup2", buf[0..try file.readAll(&buf)]);
}
test "writev longer than IOV_MAX" {
if (native_os == .windows or native_os == .wasi) return error.SkipZigTest;
var tmp = tmpDir(.{});
defer tmp.cleanup();
var file = try tmp.dir.createFile("pwritev", .{});
defer file.close();
const iovecs = [_]posix.iovec_const{.{ .base = "a", .len = 1 }} ** (posix.IOV_MAX + 1);
const amt = try file.writev(&iovecs);
try testing.expectEqual(@as(usize, posix.IOV_MAX), amt);
}
test "POSIX file locking with fcntl" {
if (native_os == .windows or native_os == .wasi) {
// Not POSIX.
return error.SkipZigTest;
}
if (true) {
// https://github.com/ziglang/zig/issues/11074
return error.SkipZigTest;
}
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
// Create a temporary lock file
var file = try tmp.dir.createFile("lock", .{ .read = true });
defer file.close();
try file.setEndPos(2);
const fd = file.handle;
// Place an exclusive lock on the first byte, and a shared lock on the second byte:
var struct_flock = std.mem.zeroInit(posix.Flock, .{ .type = posix.F.WRLCK });
_ = try posix.fcntl(fd, posix.F.SETLK, @intFromPtr(&struct_flock));
struct_flock.start = 1;
struct_flock.type = posix.F.RDLCK;
_ = try posix.fcntl(fd, posix.F.SETLK, @intFromPtr(&struct_flock));
// Check the locks in a child process:
const pid = try posix.fork();
if (pid == 0) {
// child expects be denied the exclusive lock:
struct_flock.start = 0;
struct_flock.type = posix.F.WRLCK;
try expectError(error.Locked, posix.fcntl(fd, posix.F.SETLK, @intFromPtr(&struct_flock)));
// child expects to get the shared lock:
struct_flock.start = 1;
struct_flock.type = posix.F.RDLCK;
_ = try posix.fcntl(fd, posix.F.SETLK, @intFromPtr(&struct_flock));
// child waits for the exclusive lock in order to test deadlock:
struct_flock.start = 0;
struct_flock.type = posix.F.WRLCK;
_ = try posix.fcntl(fd, posix.F.SETLKW, @intFromPtr(&struct_flock));
// child exits without continuing:
posix.exit(0);
} else {
// parent waits for child to get shared lock:
std.time.sleep(1 * std.time.ns_per_ms);
// parent expects deadlock when attempting to upgrade the shared lock to exclusive:
struct_flock.start = 1;
struct_flock.type = posix.F.WRLCK;
try expectError(error.DeadLock, posix.fcntl(fd, posix.F.SETLKW, @intFromPtr(&struct_flock)));
// parent releases exclusive lock:
struct_flock.start = 0;
struct_flock.type = posix.F.UNLCK;
_ = try posix.fcntl(fd, posix.F.SETLK, @intFromPtr(&struct_flock));
// parent releases shared lock:
struct_flock.start = 1;
struct_flock.type = posix.F.UNLCK;
_ = try posix.fcntl(fd, posix.F.SETLK, @intFromPtr(&struct_flock));
// parent waits for child:
const result = posix.waitpid(pid, 0);
try expect(result.status == 0 * 256);
}
}
test "rename smoke test" {
if (native_os == .wasi) return error.SkipZigTest;
if (native_os == .windows) return error.SkipZigTest;
var tmp = tmpDir(.{});
defer tmp.cleanup();
// Get base abs path
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const allocator = arena.allocator();
const base_path = blk: {
const relative_path = try fs.path.join(allocator, &[_][]const u8{ ".zig-cache", "tmp", tmp.sub_path[0..] });
break :blk try fs.realpathAlloc(allocator, relative_path);
};
var file_path: []u8 = undefined;
var fd: posix.fd_t = undefined;
const mode: posix.mode_t = if (native_os == .windows) 0 else 0o666;
// Create some file using `open`.
file_path = try fs.path.join(allocator, &[_][]const u8{ base_path, "some_file" });
fd = try posix.open(file_path, .{ .ACCMODE = .RDWR, .CREAT = true, .EXCL = true }, mode);
posix.close(fd);
// Rename the file
var new_file_path = try fs.path.join(allocator, &[_][]const u8{ base_path, "some_other_file" });
try posix.rename(file_path, new_file_path);
// Try opening renamed file
file_path = try fs.path.join(allocator, &[_][]const u8{ base_path, "some_other_file" });
fd = try posix.open(file_path, .{ .ACCMODE = .RDWR }, mode);
posix.close(fd);
// Try opening original file - should fail with error.FileNotFound
file_path = try fs.path.join(allocator, &[_][]const u8{ base_path, "some_file" });
try expectError(error.FileNotFound, posix.open(file_path, .{ .ACCMODE = .RDWR }, mode));
// Create some directory
file_path = try fs.path.join(allocator, &[_][]const u8{ base_path, "some_dir" });
try posix.mkdir(file_path, mode);
// Rename the directory
new_file_path = try fs.path.join(allocator, &[_][]const u8{ base_path, "some_other_dir" });
try posix.rename(file_path, new_file_path);
// Try opening renamed directory
file_path = try fs.path.join(allocator, &[_][]const u8{ base_path, "some_other_dir" });
fd = try posix.open(file_path, .{ .ACCMODE = .RDONLY, .DIRECTORY = true }, mode);
posix.close(fd);
// Try opening original directory - should fail with error.FileNotFound
file_path = try fs.path.join(allocator, &[_][]const u8{ base_path, "some_dir" });
try expectError(error.FileNotFound, posix.open(file_path, .{ .ACCMODE = .RDONLY, .DIRECTORY = true }, mode));
}
test "access smoke test" {
if (native_os == .wasi) return error.SkipZigTest;
if (native_os == .windows) return error.SkipZigTest;
var tmp = tmpDir(.{});
defer tmp.cleanup();
// Get base abs path
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const allocator = arena.allocator();
const base_path = blk: {
const relative_path = try fs.path.join(allocator, &[_][]const u8{ ".zig-cache", "tmp", tmp.sub_path[0..] });
break :blk try fs.realpathAlloc(allocator, relative_path);
};
var file_path: []u8 = undefined;
var fd: posix.fd_t = undefined;
const mode: posix.mode_t = if (native_os == .windows) 0 else 0o666;
// Create some file using `open`.
file_path = try fs.path.join(allocator, &[_][]const u8{ base_path, "some_file" });
fd = try posix.open(file_path, .{ .ACCMODE = .RDWR, .CREAT = true, .EXCL = true }, mode);
posix.close(fd);
// Try to access() the file
file_path = try fs.path.join(allocator, &[_][]const u8{ base_path, "some_file" });
if (native_os == .windows) {
try posix.access(file_path, posix.F_OK);
} else {
try posix.access(file_path, posix.F_OK | posix.W_OK | posix.R_OK);
}
// Try to access() a non-existent file - should fail with error.FileNotFound
file_path = try fs.path.join(allocator, &[_][]const u8{ base_path, "some_other_file" });
try expectError(error.FileNotFound, posix.access(file_path, posix.F_OK));
// Create some directory
file_path = try fs.path.join(allocator, &[_][]const u8{ base_path, "some_dir" });
try posix.mkdir(file_path, mode);
// Try to access() the directory
file_path = try fs.path.join(allocator, &[_][]const u8{ base_path, "some_dir" });
try posix.access(file_path, posix.F_OK);
}
test "timerfd" {
if (native_os != .linux) return error.SkipZigTest;
const tfd = try posix.timerfd_create(.MONOTONIC, .{ .CLOEXEC = true });
defer posix.close(tfd);
// Fire event 10_000_000ns = 10ms after the posix.timerfd_settime call.
var sit: linux.itimerspec = .{ .it_interval = .{ .sec = 0, .nsec = 0 }, .it_value = .{ .sec = 0, .nsec = 10 * (1000 * 1000) } };
try posix.timerfd_settime(tfd, .{}, &sit, null);
var fds: [1]posix.pollfd = .{.{ .fd = tfd, .events = linux.POLL.IN, .revents = 0 }};
try expectEqual(@as(usize, 1), try posix.poll(&fds, -1)); // -1 => infinite waiting
const git = try posix.timerfd_gettime(tfd);
const expect_disarmed_timer: linux.itimerspec = .{ .it_interval = .{ .sec = 0, .nsec = 0 }, .it_value = .{ .sec = 0, .nsec = 0 } };
try expectEqual(expect_disarmed_timer, git);
}
test "isatty" {
var tmp = tmpDir(.{});
defer tmp.cleanup();
var file = try tmp.dir.createFile("foo", .{});
defer file.close();
try expectEqual(posix.isatty(file.handle), false);
}
test "read with empty buffer" {
if (native_os == .wasi) return error.SkipZigTest;
var tmp = tmpDir(.{});
defer tmp.cleanup();
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const allocator = arena.allocator();
// Get base abs path
const base_path = blk: {
const relative_path = try fs.path.join(allocator, &[_][]const u8{ ".zig-cache", "tmp", tmp.sub_path[0..] });
break :blk try fs.realpathAlloc(allocator, relative_path);
};
const file_path: []u8 = try fs.path.join(allocator, &[_][]const u8{ base_path, "some_file" });
var file = try fs.cwd().createFile(file_path, .{ .read = true });
defer file.close();
const bytes = try allocator.alloc(u8, 0);
_ = try posix.read(file.handle, bytes);
}
test "pread with empty buffer" {
if (native_os == .wasi) return error.SkipZigTest;
var tmp = tmpDir(.{});
defer tmp.cleanup();
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const allocator = arena.allocator();
// Get base abs path
const base_path = blk: {
const relative_path = try fs.path.join(allocator, &[_][]const u8{ ".zig-cache", "tmp", tmp.sub_path[0..] });
break :blk try fs.realpathAlloc(allocator, relative_path);
};
const file_path: []u8 = try fs.path.join(allocator, &[_][]const u8{ base_path, "some_file" });
var file = try fs.cwd().createFile(file_path, .{ .read = true });
defer file.close();
const bytes = try allocator.alloc(u8, 0);
_ = try posix.pread(file.handle, bytes, 0);
}
test "write with empty buffer" {
if (native_os == .wasi) return error.SkipZigTest;
var tmp = tmpDir(.{});
defer tmp.cleanup();
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const allocator = arena.allocator();
// Get base abs path
const base_path = blk: {
const relative_path = try fs.path.join(allocator, &[_][]const u8{ ".zig-cache", "tmp", tmp.sub_path[0..] });
break :blk try fs.realpathAlloc(allocator, relative_path);
};
const file_path: []u8 = try fs.path.join(allocator, &[_][]const u8{ base_path, "some_file" });
var file = try fs.cwd().createFile(file_path, .{});
defer file.close();
const bytes = try allocator.alloc(u8, 0);
_ = try posix.write(file.handle, bytes);
}
test "pwrite with empty buffer" {
if (native_os == .wasi) return error.SkipZigTest;
var tmp = tmpDir(.{});
defer tmp.cleanup();
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const allocator = arena.allocator();
// Get base abs path
const base_path = blk: {
const relative_path = try fs.path.join(allocator, &[_][]const u8{ ".zig-cache", "tmp", tmp.sub_path[0..] });
break :blk try fs.realpathAlloc(allocator, relative_path);
};
const file_path: []u8 = try fs.path.join(allocator, &[_][]const u8{ base_path, "some_file" });
var file = try fs.cwd().createFile(file_path, .{});
defer file.close();
const bytes = try allocator.alloc(u8, 0);
_ = try posix.pwrite(file.handle, bytes, 0);
}
fn expectMode(dir: posix.fd_t, file: []const u8, mode: posix.mode_t) !void {
const st = try posix.fstatat(dir, file, posix.AT.SYMLINK_NOFOLLOW);
try expectEqual(mode, st.mode & 0b111_111_111);
}
test "fchmodat smoke test" {
if (!std.fs.has_executable_bit) return error.SkipZigTest;
var tmp = tmpDir(.{});
defer tmp.cleanup();
try expectError(error.FileNotFound, posix.fchmodat(tmp.dir.fd, "regfile", 0o666, 0));
const fd = try posix.openat(
tmp.dir.fd,
"regfile",
.{ .ACCMODE = .WRONLY, .CREAT = true, .EXCL = true, .TRUNC = true },
0o644,
);
posix.close(fd);
if (builtin.cpu.arch == .riscv32 and builtin.os.tag == .linux and !builtin.link_libc) return error.SkipZigTest; // No `fstatat()`.
try posix.symlinkat("regfile", tmp.dir.fd, "symlink");
const sym_mode = blk: {
const st = try posix.fstatat(tmp.dir.fd, "symlink", posix.AT.SYMLINK_NOFOLLOW);
break :blk st.mode & 0b111_111_111;
};
try posix.fchmodat(tmp.dir.fd, "regfile", 0o640, 0);
try expectMode(tmp.dir.fd, "regfile", 0o640);
try posix.fchmodat(tmp.dir.fd, "regfile", 0o600, posix.AT.SYMLINK_NOFOLLOW);
try expectMode(tmp.dir.fd, "regfile", 0o600);
try posix.fchmodat(tmp.dir.fd, "symlink", 0o640, 0);
try expectMode(tmp.dir.fd, "regfile", 0o640);
try expectMode(tmp.dir.fd, "symlink", sym_mode);
var test_link = true;
posix.fchmodat(tmp.dir.fd, "symlink", 0o600, posix.AT.SYMLINK_NOFOLLOW) catch |err| switch (err) {
error.OperationNotSupported => test_link = false,
else => |e| return e,
};
if (test_link)
try expectMode(tmp.dir.fd, "symlink", 0o600);
try expectMode(tmp.dir.fd, "regfile", 0o640);
}
const CommonOpenFlags = packed struct {
ACCMODE: posix.ACCMODE = .RDONLY,
CREAT: bool = false,
EXCL: bool = false,
LARGEFILE: bool = false,
DIRECTORY: bool = false,
CLOEXEC: bool = false,
NONBLOCK: bool = false,
pub fn lower(cof: CommonOpenFlags) posix.O {
var result: posix.O = if (native_os == .wasi) .{
.read = cof.ACCMODE != .WRONLY,
.write = cof.ACCMODE != .RDONLY,
} else .{
.ACCMODE = cof.ACCMODE,
};
result.CREAT = cof.CREAT;
result.EXCL = cof.EXCL;
result.DIRECTORY = cof.DIRECTORY;
result.NONBLOCK = cof.NONBLOCK;
if (@hasField(posix.O, "CLOEXEC")) result.CLOEXEC = cof.CLOEXEC;
if (@hasField(posix.O, "LARGEFILE")) result.LARGEFILE = cof.LARGEFILE;
return result;
}
};