1const std = @import("std");
2const Io = std.Io;
3
4pub fn main(init: std.process.Init.Minimal) !void {
5 // make sure safety checks are enabled even in release modes
6 var gpa_state: std.heap.DebugAllocator(.{ .safety = true }) = .{};
7 defer if (gpa_state.deinit() != .ok) {
8 @panic("found memory leaks");
9 };
10 const gpa = gpa_state.allocator();
11
12 var threaded: Io.Threaded = .init(gpa, .{
13 .argv0 = .init(init.args),
14 .environ = init.environ,
15 });
16 defer threaded.deinit();
17 const io = threaded.io();
18
19 const process_cwd_path = try std.process.currentPathAlloc(io, gpa);
20 defer gpa.free(process_cwd_path);
21
22 var environ_map = try init.environ.createMap(gpa);
23 defer environ_map.deinit();
24
25 var it = try init.args.iterateAllocator(gpa);
26 defer it.deinit();
27 _ = it.next() orelse unreachable; // skip binary name
28 const child_path, const needs_free = child_path: {
29 const child_path = it.next() orelse unreachable;
30 const cwd_path = it.next() orelse break :child_path .{ child_path, false };
31 // If there is a third argument, it is the current CWD somewhere within the cache directory.
32 // In that case, modify the child path in order to test spawning a path with a leading `..` component.
33 break :child_path .{ try std.fs.path.relative(gpa, process_cwd_path, &environ_map, cwd_path, child_path), true };
34 };
35 defer if (needs_free) gpa.free(child_path);
36
37 var child = try std.process.spawn(io, .{
38 .argv = &.{ child_path, "hello arg" },
39 .stdin = .pipe,
40 .stdout = .pipe,
41 .stderr = .inherit,
42 });
43
44 const child_stdin = child.stdin.?;
45 try child_stdin.writeStreamingAll(io, "hello from stdin"); // verified in child
46 child_stdin.close(io);
47 child.stdin = null;
48
49 const hello_stdout = "hello from stdout";
50 var buf: [hello_stdout.len]u8 = undefined;
51 var stdout_reader = child.stdout.?.readerStreaming(io, &.{});
52 const n = try stdout_reader.interface.readSliceShort(&buf);
53 if (!std.mem.eql(u8, buf[0..n], hello_stdout)) {
54 testError(io, "child stdout: '{s}'; want '{s}'", .{ buf[0..n], hello_stdout });
55 }
56
57 switch (try child.wait(io)) {
58 .exited => |code| {
59 const child_ok_code = 42; // set by child if no test errors
60 if (code != child_ok_code) {
61 testError(io, "child exit code: {d}; want {d}", .{ code, child_ok_code });
62 }
63 },
64 else => |term| testError(io, "abnormal child exit: {}", .{term}),
65 }
66 if (parent_test_error) return error.ParentTestError;
67
68 // Check that FileNotFound is consistent across platforms when trying to spawn an executable that doesn't exist
69 const missing_child_path = try std.mem.concat(gpa, u8, &.{ child_path, "_intentionally_missing" });
70 defer gpa.free(missing_child_path);
71 try std.testing.expectError(error.FileNotFound, std.process.run(gpa, io, .{ .argv = &.{missing_child_path} }));
72}
73
74var parent_test_error = false;
75
76fn testError(io: Io, comptime fmt: []const u8, args: anytype) void {
77 var stderr_writer = Io.File.stderr().writer(io, &.{});
78 const stderr = &stderr_writer.interface;
79 stderr.print("PARENT TEST ERROR: ", .{}) catch {};
80 stderr.print(fmt, args) catch {};
81 if (fmt[fmt.len - 1] != '\n') {
82 stderr.writeByte('\n') catch {};
83 }
84 parent_test_error = true;
85}