mod common;
use std::net::TcpStream;
use std::process::{Child, Command, Stdio};
use std::time::{Duration, Instant};
struct ServeChild {
child: Child,
port: u16,
_home: boxlite_test_utils::home::PerTestBoxHome,
}
impl ServeChild {
fn start() -> Self {
let home = boxlite_test_utils::home::PerTestBoxHome::new();
let port = std::net::TcpListener::bind("127.0.0.1:0")
.expect("bind ephemeral")
.local_addr()
.unwrap()
.port();
let mut cmd = Command::new(env!("CARGO_BIN_EXE_boxlite"));
cmd.arg("--home").arg(&home.path);
for reg in boxlite_test_utils::TEST_REGISTRIES {
cmd.arg("--registry").arg(reg);
}
cmd.args(["serve", "--host", "127.0.0.1", "--port", &port.to_string()])
.stdout(Stdio::null())
.stderr(Stdio::null());
let child = cmd.spawn().expect("spawn boxlite serve");
let serve = Self {
child,
port,
_home: home,
};
serve.wait_ready();
serve
}
fn url(&self) -> String {
format!("http://127.0.0.1:{}", self.port)
}
fn client(&self, args: &[&str]) -> std::process::Output {
Command::new(env!("CARGO_BIN_EXE_boxlite"))
.args(["--url", &self.url()])
.args(args)
.output()
.expect("boxlite --url")
}
fn wait_until_no_boxes(&self) {
let deadline = Instant::now() + Duration::from_secs(30);
loop {
let out = self.client(&["ps", "-a"]);
let listed = String::from_utf8_lossy(&out.stdout)
.lines()
.filter(|l| l.contains("alpine"))
.count();
if out.status.success() && listed == 0 {
return;
}
assert!(
Instant::now() < deadline,
"server still has {listed} box(es) after the run — they never stopped"
);
std::thread::sleep(Duration::from_millis(500));
}
}
fn wait_ready(&self) {
let deadline = Instant::now() + Duration::from_secs(45);
while Instant::now() < deadline {
if TcpStream::connect(("127.0.0.1", self.port)).is_ok() {
let ok = Command::new(env!("CARGO_BIN_EXE_boxlite"))
.args(["--url", &self.url(), "ps"])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false);
if ok {
return;
}
}
std::thread::sleep(Duration::from_millis(200));
}
panic!("boxlite serve did not become ready on port {}", self.port);
}
}
impl Drop for ServeChild {
fn drop(&mut self) {
let pid = self.child.id().to_string();
let _ = Command::new("kill").args(["-s", "INT", &pid]).status();
let deadline = Instant::now() + Duration::from_secs(10);
loop {
match self.child.try_wait() {
Ok(Some(_)) => break,
Ok(None) if Instant::now() < deadline => {
std::thread::sleep(Duration::from_millis(100));
}
_ => {
let _ = self.child.kill();
let _ = self.child.wait();
break;
}
}
}
}
}
#[test]
fn run_over_rest_streams_output_and_propagates_exit_code() {
let serve = ServeChild::start();
let client_home = boxlite_test_utils::home::PerTestBoxHome::new();
let out = Command::new(env!("CARGO_BIN_EXE_boxlite"))
.arg("--home")
.arg(&client_home.path)
.arg("--url")
.arg(serve.url())
.args([
"run",
"--rm",
"alpine:latest",
"sh",
"-c",
"echo hello-over-rest; exit 7",
])
.output()
.expect("run --url");
let stdout = String::from_utf8_lossy(&out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stdout.contains("hello-over-rest"),
"the main command's stdout must round-trip over REST — stdout={stdout:?} stderr={stderr:?}"
);
assert_eq!(
out.status.code(),
Some(7),
"the main command's exit code must propagate over REST — stderr={stderr:?}"
);
serve.wait_until_no_boxes();
}