use std::{ fs, path::{Path, PathBuf}, process::Command, sync::atomic::{AtomicU64, Ordering}, time::{SystemTime, UNIX_EPOCH}, }; use lanspread_compat::catalog_publisher::cli::HELP; static TEMP_SEQUENCE: AtomicU64 = AtomicU64::new(0); struct TempDir(PathBuf); impl TempDir { fn new() -> Self { let nanos = SystemTime::now() .duration_since(UNIX_EPOCH) .expect("clock should follow epoch") .as_nanos(); let sequence = TEMP_SEQUENCE.fetch_add(1, Ordering::Relaxed); let path = std::env::temp_dir().join(format!( "lanspread-publisher-cli-{}-{nanos}-{sequence}", std::process::id() )); fs::create_dir(&path).expect("temporary directory should be created"); Self(path) } } impl Drop for TempDir { fn drop(&mut self) { let _ = fs::remove_dir_all(&self.0); } } fn publisher() -> Command { Command::new(env!("CARGO_BIN_EXE_lanspread-catalog-publisher")) } fn catalog_db() -> PathBuf { Path::new(env!("CARGO_MANIFEST_DIR")).join("../lanspread-tauri-deno-ts/src-tauri/game.db") } fn packages_dir() -> PathBuf { Path::new(env!("CARGO_MANIFEST_DIR")).join("../lanspread-peer-cli/fixtures/fixture-persona") } fn test_unrar() -> Option { #[cfg(target_os = "linux")] { let bundled = Path::new(env!("CARGO_MANIFEST_DIR")) .join("../lanspread-tauri-deno-ts/src-tauri/binaries/unrar-x86_64-unknown-linux-gnu"); if bundled.is_file() { return Some(bundled); } } ["/usr/local/bin/unrar", "/usr/bin/unrar"] .into_iter() .map(PathBuf::from) .find(|path| path.is_file()) } async fn create_single_game_catalog(path: &Path) { use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions}; let pool = SqlitePoolOptions::new() .max_connections(1) .connect_with( SqliteConnectOptions::new() .filename(path) .create_if_missing(true), ) .await .expect("test catalog should open"); sqlx::query( "CREATE TABLE games (game_id TEXT NOT NULL, game_version TEXT NOT NULL, db_id INTEGER NOT NULL)", ) .execute(&pool) .await .expect("games table should be created"); sqlx::query("INSERT INTO games (game_id, game_version, db_id) VALUES ('css', '20240623', 1)") .execute(&pool) .await .expect("css row should be inserted"); pool.close().await; } #[test] fn help_is_exact_and_error_output_is_separate() { let help = publisher() .arg("--help") .output() .expect("publisher should run"); assert!(help.status.success()); assert_eq!( String::from_utf8(help.stdout).expect("UTF-8 stdout"), format!("{HELP}\n") ); assert!(help.stderr.is_empty()); let error = publisher() .args(["check", "--catalog-db"]) .arg(catalog_db()) .output() .expect("publisher should run"); assert!(!error.status.success()); assert!(error.stdout.is_empty()); assert_eq!( String::from_utf8(error.stderr).expect("UTF-8 stderr"), "error: select games with --all or at least one --game-id\n" ); } #[tokio::test] async fn complete_real_package_round_trips_through_generate_and_check_commands() { let Some(unrar) = test_unrar() else { return; }; let temp = TempDir::new(); let fixture_catalog = temp.0.join("game.db"); create_single_game_catalog(&fixture_catalog).await; let manifests = temp.0.join("manifests"); let generated = publisher() .arg("generate") .arg("--catalog-db") .arg(&fixture_catalog) .arg("--packages-dir") .arg(packages_dir()) .arg("--manifests-dir") .arg(&manifests) .arg("--unrar") .arg(unrar) .arg("--all") .output() .expect("publisher should run"); assert!( generated.status.success(), "generation failed: {}", String::from_utf8_lossy(&generated.stderr) ); assert!(generated.stderr.is_empty()); let generated = String::from_utf8(generated.stdout).expect("UTF-8 stdout"); let generated_lines = generated.lines().collect::>(); assert_eq!(generated_lines.len(), 2); let prefix = "generated game_id=css game_version=20240623 content_id="; let content_id = generated_lines[0] .strip_prefix(prefix) .expect("stable generated record prefix"); assert_eq!(content_id.len(), 64); assert!( content_id .bytes() .all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase()) ); assert_eq!(generated_lines[1], "generated total=1"); let checked = publisher() .arg("check") .arg("--catalog-db") .arg(fixture_catalog) .arg("--manifests-dir") .arg(manifests) .args(["--game-id", "css"]) .output() .expect("publisher should run"); assert!( checked.status.success(), "check failed: {}", String::from_utf8_lossy(&checked.stderr) ); assert!(checked.stderr.is_empty()); assert_eq!( String::from_utf8(checked.stdout).expect("UTF-8 stdout"), format!( "checked game_id=css game_version=20240623 content_id={content_id}\nchecked total=1\n" ) ); }