use std::process::{Command, Output};
use std::sync::OnceLock;
use testcontainers::{ContainerAsync, GenericImage, ImageExt, runners::AsyncRunner};
const TEST_IMAGE_TAG: &str = "fp-dotfiles-manager-test-fedora:latest";
static BUILD_TEST_IMAGE_RESULT: OnceLock<Result<(), String>> = OnceLock::new();
static DOCKER_START_LOCK: OnceLock<tokio::sync::Mutex<()>> = OnceLock::new();
fn ensure_test_image() -> Result<(), String> {
BUILD_TEST_IMAGE_RESULT
.get_or_init(|| {
let dockerfile = format!("{}/tests/common/fedora.Dockerfile", env!("CARGO_MANIFEST_DIR"));
let context_dir = format!("{}/tests/common", env!("CARGO_MANIFEST_DIR"));
let status = Command::new("docker")
.arg("build")
.arg("-t")
.arg(TEST_IMAGE_TAG)
.arg("-f")
.arg(&dockerfile)
.arg(&context_dir)
.status()
.map_err(|e| format!("Failed to build cached Fedora test image: {}", e))?;
if status.success() {
Ok(())
} else {
Err("Failed to build cached Fedora test image".to_string())
}
})
.clone()
}
async fn acquire_docker_start_lock() -> tokio::sync::MutexGuard<'static, ()> {
DOCKER_START_LOCK
.get_or_init(|| tokio::sync::Mutex::new(()))
.lock()
.await
}
pub async fn setup_fedora_container() -> ContainerAsync<GenericImage> {
ensure_test_image().expect("Failed to build cached Fedora test image");
let _guard = acquire_docker_start_lock().await;
let image = GenericImage::new("fp-dotfiles-manager-test-fedora", "latest")
.with_cmd(["sleep", "infinity"])
.with_network("host");
let container = image
.start()
.await
.expect("Failed to start Fedora container");
let container_id = container.id();
let mut binary_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("target/release-static/fp-dotfiles-manager");
if !binary_path.exists() {
binary_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("target/release/fp-dotfiles-manager");
}
if !binary_path.exists() {
println!("Binary not pre-built. Compiling release binary...");
let status = Command::new("cargo")
.arg("build")
.arg("--release")
.status()
.expect("Failed to compile release binary");
assert!(status.success(), "Failed to compile release binary");
binary_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("target/release/fp-dotfiles-manager");
}
let status = Command::new("docker")
.arg("cp")
.arg(&binary_path)
.arg(format!("{}:/usr/local/bin/fp-dotfiles-manager", container_id))
.status()
.expect("Failed to copy binary into container");
assert!(status.success(), "Failed to copy binary into container");
let status = Command::new("docker")
.arg("exec")
.arg(container_id)
.arg("chmod")
.arg("+x")
.arg("/usr/local/bin/fp-dotfiles-manager")
.status()
.expect("Failed to chmod binary");
assert!(status.success(), "Failed to chmod binary in container");
container
}
#[allow(dead_code)]
pub async fn run_manager_cmd(container: &ContainerAsync<GenericImage>, args: &[&str]) -> Output {
Command::new("docker")
.arg("exec")
.arg("-e")
.arg("EDITOR=true")
.arg(container.id())
.arg("fp-dotfiles-manager")
.args(args)
.output()
.expect("Failed to execute fp-dotfiles-manager inside container")
}
#[allow(dead_code)]
pub async fn run_manager_with_stdin(
container: &ContainerAsync<GenericImage>,
args: &[&str],
stdin_input: &str,
) -> Output {
use std::io::Write;
use std::process::Stdio;
let mut child = Command::new("docker")
.arg("exec")
.arg("-i")
.arg("-e")
.arg("EDITOR=true")
.arg(container.id())
.arg("fp-dotfiles-manager")
.args(args)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("Failed to spawn fp-dotfiles-manager inside container");
{
let mut stdin = child.stdin.take().expect("Failed to open stdin");
stdin.write_all(stdin_input.as_bytes()).expect("Failed to write to stdin");
}
child.wait_with_output().expect("Failed to read output")
}
#[allow(dead_code)]
pub async fn run_manager_success(container: &ContainerAsync<GenericImage>, args: &[&str]) -> String {
let output = run_manager_cmd(container, args).await;
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
if !output.status.success() {
panic!("Command {:?} failed.\nSTDOUT:\n{}\nSTDERR:\n{}", args, stdout, stderr);
}
stdout
}
#[allow(dead_code)]
pub fn write_file_in_container(container: &ContainerAsync<GenericImage>, path: &str, content: &str) {
let command = format!("mkdir -p \"$(dirname '{path}')\" && cat > '{path}' <<'EOF'\n{content}\nEOF");
let status = Command::new("docker")
.arg("exec")
.arg(container.id())
.arg("sh")
.arg("-lc")
.arg(command)
.status()
.expect("Failed to write file in container");
assert!(status.success(), "Failed to write file in container");
}
#[allow(dead_code)]
pub fn read_file_in_container(container: &ContainerAsync<GenericImage>, path: &str) -> String {
let output = Command::new("docker")
.arg("exec")
.arg(container.id())
.arg("cat")
.arg(path)
.output()
.expect("Failed to read file from container");
assert!(output.status.success(), "Failed to read file: {path}");
String::from_utf8_lossy(&output.stdout).into_owned()
}