Skip to content

Commit 1f02662

Browse files
committed
test(InteractiveProcess): add writeToStdin tests
1 parent e3c67be commit 1f02662

File tree

2 files changed

+87
-21
lines changed

2 files changed

+87
-21
lines changed

src/root.zig

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
const std = @import("std");
2+
const fs = std.fs;
23
const testing = std.testing;
34
const Child = std.process.Child;
45
const Writer = std.Io.Writer;
@@ -165,33 +166,43 @@ pub const SpawnOptions = struct {
165166

166167
/// Manages a long-running, interactive child process for testing.
167168
pub const InteractiveProcess = struct {
169+
const Self = @This();
170+
168171
child: Child,
172+
pid: Child.Id,
169173
stdout_buffer: [1024]u8 = undefined,
170174
stdin_buffer: [1024]u8 = undefined,
171175
stderr_buffer: [1024]u8 = undefined,
172176

173177
/// Cleans up resources and ensures the child process is terminated.
174178
/// This should always be called, typically with `defer`.
175-
pub fn deinit(self: *InteractiveProcess) void {
179+
pub fn deinit(self: *Self) void {
176180
if (self.child.stdin) |stdin_file| {
177181
stdin_file.close();
178182
self.child.stdin = null;
179183
}
180184
_ = self.child.wait() catch {};
181185
}
182186

187+
pub const WriteStdinError = error{
188+
// This means process already exited
189+
ProcessExited,
190+
// This means something wrong when writing to stdin
191+
WriteFailed,
192+
};
193+
183194
/// 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;
195+
pub fn writeToStdin(self: *Self, bytes: []const u8) WriteStdinError!void {
196+
const stdin_file = self.child.stdin orelse return error.ProcessExited;
186197
var stdin_writer = stdin_file.writer(&self.stdin_buffer);
187198
var stdin = &stdin_writer.interface;
188-
try stdin.writeAll(bytes);
189-
try stdin.flush();
199+
stdin.writeAll(bytes) catch return error.WriteFailed;
200+
stdin.flush() catch return error.WriteFailed;
190201
}
191202

192203
/// Reads from the child's stdout until a newline is found or the buffer is full.
193204
/// The returned slice does not include the newline character.
194-
pub fn readLineFromStdout(self: *InteractiveProcess) ![]const u8 {
205+
pub fn readLineFromStdout(self: *Self) ![]const u8 {
195206
const stdout_file = self.child.stdout orelse return error.MissingStdout;
196207
var stdout_reader = stdout_file.reader(&self.stdout_buffer);
197208
var stdout = &stdout_reader.interface;
@@ -202,7 +213,7 @@ pub const InteractiveProcess = struct {
202213
}
203214

204215
/// Reads from the child's stderr until a newline is found.
205-
pub fn readLineFromStderr(self: *InteractiveProcess) ![]const u8 {
216+
pub fn readLineFromStderr(self: *Self) ![]const u8 {
206217
const stderr_file = self.child.stderr orelse return error.MissingStderr;
207218
var stderr_reader = stderr_file.reader(&self.stderr_buffer);
208219
var stderr = &stderr_reader.interface;
@@ -211,6 +222,28 @@ pub const InteractiveProcess = struct {
211222
const trimmed = std.mem.trimEnd(u8, line, "\r");
212223
return trimmed;
213224
}
225+
226+
// TODO: add expectStdout
227+
// TODO: add expectStderr
228+
229+
pub fn expectStdout(self: *Self, expected: []const u8) !void {
230+
var stderr_writer = fs.File.stdout().writer(&self.stderr_buffer);
231+
var stderr = &stderr_writer.interface;
232+
233+
const actual = self.readLineFromStdout() catch |err| {
234+
try stderr.print("\n\n--- Test Expectation Failed ---\n", .{});
235+
try stderr.print("Expected to read from stdout:\n{s}\n\n", .{expected});
236+
try stderr.print("But the read operation failed with error: {any}\n", .{err});
237+
try stderr.print("---------------------------------\n\n", .{});
238+
try stderr.flush();
239+
return err;
240+
};
241+
242+
std.testing.expectEqualStrings(expected, actual) catch |err| {
243+
try stderr.print("\n\n--- HELLO ---\n", .{});
244+
return err;
245+
};
246+
}
214247
};
215248

216249
/// Spawns an executable for interactive testing.
@@ -229,10 +262,6 @@ pub fn spawn(options: SpawnOptions) !InteractiveProcess {
229262
child.stderr_behavior = .Pipe;
230263

231264
try child.spawn();
232-
errdefer {
233-
// If anything fails after spawn, ensure we kill the process
234-
_ = child.kill() catch {};
235-
}
236265

237-
return InteractiveProcess{ .child = child };
266+
return InteractiveProcess{ .child = child, .pid = child.id };
238267
}

src/test.zig

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
const std = @import("std");
2+
const builtin = @import("builtin");
23
const testing = std.testing;
34
const cmdtest = @import("cmdtest");
45

@@ -170,19 +171,55 @@ test "run: with env_map" {
170171
try testing.expectEqualStrings("hello-env\n", result.stdout);
171172
}
172173

173-
test "spawn: interactive mode" {
174-
const argv = &[_][]const u8{ "cmdtest", "--interactive" };
174+
// test "spawn: interactive mode" {
175+
// const argv = &[_][]const u8{ "cmdtest", "--exit", "42" };
176+
// var proc = try cmdtest.spawn(.{ .argv = argv });
177+
// defer proc.deinit();
178+
179+
// try proc.writeToStdin("PING\n"); // I expect this to be fail as process exit with 42 as code
180+
// try proc.expectStdout("TEST");
181+
182+
// try proc.writeToStdin("ECHO works\n");
183+
// try testing.expectEqualStrings("works", try proc.readLineFromStdout());
184+
185+
// try proc.writeToStdin("EXIT\n");
186+
187+
// const term = try proc.child.wait();
188+
// try testing.expectEqual(@as(u8, 0), term.Exited);
189+
// }
190+
191+
test "writeToStdin: running process that accepts stdin" {
192+
const argv = &[_][]const u8{"cat"};
175193
var proc = try cmdtest.spawn(.{ .argv = argv });
176194
defer proc.deinit();
177195

178-
try proc.writeToStdin("PING\n");
179-
try testing.expectEqualStrings("PONG", try proc.readLineFromStdout());
196+
// This write should succeed without error.
197+
try proc.writeToStdin("data\n");
198+
try proc.expectStdout("data");
199+
}
200+
201+
test "writeToStdin: process confirmed exited" {
202+
const argv = &[_][]const u8{ "echo", "42" };
203+
var proc = try cmdtest.spawn(.{ .argv = argv });
204+
defer proc.deinit();
205+
_ = try proc.child.wait();
206+
try testing.expectError(error.ProcessExited, proc.writeToStdin("data\n"));
207+
}
180208

181-
try proc.writeToStdin("ECHO works\n");
182-
try testing.expectEqualStrings("works", try proc.readLineFromStdout());
209+
test "writeToStdin: running process that ignores stdin" {
210+
const argv = &[_][]const u8{ "sleep", "1" };
211+
var proc = try cmdtest.spawn(.{ .argv = argv });
212+
defer proc.deinit();
213+
try proc.writeToStdin("this is ignored\n");
214+
}
183215

184-
try proc.writeToStdin("EXIT\n");
216+
test "writeToStdin: process died unexpectedly" {
217+
// NOTE: skipped for now, this race condition is known issue
218+
if (builtin.is_test) return error.SkipZigTest;
185219

186-
const term = try proc.child.wait();
187-
try testing.expectEqual(@as(u8, 0), term.Exited);
220+
const argv = &[_][]const u8{"ls"};
221+
var proc = try cmdtest.spawn(.{ .argv = argv });
222+
defer proc.deinit();
223+
try testing.expectError(error.ProcessExited, proc.writeToStdin("data\n"));
224+
// TODO: handle this
188225
}

0 commit comments

Comments
 (0)