@@ -5,8 +5,9 @@ use crate::types::{KernelReport, TestCategory, TestRecord, TestResult};
55use chrono:: Utc ;
66use jupyter_protocol:: connection_info:: { ConnectionInfo , Transport } ;
77use jupyter_protocol:: messaging:: {
8- ExecuteRequest , ExecutionState , InputReply , JupyterMessage , JupyterMessageContent ,
9- KernelInfoReply , KernelInfoRequest , ReplyStatus , ShutdownRequest , Status ,
8+ CommClose , CommOpen , ExecuteRequest , ExecutionState , InputReply , JupyterMessage ,
9+ JupyterMessageContent , KernelInfoReply , KernelInfoRequest , ReplyStatus , ShutdownRequest ,
10+ Status ,
1011} ;
1112use runtimelib:: {
1213 create_client_control_connection, create_client_heartbeat_connection,
@@ -224,6 +225,60 @@ impl KernelUnderTest {
224225 . map_err ( |e| HarnessError :: ProtocolError ( e. to_string ( ) ) )
225226 }
226227
228+ /// Send a request on shell and wait for reply, also collecting IOPub messages.
229+ pub async fn shell_request_with_iopub (
230+ & mut self ,
231+ content : impl Into < JupyterMessageContent > ,
232+ ) -> Result < ( JupyterMessage , Vec < JupyterMessage > ) > {
233+ let request: JupyterMessage = JupyterMessage :: new ( content, None ) ;
234+ let msg_id = request. header . msg_id . clone ( ) ;
235+
236+ self . shell
237+ . send ( request)
238+ . await
239+ . map_err ( |e| HarnessError :: ProtocolError ( e. to_string ( ) ) ) ?;
240+
241+ // Collect IOPub messages until idle
242+ let mut iopub_messages = Vec :: new ( ) ;
243+ let start = Instant :: now ( ) ;
244+
245+ loop {
246+ if start. elapsed ( ) > self . test_timeout {
247+ return Err ( HarnessError :: Timeout ( "iopub idle" . to_string ( ) ) ) ;
248+ }
249+
250+ match timeout ( Duration :: from_millis ( 100 ) , self . iopub . read ( ) ) . await {
251+ Ok ( Ok ( msg) ) => {
252+ if msg. parent_header . as_ref ( ) . map ( |h| & h. msg_id ) == Some ( & msg_id) {
253+ let is_idle = matches ! (
254+ & msg. content,
255+ JupyterMessageContent :: Status ( Status { execution_state } )
256+ if * execution_state == ExecutionState :: Idle
257+ ) ;
258+ iopub_messages. push ( msg) ;
259+ if is_idle {
260+ break ;
261+ }
262+ }
263+ }
264+ Ok ( Err ( e) ) => {
265+ return Err ( HarnessError :: ProtocolError ( e. to_string ( ) ) ) ;
266+ }
267+ Err ( _) => {
268+ // Timeout on this read, continue
269+ }
270+ }
271+ }
272+
273+ // Read shell reply
274+ let reply = timeout ( self . test_timeout , self . shell . read ( ) )
275+ . await
276+ . map_err ( |_| HarnessError :: Timeout ( "shell reply" . to_string ( ) ) ) ?
277+ . map_err ( |e| HarnessError :: ProtocolError ( e. to_string ( ) ) ) ?;
278+
279+ Ok ( ( reply, iopub_messages) )
280+ }
281+
227282 /// Send a request on control and wait for reply.
228283 pub async fn control_request (
229284 & mut self ,
@@ -398,6 +453,47 @@ impl KernelUnderTest {
398453 & mut self . stdin
399454 }
400455
456+ /// Send comm_open and check if kernel rejects it (returns true if rejected).
457+ pub async fn send_comm_open ( & mut self , msg : CommOpen ) -> Result < bool > {
458+ let comm_id = msg. comm_id . clone ( ) ;
459+ let request: JupyterMessage = JupyterMessage :: new ( msg, None ) ;
460+
461+ self . shell
462+ . send ( request)
463+ . await
464+ . map_err ( |e| HarnessError :: ProtocolError ( e. to_string ( ) ) ) ?;
465+
466+ // Brief wait for potential comm_close rejection on IOPub
467+ let start = Instant :: now ( ) ;
468+ while start. elapsed ( ) < Duration :: from_millis ( 500 ) {
469+ match timeout ( Duration :: from_millis ( 100 ) , self . iopub . read ( ) ) . await {
470+ Ok ( Ok ( msg) ) => {
471+ if let JupyterMessageContent :: CommClose ( close) = & msg. content {
472+ if close. comm_id == comm_id {
473+ return Ok ( true ) ; // Rejected
474+ }
475+ }
476+ }
477+ _ => { }
478+ }
479+ }
480+
481+ Ok ( false ) // Not rejected (accepted or ignored)
482+ }
483+
484+ /// Send comm_close to clean up a comm.
485+ pub async fn send_comm_close ( & mut self , msg : CommClose ) -> Result < ( ) > {
486+ let request: JupyterMessage = JupyterMessage :: new ( msg, None ) ;
487+ self . shell
488+ . send ( request)
489+ . await
490+ . map_err ( |e| HarnessError :: ProtocolError ( e. to_string ( ) ) ) ?;
491+
492+ // Brief wait for processing
493+ tokio:: time:: sleep ( Duration :: from_millis ( 100 ) ) . await ;
494+ Ok ( ( ) )
495+ }
496+
401497 /// Shutdown the kernel cleanly.
402498 pub async fn shutdown ( mut self ) -> Result < ( ) > {
403499 let request = ShutdownRequest { restart : false } ;
0 commit comments