#![cfg(target_os = "linux")]
use std::io::Write as _;
use std::process::Command;
use std::sync::atomic::{AtomicU32, Ordering};
use std::time::{Duration, Instant};
use assert_cmd::cargo::cargo_bin;
use predicates::str::contains;
use tempfile::NamedTempFile;
fn docker_available() -> bool {
Command::new("docker")
.arg("info")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
}
fn unique_project(prefix: &str) -> String {
static COUNTER: AtomicU32 = AtomicU32::new(0);
let seq = COUNTER.fetch_add(1, Ordering::Relaxed);
format!("ls-smoke-{prefix}-{}-{seq}", std::process::id())
}
struct ProjectCleanup {
project: String,
}
impl ProjectCleanup {
fn new(project: impl Into<String>) -> Self {
Self {
project: project.into(),
}
}
}
impl Drop for ProjectCleanup {
fn drop(&mut self) {
let label = format!("label=lightshuttle.project={}", self.project);
if let Ok(listed) = Command::new("docker")
.args(["ps", "-aq", "--filter", &label])
.output()
{
let ids = String::from_utf8_lossy(&listed.stdout);
for id in ids.split_whitespace() {
let _ = Command::new("docker").args(["rm", "-f", id]).output();
}
}
let network = format!("lightshuttle-{}", self.project);
let _ = Command::new("docker")
.args(["network", "rm", &network])
.output();
}
}
fn write_temp_manifest(content: &str) -> NamedTempFile {
let mut f = NamedTempFile::new().expect("temp file created");
f.write_all(content.as_bytes())
.expect("manifest written to temp file");
f
}
fn wait_for_running(project: &str, deadline: Duration) {
let label = format!("label=lightshuttle.project={project}");
let start = Instant::now();
loop {
let output = Command::new("docker")
.args(["ps", "-q", "--filter", &label, "--filter", "status=running"])
.output()
.expect("docker ps runs");
let stdout = String::from_utf8_lossy(&output.stdout);
if !stdout.trim().is_empty() {
return;
}
assert!(
start.elapsed() < deadline,
"timed out waiting for project `{project}` containers to reach status=running",
);
std::thread::sleep(Duration::from_millis(300));
}
}
#[test]
#[ignore = "requires a running Docker daemon"]
fn up_boots_then_down_tears_down() {
if !docker_available() {
eprintln!("skipping: docker not available");
return;
}
let project = unique_project("up");
let _guard = ProjectCleanup::new(project.clone());
let manifest = format!(
r#"
project:
name: {project}
resources:
app:
container:
image: alpine:3.20
command: ["sh", "-c", "sleep 120"]
"#
);
let manifest_file = write_temp_manifest(&manifest);
let path = manifest_file.path().to_str().expect("path is UTF-8");
let mut up = Command::new(cargo_bin("lightshuttle"))
.args(["--file", path, "up"])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()
.expect("lightshuttle up spawns");
wait_for_running(&project, Duration::from_secs(60));
assert_cmd::Command::new(cargo_bin("lightshuttle"))
.args(["--file", path, "down"])
.assert()
.success()
.stdout(contains("stopped:"));
let _ = up.kill();
let _ = up.wait();
}