| 1 | const std = @import("std"); |
| 2 | const Io = std.Io; |
| 3 | |
| 4 | pub 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 | |
| 74 | var parent_test_error = false; |
| 75 | |
| 76 | fn 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 | } |