@@ -1829,3 +1829,272 @@ except Exception as e:
18291829 }
18301830 }
18311831}
1832+
1833+ #[ tokio:: test]
1834+ async fn test_kernel_interrupt ( ) {
1835+ let test_result = tokio:: time:: timeout ( Duration :: from_secs ( 30 ) , async {
1836+ let python_cmd = if let Some ( cmd) = get_python_executable ( ) . await {
1837+ cmd
1838+ } else {
1839+ println ! ( "Skipping test: No Python executable found" ) ;
1840+ return ;
1841+ } ;
1842+
1843+ if !is_ipykernel_available ( ) . await {
1844+ println ! ( "Skipping test: ipykernel not available for {}" , python_cmd) ;
1845+ return ;
1846+ }
1847+
1848+ let server = TestServer :: start ( ) . await ;
1849+ let client = server. create_client ( ) . await ;
1850+
1851+ let session_id = format ! ( "interrupt-test-session-{}" , Uuid :: new_v4( ) ) ;
1852+
1853+ // Create a session with Signal interrupt mode for Windows
1854+ #[ cfg_attr( not( windows) , allow( unused_mut) ) ]
1855+ let mut new_session = create_test_session ( session_id. clone ( ) , & python_cmd) ;
1856+ #[ cfg( windows) ]
1857+ {
1858+ new_session. interrupt_mode = InterruptMode :: Signal ;
1859+ }
1860+
1861+ // Create and start the kernel session
1862+ let _created_session_id = create_session_with_client ( & client, new_session) . await ;
1863+
1864+ println ! ( "Starting kernel session for interrupt test..." ) ;
1865+ let start_response = client
1866+ . start_session ( session_id. clone ( ) )
1867+ . await
1868+ . expect ( "Failed to start session" ) ;
1869+
1870+ println ! ( "Start response: {:?}" , start_response) ;
1871+
1872+ // Check if the session started successfully
1873+ match & start_response {
1874+ kallichore_api:: StartSessionResponse :: Started ( _) => {
1875+ println ! ( "Kernel started successfully" ) ;
1876+ }
1877+ kallichore_api:: StartSessionResponse :: StartFailed ( error) => {
1878+ println ! ( "Kernel failed to start: {:?}" , error) ;
1879+ println ! ( "Skipping interrupt test due to startup failure" ) ;
1880+ return ;
1881+ }
1882+ _ => {
1883+ println ! ( "Unexpected start response: {:?}" , start_response) ;
1884+ println ! ( "Skipping interrupt test" ) ;
1885+ return ;
1886+ }
1887+ }
1888+
1889+ // Wait for kernel to fully start
1890+ tokio:: time:: sleep ( Duration :: from_millis ( 1500 ) ) . await ;
1891+
1892+ // Create a websocket connection
1893+ let ws_url = format ! (
1894+ "ws://localhost:{}/sessions/{}/channels" ,
1895+ server. port( ) ,
1896+ session_id
1897+ ) ;
1898+
1899+ let mut comm = CommunicationChannel :: create_websocket ( & ws_url)
1900+ . await
1901+ . expect ( "Failed to create websocket" ) ;
1902+
1903+ // Wait for websocket to be ready
1904+ tokio:: time:: sleep ( Duration :: from_millis ( 500 ) ) . await ;
1905+
1906+ // Execute code that will take a long time (loop that prints numbers)
1907+ let long_running_code = r#"
1908+ import time
1909+ for i in range(100):
1910+ print(f"Iteration {i}")
1911+ time.sleep(0.1)
1912+ print("All iterations completed!")
1913+ "# ;
1914+
1915+ let execute_request = WebsocketMessage :: Jupyter ( JupyterMessage {
1916+ header : JupyterMessageHeader {
1917+ msg_id : Uuid :: new_v4 ( ) . to_string ( ) ,
1918+ msg_type : "execute_request" . to_string ( ) ,
1919+ } ,
1920+ parent_header : None ,
1921+ channel : JupyterChannel :: Shell ,
1922+ content : serde_json:: json!( {
1923+ "code" : long_running_code,
1924+ "silent" : false ,
1925+ "store_history" : false ,
1926+ "user_expressions" : { } ,
1927+ "allow_stdin" : false ,
1928+ "stop_on_error" : true
1929+ } ) ,
1930+ metadata : serde_json:: json!( { } ) ,
1931+ buffers : vec ! [ ] ,
1932+ } ) ;
1933+
1934+ println ! ( "Sending long-running execute_request..." ) ;
1935+ comm. send_message ( & execute_request)
1936+ . await
1937+ . expect ( "Failed to send execute_request" ) ;
1938+
1939+ // Wait for some iterations to be printed
1940+ println ! ( "Waiting for some iterations to be printed..." ) ;
1941+ let mut iterations_printed = Vec :: new ( ) ;
1942+ let start_time = std:: time:: Instant :: now ( ) ;
1943+
1944+ // Collect some output for 1 second
1945+ while start_time. elapsed ( ) < Duration :: from_secs ( 1 ) {
1946+ match tokio:: time:: timeout ( Duration :: from_millis ( 100 ) , comm. receive_message ( ) ) . await {
1947+ Ok ( Ok ( Some ( message_text) ) ) => {
1948+ if let Ok ( WebsocketMessage :: Jupyter ( jupyter_msg) ) =
1949+ serde_json:: from_str :: < WebsocketMessage > ( & message_text)
1950+ {
1951+ if jupyter_msg. header . msg_type == "stream" {
1952+ if let Some ( text) =
1953+ jupyter_msg. content . get ( "text" ) . and_then ( |v| v. as_str ( ) )
1954+ {
1955+ println ! ( "Received output: {}" , text) ;
1956+ // Extract iteration numbers
1957+ for line in text. lines ( ) {
1958+ if let Some ( num_str) = line. strip_prefix ( "Iteration " ) {
1959+ if let Ok ( num) = num_str. parse :: < i32 > ( ) {
1960+ iterations_printed. push ( num) ;
1961+ }
1962+ }
1963+ }
1964+ }
1965+ }
1966+ }
1967+ }
1968+ _ => { }
1969+ }
1970+ }
1971+
1972+ println ! (
1973+ "Collected {} iterations before interrupt: {:?}" ,
1974+ iterations_printed. len( ) ,
1975+ iterations_printed
1976+ ) ;
1977+
1978+ // Verify we got some iterations (at least 5, should be around 10)
1979+ assert ! (
1980+ iterations_printed. len( ) >= 5 ,
1981+ "Expected at least 5 iterations before interrupt, got {}" ,
1982+ iterations_printed. len( )
1983+ ) ;
1984+
1985+ // Now interrupt the kernel using the interrupt endpoint
1986+ println ! ( "Sending interrupt request via HTTP API..." ) ;
1987+ let interrupt_response = client
1988+ . interrupt_session ( session_id. clone ( ) )
1989+ . await
1990+ . expect ( "Failed to send interrupt" ) ;
1991+
1992+ println ! ( "Interrupt response: {:?}" , interrupt_response) ;
1993+
1994+ // Verify the interrupt was accepted
1995+ match interrupt_response {
1996+ kallichore_api:: InterruptSessionResponse :: Interrupted ( _) => {
1997+ println ! ( "Interrupt request was accepted" ) ;
1998+ }
1999+ _ => {
2000+ panic ! (
2001+ "Expected interrupt to succeed, got: {:?}" ,
2002+ interrupt_response
2003+ ) ;
2004+ }
2005+ }
2006+
2007+ // Now collect more output and verify that:
2008+ // 1. We receive an error or execute_reply indicating interruption
2009+ // 2. We did NOT receive "All iterations completed!"
2010+ println ! ( "Collecting output after interrupt..." ) ;
2011+ let mut all_output = String :: new ( ) ;
2012+ let mut execute_reply_received = false ;
2013+ let start_time = std:: time:: Instant :: now ( ) ;
2014+
2015+ while start_time. elapsed ( ) < Duration :: from_secs ( 5 ) {
2016+ match tokio:: time:: timeout ( Duration :: from_millis ( 200 ) , comm. receive_message ( ) ) . await {
2017+ Ok ( Ok ( Some ( message_text) ) ) => {
2018+ if let Ok ( WebsocketMessage :: Jupyter ( jupyter_msg) ) =
2019+ serde_json:: from_str :: < WebsocketMessage > ( & message_text)
2020+ {
2021+ println ! ( "Received message type: {}" , jupyter_msg. header. msg_type) ;
2022+
2023+ if jupyter_msg. header . msg_type == "stream" {
2024+ if let Some ( text) =
2025+ jupyter_msg. content . get ( "text" ) . and_then ( |v| v. as_str ( ) )
2026+ {
2027+ all_output. push_str ( text) ;
2028+ println ! ( "Stream output: {}" , text) ;
2029+ }
2030+ } else if jupyter_msg. header . msg_type == "error" {
2031+ println ! ( "Received error message: {:?}" , jupyter_msg. content) ;
2032+ all_output. push_str ( "ERROR_RECEIVED\n " ) ;
2033+ } else if jupyter_msg. header . msg_type == "execute_reply" {
2034+ println ! ( "Received execute_reply: {:?}" , jupyter_msg. content) ;
2035+ if let Some ( status) =
2036+ jupyter_msg. content . get ( "status" ) . and_then ( |v| v. as_str ( ) )
2037+ {
2038+ println ! ( "Execute reply status: {}" , status) ;
2039+ if status == "error" {
2040+ all_output. push_str ( "EXECUTE_ERROR\n " ) ;
2041+ }
2042+ }
2043+ execute_reply_received = true ;
2044+ break ;
2045+ }
2046+ }
2047+ }
2048+ _ => { }
2049+ }
2050+ }
2051+
2052+ println ! ( "All output after interrupt:\n {}" , all_output) ;
2053+
2054+ // Verify we got an execute_reply
2055+ assert ! (
2056+ execute_reply_received,
2057+ "Expected to receive execute_reply after interrupt"
2058+ ) ;
2059+
2060+ // Verify that the code did NOT complete all iterations
2061+ assert ! (
2062+ !all_output. contains( "All iterations completed!" ) ,
2063+ "Code should have been interrupted before completion"
2064+ ) ;
2065+
2066+ // Verify that we got SOME output (the interrupt didn't happen immediately)
2067+ assert ! (
2068+ iterations_printed. len( ) > 0 ,
2069+ "Expected some iterations to complete before interrupt"
2070+ ) ;
2071+
2072+ // Verify that we did NOT get all 100 iterations
2073+ assert ! (
2074+ iterations_printed. len( ) < 100 ,
2075+ "Expected interrupt to prevent all iterations from completing"
2076+ ) ;
2077+
2078+ println ! (
2079+ "✓ Interrupt test passed! Code was interrupted after {} iterations (out of 100)" ,
2080+ iterations_printed. len( )
2081+ ) ;
2082+
2083+ // Clean up
2084+ if let Err ( e) = comm. close ( ) . await {
2085+ println ! ( "Failed to close communication channel: {}" , e) ;
2086+ }
2087+
2088+ drop ( server) ;
2089+ } )
2090+ . await ;
2091+
2092+ match test_result {
2093+ Ok ( _) => {
2094+ println ! ( "Kernel interrupt test completed successfully" ) ;
2095+ }
2096+ Err ( _) => {
2097+ panic ! ( "Kernel interrupt test timed out after 30 seconds" ) ;
2098+ }
2099+ }
2100+ }
0 commit comments