Skip to content

Commit b60bf1a

Browse files
authored
Merge pull request #4 from runtimed/stdin-testing
Implement stdin input_request test
2 parents 792473b + ead587a commit b60bf1a

2 files changed

Lines changed: 132 additions & 4 deletions

File tree

src/harness.rs

Lines changed: 90 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ use crate::types::{KernelReport, TestCategory, TestRecord, TestResult};
55
use chrono::Utc;
66
use jupyter_protocol::connection_info::{ConnectionInfo, Transport};
77
use 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
};
1111
use 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())

src/tests.rs

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -572,9 +572,49 @@ fn test_execute_result(
572572
// =============================================================================
573573

574574
fn test_stdin_input_request(
575-
_kernel: &mut KernelUnderTest,
575+
kernel: &mut KernelUnderTest,
576576
) -> Pin<Box<dyn Future<Output = TestResult> + Send + '_>> {
577-
Box::pin(async move { TestResult::Unsupported })
577+
Box::pin(async move {
578+
let code = kernel.snippets().input_prompt.to_string();
579+
580+
// Skip if the language doesn't support stdin
581+
if code.contains("doesn't support") || code.contains("stdin varies") {
582+
return TestResult::Unsupported;
583+
}
584+
585+
let mock_input = "test_input_42";
586+
587+
match kernel.execute_with_stdin(&code, mock_input).await {
588+
Ok((reply, _iopub, received_input_request)) => {
589+
if !received_input_request {
590+
return TestResult::fail(
591+
"No input_request received on stdin channel",
592+
FailureKind::UnexpectedContent,
593+
);
594+
}
595+
596+
// Check if execute succeeded
597+
if let JupyterMessageContent::ExecuteReply(er) = &reply.content {
598+
if er.status == ReplyStatus::Ok {
599+
// Check if our input appeared in output (for Python: print(input(...)))
600+
// Some kernels echo the result, some don't - we just verify execution completed
601+
TestResult::Pass
602+
} else {
603+
TestResult::fail(
604+
format!("execute_reply status: {:?}", er.status),
605+
FailureKind::KernelError,
606+
)
607+
}
608+
} else {
609+
TestResult::fail(
610+
format!("Expected execute_reply, got {:?}", reply.content.message_type()),
611+
FailureKind::UnexpectedMessageType,
612+
)
613+
}
614+
}
615+
Err(e) => TestResult::fail(e.to_string(), FailureKind::HarnessError),
616+
}
617+
})
578618
}
579619

580620
fn test_comms_lifecycle(

0 commit comments

Comments
 (0)