@@ -5,8 +5,8 @@ use crate::types::{KernelReport, TestCategory, TestRecord, TestResult};
55use chrono:: Utc ;
66use jupyter_protocol:: connection_info:: { ConnectionInfo , Transport } ;
77use jupyter_protocol:: messaging:: {
8- ExecuteRequest , ExecutionState , JupyterMessage , JupyterMessageContent , KernelInfoReply ,
9- KernelInfoRequest , ShutdownRequest , Status ,
8+ ExecuteRequest , ExecutionState , InputReply , JupyterMessage , JupyterMessageContent ,
9+ KernelInfoReply , KernelInfoRequest , ReplyStatus , ShutdownRequest , Status ,
1010} ;
1111use runtimelib:: {
1212 create_client_control_connection, create_client_heartbeat_connection,
@@ -297,6 +297,94 @@ impl KernelUnderTest {
297297 Ok ( ( reply, iopub_messages) )
298298 }
299299
300+ /// Execute code that may request stdin input, providing a mock response.
301+ ///
302+ /// Returns the execute_reply, IOPub messages, and whether an input_request was received.
303+ pub async fn execute_with_stdin (
304+ & mut self ,
305+ code : & str ,
306+ input_response : & str ,
307+ ) -> Result < ( JupyterMessage , Vec < JupyterMessage > , bool ) > {
308+ let mut request = ExecuteRequest :: new ( code. to_string ( ) ) ;
309+ request. allow_stdin = true ;
310+ let msg: JupyterMessage = request. into ( ) ;
311+ let msg_id = msg. header . msg_id . clone ( ) ;
312+
313+ self . shell
314+ . send ( msg)
315+ . await
316+ . map_err ( |e| HarnessError :: ProtocolError ( e. to_string ( ) ) ) ?;
317+
318+ let mut iopub_messages = Vec :: new ( ) ;
319+ let mut received_input_request = false ;
320+ let start = Instant :: now ( ) ;
321+
322+ // Poll both IOPub and stdin until we see idle
323+ loop {
324+ if start. elapsed ( ) > self . test_timeout {
325+ return Err ( HarnessError :: Timeout ( "iopub idle (stdin test)" . to_string ( ) ) ) ;
326+ }
327+
328+ // Check for stdin input_request
329+ match timeout ( Duration :: from_millis ( 50 ) , self . stdin . read ( ) ) . await {
330+ Ok ( Ok ( stdin_msg) ) => {
331+ if let JupyterMessageContent :: InputRequest ( _req) = & stdin_msg. content {
332+ received_input_request = true ;
333+ // Send input_reply with our mock response
334+ let reply = InputReply {
335+ value : input_response. to_string ( ) ,
336+ status : ReplyStatus :: Ok ,
337+ error : None ,
338+ } ;
339+ let reply_msg = JupyterMessage :: new ( reply, Some ( & stdin_msg) ) ;
340+ self . stdin
341+ . send ( reply_msg)
342+ . await
343+ . map_err ( |e| HarnessError :: ProtocolError ( e. to_string ( ) ) ) ?;
344+ }
345+ }
346+ Ok ( Err ( e) ) => {
347+ // Log but don't fail on stdin errors
348+ eprintln ! ( "stdin read error: {}" , e) ;
349+ }
350+ Err ( _) => {
351+ // Timeout on stdin read, that's fine
352+ }
353+ }
354+
355+ // Check for IOPub messages
356+ match timeout ( Duration :: from_millis ( 50 ) , self . iopub . read ( ) ) . await {
357+ Ok ( Ok ( msg) ) => {
358+ if msg. parent_header . as_ref ( ) . map ( |h| & h. msg_id ) == Some ( & msg_id) {
359+ let is_idle = matches ! (
360+ & msg. content,
361+ JupyterMessageContent :: Status ( Status { execution_state } )
362+ if * execution_state == ExecutionState :: Idle
363+ ) ;
364+ iopub_messages. push ( msg) ;
365+ if is_idle {
366+ break ;
367+ }
368+ }
369+ }
370+ Ok ( Err ( e) ) => {
371+ return Err ( HarnessError :: ProtocolError ( e. to_string ( ) ) ) ;
372+ }
373+ Err ( _) => {
374+ // Timeout on this read, continue loop
375+ }
376+ }
377+ }
378+
379+ // Read the execute_reply
380+ let reply = timeout ( self . test_timeout , self . shell . read ( ) )
381+ . await
382+ . map_err ( |_| HarnessError :: Timeout ( "execute_reply (stdin test)" . to_string ( ) ) ) ?
383+ . map_err ( |e| HarnessError :: ProtocolError ( e. to_string ( ) ) ) ?;
384+
385+ Ok ( ( reply, iopub_messages, received_input_request) )
386+ }
387+
300388 /// Test heartbeat.
301389 pub async fn heartbeat ( & mut self ) -> Result < ( ) > {
302390 timeout ( self . test_timeout , self . heartbeat . single_heartbeat ( ) )
0 commit comments