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; +}