use std::process::Command; use crate::process::CommandResult; /// Error type for package management operations #[derive(Debug)] pub enum PackageError { /// Command failed with error message CommandFailed(String), /// Command execution failed with IO error CommandExecutionFailed(std::io::Error), /// Unsupported platform UnsupportedPlatform(String), /// Other error Other(String), } impl std::fmt::Display for PackageError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { PackageError::CommandFailed(msg) => write!(f, "Command failed: {}", msg), PackageError::CommandExecutionFailed(e) => write!(f, "Command execution failed: {}", e), PackageError::UnsupportedPlatform(msg) => write!(f, "Unsupported platform: {}", msg), PackageError::Other(msg) => write!(f, "Error: {}", msg), } } } impl std::error::Error for PackageError {} /// Platform enum for detecting the current operating system #[derive(Debug, Clone, Copy, PartialEq)] pub enum Platform { /// Ubuntu Linux Ubuntu, /// macOS MacOS, /// Unknown platform Unknown, } impl Platform { /// Detect the current platform pub fn detect() -> Self { // Check for macOS if std::path::Path::new("/usr/bin/sw_vers").exists() { return Platform::MacOS; } // Check for Ubuntu if std::path::Path::new("/etc/lsb-release").exists() { // Read the file to confirm it's Ubuntu if let Ok(content) = std::fs::read_to_string("/etc/lsb-release") { if content.contains("Ubuntu") { return Platform::Ubuntu; } } } Platform::Unknown } } /// Thread-local storage for debug flag thread_local! { static DEBUG: std::cell::RefCell = std::cell::RefCell::new(false); } /// Set the debug flag for the current thread pub fn set_thread_local_debug(debug: bool) { DEBUG.with(|cell| { *cell.borrow_mut() = debug; }); } /// Get the debug flag for the current thread pub fn thread_local_debug() -> bool { DEBUG.with(|cell| { *cell.borrow() }) } /// Execute a package management command and return the result pub fn execute_package_command(args: &[&str], debug: bool) -> Result { // Save the current debug flag let previous_debug = thread_local_debug(); // Set the thread-local debug flag set_thread_local_debug(debug); if debug { println!("Executing command: {}", args.join(" ")); } let output = Command::new(args[0]) .args(&args[1..]) .output(); // Restore the previous debug flag set_thread_local_debug(previous_debug); match output { Ok(output) => { let stdout = String::from_utf8_lossy(&output.stdout).to_string(); let stderr = String::from_utf8_lossy(&output.stderr).to_string(); let result = CommandResult { stdout, stderr, success: output.status.success(), code: output.status.code().unwrap_or(-1), }; // Always output stdout/stderr when debug is true if debug { if !result.stdout.is_empty() { println!("Command stdout: {}", result.stdout); } if !result.stderr.is_empty() { println!("Command stderr: {}", result.stderr); } if result.success { println!("Command succeeded with code {}", result.code); } else { println!("Command failed with code {}", result.code); } } if result.success { Ok(result) } else { // If command failed and debug is false, output stderr if !debug { println!("Command failed with code {}: {}", result.code, result.stderr.trim()); } Err(PackageError::CommandFailed(format!("Command failed with code {}: {}", result.code, result.stderr.trim()))) } }, Err(e) => { // Always output error information println!("Command execution failed: {}", e); Err(PackageError::CommandExecutionFailed(e)) } } } /// Trait for package managers pub trait PackageManager { /// Install a package fn install(&self, package: &str) -> Result; /// Remove a package fn remove(&self, package: &str) -> Result; /// Update package lists fn update(&self) -> Result; /// Upgrade installed packages fn upgrade(&self) -> Result; /// List installed packages fn list_installed(&self) -> Result, PackageError>; /// Search for packages fn search(&self, query: &str) -> Result, PackageError>; /// Check if a package is installed fn is_installed(&self, package: &str) -> Result; } /// APT package manager for Ubuntu pub struct AptPackageManager { debug: bool, } impl AptPackageManager { /// Create a new APT package manager pub fn new(debug: bool) -> Self { Self { debug } } } impl PackageManager for AptPackageManager { fn install(&self, package: &str) -> Result { // Use -y to make it non-interactive and --quiet to reduce output execute_package_command(&["apt-get", "install", "-y", "--quiet", package], self.debug) } fn remove(&self, package: &str) -> Result { // Use -y to make it non-interactive and --quiet to reduce output execute_package_command(&["apt-get", "remove", "-y", "--quiet", package], self.debug) } fn update(&self) -> Result { // Use -y to make it non-interactive and --quiet to reduce output execute_package_command(&["apt-get", "update", "-y", "--quiet"], self.debug) } fn upgrade(&self) -> Result { // Use -y to make it non-interactive and --quiet to reduce output execute_package_command(&["apt-get", "upgrade", "-y", "--quiet"], self.debug) } fn list_installed(&self) -> Result, PackageError> { let result = execute_package_command(&["dpkg", "--get-selections"], self.debug)?; let packages = result.stdout .lines() .filter_map(|line| { let parts: Vec<&str> = line.split_whitespace().collect(); if parts.len() >= 2 && parts[1] == "install" { Some(parts[0].to_string()) } else { None } }) .collect(); Ok(packages) } fn search(&self, query: &str) -> Result, PackageError> { let result = execute_package_command(&["apt-cache", "search", query], self.debug)?; let packages = result.stdout .lines() .map(|line| { let parts: Vec<&str> = line.split_whitespace().collect(); if !parts.is_empty() { parts[0].to_string() } else { String::new() } }) .filter(|s| !s.is_empty()) .collect(); Ok(packages) } fn is_installed(&self, package: &str) -> Result { let result = execute_package_command(&["dpkg", "-s", package], self.debug); match result { Ok(cmd_result) => Ok(cmd_result.success), Err(_) => Ok(false), } } } /// Homebrew package manager for macOS pub struct BrewPackageManager { debug: bool, } impl BrewPackageManager { /// Create a new Homebrew package manager pub fn new(debug: bool) -> Self { Self { debug } } } impl PackageManager for BrewPackageManager { fn install(&self, package: &str) -> Result { // Use --quiet to reduce output execute_package_command(&["brew", "install", "--quiet", package], self.debug) } fn remove(&self, package: &str) -> Result { // Use --quiet to reduce output execute_package_command(&["brew", "uninstall", "--quiet", package], self.debug) } fn update(&self) -> Result { // Use --quiet to reduce output execute_package_command(&["brew", "update", "--quiet"], self.debug) } fn upgrade(&self) -> Result { // Use --quiet to reduce output execute_package_command(&["brew", "upgrade", "--quiet"], self.debug) } fn list_installed(&self) -> Result, PackageError> { let result = execute_package_command(&["brew", "list", "--formula"], self.debug)?; let packages = result.stdout .lines() .map(|line| line.trim().to_string()) .filter(|s| !s.is_empty()) .collect(); Ok(packages) } fn search(&self, query: &str) -> Result, PackageError> { let result = execute_package_command(&["brew", "search", query], self.debug)?; let packages = result.stdout .lines() .map(|line| line.trim().to_string()) .filter(|s| !s.is_empty()) .collect(); Ok(packages) } fn is_installed(&self, package: &str) -> Result { let result = execute_package_command(&["brew", "list", package], self.debug); match result { Ok(cmd_result) => Ok(cmd_result.success), Err(_) => Ok(false), } } } /// PackHero factory for package management pub struct PackHero { platform: Platform, debug: bool, } impl PackHero { /// Create a new PackHero instance pub fn new() -> Self { let platform = Platform::detect(); Self { platform, debug: false, } } /// Set the debug mode pub fn set_debug(&mut self, debug: bool) -> &mut Self { self.debug = debug; self } /// Get the debug mode pub fn debug(&self) -> bool { self.debug } /// Get the detected platform pub fn platform(&self) -> Platform { self.platform } /// Get a package manager for the current platform fn get_package_manager(&self) -> Result, PackageError> { match self.platform { Platform::Ubuntu => Ok(Box::new(AptPackageManager::new(self.debug))), Platform::MacOS => Ok(Box::new(BrewPackageManager::new(self.debug))), Platform::Unknown => Err(PackageError::UnsupportedPlatform("Unsupported platform".to_string())), } } /// Install a package pub fn install(&self, package: &str) -> Result { let pm = self.get_package_manager()?; pm.install(package) } /// Remove a package pub fn remove(&self, package: &str) -> Result { let pm = self.get_package_manager()?; pm.remove(package) } /// Update package lists pub fn update(&self) -> Result { let pm = self.get_package_manager()?; pm.update() } /// Upgrade installed packages pub fn upgrade(&self) -> Result { let pm = self.get_package_manager()?; pm.upgrade() } /// List installed packages pub fn list_installed(&self) -> Result, PackageError> { let pm = self.get_package_manager()?; pm.list_installed() } /// Search for packages pub fn search(&self, query: &str) -> Result, PackageError> { let pm = self.get_package_manager()?; pm.search(query) } /// Check if a package is installed pub fn is_installed(&self, package: &str) -> Result { let pm = self.get_package_manager()?; pm.is_installed(package) } } #[cfg(test)] mod tests { // Import the std::process::Command directly for some test-specific commands use std::process::Command as StdCommand; use super::*; use std::sync::{Arc, Mutex}; #[test] fn test_platform_detection() { // This test will return different results depending on the platform it's run on let platform = Platform::detect(); println!("Detected platform: {:?}", platform); // Just ensure it doesn't panic assert!(true); } #[test] fn test_debug_flag() { // Test setting and getting the debug flag set_thread_local_debug(true); assert_eq!(thread_local_debug(), true); set_thread_local_debug(false); assert_eq!(thread_local_debug(), false); } #[test] fn test_package_error_display() { // Test the Display implementation for PackageError let err1 = PackageError::CommandFailed("command failed".to_string()); assert_eq!(err1.to_string(), "Command failed: command failed"); let err2 = PackageError::UnsupportedPlatform("test platform".to_string()); assert_eq!(err2.to_string(), "Unsupported platform: test platform"); let err3 = PackageError::Other("other error".to_string()); assert_eq!(err3.to_string(), "Error: other error"); // We can't easily test CommandExecutionFailed because std::io::Error doesn't implement PartialEq } // Mock package manager for testing struct MockPackageManager { debug: bool, install_called: Arc>, remove_called: Arc>, update_called: Arc>, upgrade_called: Arc>, list_installed_called: Arc>, search_called: Arc>, is_installed_called: Arc>, // Control what the mock returns should_succeed: bool, } impl MockPackageManager { fn new(debug: bool, should_succeed: bool) -> Self { Self { debug, install_called: Arc::new(Mutex::new(false)), remove_called: Arc::new(Mutex::new(false)), update_called: Arc::new(Mutex::new(false)), upgrade_called: Arc::new(Mutex::new(false)), list_installed_called: Arc::new(Mutex::new(false)), search_called: Arc::new(Mutex::new(false)), is_installed_called: Arc::new(Mutex::new(false)), should_succeed, } } } impl PackageManager for MockPackageManager { fn install(&self, package: &str) -> Result { *self.install_called.lock().unwrap() = true; if self.should_succeed { Ok(CommandResult { stdout: format!("Installed package {}", package), stderr: String::new(), success: true, code: 0, }) } else { Err(PackageError::CommandFailed("Mock install failed".to_string())) } } fn remove(&self, package: &str) -> Result { *self.remove_called.lock().unwrap() = true; if self.should_succeed { Ok(CommandResult { stdout: format!("Removed package {}", package), stderr: String::new(), success: true, code: 0, }) } else { Err(PackageError::CommandFailed("Mock remove failed".to_string())) } } fn update(&self) -> Result { *self.update_called.lock().unwrap() = true; if self.should_succeed { Ok(CommandResult { stdout: "Updated package lists".to_string(), stderr: String::new(), success: true, code: 0, }) } else { Err(PackageError::CommandFailed("Mock update failed".to_string())) } } fn upgrade(&self) -> Result { *self.upgrade_called.lock().unwrap() = true; if self.should_succeed { Ok(CommandResult { stdout: "Upgraded packages".to_string(), stderr: String::new(), success: true, code: 0, }) } else { Err(PackageError::CommandFailed("Mock upgrade failed".to_string())) } } fn list_installed(&self) -> Result, PackageError> { *self.list_installed_called.lock().unwrap() = true; if self.should_succeed { Ok(vec!["package1".to_string(), "package2".to_string()]) } else { Err(PackageError::CommandFailed("Mock list_installed failed".to_string())) } } fn search(&self, query: &str) -> Result, PackageError> { *self.search_called.lock().unwrap() = true; if self.should_succeed { Ok(vec![format!("result1-{}", query), format!("result2-{}", query)]) } else { Err(PackageError::CommandFailed("Mock search failed".to_string())) } } fn is_installed(&self, package: &str) -> Result { *self.is_installed_called.lock().unwrap() = true; if self.should_succeed { Ok(package == "installed-package") } else { Err(PackageError::CommandFailed("Mock is_installed failed".to_string())) } } } // Custom PackHero for testing with a mock package manager struct TestPackHero { platform: Platform, debug: bool, mock_manager: MockPackageManager, } impl TestPackHero { fn new(platform: Platform, debug: bool, should_succeed: bool) -> Self { Self { platform, debug, mock_manager: MockPackageManager::new(debug, should_succeed), } } fn get_package_manager(&self) -> Result<&dyn PackageManager, PackageError> { match self.platform { Platform::Ubuntu | Platform::MacOS => Ok(&self.mock_manager), Platform::Unknown => Err(PackageError::UnsupportedPlatform("Unsupported platform".to_string())), } } fn install(&self, package: &str) -> Result { let pm = self.get_package_manager()?; pm.install(package) } fn remove(&self, package: &str) -> Result { let pm = self.get_package_manager()?; pm.remove(package) } fn update(&self) -> Result { let pm = self.get_package_manager()?; pm.update() } fn upgrade(&self) -> Result { let pm = self.get_package_manager()?; pm.upgrade() } fn list_installed(&self) -> Result, PackageError> { let pm = self.get_package_manager()?; pm.list_installed() } fn search(&self, query: &str) -> Result, PackageError> { let pm = self.get_package_manager()?; pm.search(query) } fn is_installed(&self, package: &str) -> Result { let pm = self.get_package_manager()?; pm.is_installed(package) } } #[test] fn test_packhero_with_mock_success() { // Test PackHero with a mock package manager that succeeds let hero = TestPackHero::new(Platform::Ubuntu, false, true); // Test install let result = hero.install("test-package"); assert!(result.is_ok()); assert!(*hero.mock_manager.install_called.lock().unwrap()); // Test remove let result = hero.remove("test-package"); assert!(result.is_ok()); assert!(*hero.mock_manager.remove_called.lock().unwrap()); // Test update let result = hero.update(); assert!(result.is_ok()); assert!(*hero.mock_manager.update_called.lock().unwrap()); // Test upgrade let result = hero.upgrade(); assert!(result.is_ok()); assert!(*hero.mock_manager.upgrade_called.lock().unwrap()); // Test list_installed let result = hero.list_installed(); assert!(result.is_ok()); assert_eq!(result.unwrap(), vec!["package1".to_string(), "package2".to_string()]); assert!(*hero.mock_manager.list_installed_called.lock().unwrap()); // Test search let result = hero.search("query"); assert!(result.is_ok()); assert_eq!(result.unwrap(), vec!["result1-query".to_string(), "result2-query".to_string()]); assert!(*hero.mock_manager.search_called.lock().unwrap()); // Test is_installed let result = hero.is_installed("installed-package"); assert!(result.is_ok()); assert!(result.unwrap()); assert!(*hero.mock_manager.is_installed_called.lock().unwrap()); let result = hero.is_installed("not-installed-package"); assert!(result.is_ok()); assert!(!result.unwrap()); } #[test] fn test_packhero_with_mock_failure() { // Test PackHero with a mock package manager that fails let hero = TestPackHero::new(Platform::Ubuntu, false, false); // Test install let result = hero.install("test-package"); assert!(result.is_err()); assert!(*hero.mock_manager.install_called.lock().unwrap()); // Test remove let result = hero.remove("test-package"); assert!(result.is_err()); assert!(*hero.mock_manager.remove_called.lock().unwrap()); // Test update let result = hero.update(); assert!(result.is_err()); assert!(*hero.mock_manager.update_called.lock().unwrap()); // Test upgrade let result = hero.upgrade(); assert!(result.is_err()); assert!(*hero.mock_manager.upgrade_called.lock().unwrap()); // Test list_installed let result = hero.list_installed(); assert!(result.is_err()); assert!(*hero.mock_manager.list_installed_called.lock().unwrap()); // Test search let result = hero.search("query"); assert!(result.is_err()); assert!(*hero.mock_manager.search_called.lock().unwrap()); // Test is_installed let result = hero.is_installed("installed-package"); assert!(result.is_err()); assert!(*hero.mock_manager.is_installed_called.lock().unwrap()); } #[test] fn test_packhero_unsupported_platform() { // Test PackHero with an unsupported platform let hero = TestPackHero::new(Platform::Unknown, false, true); // All operations should fail with UnsupportedPlatform error let result = hero.install("test-package"); assert!(result.is_err()); match result { Err(PackageError::UnsupportedPlatform(_)) => (), _ => panic!("Expected UnsupportedPlatform error"), } let result = hero.remove("test-package"); assert!(result.is_err()); match result { Err(PackageError::UnsupportedPlatform(_)) => (), _ => panic!("Expected UnsupportedPlatform error"), } let result = hero.update(); assert!(result.is_err()); match result { Err(PackageError::UnsupportedPlatform(_)) => (), _ => panic!("Expected UnsupportedPlatform error"), } } // Real-world tests that actually install and remove packages on Ubuntu // These tests will only run on Ubuntu and will be skipped on other platforms #[test] fn test_real_package_operations_on_ubuntu() { // Check if we're on Ubuntu let platform = Platform::detect(); if platform != Platform::Ubuntu { println!("Skipping real package operations test on non-Ubuntu platform: {:?}", platform); return; } println!("Running real package operations test on Ubuntu"); // Create a PackHero instance with debug enabled let mut hero = PackHero::new(); hero.set_debug(true); // Test package to install/remove let test_package = "wget"; // First, check if the package is already installed let is_installed_before = match hero.is_installed(test_package) { Ok(result) => result, Err(e) => { println!("Error checking if package is installed: {}", e); return; } }; println!("Package {} is installed before test: {}", test_package, is_installed_before); // If the package is already installed, we'll remove it first if is_installed_before { println!("Removing existing package {} before test", test_package); match hero.remove(test_package) { Ok(_) => println!("Successfully removed package {}", test_package), Err(e) => { println!("Error removing package {}: {}", test_package, e); return; } } // Verify it was removed match hero.is_installed(test_package) { Ok(is_installed) => { if is_installed { println!("Failed to remove package {}", test_package); return; } else { println!("Verified package {} was removed", test_package); } }, Err(e) => { println!("Error checking if package is installed after removal: {}", e); return; } } } // Now install the package println!("Installing package {}", test_package); match hero.install(test_package) { Ok(_) => println!("Successfully installed package {}", test_package), Err(e) => { println!("Error installing package {}: {}", test_package, e); return; } } // Verify it was installed match hero.is_installed(test_package) { Ok(is_installed) => { if !is_installed { println!("Failed to install package {}", test_package); return; } else { println!("Verified package {} was installed", test_package); } }, Err(e) => { println!("Error checking if package is installed after installation: {}", e); return; } } // Test the search functionality println!("Searching for packages with 'wget'"); match hero.search("wget") { Ok(results) => { println!("Search results: {:?}", results); assert!(results.iter().any(|r| r.contains("wget")), "Search results should contain wget"); }, Err(e) => { println!("Error searching for packages: {}", e); return; } } // Test listing installed packages println!("Listing installed packages"); match hero.list_installed() { Ok(packages) => { println!("Found {} installed packages", packages.len()); // Check if our test package is in the list assert!(packages.iter().any(|p| p == test_package), "Installed packages list should contain {}", test_package); }, Err(e) => { println!("Error listing installed packages: {}", e); return; } } // Now remove the package if it wasn't installed before if !is_installed_before { println!("Removing package {} after test", test_package); match hero.remove(test_package) { Ok(_) => println!("Successfully removed package {}", test_package), Err(e) => { println!("Error removing package {}: {}", test_package, e); return; } } // Verify it was removed match hero.is_installed(test_package) { Ok(is_installed) => { if is_installed { println!("Failed to remove package {}", test_package); return; } else { println!("Verified package {} was removed", test_package); } }, Err(e) => { println!("Error checking if package is installed after removal: {}", e); return; } } } // Test update functionality println!("Testing package list update"); match hero.update() { Ok(_) => println!("Successfully updated package lists"), Err(e) => { println!("Error updating package lists: {}", e); return; } } println!("All real package operations tests passed on Ubuntu"); } // Test to check if apt-get is available on the system #[test] fn test_apt_get_availability() { // This test checks if apt-get is available on the system let output = StdCommand::new("which") .arg("apt-get") .output() .expect("Failed to execute which apt-get"); let success = output.status.success(); let stdout = String::from_utf8_lossy(&output.stdout).to_string(); println!("apt-get available: {}", success); if success { println!("apt-get path: {}", stdout.trim()); } // On Ubuntu, this should pass if Platform::detect() == Platform::Ubuntu { assert!(success, "apt-get should be available on Ubuntu"); } } }