From 0edccc1079003e2c96f5c808fdaf00d465e6e004 Mon Sep 17 00:00:00 2001 From: Pat Tullmann Date: Tue, 5 Aug 2025 17:14:59 -0700 Subject: [PATCH 1/7] Move some Thread tests out of posix/test.zig into Thread.zig These tests aren't (directly) using Posix APIs, so they don't need to be in posix/test.zig. Put them over with the code and tests in Thread.zig. Since the spawn/join test in the posix code was redundant, just dropped that one. --- lib/std/Thread.zig | 37 ++++++++++++++++++++++++ lib/std/posix/test.zig | 65 ------------------------------------------ 2 files changed, 37 insertions(+), 65 deletions(-) diff --git a/lib/std/Thread.zig b/lib/std/Thread.zig index c4b955c9f2aa..5f667497fd73 100644 --- a/lib/std/Thread.zig +++ b/lib/std/Thread.zig @@ -1637,3 +1637,40 @@ test detach { event.wait(); try std.testing.expectEqual(value, 1); } + +test "Thread.getCpuCount" { + if (native_os == .wasi) return error.SkipZigTest; + + const cpu_count = try Thread.getCpuCount(); + try std.testing.expect(cpu_count >= 1); +} + +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 std.testing.expect(Thread.getCurrentId() != thread_current_id); +} + +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; +} diff --git a/lib/std/posix/test.zig b/lib/std/posix/test.zig index 5fb0d4bb9c51..498873a7acfe 100644 --- a/lib/std/posix/test.zig +++ b/lib/std/posix/test.zig @@ -7,7 +7,6 @@ const expectError = testing.expectError; const fs = std.fs; const mem = std.mem; const elf = std.elf; -const Thread = std.Thread; const linux = std.os.linux; const a = std.testing.allocator; @@ -446,70 +445,6 @@ test "readlinkat" { 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; From e46ddeeb296aeb259d1bb9f96cb977f4868309e7 Mon Sep 17 00:00:00 2001 From: Pat Tullmann Date: Fri, 1 Aug 2025 12:43:59 -0700 Subject: [PATCH 2/7] posix/test.zig: "sigset_t bits" test fixes Re-enable the test. Will trigger #24380 as-is, but follow-on change moes this code over to test/standalone. Make the test a bit easier to debug by stashing the "seen" signal number in the shared `seen_sig` (instead of just incrementing a counter for each hit). And only doing so if the `seen_sig` is zero. --- lib/std/posix/test.zig | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/lib/std/posix/test.zig b/lib/std/posix/test.zig index 498873a7acfe..5b83a4ac8dc8 100644 --- a/lib/std/posix/test.zig +++ b/lib/std/posix/test.zig @@ -949,14 +949,11 @@ test "sigset_t bits" { if (native_os == .wasi or native_os == .windows) return error.SkipZigTest; - if (true) { - // https://github.com/ziglang/zig/issues/24380 - return error.SkipZigTest; - } + const NO_SIG: i32 = 0; const S = struct { var expected_sig: i32 = undefined; - var handler_called_count: u32 = 0; + var seen_sig: i32 = NO_SIG; fn handler(sig: i32, info: *const posix.siginfo_t, ctx_ptr: ?*anyopaque) callconv(.c) void { _ = ctx_ptr; @@ -965,23 +962,25 @@ test "sigset_t bits" { .netbsd => info.info.signo, else => info.signo, }; - if (sig == expected_sig and sig == info_sig) { - handler_called_count += 1; + if (seen_sig == NO_SIG and sig == expected_sig and sig == info_sig) { + seen_sig = sig; } } }; - const self_pid = posix.system.getpid(); + // Assume this is a single-threaded process where the current thread has the + // 'pid' thread id. (The sigprocmask calls are thread-private state.) + const self_tid = posix.system.getpid(); // To check that sigset_t mapping matches kernel (think u32/u64 mismatches on // big-endian), try sending a blocked signal to make sure the mask matches the // signal. (Send URG and CHLD because they're ignored by default in the // debugger, vs. USR1 or other named signals) - inline for ([_]usize{ posix.SIG.URG, posix.SIG.CHLD, 62, 94, 126 }) |test_signo| { + inline for ([_]i32{ posix.SIG.URG, posix.SIG.CHLD, 62, 94, 126 }) |test_signo| { if (test_signo >= posix.NSIG) continue; S.expected_sig = test_signo; - S.handler_called_count = 0; + S.seen_sig = NO_SIG; const sa: posix.Sigaction = .{ .handler = .{ .sigaction = &S.handler }, @@ -1001,18 +1000,18 @@ test "sigset_t bits" { // qemu maps target signals to host signals 1-to-1, so targets // with more signals than the host will fail to send the signal. - const rc = posix.system.kill(self_pid, test_signo); + const rc = posix.system.kill(self_tid, test_signo); switch (posix.errno(rc)) { .SUCCESS => { // See that the signal is blocked, then unblocked - try testing.expectEqual(0, S.handler_called_count); + try testing.expectEqual(NO_SIG, S.seen_sig); posix.sigprocmask(posix.SIG.UNBLOCK, &block_one, null); - try testing.expectEqual(1, S.handler_called_count); + try testing.expectEqual(test_signo, S.seen_sig); }, .INVAL => { // Signal won't get delviered. Just clean up. posix.sigprocmask(posix.SIG.UNBLOCK, &block_one, null); - try testing.expectEqual(0, S.handler_called_count); + try testing.expectEqual(NO_SIG, S.seen_sig); }, else => |errno| return posix.unexpectedErrno(errno), } From c614d8d0081782116e94f3c8bfbf4ea515e7a3f3 Mon Sep 17 00:00:00 2001 From: Pat Tullmann Date: Wed, 6 Aug 2025 21:50:56 -0700 Subject: [PATCH 3/7] standalone posix tests: add skeleton Add build.zig, README and empty test files. --- test/standalone/build.zig.zon | 3 ++ test/standalone/posix/README.md | 8 ++++ test/standalone/posix/build.zig | 72 +++++++++++++++++++++++++++++ test/standalone/posix/cwd.zig | 1 + test/standalone/posix/getenv.zig | 1 + test/standalone/posix/relpaths.zig | 1 + test/standalone/posix/sigaction.zig | 1 + 7 files changed, 87 insertions(+) create mode 100644 test/standalone/posix/README.md create mode 100644 test/standalone/posix/build.zig create mode 100644 test/standalone/posix/cwd.zig create mode 100644 test/standalone/posix/getenv.zig create mode 100644 test/standalone/posix/relpaths.zig create mode 100644 test/standalone/posix/sigaction.zig diff --git a/test/standalone/build.zig.zon b/test/standalone/build.zig.zon index c57837c8e660..8aaf2054b0bc 100644 --- a/test/standalone/build.zig.zon +++ b/test/standalone/build.zig.zon @@ -211,6 +211,9 @@ .tsan = .{ .path = "tsan", }, + .posix = .{ + .path = "posix", + }, }, .paths = .{ "build.zig", diff --git a/test/standalone/posix/README.md b/test/standalone/posix/README.md new file mode 100644 index 000000000000..488c9c020599 --- /dev/null +++ b/test/standalone/posix/README.md @@ -0,0 +1,8 @@ +## Zig standalone POSIX tests + +This directory is just for std.posix-related test cases that depend on +process-wide state like the current-working directory, signal handlers, +fork, the main thread, environment variables, etc. Most tests (e.g, +around file descriptors, etc) are with the unit tests in +`lib/std/posix/test.zig`. New tests should be with the unit tests, unless +there is a specific reason they cannot. diff --git a/test/standalone/posix/build.zig b/test/standalone/posix/build.zig new file mode 100644 index 000000000000..71446256acdd --- /dev/null +++ b/test/standalone/posix/build.zig @@ -0,0 +1,72 @@ +const std = @import("std"); +const builtin = @import("builtin"); + +const Case = struct { + src_path: []const u8, +}; + +const cases = [_]Case{ + .{ + .src_path = "cwd.zig", + }, + .{ + .src_path = "getenv.zig", + }, + .{ + .src_path = "sigaction.zig", + }, + .{ + .src_path = "relpaths.zig", + }, +}; + +pub fn build(b: *std.Build) void { + const test_step = b.step("test", "Run POSIX standalone test cases"); + b.default_step = test_step; + + const optimize = b.standardOptimizeOption(.{}); + + const default_target = b.resolveTargetQuery(.{}); + + // Run each test case built against libc-less, glibc, and musl. + for (cases) |case| { + const run_def = run_exe(b, optimize, &case, default_target, false); + test_step.dependOn(&run_def.step); + + if (default_target.result.os.tag == .linux) { + const gnu_target = b.resolveTargetQuery(.{ .abi = .gnu }); + const musl_target = b.resolveTargetQuery(.{ .abi = .musl }); + + const run_gnu = run_exe(b, optimize, &case, gnu_target, true); + const run_musl = run_exe(b, optimize, &case, musl_target, true); + + test_step.dependOn(&run_gnu.step); + test_step.dependOn(&run_musl.step); + } else { + const run_libc = run_exe(b, optimize, &case, default_target, true); + test_step.dependOn(&run_libc.step); + } + } +} + +fn run_exe(b: *std.Build, optimize: std.builtin.OptimizeMode, case: *const Case, target: std.Build.ResolvedTarget, link_libc: bool) *std.Build.Step.Run { + const exe_name = b.fmt("test-posix-{s}{s}{s}", .{ + std.fs.path.stem(case.src_path), + if (link_libc) "-libc" else "", + if (link_libc and target.result.isGnuLibC()) "-gnu" else if (link_libc and target.result.isMuslLibC()) "-musl" else "", + }); + + const exe = b.addExecutable(.{ + .name = exe_name, + .root_module = b.createModule(.{ + .root_source_file = b.path(case.src_path), + .link_libc = link_libc, + .optimize = optimize, + .target = target, + }), + }); + + const run_cmd = b.addRunArtifact(exe); + + return run_cmd; +} diff --git a/test/standalone/posix/cwd.zig b/test/standalone/posix/cwd.zig new file mode 100644 index 000000000000..bbb7b6ed703d --- /dev/null +++ b/test/standalone/posix/cwd.zig @@ -0,0 +1 @@ +pub fn main() !void {} diff --git a/test/standalone/posix/getenv.zig b/test/standalone/posix/getenv.zig new file mode 100644 index 000000000000..bbb7b6ed703d --- /dev/null +++ b/test/standalone/posix/getenv.zig @@ -0,0 +1 @@ +pub fn main() !void {} diff --git a/test/standalone/posix/relpaths.zig b/test/standalone/posix/relpaths.zig new file mode 100644 index 000000000000..bbb7b6ed703d --- /dev/null +++ b/test/standalone/posix/relpaths.zig @@ -0,0 +1 @@ +pub fn main() !void {} diff --git a/test/standalone/posix/sigaction.zig b/test/standalone/posix/sigaction.zig new file mode 100644 index 000000000000..bbb7b6ed703d --- /dev/null +++ b/test/standalone/posix/sigaction.zig @@ -0,0 +1 @@ +pub fn main() !void {} From 020eb622ee344d211946738ac1013deb60697381 Mon Sep 17 00:00:00 2001 From: Pat Tullmann Date: Mon, 8 Sep 2025 21:31:51 -0700 Subject: [PATCH 4/7] standalone posix test for current working directory --- lib/std/posix/test.zig | 71 ++------------------------------ test/standalone/posix/cwd.zig | 76 ++++++++++++++++++++++++++++++++++- 2 files changed, 78 insertions(+), 69 deletions(-) diff --git a/lib/std/posix/test.zig b/lib/std/posix/test.zig index 5b83a4ac8dc8..10649e9c0cf1 100644 --- a/lib/std/posix/test.zig +++ b/lib/std/posix/test.zig @@ -17,6 +17,9 @@ const AtomicOrder = std.builtin.AtomicOrder; const native_os = builtin.target.os.tag; const tmpDir = std.testing.tmpDir; +// NOTE: several additional tests are in test/standalone/posix/. Any tests that mutate +// process-wide POSIX state (cwd, signals, etc) cannot be Zig unit tests and should be over there. + // https://github.com/ziglang/zig/issues/20288 test "WTF-8 to WTF-16 conversion buffer overflows" { if (native_os != .windows) return error.SkipZigTest; @@ -39,68 +42,6 @@ test "check WASI CWD" { } } -test "chdir absolute parent" { - if (native_os == .wasi) return error.SkipZigTest; - - // Restore default CWD at end of test. - const orig_cwd = try fs.cwd().openDir(".", .{}); - defer orig_cwd.setAsCwd() catch unreachable; - - // 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 - const parent = fs.path.dirname(old_cwd) orelse unreachable; // old_cwd should be absolute - try posix.chdir(parent); - - 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)); -} - -test "chdir relative" { - if (native_os == .wasi) return error.SkipZigTest; - - var tmp = tmpDir(.{}); - defer tmp.cleanup(); - - // Restore default CWD at end of test. - const orig_cwd = try fs.cwd().openDir(".", .{}); - defer orig_cwd.setAsCwd() catch unreachable; - - // Use the tmpDir parent_dir as the "base" for the test. Then cd into the child - try tmp.parent_dir.setAsCwd(); - - // Capture base working directory path, to build expected full path - var base_cwd_buf: [fs.max_path_bytes]u8 = undefined; - const base_cwd = try posix.getcwd(base_cwd_buf[0..]); - - const dir_name = &tmp.sub_path; - const expected_path = try fs.path.resolve(a, &.{ base_cwd, dir_name }); - defer a.free(expected_path); - - // change current working directory to new directory - try posix.chdir(dir_name); - - 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 - const resolved_cwd = try fs.path.resolve(a, &.{new_cwd}); - defer a.free(resolved_cwd); - - try expect(mem.eql(u8, expected_path, resolved_cwd)); -} - test "open smoke test" { if (native_os == .wasi) return error.SkipZigTest; if (native_os == .windows) return error.SkipZigTest; @@ -455,12 +396,6 @@ test "getrandom" { 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 "getuid" { if (native_os == .windows or native_os == .wasi) return error.SkipZigTest; _ = posix.getuid(); diff --git a/test/standalone/posix/cwd.zig b/test/standalone/posix/cwd.zig index bbb7b6ed703d..43dcc63bfeb2 100644 --- a/test/standalone/posix/cwd.zig +++ b/test/standalone/posix/cwd.zig @@ -1 +1,75 @@ -pub fn main() !void {} +const std = @import("std"); +const builtin = @import("builtin"); + +const path_max = std.fs.max_path_bytes; + +pub fn main() !void { + if (builtin.target.os.tag == .wasi) { + // WASI doesn't support changing the working directory at all. + return; + } + + var Allocator = std.heap.DebugAllocator(.{}){}; + const a = Allocator.allocator(); + defer std.debug.assert(Allocator.deinit() == .ok); + + try test_chdir_self(); + try test_chdir_absolute(); + try test_chdir_relative(a); +} + +// get current working directory and expect it to match given path +fn expect_cwd(expected_cwd: []const u8) !void { + var cwd_buf: [path_max]u8 = undefined; + const actual_cwd = try std.posix.getcwd(cwd_buf[0..]); + try std.testing.expectEqualStrings(actual_cwd, expected_cwd); +} + +fn test_chdir_self() !void { + var old_cwd_buf: [path_max]u8 = undefined; + const old_cwd = try std.posix.getcwd(old_cwd_buf[0..]); + + // Try changing to the current directory + try std.posix.chdir(old_cwd); + try expect_cwd(old_cwd); +} + +fn test_chdir_absolute() !void { + var old_cwd_buf: [path_max]u8 = undefined; + const old_cwd = try std.posix.getcwd(old_cwd_buf[0..]); + + const parent = std.fs.path.dirname(old_cwd) orelse unreachable; // old_cwd should be absolute + + // Try changing to the parent via a full path + try std.posix.chdir(parent); + + try expect_cwd(parent); +} + +fn test_chdir_relative(a: std.mem.Allocator) !void { + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + + // Use the tmpDir parent_dir as the "base" for the test. Then cd into the child + try tmp.parent_dir.setAsCwd(); + + // Capture base working directory path, to build expected full path + var base_cwd_buf: [path_max]u8 = undefined; + const base_cwd = try std.posix.getcwd(base_cwd_buf[0..]); + + const relative_dir_name = &tmp.sub_path; + const expected_path = try std.fs.path.resolve(a, &.{ base_cwd, relative_dir_name }); + defer a.free(expected_path); + + // change current working directory to new test directory + try std.posix.chdir(relative_dir_name); + + var new_cwd_buf: [path_max]u8 = undefined; + const new_cwd = try std.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 + const resolved_cwd = try std.fs.path.resolve(a, &.{new_cwd}); + defer a.free(resolved_cwd); + + try std.testing.expectEqualStrings(expected_path, resolved_cwd); +} From aa1d2adffc78167b38b34afeb683c152ad5767f4 Mon Sep 17 00:00:00 2001 From: Pat Tullmann Date: Mon, 8 Sep 2025 22:08:04 -0700 Subject: [PATCH 5/7] standalone posix test for env vars --- lib/std/posix/test.zig | 18 ----------------- test/standalone/posix/build.zig | 8 ++++++++ test/standalone/posix/getenv.zig | 33 +++++++++++++++++++++++++++++++- 3 files changed, 40 insertions(+), 19 deletions(-) diff --git a/lib/std/posix/test.zig b/lib/std/posix/test.zig index 10649e9c0cf1..e6906293c776 100644 --- a/lib/std/posix/test.zig +++ b/lib/std/posix/test.zig @@ -607,24 +607,6 @@ test "mmap" { } } -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.getenv("") == null); - try expect(posix.getenv("BOGUSDOESNOTEXISTENVVAR") == null); - if (builtin.link_libc) { - try testing.expectEqualStrings(posix.getenv("USER") orelse "", mem.span(std.c.getenv("USER") orelse "")); - } - try expect(posix.getenvZ("BOGUSDOESNOTEXISTENVVAR") == null); - } -} - test "fcntl" { if (native_os == .windows or native_os == .wasi) return error.SkipZigTest; diff --git a/test/standalone/posix/build.zig b/test/standalone/posix/build.zig index 71446256acdd..52ec99628db2 100644 --- a/test/standalone/posix/build.zig +++ b/test/standalone/posix/build.zig @@ -3,6 +3,7 @@ const builtin = @import("builtin"); const Case = struct { src_path: []const u8, + set_env_vars: bool = false, }; const cases = [_]Case{ @@ -11,6 +12,7 @@ const cases = [_]Case{ }, .{ .src_path = "getenv.zig", + .set_env_vars = true, }, .{ .src_path = "sigaction.zig", @@ -68,5 +70,11 @@ fn run_exe(b: *std.Build, optimize: std.builtin.OptimizeMode, case: *const Case, const run_cmd = b.addRunArtifact(exe); + if (case.set_env_vars) { + run_cmd.setEnvironmentVariable("ZIG_TEST_POSIX_1EQ", "test=variable"); + run_cmd.setEnvironmentVariable("ZIG_TEST_POSIX_3EQ", "=test=variable="); + run_cmd.setEnvironmentVariable("ZIG_TEST_POSIX_EMPTY", ""); + } + return run_cmd; } diff --git a/test/standalone/posix/getenv.zig b/test/standalone/posix/getenv.zig index bbb7b6ed703d..8b28d09ae76d 100644 --- a/test/standalone/posix/getenv.zig +++ b/test/standalone/posix/getenv.zig @@ -1 +1,32 @@ -pub fn main() !void {} +// test getting environment variables + +const std = @import("std"); +const builtin = @import("builtin"); + +pub fn main() !void { + if (builtin.target.os.tag == .windows) { + return; // Windows env strings are WTF-16, so not supported by Zig's std.posix.getenv() + } + + if (builtin.target.os.tag == .wasi and !builtin.link_libc) { + return; // std.posix.getenv is not supported on WASI due to the need of allocation + } + + // Test some unset env vars: + + try std.testing.expectEqual(std.posix.getenv(""), null); + try std.testing.expectEqual(std.posix.getenv("BOGUSDOESNOTEXISTENVVAR"), null); + try std.testing.expectEqual(std.posix.getenvZ("BOGUSDOESNOTEXISTENVVAR"), null); + + if (builtin.link_libc) { + // Test if USER matches what C library sees + const expected = std.mem.span(std.c.getenv("USER") orelse ""); + const actual = std.posix.getenv("USER") orelse ""; + try std.testing.expectEqualStrings(expected, actual); + } + + // env vars set by our build.zig run step: + try std.testing.expectEqualStrings("", std.posix.getenv("ZIG_TEST_POSIX_EMPTY") orelse "invalid"); + try std.testing.expectEqualStrings("test=variable", std.posix.getenv("ZIG_TEST_POSIX_1EQ") orelse "invalid"); + try std.testing.expectEqualStrings("=test=variable=", std.posix.getenv("ZIG_TEST_POSIX_3EQ") orelse "invalid"); +} From ca09629beeb899c418b16e5c180daf4d3f5ba3f9 Mon Sep 17 00:00:00 2001 From: Pat Tullmann Date: Mon, 8 Sep 2025 22:52:32 -0700 Subject: [PATCH 6/7] standalone posix tests for relative path linking --- lib/std/posix/test.zig | 88 --------------------------- test/standalone/posix/relpaths.zig | 95 +++++++++++++++++++++++++++++- 2 files changed, 94 insertions(+), 89 deletions(-) diff --git a/lib/std/posix/test.zig b/lib/std/posix/test.zig index e6906293c776..b6a203ae6d6d 100644 --- a/lib/std/posix/test.zig +++ b/lib/std/posix/test.zig @@ -167,45 +167,6 @@ test "openat smoke test" { } } -test "symlink with relative paths" { - if (native_os == .wasi) return error.SkipZigTest; // Can symlink, but can't change into tmpDir - - var tmp = tmpDir(.{}); - defer tmp.cleanup(); - - const target_name = "symlink-target"; - const symlink_name = "symlinker"; - - // Restore default CWD at end of test. - const orig_cwd = try fs.cwd().openDir(".", .{}); - defer orig_cwd.setAsCwd() catch unreachable; - - // Create the target file - try tmp.dir.writeFile(.{ .sub_path = target_name, .data = "nonsense" }); - - // Want to test relative paths, so cd into the tmpdir for this test - try tmp.dir.setAsCwd(); - - if (native_os == .windows) { - const wtarget_name = try std.unicode.wtf8ToWtf16LeAllocZ(a, target_name); - const wsymlink_name = try std.unicode.wtf8ToWtf16LeAllocZ(a, symlink_name); - defer a.free(wtarget_name); - defer a.free(wsymlink_name); - - std.os.windows.CreateSymbolicLink(tmp.dir.fd, wsymlink_name, wtarget_name, 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.symlink(target_name, symlink_name); - } - - var buffer: [fs.max_path_bytes]u8 = undefined; - const given = try posix.readlink(symlink_name, buffer[0..]); - try expect(mem.eql(u8, target_name, given)); -} - test "readlink on Windows" { if (native_os != .windows) return error.SkipZigTest; @@ -220,55 +181,6 @@ fn testReadlink(target_path: []const u8, symlink_path: []const u8) !void { try expect(mem.eql(u8, target_path, given)); } -test "link with relative paths" { - if (native_os == .wasi) return error.SkipZigTest; // Can link, but can't change into tmpDir - if ((builtin.cpu.arch == .riscv32 or builtin.cpu.arch.isLoongArch()) and builtin.os.tag == .linux and !builtin.link_libc) return error.SkipZigTest; // No `fstat()`. - if (builtin.cpu.arch.isMIPS64()) return error.SkipZigTest; // `nstat.nlink` assertion is failing with LLVM 20+ for unclear reasons. - - switch (native_os) { - .wasi, .linux, .solaris, .illumos => {}, - else => return error.SkipZigTest, - } - - var tmp = tmpDir(.{}); - defer tmp.cleanup(); - - // Restore default CWD at end of test. - const orig_cwd = try fs.cwd().openDir(".", .{}); - defer orig_cwd.setAsCwd() catch unreachable; - - const target_name = "link-target"; - const link_name = "newlink"; - - try tmp.dir.writeFile(.{ .sub_path = target_name, .data = "example" }); - - // Test 1: create the relative link from inside tmp - try tmp.dir.setAsCwd(); - try posix.link(target_name, link_name); - - // Verify - const efd = try tmp.dir.openFile(target_name, .{}); - defer efd.close(); - - const nfd = try tmp.dir.openFile(link_name, .{}); - 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); - } - - // Test 2: Remove the link and see the stats update - try posix.unlink(link_name); - - { - const estat = try posix.fstat(efd.handle); - try testing.expectEqual(@as(@TypeOf(estat.nlink), 1), estat.nlink); - } -} - test "linkat with different directories" { if ((builtin.cpu.arch == .riscv32 or builtin.cpu.arch.isLoongArch()) and builtin.os.tag == .linux and !builtin.link_libc) return error.SkipZigTest; // No `fstatat()`. if (builtin.cpu.arch.isMIPS64()) return error.SkipZigTest; // `nstat.nlink` assertion is failing with LLVM 20+ for unclear reasons. diff --git a/test/standalone/posix/relpaths.zig b/test/standalone/posix/relpaths.zig index bbb7b6ed703d..c6e0418ec653 100644 --- a/test/standalone/posix/relpaths.zig +++ b/test/standalone/posix/relpaths.zig @@ -1 +1,94 @@ -pub fn main() !void {} +// Test relative paths through POSIX APIS. These tests have to change the cwd, so +// they shouldn't be Zig unit tests. + +const std = @import("std"); +const builtin = @import("builtin"); + +pub fn main() !void { + if (builtin.target.os.tag == .wasi) return; // Can link, but can't change into tmpDir + + var Allocator = std.heap.DebugAllocator(.{}){}; + const a = Allocator.allocator(); + defer std.debug.assert(Allocator.deinit() == .ok); + + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + + // Want to test relative paths, so cd into the tmpdir for these tests + try tmp.dir.setAsCwd(); + + try test_symlink(a, tmp); + try test_link(tmp); +} + +fn test_symlink(a: std.mem.Allocator, tmp: std.testing.TmpDir) !void { + const target_name = "symlink-target"; + const symlink_name = "symlinker"; + + // Create the target file + try tmp.dir.writeFile(.{ .sub_path = target_name, .data = "nonsense" }); + + if (builtin.target.os.tag == .windows) { + const wtarget_name = try std.unicode.wtf8ToWtf16LeAllocZ(a, target_name); + const wsymlink_name = try std.unicode.wtf8ToWtf16LeAllocZ(a, symlink_name); + defer a.free(wtarget_name); + defer a.free(wsymlink_name); + + std.os.windows.CreateSymbolicLink(tmp.dir.fd, wsymlink_name, wtarget_name, false) catch |err| switch (err) { + // Symlink requires admin privileges on windows, so this test can legitimately fail. + error.AccessDenied => return, + else => return err, + }; + } else { + try std.posix.symlink(target_name, symlink_name); + } + + var buffer: [std.fs.max_path_bytes]u8 = undefined; + const given = try std.posix.readlink(symlink_name, buffer[0..]); + try std.testing.expectEqualStrings(target_name, given); +} + +fn test_link(tmp: std.testing.TmpDir) !void { + switch (builtin.target.os.tag) { + .linux, .solaris, .illumos => {}, + else => return, + } + + if ((builtin.cpu.arch == .riscv32 or builtin.cpu.arch.isLoongArch()) and builtin.target.os.tag == .linux and !builtin.link_libc) { + return; // No `fstat()`. + } + + if (builtin.cpu.arch.isMIPS64()) { + return; // `nstat.nlink` assertion is failing with LLVM 20+ for unclear reasons. + } + + const target_name = "link-target"; + const link_name = "newlink"; + + try tmp.dir.writeFile(.{ .sub_path = target_name, .data = "example" }); + + // Test 1: create the relative link from inside tmp + try std.posix.link(target_name, link_name); + + // Verify + const efd = try tmp.dir.openFile(target_name, .{}); + defer efd.close(); + + const nfd = try tmp.dir.openFile(link_name, .{}); + defer nfd.close(); + + { + const estat = try std.posix.fstat(efd.handle); + const nstat = try std.posix.fstat(nfd.handle); + try std.testing.expectEqual(estat.ino, nstat.ino); + try std.testing.expectEqual(@as(@TypeOf(nstat.nlink), 2), nstat.nlink); + } + + // Test 2: Remove the link and see the stats update + try std.posix.unlink(link_name); + + { + const estat = try std.posix.fstat(efd.handle); + try std.testing.expectEqual(@as(@TypeOf(estat.nlink), 1), estat.nlink); + } +} From bd4617033e09979398e65907a1d9c7f1b6e1d124 Mon Sep 17 00:00:00 2001 From: Pat Tullmann Date: Mon, 8 Sep 2025 23:04:42 -0700 Subject: [PATCH 7/7] standalone posix tests for sigaction Fixes #24380 --- lib/std/posix/test.zig | 147 -------------------------- test/standalone/posix/sigaction.zig | 154 +++++++++++++++++++++++++++- 2 files changed, 153 insertions(+), 148 deletions(-) diff --git a/lib/std/posix/test.zig b/lib/std/posix/test.zig index b6a203ae6d6d..85651c13a5f5 100644 --- a/lib/std/posix/test.zig +++ b/lib/std/posix/test.zig @@ -703,153 +703,6 @@ test "sigset add/del" { } } -test "sigaction" { - if (native_os == .wasi or native_os == .windows) - 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 test_signo = posix.SIG.URG; // URG only because it is ignored by default in debuggers - - 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. - const info_sig = switch (native_os) { - .netbsd => info.info.signo, - else => info.signo, - }; - if (sig == test_signo and sig == info_sig) { - handler_called_count += 1; - } - } - }; - - var sa: posix.Sigaction = .{ - .handler = .{ .sigaction = &S.handler }, - .mask = posix.sigemptyset(), - .flags = posix.SA.SIGINFO | posix.SA.RESETHAND, - }; - - var old_sa: posix.Sigaction = undefined; - - // Install the new signal handler. - posix.sigaction(test_signo, &sa, null); - - // Check that we can read it back correctly. - posix.sigaction(test_signo, 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(test_signo); - try testing.expectEqual(1, S.handler_called_count); - - // Check if passing RESETHAND correctly reset the handler to SIG_DFL - posix.sigaction(test_signo, 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(test_signo, &sa, null); - try posix.raise(test_signo); - try testing.expectEqual(2, S.handler_called_count); - - // Now set the signal to ignored - sa.handler = .{ .handler = posix.SIG.IGN }; - sa.flags = 0; - posix.sigaction(test_signo, &sa, null); - - // Re-raise to ensure handler is actually ignored - try posix.raise(test_signo); - try testing.expectEqual(2, S.handler_called_count); - - // Ensure that ignored state is returned when querying - posix.sigaction(test_signo, null, &old_sa); - try testing.expectEqual(posix.SIG.IGN, old_sa.handler.handler); -} - -test "sigset_t bits" { - if (native_os == .wasi or native_os == .windows) - return error.SkipZigTest; - - const NO_SIG: i32 = 0; - - const S = struct { - var expected_sig: i32 = undefined; - var seen_sig: i32 = NO_SIG; - - fn handler(sig: i32, info: *const posix.siginfo_t, ctx_ptr: ?*anyopaque) callconv(.c) void { - _ = ctx_ptr; - - const info_sig = switch (native_os) { - .netbsd => info.info.signo, - else => info.signo, - }; - if (seen_sig == NO_SIG and sig == expected_sig and sig == info_sig) { - seen_sig = sig; - } - } - }; - - // Assume this is a single-threaded process where the current thread has the - // 'pid' thread id. (The sigprocmask calls are thread-private state.) - const self_tid = posix.system.getpid(); - - // To check that sigset_t mapping matches kernel (think u32/u64 mismatches on - // big-endian), try sending a blocked signal to make sure the mask matches the - // signal. (Send URG and CHLD because they're ignored by default in the - // debugger, vs. USR1 or other named signals) - inline for ([_]i32{ posix.SIG.URG, posix.SIG.CHLD, 62, 94, 126 }) |test_signo| { - if (test_signo >= posix.NSIG) continue; - - S.expected_sig = test_signo; - S.seen_sig = NO_SIG; - - const sa: posix.Sigaction = .{ - .handler = .{ .sigaction = &S.handler }, - .mask = posix.sigemptyset(), - .flags = posix.SA.SIGINFO | posix.SA.RESETHAND, - }; - - var old_sa: posix.Sigaction = undefined; - - // Install the new signal handler. - posix.sigaction(test_signo, &sa, &old_sa); - - // block the signal and see that its delayed until unblocked - var block_one: posix.sigset_t = posix.sigemptyset(); - posix.sigaddset(&block_one, test_signo); - posix.sigprocmask(posix.SIG.BLOCK, &block_one, null); - - // qemu maps target signals to host signals 1-to-1, so targets - // with more signals than the host will fail to send the signal. - const rc = posix.system.kill(self_tid, test_signo); - switch (posix.errno(rc)) { - .SUCCESS => { - // See that the signal is blocked, then unblocked - try testing.expectEqual(NO_SIG, S.seen_sig); - posix.sigprocmask(posix.SIG.UNBLOCK, &block_one, null); - try testing.expectEqual(test_signo, S.seen_sig); - }, - .INVAL => { - // Signal won't get delviered. Just clean up. - posix.sigprocmask(posix.SIG.UNBLOCK, &block_one, null); - try testing.expectEqual(NO_SIG, S.seen_sig); - }, - else => |errno| return posix.unexpectedErrno(errno), - } - - // Restore original handler - posix.sigaction(test_signo, &old_sa, null); - } -} - test "dup & dup2" { switch (native_os) { .linux, .solaris, .illumos => {}, diff --git a/test/standalone/posix/sigaction.zig b/test/standalone/posix/sigaction.zig index bbb7b6ed703d..4f5f81ad6653 100644 --- a/test/standalone/posix/sigaction.zig +++ b/test/standalone/posix/sigaction.zig @@ -1 +1,153 @@ -pub fn main() !void {} +const std = @import("std"); +const builtin = @import("builtin"); + +const native_os = builtin.target.os.tag; + +pub fn main() !void { + if (native_os == .wasi or native_os == .windows) { + return; // no sigaction + } + + try test_sigaction(); + try test_sigset_bits(); +} + +fn test_sigaction() !void { + if (native_os == .macos and builtin.target.cpu.arch == .x86_64) { + return; // https://github.com/ziglang/zig/issues/15381 + } + + const test_signo = std.posix.SIG.URG; // URG only because it is ignored by default in debuggers + + const S = struct { + var handler_called_count: u32 = 0; + + fn handler(sig: i32, info: *const std.posix.siginfo_t, ctx_ptr: ?*anyopaque) callconv(.c) void { + _ = ctx_ptr; + // Check that we received the correct signal. + const info_sig = switch (native_os) { + .netbsd => info.info.signo, + else => info.signo, + }; + if (sig == test_signo and sig == info_sig) { + handler_called_count += 1; + } + } + }; + + var sa: std.posix.Sigaction = .{ + .handler = .{ .sigaction = &S.handler }, + .mask = std.posix.sigemptyset(), + .flags = std.posix.SA.SIGINFO | std.posix.SA.RESETHAND, + }; + + var old_sa: std.posix.Sigaction = undefined; + + // Install the new signal handler. + std.posix.sigaction(test_signo, &sa, null); + + // Check that we can read it back correctly. + std.posix.sigaction(test_signo, null, &old_sa); + try std.testing.expectEqual(&S.handler, old_sa.handler.sigaction.?); + try std.testing.expect((old_sa.flags & std.posix.SA.SIGINFO) != 0); + + // Invoke the handler. + try std.posix.raise(test_signo); + try std.testing.expectEqual(1, S.handler_called_count); + + // Check if passing RESETHAND correctly reset the handler to SIG_DFL + std.posix.sigaction(test_signo, null, &old_sa); + try std.testing.expectEqual(std.posix.SIG.DFL, old_sa.handler.handler); + + // Reinstall the signal w/o RESETHAND and re-raise + sa.flags = std.posix.SA.SIGINFO; + std.posix.sigaction(test_signo, &sa, null); + try std.posix.raise(test_signo); + try std.testing.expectEqual(2, S.handler_called_count); + + // Now set the signal to ignored + sa.handler = .{ .handler = std.posix.SIG.IGN }; + sa.flags = 0; + std.posix.sigaction(test_signo, &sa, null); + + // Re-raise to ensure handler is actually ignored + try std.posix.raise(test_signo); + try std.testing.expectEqual(2, S.handler_called_count); + + // Ensure that ignored state is returned when querying + std.posix.sigaction(test_signo, null, &old_sa); + try std.testing.expectEqual(std.posix.SIG.IGN, old_sa.handler.handler); +} + +fn test_sigset_bits() !void { + const NO_SIG: i32 = 0; + + const S = struct { + var expected_sig: i32 = undefined; + var seen_sig: i32 = NO_SIG; + + fn handler(sig: i32, info: *const std.posix.siginfo_t, ctx_ptr: ?*anyopaque) callconv(.c) void { + _ = ctx_ptr; + + const info_sig = switch (native_os) { + .netbsd => info.info.signo, + else => info.signo, + }; + if (seen_sig == NO_SIG and sig == expected_sig and sig == info_sig) { + seen_sig = sig; + } + } + }; + + // Assume this is a single-threaded process where the current thread has the + // 'pid' thread id. (The sigprocmask calls are thread-private state.) + const self_tid = std.posix.system.getpid(); + + // To check that sigset_t mapping matches kernel (think u32/u64 mismatches on + // big-endian), try sending a blocked signal to make sure the mask matches the + // signal. (Send URG and CHLD because they're ignored by default in the + // debugger, vs. USR1 or other named signals) + inline for ([_]i32{ std.posix.SIG.URG, std.posix.SIG.CHLD, 62, 94, 126 }) |test_signo| { + if (test_signo >= std.posix.NSIG) continue; + + S.expected_sig = test_signo; + S.seen_sig = NO_SIG; + + const sa: std.posix.Sigaction = .{ + .handler = .{ .sigaction = &S.handler }, + .mask = std.posix.sigemptyset(), + .flags = std.posix.SA.SIGINFO | std.posix.SA.RESETHAND, + }; + + var old_sa: std.posix.Sigaction = undefined; + + // Install the new signal handler. + std.posix.sigaction(test_signo, &sa, &old_sa); + + // block the signal and see that its delayed until unblocked + var block_one: std.posix.sigset_t = std.posix.sigemptyset(); + std.posix.sigaddset(&block_one, test_signo); + std.posix.sigprocmask(std.posix.SIG.BLOCK, &block_one, null); + + // qemu maps target signals to host signals 1-to-1, so targets + // with more signals than the host will fail to send the signal. + const rc = std.posix.system.kill(self_tid, test_signo); + switch (std.posix.errno(rc)) { + .SUCCESS => { + // See that the signal is blocked, then unblocked + try std.testing.expectEqual(NO_SIG, S.seen_sig); + std.posix.sigprocmask(std.posix.SIG.UNBLOCK, &block_one, null); + try std.testing.expectEqual(test_signo, S.seen_sig); + }, + .INVAL => { + // Signal won't get delviered. Just clean up. + std.posix.sigprocmask(std.posix.SIG.UNBLOCK, &block_one, null); + try std.testing.expectEqual(NO_SIG, S.seen_sig); + }, + else => |errno| return std.posix.unexpectedErrno(errno), + } + + // Restore original handler + std.posix.sigaction(test_signo, &old_sa, null); + } +}