|
1 | 1 | const std = @import("std"); |
2 | 2 | const testing = std.testing; |
3 | 3 | const Child = std.process.Child; |
| 4 | +const Writer = std.Io.Writer; |
| 5 | +const Reader = std.Io.Reader; |
| 6 | +const ArrayList = std.ArrayList; |
4 | 7 |
|
5 | 8 | /// Options for `run` controlling I/O, allocator, and output limits. |
6 | 9 | pub const RunOptions = struct { |
@@ -151,3 +154,85 @@ pub fn run(options: RunOptions) !RunResult { |
151 | 154 | .allocator = options.allocator, |
152 | 155 | }; |
153 | 156 | } |
| 157 | + |
| 158 | +/// Options for `spawn` controlling the child process environment. |
| 159 | +pub const SpawnOptions = struct { |
| 160 | + argv: []const []const u8, |
| 161 | + allocator: std.mem.Allocator = testing.allocator, |
| 162 | + cwd: ?[]const u8 = null, |
| 163 | + env_map: ?*const std.process.EnvMap = null, |
| 164 | +}; |
| 165 | + |
| 166 | +/// Manages a long-running, interactive child process for testing. |
| 167 | +pub const InteractiveProcess = struct { |
| 168 | + child: Child, |
| 169 | + stdout_buffer: [1024]u8 = undefined, |
| 170 | + stdin_buffer: [1024]u8 = undefined, |
| 171 | + stderr_buffer: [1024]u8 = undefined, |
| 172 | + |
| 173 | + /// Cleans up resources and ensures the child process is terminated. |
| 174 | + /// This should always be called, typically with `defer`. |
| 175 | + pub fn deinit(self: *InteractiveProcess) void { |
| 176 | + if (self.child.stdin) |stdin_file| { |
| 177 | + stdin_file.close(); |
| 178 | + self.child.stdin = null; |
| 179 | + } |
| 180 | + _ = self.child.wait() catch {}; |
| 181 | + } |
| 182 | + |
| 183 | + /// Writes bytes to the child process's stdin. |
| 184 | + pub fn writeToStdin(self: *InteractiveProcess, bytes: []const u8) !void { |
| 185 | + const stdin_file = self.child.stdin orelse return error.MissingStdin; |
| 186 | + var stdin_writer = stdin_file.writer(&self.stdin_buffer); |
| 187 | + var stdin = &stdin_writer.interface; |
| 188 | + try stdin.writeAll(bytes); |
| 189 | + try stdin.flush(); |
| 190 | + } |
| 191 | + |
| 192 | + /// Reads from the child's stdout until a newline is found or the buffer is full. |
| 193 | + /// The returned slice does not include the newline character. |
| 194 | + pub fn readLineFromStdout(self: *InteractiveProcess) ![]const u8 { |
| 195 | + const stdout_file = self.child.stdout orelse return error.MissingStdout; |
| 196 | + var stdout_reader = stdout_file.reader(&self.stdout_buffer); |
| 197 | + var stdout = &stdout_reader.interface; |
| 198 | + const line = try stdout.takeDelimiter('\n') orelse return error.EmptyLine; |
| 199 | + // Handle potential CR on Windows if the child outputs CRLF |
| 200 | + const trimmed = std.mem.trimEnd(u8, line, "\r"); |
| 201 | + return trimmed; |
| 202 | + } |
| 203 | + |
| 204 | + /// Reads from the child's stderr until a newline is found. |
| 205 | + pub fn readLineFromStderr(self: *InteractiveProcess) ![]const u8 { |
| 206 | + const stderr_file = self.child.stderr orelse return error.MissingStderr; |
| 207 | + var stderr_reader = stderr_file.reader(&self.stderr_buffer); |
| 208 | + var stderr = &stderr_reader.interface; |
| 209 | + const line = try stderr.takeDelimiter('\n') orelse return error.EmptyLine; |
| 210 | + // Handle potential CR on Windows if the child outputs CRLF |
| 211 | + const trimmed = std.mem.trimEnd(u8, line, "\r"); |
| 212 | + return trimmed; |
| 213 | + } |
| 214 | +}; |
| 215 | + |
| 216 | +/// Spawns an executable for interactive testing. |
| 217 | +/// |
| 218 | +/// Returns an `InteractiveProcess` object to manage the child process's |
| 219 | +/// lifecycle and I/O. The caller is responsible for calling `deinit()` |
| 220 | +/// on the returned object to ensure cleanup. |
| 221 | +pub fn spawn(options: SpawnOptions) !InteractiveProcess { |
| 222 | + var child = Child.init(options.argv, options.allocator); |
| 223 | + child.cwd = options.cwd; |
| 224 | + child.env_map = options.env_map; |
| 225 | + |
| 226 | + // We need pipes for all streams to interact with them |
| 227 | + child.stdin_behavior = .Pipe; |
| 228 | + child.stdout_behavior = .Pipe; |
| 229 | + child.stderr_behavior = .Pipe; |
| 230 | + |
| 231 | + try child.spawn(); |
| 232 | + errdefer { |
| 233 | + // If anything fails after spawn, ensure we kill the process |
| 234 | + _ = child.kill() catch {}; |
| 235 | + } |
| 236 | + |
| 237 | + return InteractiveProcess{ .child = child }; |
| 238 | +} |
0 commit comments