use std::fmt::Write as _; use std::process::Stdio; use tempfile::TempDir; use tokio::process::Command; fn witryna_bin() -> std::path::PathBuf { let mut path = std::path::PathBuf::from(env!("CARGO_BIN_EXE_witryna")); if !path.exists() { path = std::path::PathBuf::from("target/debug/witryna"); } path } async fn write_cleanup_config( dir: &std::path::Path, sites: &[&str], max_builds: u32, ) -> (std::path::PathBuf, std::path::PathBuf, std::path::PathBuf) { let base_dir = dir.join("data"); let log_dir = dir.join("logs"); tokio::fs::create_dir_all(&base_dir).await.unwrap(); tokio::fs::create_dir_all(&log_dir).await.unwrap(); let mut sites_toml = String::new(); for name in sites { write!( sites_toml, r#" [[sites]] name = "{name}" repo_url = "https://example.com/{name}.git" branch = "main" "# ) .unwrap(); } let config_path = dir.join("witryna.toml"); let config = format!( r#"listen_address = "127.0.0.1:0" container_runtime = "podman" base_dir = "{base_dir}" log_dir = "{log_dir}" log_level = "info" max_builds_to_keep = {max_builds} {sites_toml}"#, base_dir = base_dir.display(), log_dir = log_dir.display(), ); tokio::fs::write(&config_path, config).await.unwrap(); (config_path, base_dir, log_dir) } async fn create_fake_builds( base_dir: &std::path::Path, log_dir: &std::path::Path, site: &str, timestamps: &[&str], ) { let builds_dir = base_dir.join("builds").join(site); let site_log_dir = log_dir.join(site); tokio::fs::create_dir_all(&builds_dir).await.unwrap(); tokio::fs::create_dir_all(&site_log_dir).await.unwrap(); for ts in timestamps { tokio::fs::create_dir_all(builds_dir.join(ts)) .await .unwrap(); tokio::fs::write(site_log_dir.join(format!("{ts}.log")), "build log") .await .unwrap(); } } // --------------------------------------------------------------------------- // Tier 1: no container runtime / git needed // --------------------------------------------------------------------------- #[tokio::test] async fn cli_cleanup_unknown_site() { let tempdir = TempDir::new().unwrap(); let (config_path, _, _) = write_cleanup_config(tempdir.path(), &["real-site"], 5).await; let output = Command::new(witryna_bin()) .args([ "cleanup", "--config", config_path.to_str().unwrap(), "nonexistent", ]) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .output() .await .unwrap(); assert!(!output.status.success(), "should exit non-zero"); let stderr = String::from_utf8_lossy(&output.stderr); assert!( stderr.contains("not found"), "should mention 'not found', got: {stderr}" ); } #[tokio::test] async fn cli_cleanup_keep_zero_refused() { let tempdir = TempDir::new().unwrap(); let (config_path, _, _) = write_cleanup_config(tempdir.path(), &["my-site"], 5).await; let output = Command::new(witryna_bin()) .args([ "cleanup", "--config", config_path.to_str().unwrap(), "--keep", "0", ]) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .output() .await .unwrap(); assert!(!output.status.success(), "should exit non-zero"); let stderr = String::from_utf8_lossy(&output.stderr); assert!( stderr.contains("--keep 0 would delete all builds"), "should refuse --keep 0, got: {stderr}" ); } #[tokio::test] async fn cli_cleanup_disabled_when_max_zero() { let tempdir = TempDir::new().unwrap(); let (config_path, _, _) = write_cleanup_config(tempdir.path(), &["my-site"], 0).await; let output = Command::new(witryna_bin()) .args(["cleanup", "--config", config_path.to_str().unwrap()]) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .output() .await .unwrap(); assert!( output.status.success(), "should exit 0, stderr: {}", String::from_utf8_lossy(&output.stderr) ); let stderr = String::from_utf8_lossy(&output.stderr); assert!( stderr.contains("cleanup disabled"), "should say 'cleanup disabled', got: {stderr}" ); } #[tokio::test] async fn cli_cleanup_removes_old_builds() { let tempdir = TempDir::new().unwrap(); let (config_path, base_dir, log_dir) = write_cleanup_config(tempdir.path(), &["site-a", "site-b"], 5).await; let timestamps = &[ "20260126-100000-000001", "20260126-100000-000002", "20260126-100000-000003", "20260126-100000-000004", ]; create_fake_builds(&base_dir, &log_dir, "site-a", timestamps).await; create_fake_builds(&base_dir, &log_dir, "site-b", timestamps).await; let output = Command::new(witryna_bin()) .args([ "cleanup", "--config", config_path.to_str().unwrap(), "--keep", "2", ]) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .output() .await .unwrap(); assert!( output.status.success(), "should exit 0, stderr: {}", String::from_utf8_lossy(&output.stderr) ); let stderr = String::from_utf8_lossy(&output.stderr); assert!( stderr.contains("site-a: removed"), "should report site-a removals, got: {stderr}" ); assert!( stderr.contains("site-b: removed"), "should report site-b removals, got: {stderr}" ); assert!( stderr.contains("total:"), "should print total summary for multi-site, got: {stderr}" ); // Verify filesystem: oldest 2 gone, newest 2 remain for each site for site in &["site-a", "site-b"] { let builds = base_dir.join("builds").join(site); assert!(!builds.join("20260126-100000-000001").exists()); assert!(!builds.join("20260126-100000-000002").exists()); assert!(builds.join("20260126-100000-000003").exists()); assert!(builds.join("20260126-100000-000004").exists()); let logs = log_dir.join(site); assert!(!logs.join("20260126-100000-000001.log").exists()); assert!(!logs.join("20260126-100000-000002.log").exists()); assert!(logs.join("20260126-100000-000003.log").exists()); assert!(logs.join("20260126-100000-000004.log").exists()); } } #[tokio::test] async fn cli_cleanup_single_site_filter() { let tempdir = TempDir::new().unwrap(); let (config_path, base_dir, log_dir) = write_cleanup_config(tempdir.path(), &["site-a", "site-b"], 5).await; let timestamps = &[ "20260126-100000-000001", "20260126-100000-000002", "20260126-100000-000003", "20260126-100000-000004", ]; create_fake_builds(&base_dir, &log_dir, "site-a", timestamps).await; create_fake_builds(&base_dir, &log_dir, "site-b", timestamps).await; let output = Command::new(witryna_bin()) .args([ "cleanup", "--config", config_path.to_str().unwrap(), "--keep", "2", "site-a", ]) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .output() .await .unwrap(); assert!( output.status.success(), "should exit 0, stderr: {}", String::from_utf8_lossy(&output.stderr) ); let stderr = String::from_utf8_lossy(&output.stderr); // site-a should be cleaned assert!( stderr.contains("site-a: removed"), "should report site-a removals, got: {stderr}" ); // site-b should be untouched — not mentioned in output assert!( !stderr.contains("site-b"), "site-b should not appear in output, got: {stderr}" ); // No total line for single-site cleanup assert!( !stderr.contains("total:"), "should not print total for single site, got: {stderr}" ); // Verify site-b filesystem is untouched let site_b_builds = base_dir.join("builds").join("site-b"); assert!(site_b_builds.join("20260126-100000-000001").exists()); assert!(site_b_builds.join("20260126-100000-000004").exists()); } #[tokio::test] async fn cli_cleanup_keep_overrides_config() { let tempdir = TempDir::new().unwrap(); // Config says max_builds_to_keep = 1 let (config_path, base_dir, log_dir) = write_cleanup_config(tempdir.path(), &["my-site"], 1).await; let timestamps = &[ "20260126-100000-000001", "20260126-100000-000002", "20260126-100000-000003", "20260126-100000-000004", ]; create_fake_builds(&base_dir, &log_dir, "my-site", timestamps).await; // --keep 3 should override config's max_builds_to_keep=1 let output = Command::new(witryna_bin()) .args([ "cleanup", "--config", config_path.to_str().unwrap(), "--keep", "3", ]) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .output() .await .unwrap(); assert!( output.status.success(), "should exit 0, stderr: {}", String::from_utf8_lossy(&output.stderr) ); // With --keep 3 and 4 builds: only 1 should be removed let builds = base_dir.join("builds").join("my-site"); assert!( !builds.join("20260126-100000-000001").exists(), "oldest should be removed" ); assert!( builds.join("20260126-100000-000002").exists(), "second should remain" ); assert!( builds.join("20260126-100000-000003").exists(), "third should remain" ); assert!( builds.join("20260126-100000-000004").exists(), "newest should remain" ); }