11const std = @import ("std" );
2+ const fs = std .fs ;
23const testing = std .testing ;
34const Child = std .process .Child ;
45const Writer = std .Io .Writer ;
@@ -165,33 +166,43 @@ pub const SpawnOptions = struct {
165166
166167/// Manages a long-running, interactive child process for testing.
167168pub 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}
0 commit comments