Skip to content

Commit

Permalink
test/standalone: reinstate std.ChildProcess tests
Browse files Browse the repository at this point in the history
67d5bfe 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.
  • Loading branch information
x1ddos authored and Jan Philipp Hafer committed Feb 21, 2023
1 parent b52be97 commit 536cf1d
Show file tree
Hide file tree
Showing 4 changed files with 128 additions and 0 deletions.
1 change: 1 addition & 0 deletions test/standalone.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
22 changes: 22 additions & 0 deletions test/standalone/child_process/build.zig
Original file line number Diff line number Diff line change
@@ -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);
}
50 changes: 50 additions & 0 deletions test/standalone/child_process/child.zig
Original file line number Diff line number Diff line change
@@ -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;
}
55 changes: 55 additions & 0 deletions test/standalone/child_process/main.zig
Original file line number Diff line number Diff line change
@@ -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;
}

0 comments on commit 536cf1d

Please sign in to comment.