use larust_core::__internal::{handoff, listener};
use std::io::{Read, Write};
use std::net::{TcpListener, TcpStream};
use std::time::Duration;
fn bind_ephemeral() -> TcpListener {
listener::bind("127.0.0.1:0".parse().unwrap()).expect("ephemeral bind failed")
}
#[tokio::test]
async fn a_healthy_replacement_becomes_ready_and_serves_on_the_inherited_listener() {
let parent_listener = bind_ephemeral();
let port = parent_listener.local_addr().unwrap().port();
let exe = env!("CARGO_BIN_EXE_graceful_shutdown_fixture");
let outcome = handoff::spawn_replacement_and_wait_for_ready(
&parent_listener,
exe.as_ref(),
Duration::from_secs(10),
true,
)
.await
.expect("spawn_replacement_and_wait_for_ready returned an error");
let mut child = outcome.expect("a healthy replacement should have become ready");
let mut stream =
TcpStream::connect(("127.0.0.1", port)).expect("connect to replacement failed");
stream
.set_read_timeout(Some(Duration::from_secs(5)))
.unwrap();
write!(
stream,
"GET /fast HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\n\r\n"
)
.unwrap();
let mut response = String::new();
stream.read_to_string(&mut response).unwrap();
assert!(
response.contains("fast-ok"),
"replacement did not serve the expected response: {response}"
);
let _ = child.kill().await;
let _ = child.wait().await;
}
#[tokio::test]
async fn a_replacement_that_crashes_immediately_is_reported_as_not_ready() {
let parent_listener = bind_ephemeral();
let exe = env!("CARGO_BIN_EXE_handoff_crash_fixture");
let outcome = handoff::spawn_replacement_and_wait_for_ready(
&parent_listener,
exe.as_ref(),
Duration::from_secs(5),
true,
)
.await
.expect("spawn_replacement_and_wait_for_ready returned an error");
assert!(
outcome.is_none(),
"a replacement that crashed immediately should not be reported as ready"
);
drop(parent_listener);
}
#[tokio::test]
async fn a_replacement_that_hangs_without_announcing_readiness_times_out() {
let parent_listener = bind_ephemeral();
let exe = env!("CARGO_BIN_EXE_handoff_hangs_fixture");
let started = std::time::Instant::now();
let outcome = handoff::spawn_replacement_and_wait_for_ready(
&parent_listener,
exe.as_ref(),
Duration::from_millis(800),
true,
)
.await
.expect("spawn_replacement_and_wait_for_ready returned an error");
let elapsed = started.elapsed();
assert!(
outcome.is_none(),
"a replacement that never announces readiness should not be reported as ready"
);
assert!(
elapsed < Duration::from_secs(5),
"spawn_replacement_and_wait_for_ready took {elapsed:?}, expected it to give up around \
the 800ms timeout"
);
}
#[tokio::test]
async fn a_healthy_replacements_stdout_is_inherited_not_piped() {
let parent_listener = bind_ephemeral();
let port = parent_listener.local_addr().unwrap().port();
let exe = env!("CARGO_BIN_EXE_graceful_shutdown_fixture");
let outcome = handoff::spawn_replacement_and_wait_for_ready(
&parent_listener,
exe.as_ref(),
Duration::from_secs(10),
true,
)
.await
.expect("spawn_replacement_and_wait_for_ready returned an error");
let mut child = outcome.expect("a healthy replacement should have become ready");
assert!(
child.stdout.is_none(),
"stdout should be inherited (Stdio::inherit()), not captured/piped - a piped stdout \
whose reader gets dropped after the handshake is exactly the bug this test guards \
against"
);
let mut stream =
TcpStream::connect(("127.0.0.1", port)).expect("connect to replacement failed");
stream
.set_read_timeout(Some(Duration::from_secs(5)))
.unwrap();
write!(
stream,
"GET /fast HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\n\r\n"
)
.unwrap();
let mut response = String::new();
stream.read_to_string(&mut response).unwrap();
assert!(
response.contains("fast-ok"),
"replacement did not serve the expected response: {response}"
);
let _ = child.kill().await;
let _ = child.wait().await;
}