//! Integration tests for the tidalctl binary. //! //! Tests run the actual binary as a subprocess via `std::process::Command`. #![forbid(unsafe_code)] #![allow(clippy::unwrap_used)] use std::process::Command; use tidaldb::db::temp::TempTidalHome; use tidaldb::wal::{SignalEvent, WalConfig, WalHandle}; fn tidalctl_bin() -> Command { Command::new(env!("CARGO_BIN_EXE_tidalctl")) } /// Create a `TempTidalHome` with WAL segments and a checkpoint. /// Returns the home so it stays alive (not dropped) for the test. fn home_with_wal_data() -> TempTidalHome { let home = TempTidalHome::new().expect("create temp home"); let paths = home.paths(); paths.ensure_all().expect("create subdirs"); let config = WalConfig { dir: home.path().to_path_buf(), ..WalConfig::default() }; let (handle, _replayed) = WalHandle::open(config).expect("open WAL"); for i in 1..=5 { let event = SignalEvent { entity_id: i, signal_type: 1, weight: 1.0, timestamp_nanos: i * 1_000_000_000, }; let _seq = handle.append(event).expect("append event"); } handle.checkpoint(3).expect("checkpoint"); handle.shutdown().expect("shutdown WAL"); home } #[test] fn status_with_wal_segments_exits_0() { let home = home_with_wal_data(); let output = tidalctl_bin() .args(["status", "--path", home.path().to_str().unwrap()]) .output() .expect("run tidalctl"); assert!( output.status.success(), "exit code: {}, stderr: {}", output.status, String::from_utf8_lossy(&output.stderr) ); let stdout = String::from_utf8(output.stdout).expect("utf8"); let json: serde_json::Value = serde_json::from_str(&stdout).expect("parse JSON"); assert_eq!(json["status"], "ok"); assert!( json["wal"]["segments"].as_u64().unwrap_or(0) > 0, "should have segments: {stdout}" ); assert!( json["wal"]["checkpoint_seq"].as_u64().unwrap_or(0) > 0, "should have checkpoint: {stdout}" ); assert!(json["version"].is_string()); assert!(json["build_hash"].is_string()); assert!(json["dirs"]["base"].is_string()); } #[test] fn status_empty_dir_shows_empty() { let home = TempTidalHome::new().expect("create temp home"); let paths = home.paths(); paths.ensure_all().expect("create subdirs"); let output = tidalctl_bin() .args(["status", "--path", home.path().to_str().unwrap()]) .output() .expect("run tidalctl"); assert!(output.status.success()); let stdout = String::from_utf8(output.stdout).expect("utf8"); let json: serde_json::Value = serde_json::from_str(&stdout).expect("parse JSON"); assert_eq!(json["status"], "empty"); } #[test] fn status_nonexistent_path_exits_0_with_empty() { // When the WAL dir doesn't exist, gather_wal_state returns segments=0 // which maps to "empty" status. The base dir may or may not exist. let output = tidalctl_bin() .args(["status", "--path", "/nonexistent/path/that/does/not/exist"]) .output() .expect("run tidalctl"); let stdout = String::from_utf8(output.stdout).expect("utf8"); // This should still be valid JSON regardless of status if output.status.success() { let json: serde_json::Value = serde_json::from_str(&stdout).expect("parse JSON"); assert!( json["status"] == "empty" || json["status"] == "error", "status should be empty or error: {stdout}" ); } else { let stderr = String::from_utf8(output.stderr).expect("utf8"); let json: serde_json::Value = serde_json::from_str(&stderr).expect("parse error JSON"); assert!(json["error"].is_string()); } } #[test] fn paths_exits_0_with_all_dirs() { let home = TempTidalHome::new().expect("create temp home"); let paths = home.paths(); paths.ensure_all().expect("create subdirs"); let output = tidalctl_bin() .args(["paths", "--path", home.path().to_str().unwrap()]) .output() .expect("run tidalctl"); assert!(output.status.success()); let stdout = String::from_utf8(output.stdout).expect("utf8"); let json: serde_json::Value = serde_json::from_str(&stdout).expect("parse JSON"); // Check all six directory paths are present assert!(json["base"].is_string(), "missing base: {stdout}"); assert!(json["wal"].is_string(), "missing wal: {stdout}"); assert!(json["items"].is_string(), "missing items: {stdout}"); assert!(json["users"].is_string(), "missing users: {stdout}"); assert!(json["creators"].is_string(), "missing creators: {stdout}"); assert!(json["cache"].is_string(), "missing cache: {stdout}"); // Check exists map assert!(json["exists"]["base"].is_boolean()); assert!(json["exists"]["wal"].is_boolean()); assert_eq!(json["exists"]["base"], true); assert_eq!(json["exists"]["wal"], true); } #[test] fn paths_nonexistent_dir_shows_false_exists() { let output = tidalctl_bin() .args(["paths", "--path", "/nonexistent/does/not/exist"]) .output() .expect("run tidalctl"); assert!(output.status.success()); let stdout = String::from_utf8(output.stdout).expect("utf8"); let json: serde_json::Value = serde_json::from_str(&stdout).expect("parse JSON"); assert_eq!(json["exists"]["base"], false); assert_eq!(json["exists"]["wal"], false); } #[test] fn pretty_flag_produces_indented_json() { let home = TempTidalHome::new().expect("create temp home"); let output = tidalctl_bin() .args(["paths", "--path", home.path().to_str().unwrap(), "--pretty"]) .output() .expect("run tidalctl"); assert!(output.status.success()); let stdout = String::from_utf8(output.stdout).expect("utf8"); // Pretty-printed JSON should have newlines and indentation assert!( stdout.contains('\n'), "pretty output should have newlines: {stdout}" ); assert!( stdout.contains(" "), "pretty output should have indentation: {stdout}" ); // Should still be valid JSON let _json: serde_json::Value = serde_json::from_str(&stdout).expect("parse JSON"); } #[test] fn bad_command_exits_1() { let output = tidalctl_bin() .args(["nonexistent_command", "--path", "/tmp"]) .output() .expect("run tidalctl"); assert!(!output.status.success(), "should exit 1 for bad command"); let stderr = String::from_utf8(output.stderr).expect("utf8"); assert!( stderr.contains("error"), "stderr should contain error: {stderr}" ); } #[test] fn no_args_exits_1() { let output = tidalctl_bin().output().expect("run tidalctl"); assert!(!output.status.success(), "should exit 1 with no args"); } #[test] fn missing_path_flag_exits_1() { let output = tidalctl_bin() .args(["status"]) .output() .expect("run tidalctl"); assert!(!output.status.success(), "should exit 1 without --path"); let stderr = String::from_utf8(output.stderr).expect("utf8"); assert!( stderr.contains("error"), "stderr should contain error: {stderr}" ); }