From 536cf1d8a0fdac8e21414cf861adb511dcb648b3 Mon Sep 17 00:00:00 2001 From: alex Date: Sun, 2 Oct 2022 14:42:20 +0200 Subject: [PATCH] test/standalone: reinstate std.ChildProcess tests 67d5bfef removed std.ChildProcess tests, suggesting to make them standalone instead. This commit does exactly that after the bug creating SIGPIPE in RealeaseFast is no more with LLVM 15.0.5. Thanks to @x1ddos for the idea with the compile artifacts and PR improvements. --- test/standalone.zig | 1 + test/standalone/child_process/build.zig | 22 ++++++++++ test/standalone/child_process/child.zig | 50 ++++++++++++++++++++++ test/standalone/child_process/main.zig | 55 +++++++++++++++++++++++++ 4 files changed, 128 insertions(+) create mode 100644 test/standalone/child_process/build.zig create mode 100644 test/standalone/child_process/child.zig create mode 100644 test/standalone/child_process/main.zig diff --git a/test/standalone.zig b/test/standalone.zig index 965139235c35..6014ae832514 100644 --- a/test/standalone.zig +++ b/test/standalone.zig @@ -65,6 +65,7 @@ pub fn addCases(cases: *tests.StandaloneContext) void { (builtin.os.tag != .windows or builtin.cpu.arch != .aarch64)) { cases.addBuildFile("test/standalone/load_dynamic_library/build.zig", .{}); + cases.addBuildFile("test/standalone/child_process/build.zig", .{}); } if (builtin.os.tag == .windows) { diff --git a/test/standalone/child_process/build.zig b/test/standalone/child_process/build.zig new file mode 100644 index 000000000000..c6308d37c8cc --- /dev/null +++ b/test/standalone/child_process/build.zig @@ -0,0 +1,22 @@ +const Builder = @import("std").build.Builder; + +pub fn build(b: *Builder) void { + const optimize = b.standardOptimizeOption(.{}); + + const child = b.addExecutable(.{ + .name = "child", + .root_source_file = .{ .path = "child.zig" }, + .optimize = optimize, + }); + + const main = b.addExecutable(.{ + .name = "main", + .root_source_file = .{ .path = "main.zig" }, + .optimize = optimize, + }); + const run = main.run(); + run.addArtifactArg(child); + + const test_step = b.step("test", "Test it"); + test_step.dependOn(&run.step); +} diff --git a/test/standalone/child_process/child.zig b/test/standalone/child_process/child.zig new file mode 100644 index 000000000000..e9edcf9f4b8e --- /dev/null +++ b/test/standalone/child_process/child.zig @@ -0,0 +1,50 @@ +const std = @import("std"); + +// 42 is expected by parent; other values result in test failure +var exit_code: u8 = 42; + +pub fn main() !void { + var arena_state = std.heap.ArenaAllocator.init(std.heap.page_allocator); + const arena = arena_state.allocator(); + try run(arena); + arena_state.deinit(); + std.process.exit(exit_code); +} + +fn run(allocator: std.mem.Allocator) !void { + var args = try std.process.argsWithAllocator(allocator); + defer args.deinit(); + _ = args.next() orelse unreachable; // skip binary name + + // test cmd args + const hello_arg = "hello arg"; + const a1 = args.next() orelse unreachable; + if (!std.mem.eql(u8, a1, hello_arg)) { + testError("first arg: '{s}'; want '{s}'", .{ a1, hello_arg }); + } + if (args.next()) |a2| { + testError("expected only one arg; got more: {s}", .{a2}); + } + + // test stdout pipe; parent verifies + try std.io.getStdOut().writer().writeAll("hello from stdout"); + + // test stdin pipe from parent + const hello_stdin = "hello from stdin"; + var buf: [hello_stdin.len]u8 = undefined; + const stdin = std.io.getStdIn().reader(); + const n = try stdin.readAll(&buf); + if (!std.mem.eql(u8, buf[0..n], hello_stdin)) { + testError("stdin: '{s}'; want '{s}'", .{ buf[0..n], hello_stdin }); + } +} + +fn testError(comptime fmt: []const u8, args: anytype) void { + const stderr = std.io.getStdErr().writer(); + stderr.print("CHILD TEST ERROR: ", .{}) catch {}; + stderr.print(fmt, args) catch {}; + if (fmt[fmt.len - 1] != '\n') { + stderr.writeByte('\n') catch {}; + } + exit_code = 1; +} diff --git a/test/standalone/child_process/main.zig b/test/standalone/child_process/main.zig new file mode 100644 index 000000000000..e0da6070188d --- /dev/null +++ b/test/standalone/child_process/main.zig @@ -0,0 +1,55 @@ +const std = @import("std"); + +pub fn main() !void { + // make sure safety checks are enabled even in release modes + var gpa_state = std.heap.GeneralPurposeAllocator(.{ .safety = true }){}; + defer if (gpa_state.deinit()) { + @panic("found memory leaks"); + }; + const gpa = gpa_state.allocator(); + + var it = try std.process.argsWithAllocator(gpa); + defer it.deinit(); + _ = it.next() orelse unreachable; // skip binary name + const child_path = it.next() orelse unreachable; + + var child = std.ChildProcess.init(&.{ child_path, "hello arg" }, gpa); + child.stdin_behavior = .Pipe; + child.stdout_behavior = .Pipe; + child.stderr_behavior = .Inherit; + try child.spawn(); + const child_stdin = child.stdin.?; + try child_stdin.writer().writeAll("hello from stdin"); // verified in child + child_stdin.close(); + child.stdin = null; + + const hello_stdout = "hello from stdout"; + var buf: [hello_stdout.len]u8 = undefined; + const n = try child.stdout.?.reader().readAll(&buf); + if (!std.mem.eql(u8, buf[0..n], hello_stdout)) { + testError("child stdout: '{s}'; want '{s}'", .{ buf[0..n], hello_stdout }); + } + + switch (try child.wait()) { + .Exited => |code| { + const child_ok_code = 42; // set by child if no test errors + if (code != child_ok_code) { + testError("child exit code: {d}; want {d}", .{ code, child_ok_code }); + } + }, + else => |term| testError("abnormal child exit: {}", .{term}), + } + return if (parent_test_error) error.ParentTestError else {}; +} + +var parent_test_error = false; + +fn testError(comptime fmt: []const u8, args: anytype) void { + const stderr = std.io.getStdErr().writer(); + stderr.print("PARENT TEST ERROR: ", .{}) catch {}; + stderr.print(fmt, args) catch {}; + if (fmt[fmt.len - 1] != '\n') { + stderr.writeByte('\n') catch {}; + } + parent_test_error = true; +}