use microsandbox::{ExecEvent, Sandbox};
use sha2::{Digest, Sha256};
use test_utils::msb_test;
const ONE_MIB: usize = 1024 * 1024;
async fn stop_and_remove(name: &str) {
let handle = Sandbox::get(name).await.expect("get");
handle.stop().await.expect("stop");
Sandbox::remove(name).await.expect("remove");
}
#[msb_test]
async fn stdin_bytes_writes_payload_larger_than_pipe_capacity() {
let name = "stdin-bytes-1mib";
let payload = vec![b'x'; ONE_MIB];
let expected_sha = hex::encode(Sha256::digest(&payload));
let sandbox = Sandbox::builder(name)
.image("mirror.gcr.io/library/alpine")
.cpus(1)
.memory(512)
.replace()
.create()
.await
.expect("create sandbox");
let output = sandbox
.exec_with("sh", |exec| {
exec.args([
"-c",
"cat > /tmp/stdin-1mb.bin && wc -c /tmp/stdin-1mb.bin && sha256sum /tmp/stdin-1mb.bin",
])
.stdin_bytes(payload)
})
.await
.expect("write stdin payload");
stop_and_remove(name).await;
assert!(
output.status().success,
"guest command failed: stdout=`{}` stderr=`{}`",
output.stdout().unwrap_or_default(),
output.stderr().unwrap_or_default()
);
let (byte_count, actual_sha) = parse_wc_and_sha(&output.stdout().expect("stdout is utf8"));
assert_eq!(byte_count, ONE_MIB.to_string());
assert_eq!(actual_sha, expected_sha);
}
#[msb_test]
async fn stdin_bytes_waits_for_slow_reader() {
let name = "stdin-bytes-slow-reader";
let payload = vec![b'y'; ONE_MIB];
let expected_sha = hex::encode(Sha256::digest(&payload));
let sandbox = Sandbox::builder(name)
.image("mirror.gcr.io/library/alpine")
.cpus(1)
.memory(512)
.replace()
.create()
.await
.expect("create sandbox");
let output = sandbox
.exec_with("sh", |exec| {
exec.args([
"-c",
"sleep 1; cat > /tmp/stdin-slow.bin && wc -c /tmp/stdin-slow.bin && sha256sum /tmp/stdin-slow.bin",
])
.stdin_bytes(payload)
})
.await
.expect("write stdin payload");
stop_and_remove(name).await;
assert!(
output.status().success,
"guest command failed: stdout=`{}` stderr=`{}`",
output.stdout().unwrap_or_default(),
output.stderr().unwrap_or_default()
);
let (byte_count, actual_sha) = parse_wc_and_sha(&output.stdout().expect("stdout is utf8"));
assert_eq!(byte_count, ONE_MIB.to_string());
assert_eq!(actual_sha, expected_sha);
}
#[msb_test]
async fn stdin_pipe_streams_chunks_in_order() {
let name = "stdin-pipe-stream";
let chunk_size = 256 * 1024;
let chunk_count = 4;
let mut payload = Vec::with_capacity(chunk_size * chunk_count);
let mut chunks: Vec<Vec<u8>> = Vec::with_capacity(chunk_count);
for i in 0..chunk_count {
let byte = b'a' + i as u8;
let chunk = vec![byte; chunk_size];
payload.extend_from_slice(&chunk);
chunks.push(chunk);
}
let expected_sha = hex::encode(Sha256::digest(&payload));
let total_bytes = payload.len();
let sandbox = Sandbox::builder(name)
.image("mirror.gcr.io/library/alpine")
.cpus(1)
.memory(512)
.replace()
.create()
.await
.expect("create sandbox");
let mut handle = sandbox
.exec_stream_with("sh", |exec| {
exec.args([
"-c",
"cat > /tmp/stdin-stream.bin && wc -c /tmp/stdin-stream.bin && sha256sum /tmp/stdin-stream.bin",
])
.stdin_pipe()
})
.await
.expect("start exec");
let stdin = handle.take_stdin().expect("stdin pipe");
for chunk in &chunks {
stdin.write(chunk).await.expect("write chunk");
}
stdin.close().await.expect("close stdin");
let mut stdout = Vec::new();
let mut exit_code: Option<i32> = None;
while let Some(event) = handle.recv().await {
match event {
ExecEvent::Stdout(data) => stdout.extend_from_slice(&data),
ExecEvent::Exited { code } => {
exit_code = Some(code);
break;
}
ExecEvent::Failed(payload) => {
panic!("exec failed: {payload:?}");
}
_ => {}
}
}
stop_and_remove(name).await;
assert_eq!(exit_code, Some(0), "guest command exited non-zero");
let stdout_text = String::from_utf8(stdout).expect("stdout is utf8");
let (byte_count, actual_sha) = parse_wc_and_sha(&stdout_text);
assert_eq!(byte_count, total_bytes.to_string());
assert_eq!(actual_sha, expected_sha);
}
#[msb_test]
async fn stdin_bytes_reports_broken_pipe_when_child_exits_early() {
let name = "stdin-broken-pipe";
let payload = vec![b'z'; ONE_MIB];
let sandbox = Sandbox::builder(name)
.image("mirror.gcr.io/library/alpine")
.cpus(1)
.memory(512)
.replace()
.create()
.await
.expect("create sandbox");
let mut handle = sandbox
.exec_stream_with("sh", |exec| {
exec.args([
"-c",
"dd bs=1 count=16 of=/tmp/prefix.bin 2>/dev/null && wc -c /tmp/prefix.bin",
])
.stdin_bytes(payload)
})
.await
.expect("start exec");
let mut stdout = Vec::new();
let mut exit_code: Option<i32> = None;
let mut stdin_error = None;
while let Some(event) = handle.recv().await {
match event {
ExecEvent::Stdout(data) => stdout.extend_from_slice(&data),
ExecEvent::StdinError(payload) => {
if stdin_error.is_none() {
stdin_error = Some(payload);
}
}
ExecEvent::Exited { code } => {
exit_code = Some(code);
break;
}
ExecEvent::Failed(payload) => {
panic!("exec failed: {payload:?}");
}
_ => {}
}
}
stop_and_remove(name).await;
assert_eq!(exit_code, Some(0), "guest command exited non-zero");
let stdout_text = String::from_utf8(stdout).expect("stdout is utf8");
let prefix_count = stdout_text
.lines()
.next()
.and_then(|line| line.split_whitespace().next())
.expect("byte count line");
assert_eq!(
prefix_count, "16",
"child should have read exactly 16 bytes"
);
let stdin_err = stdin_error.expect("host should observe a StdinError event");
assert_eq!(
stdin_err.errno,
Some(libc::EPIPE),
"expected EPIPE on broken pipe, got errno={:?} message={}",
stdin_err.errno,
stdin_err.message,
);
}
fn parse_wc_and_sha(stdout: &str) -> (String, String) {
let mut lines = stdout.lines();
let byte_count = lines
.next()
.and_then(|line| line.split_whitespace().next())
.expect("byte count line")
.to_string();
let sha = lines
.next()
.and_then(|line| line.split_whitespace().next())
.expect("sha256 line")
.to_string();
(byte_count, sha)
}
#[msb_test]
async fn stdin_null_finite_retained_and_pty_lifetimes() {
use std::time::Duration;
use tokio::time::timeout;
let name = "stdin-mode-lifetimes";
let sandbox = Sandbox::builder(name)
.image("mirror.gcr.io/library/alpine")
.cpus(1)
.memory(512)
.replace()
.create()
.await
.expect("create sandbox");
let default = timeout(Duration::from_secs(5), sandbox.exec("cat", ["-"]))
.await
.expect("default null must reach EOF")
.unwrap();
assert!(default.status().success && default.stdout_bytes().is_empty());
let explicit = timeout(
Duration::from_secs(5),
sandbox.exec_with("cat", |e| e.stdin_null()),
)
.await
.expect("explicit null must reach EOF")
.unwrap();
assert!(explicit.status().success && explicit.stdout_bytes().is_empty());
for bytes in [Vec::new(), vec![0, 255, 10]] {
let output = timeout(
Duration::from_secs(5),
sandbox.exec_with("cat", |e| e.stdin_bytes(bytes.clone())),
)
.await
.expect("finite input must reach EOF")
.unwrap();
assert!(output.status().success);
assert_eq!(output.stdout_bytes().as_ref(), bytes);
}
let mut retained = sandbox
.exec_stream_with("cat", |e| e.stdin_pipe())
.await
.unwrap();
assert!(
timeout(Duration::from_millis(100), retained.wait())
.await
.is_err()
);
let input = retained.take_stdin().expect("retained pipe");
input.write(b"after cancelled wait\n").await.unwrap();
input.close().await.unwrap();
let output = timeout(Duration::from_secs(5), retained.collect())
.await
.unwrap()
.unwrap();
assert_eq!(output.stdout_bytes().as_ref(), b"after cancelled wait\n");
let mut delayed = sandbox
.exec_stream_with("sh", |e| e.args(["-c", "sleep 0.3; cat"]).stdin_null())
.await
.unwrap();
assert!(
timeout(Duration::from_millis(20), delayed.wait())
.await
.is_err()
);
assert!(
timeout(Duration::from_secs(5), delayed.wait())
.await
.unwrap()
.unwrap()
.success
);
let mut pty = sandbox
.exec_stream_with("sh", |e| {
e.args(["-c", "test -t 0 && echo ready; cat"])
.tty(true)
.stdin_null()
})
.await
.unwrap();
timeout(Duration::from_secs(5), async {
loop {
let event = pty.recv().await.expect("PTY closed before readiness");
if let ExecEvent::Stdout(bytes) = event
&& String::from_utf8_lossy(&bytes).contains("ready")
{
break;
}
}
})
.await
.expect("PTY must become ready");
assert!(
timeout(Duration::from_millis(100), pty.wait())
.await
.is_err()
);
pty.kill().await.unwrap();
assert!(
!timeout(Duration::from_secs(5), pty.wait())
.await
.unwrap()
.unwrap()
.success
);
stop_and_remove(name).await;
}