Skip to content

Commit 584b9c6

Browse files
committed
refactor(InteractiveProcess): rename writeToStdin to write
1 parent 1f02662 commit 584b9c6

2 files changed

Lines changed: 105 additions & 19 deletions

File tree

src/root.zig

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -184,15 +184,39 @@ pub const InteractiveProcess = struct {
184184
_ = self.child.wait() catch {};
185185
}
186186

187-
pub const WriteStdinError = error{
188-
// This means process already exited
187+
pub const WriteError = error{
188+
// This means process already exited or child.stdin is closed
189189
ProcessExited,
190190
// This means something wrong when writing to stdin
191191
WriteFailed,
192192
};
193193

194+
pub const ReadStdoutError = error{
195+
// This means process already exited
196+
ProcessExited,
197+
// This means something wrong when reading from stdout
198+
ReadFailed,
199+
};
200+
194201
/// Writes bytes to the child process's stdin.
195-
pub fn writeToStdin(self: *Self, bytes: []const u8) WriteStdinError!void {
202+
///
203+
/// This method attempts to write all bytes and flush the underlying pipe.
204+
/// If the child's stdin is not available (closed or process exited) this
205+
/// returns `error.ProcessExited`. Other I/O failures return `error.WriteFailed`.
206+
///
207+
/// Notes:
208+
/// - Writing to a child's stdin only writes into the OS pipe buffer. It does
209+
/// not guarantee the child will read or consume those bytes. For example,
210+
/// a process that intentionally ignores stdin (or never reads from it) may
211+
/// still make `write` succeed because the bytes are buffered by the OS.
212+
/// - Short-lived commands may introduce a timing/race condition: a command
213+
/// might exit quickly around the same time as a write attempt. Depending
214+
/// on timing, `write` may succeed because the write occurred before the
215+
/// kernel noticed the process exit (or due to buffering), or it may return
216+
/// `error.ProcessExited`. As a result, tests asserting a deterministic
217+
/// `error.ProcessExited` for short-lived commands can be flaky and may
218+
/// need to be skipped for reliability.
219+
pub fn write(self: *Self, bytes: []const u8) WriteError!void {
196220
const stdin_file = self.child.stdin orelse return error.ProcessExited;
197221
var stdin_writer = stdin_file.writer(&self.stdin_buffer);
198222
var stdin = &stdin_writer.interface;
@@ -202,13 +226,13 @@ pub const InteractiveProcess = struct {
202226

203227
/// Reads from the child's stdout until a newline is found or the buffer is full.
204228
/// The returned slice does not include the newline character.
205-
pub fn readLineFromStdout(self: *Self) ![]const u8 {
206-
const stdout_file = self.child.stdout orelse return error.MissingStdout;
229+
pub fn readLineFromStdout(self: *Self) ReadStdoutError![]const u8 {
230+
const stdout_file = self.child.stdout orelse return error.ProcessExited;
207231
var stdout_reader = stdout_file.reader(&self.stdout_buffer);
208232
var stdout = &stdout_reader.interface;
209-
const line = try stdout.takeDelimiter('\n') orelse return error.EmptyLine;
233+
const line = stdout.takeDelimiter('\n') catch return error.ReadFailed;
210234
// Handle potential CR on Windows if the child outputs CRLF
211-
const trimmed = std.mem.trimEnd(u8, line, "\r");
235+
const trimmed = std.mem.trimEnd(u8, line.?, "\r");
212236
return trimmed;
213237
}
214238

src/test.zig

Lines changed: 74 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -176,50 +176,112 @@ test "run: with env_map" {
176176
// var proc = try cmdtest.spawn(.{ .argv = argv });
177177
// defer proc.deinit();
178178

179-
// try proc.writeToStdin("PING\n"); // I expect this to be fail as process exit with 42 as code
179+
// try proc.write("PING\n"); // I expect this to be fail as process exit with 42 as code
180180
// try proc.expectStdout("TEST");
181181

182-
// try proc.writeToStdin("ECHO works\n");
182+
// try proc.write("ECHO works\n");
183183
// try testing.expectEqualStrings("works", try proc.readLineFromStdout());
184184

185-
// try proc.writeToStdin("EXIT\n");
185+
// try proc.write("EXIT\n");
186186

187187
// const term = try proc.child.wait();
188188
// try testing.expectEqual(@as(u8, 0), term.Exited);
189189
// }
190190

191-
test "writeToStdin: running process that accepts stdin" {
191+
test "write: running process that accepts stdin" {
192192
const argv = &[_][]const u8{"cat"};
193193
var proc = try cmdtest.spawn(.{ .argv = argv });
194194
defer proc.deinit();
195195

196196
// This write should succeed without error.
197-
try proc.writeToStdin("data\n");
197+
try proc.write("data\n");
198198
try proc.expectStdout("data");
199199
}
200200

201-
test "writeToStdin: process confirmed exited" {
201+
test "write: process confirmed exited" {
202202
const argv = &[_][]const u8{ "echo", "42" };
203203
var proc = try cmdtest.spawn(.{ .argv = argv });
204204
defer proc.deinit();
205205
_ = try proc.child.wait();
206-
try testing.expectError(error.ProcessExited, proc.writeToStdin("data\n"));
206+
try testing.expectError(error.ProcessExited, proc.write("data\n"));
207207
}
208208

209-
test "writeToStdin: running process that ignores stdin" {
209+
test "write: running process that ignores stdin" {
210210
const argv = &[_][]const u8{ "sleep", "1" };
211211
var proc = try cmdtest.spawn(.{ .argv = argv });
212212
defer proc.deinit();
213-
try proc.writeToStdin("this is ignored\n");
213+
try proc.write("this is ignored\n");
214214
}
215215

216-
test "writeToStdin: process died unexpectedly" {
216+
test "write: process died unexpectedly" {
217217
// NOTE: skipped for now, this race condition is known issue
218218
if (builtin.is_test) return error.SkipZigTest;
219219

220220
const argv = &[_][]const u8{"ls"};
221221
var proc = try cmdtest.spawn(.{ .argv = argv });
222222
defer proc.deinit();
223-
try testing.expectError(error.ProcessExited, proc.writeToStdin("data\n"));
224-
// TODO: handle this
223+
try testing.expectError(error.ProcessExited, proc.write("data\n"));
225224
}
225+
226+
test "readLineFromStdout: happy path" {
227+
const argv = &[_][]const u8{ "cmdtest", "--interactive" };
228+
var proc = try cmdtest.spawn(.{ .argv = argv });
229+
defer proc.deinit();
230+
231+
try proc.write("PING\n");
232+
try testing.expectEqualStrings("PONG", try proc.readLineFromStdout());
233+
}
234+
235+
test "readLineFromStdout: multiple lines" {
236+
const argv = &[_][]const u8{ "cmdtest", "--interactive" };
237+
var proc = try cmdtest.spawn(.{ .argv = argv });
238+
defer proc.deinit();
239+
240+
try proc.write("ECHO line one\n");
241+
try testing.expectEqualStrings("line one", try proc.readLineFromStdout());
242+
243+
try proc.write("ECHO line two\n");
244+
try testing.expectEqualStrings("line two", try proc.readLineFromStdout());
245+
}
246+
247+
test "readLineFromStdout: handles CRLF endings" {
248+
const argv = &[_][]const u8{ "cmdtest", "--interactive" };
249+
var proc = try cmdtest.spawn(.{ .argv = argv });
250+
defer proc.deinit();
251+
252+
try proc.write("ECHO line with cr\r\n");
253+
try testing.expectEqualStrings("line with cr", try proc.readLineFromStdout());
254+
}
255+
256+
test "readLineFromStdout: empty line" {
257+
const argv = &[_][]const u8{ "cmdtest", "--interactive" };
258+
var proc = try cmdtest.spawn(.{ .argv = argv });
259+
defer proc.deinit();
260+
261+
try proc.write("ECHO \n");
262+
try testing.expectEqualStrings("", try proc.readLineFromStdout());
263+
}
264+
265+
test "readLineFromStdout: process exited before read" {
266+
const argv = &[_][]const u8{ "echo", "ok" };
267+
var proc = try cmdtest.spawn(.{ .argv = argv });
268+
defer proc.deinit();
269+
270+
// Wait for the process to fully exit. Its stdout pipe will be closed.
271+
_ = try proc.child.wait();
272+
273+
// Attempting to read from a closed pipe should fail
274+
try testing.expectError(error.ProcessExited, proc.readLineFromStdout());
275+
}
276+
277+
// test "readLineFromStdout: child writes no newline" {
278+
// // `printf` is a good tool for writing output without a trailing newline
279+
// const argv = &[_][]const u8{ "printf", "no-newline" };
280+
// var proc = try cmdtest.spawn(.{ .argv = argv });
281+
// defer proc.deinit();
282+
283+
// // The read will consume "no-newline", hit EOF, and since it never found a
284+
// // '\n' delimiter, it will return an error (EndOfStream), which our
285+
// // function maps to ReadFailed.
286+
// try testing.expectError(error.ReadFailed, proc.readLineFromStdout());
287+
// }

0 commit comments

Comments
 (0)