feat: Improve package management and testing
Some checks failed
Rhai Tests / Run Rhai Tests (push) Has been cancelled
Rhai Tests / Run Rhai Tests (pull_request) Has been cancelled

- Improve platform detection logic for more robust package management.
- Enhance error handling and reporting in package commands.
- Refactor code for better readability and maintainability.
- Add comprehensive tests to cover package management functionality.
- Improve test coverage for various scenarios and edge cases.
This commit is contained in:
Mahmoud Emad 2025-05-09 09:54:32 +03:00
parent f002445c9e
commit 22f87b320e
4 changed files with 300 additions and 232 deletions

View File

@ -1,5 +1,5 @@
use std::process::Command;
use crate::process::CommandResult; use crate::process::CommandResult;
use std::process::Command;
/// Error type for package management operations /// Error type for package management operations
#[derive(Debug)] #[derive(Debug)]
@ -60,7 +60,7 @@ impl Platform {
} }
} }
/// Thread-local storage for debug flag // Thread-local storage for debug flag
thread_local! { thread_local! {
static DEBUG: std::cell::RefCell<bool> = std::cell::RefCell::new(false); static DEBUG: std::cell::RefCell<bool> = std::cell::RefCell::new(false);
} }
@ -74,9 +74,7 @@ pub fn set_thread_local_debug(debug: bool) {
/// Get the debug flag for the current thread /// Get the debug flag for the current thread
pub fn thread_local_debug() -> bool { pub fn thread_local_debug() -> bool {
DEBUG.with(|cell| { DEBUG.with(|cell| *cell.borrow())
*cell.borrow()
})
} }
/// Execute a package management command and return the result /// Execute a package management command and return the result
@ -91,9 +89,7 @@ pub fn execute_package_command(args: &[&str], debug: bool) -> Result<CommandResu
println!("Executing command: {}", args.join(" ")); println!("Executing command: {}", args.join(" "));
} }
let output = Command::new(args[0]) let output = Command::new(args[0]).args(&args[1..]).output();
.args(&args[1..])
.output();
// Restore the previous debug flag // Restore the previous debug flag
set_thread_local_debug(previous_debug); set_thread_local_debug(previous_debug);
@ -132,12 +128,19 @@ pub fn execute_package_command(args: &[&str], debug: bool) -> Result<CommandResu
} else { } else {
// If command failed and debug is false, output stderr // If command failed and debug is false, output stderr
if !debug { if !debug {
println!("Command failed with code {}: {}", result.code, result.stderr.trim()); println!(
"Command failed with code {}: {}",
result.code,
result.stderr.trim()
);
}
Err(PackageError::CommandFailed(format!(
"Command failed with code {}: {}",
result.code,
result.stderr.trim()
)))
} }
Err(PackageError::CommandFailed(format!("Command failed with code {}: {}",
result.code, result.stderr.trim())))
} }
},
Err(e) => { Err(e) => {
// Always output error information // Always output error information
println!("Command execution failed: {}", e); println!("Command execution failed: {}", e);
@ -185,7 +188,10 @@ impl AptPackageManager {
impl PackageManager for AptPackageManager { impl PackageManager for AptPackageManager {
fn install(&self, package: &str) -> Result<CommandResult, PackageError> { fn install(&self, package: &str) -> Result<CommandResult, PackageError> {
// Use -y to make it non-interactive and --quiet to reduce output // Use -y to make it non-interactive and --quiet to reduce output
execute_package_command(&["apt-get", "install", "-y", "--quiet", package], self.debug) execute_package_command(
&["apt-get", "install", "-y", "--quiet", package],
self.debug,
)
} }
fn remove(&self, package: &str) -> Result<CommandResult, PackageError> { fn remove(&self, package: &str) -> Result<CommandResult, PackageError> {
@ -205,7 +211,8 @@ impl PackageManager for AptPackageManager {
fn list_installed(&self) -> Result<Vec<String>, PackageError> { fn list_installed(&self) -> Result<Vec<String>, PackageError> {
let result = execute_package_command(&["dpkg", "--get-selections"], self.debug)?; let result = execute_package_command(&["dpkg", "--get-selections"], self.debug)?;
let packages = result.stdout let packages = result
.stdout
.lines() .lines()
.filter_map(|line| { .filter_map(|line| {
let parts: Vec<&str> = line.split_whitespace().collect(); let parts: Vec<&str> = line.split_whitespace().collect();
@ -221,7 +228,8 @@ impl PackageManager for AptPackageManager {
fn search(&self, query: &str) -> Result<Vec<String>, PackageError> { fn search(&self, query: &str) -> Result<Vec<String>, PackageError> {
let result = execute_package_command(&["apt-cache", "search", query], self.debug)?; let result = execute_package_command(&["apt-cache", "search", query], self.debug)?;
let packages = result.stdout let packages = result
.stdout
.lines() .lines()
.map(|line| { .map(|line| {
let parts: Vec<&str> = line.split_whitespace().collect(); let parts: Vec<&str> = line.split_whitespace().collect();
@ -280,7 +288,8 @@ impl PackageManager for BrewPackageManager {
fn list_installed(&self) -> Result<Vec<String>, PackageError> { fn list_installed(&self) -> Result<Vec<String>, PackageError> {
let result = execute_package_command(&["brew", "list", "--formula"], self.debug)?; let result = execute_package_command(&["brew", "list", "--formula"], self.debug)?;
let packages = result.stdout let packages = result
.stdout
.lines() .lines()
.map(|line| line.trim().to_string()) .map(|line| line.trim().to_string())
.filter(|s| !s.is_empty()) .filter(|s| !s.is_empty())
@ -290,7 +299,8 @@ impl PackageManager for BrewPackageManager {
fn search(&self, query: &str) -> Result<Vec<String>, PackageError> { fn search(&self, query: &str) -> Result<Vec<String>, PackageError> {
let result = execute_package_command(&["brew", "search", query], self.debug)?; let result = execute_package_command(&["brew", "search", query], self.debug)?;
let packages = result.stdout let packages = result
.stdout
.lines() .lines()
.map(|line| line.trim().to_string()) .map(|line| line.trim().to_string())
.filter(|s| !s.is_empty()) .filter(|s| !s.is_empty())
@ -344,7 +354,9 @@ impl PackHero {
match self.platform { match self.platform {
Platform::Ubuntu => Ok(Box::new(AptPackageManager::new(self.debug))), Platform::Ubuntu => Ok(Box::new(AptPackageManager::new(self.debug))),
Platform::MacOS => Ok(Box::new(BrewPackageManager::new(self.debug))), Platform::MacOS => Ok(Box::new(BrewPackageManager::new(self.debug))),
Platform::Unknown => Err(PackageError::UnsupportedPlatform("Unsupported platform".to_string())), Platform::Unknown => Err(PackageError::UnsupportedPlatform(
"Unsupported platform".to_string(),
)),
} }
} }
@ -394,8 +406,8 @@ impl PackHero {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
// Import the std::process::Command directly for some test-specific commands // Import the std::process::Command directly for some test-specific commands
use std::process::Command as StdCommand;
use super::*; use super::*;
use std::process::Command as StdCommand;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
#[test] #[test]
@ -435,6 +447,8 @@ mod tests {
// Mock package manager for testing // Mock package manager for testing
struct MockPackageManager { struct MockPackageManager {
// debug field is kept for consistency with real package managers
#[allow(dead_code)]
debug: bool, debug: bool,
install_called: Arc<Mutex<bool>>, install_called: Arc<Mutex<bool>>,
remove_called: Arc<Mutex<bool>>, remove_called: Arc<Mutex<bool>>,
@ -474,7 +488,9 @@ mod tests {
code: 0, code: 0,
}) })
} else { } else {
Err(PackageError::CommandFailed("Mock install failed".to_string())) Err(PackageError::CommandFailed(
"Mock install failed".to_string(),
))
} }
} }
@ -488,7 +504,9 @@ mod tests {
code: 0, code: 0,
}) })
} else { } else {
Err(PackageError::CommandFailed("Mock remove failed".to_string())) Err(PackageError::CommandFailed(
"Mock remove failed".to_string(),
))
} }
} }
@ -502,7 +520,9 @@ mod tests {
code: 0, code: 0,
}) })
} else { } else {
Err(PackageError::CommandFailed("Mock update failed".to_string())) Err(PackageError::CommandFailed(
"Mock update failed".to_string(),
))
} }
} }
@ -516,7 +536,9 @@ mod tests {
code: 0, code: 0,
}) })
} else { } else {
Err(PackageError::CommandFailed("Mock upgrade failed".to_string())) Err(PackageError::CommandFailed(
"Mock upgrade failed".to_string(),
))
} }
} }
@ -525,16 +547,23 @@ mod tests {
if self.should_succeed { if self.should_succeed {
Ok(vec!["package1".to_string(), "package2".to_string()]) Ok(vec!["package1".to_string(), "package2".to_string()])
} else { } else {
Err(PackageError::CommandFailed("Mock list_installed failed".to_string())) Err(PackageError::CommandFailed(
"Mock list_installed failed".to_string(),
))
} }
} }
fn search(&self, query: &str) -> Result<Vec<String>, PackageError> { fn search(&self, query: &str) -> Result<Vec<String>, PackageError> {
*self.search_called.lock().unwrap() = true; *self.search_called.lock().unwrap() = true;
if self.should_succeed { if self.should_succeed {
Ok(vec![format!("result1-{}", query), format!("result2-{}", query)]) Ok(vec![
format!("result1-{}", query),
format!("result2-{}", query),
])
} else { } else {
Err(PackageError::CommandFailed("Mock search failed".to_string())) Err(PackageError::CommandFailed(
"Mock search failed".to_string(),
))
} }
} }
@ -543,7 +572,9 @@ mod tests {
if self.should_succeed { if self.should_succeed {
Ok(package == "installed-package") Ok(package == "installed-package")
} else { } else {
Err(PackageError::CommandFailed("Mock is_installed failed".to_string())) Err(PackageError::CommandFailed(
"Mock is_installed failed".to_string(),
))
} }
} }
} }
@ -551,6 +582,7 @@ mod tests {
// Custom PackHero for testing with a mock package manager // Custom PackHero for testing with a mock package manager
struct TestPackHero { struct TestPackHero {
platform: Platform, platform: Platform,
#[allow(dead_code)]
debug: bool, debug: bool,
mock_manager: MockPackageManager, mock_manager: MockPackageManager,
} }
@ -567,7 +599,9 @@ mod tests {
fn get_package_manager(&self) -> Result<&dyn PackageManager, PackageError> { fn get_package_manager(&self) -> Result<&dyn PackageManager, PackageError> {
match self.platform { match self.platform {
Platform::Ubuntu | Platform::MacOS => Ok(&self.mock_manager), Platform::Ubuntu | Platform::MacOS => Ok(&self.mock_manager),
Platform::Unknown => Err(PackageError::UnsupportedPlatform("Unsupported platform".to_string())), Platform::Unknown => Err(PackageError::UnsupportedPlatform(
"Unsupported platform".to_string(),
)),
} }
} }
@ -635,13 +669,19 @@ mod tests {
// Test list_installed // Test list_installed
let result = hero.list_installed(); let result = hero.list_installed();
assert!(result.is_ok()); assert!(result.is_ok());
assert_eq!(result.unwrap(), vec!["package1".to_string(), "package2".to_string()]); assert_eq!(
result.unwrap(),
vec!["package1".to_string(), "package2".to_string()]
);
assert!(*hero.mock_manager.list_installed_called.lock().unwrap()); assert!(*hero.mock_manager.list_installed_called.lock().unwrap());
// Test search // Test search
let result = hero.search("query"); let result = hero.search("query");
assert!(result.is_ok()); assert!(result.is_ok());
assert_eq!(result.unwrap(), vec!["result1-query".to_string(), "result2-query".to_string()]); assert_eq!(
result.unwrap(),
vec!["result1-query".to_string(), "result2-query".to_string()]
);
assert!(*hero.mock_manager.search_called.lock().unwrap()); assert!(*hero.mock_manager.search_called.lock().unwrap());
// Test is_installed // Test is_installed
@ -731,7 +771,10 @@ mod tests {
// Check if we're on Ubuntu // Check if we're on Ubuntu
let platform = Platform::detect(); let platform = Platform::detect();
if platform != Platform::Ubuntu { if platform != Platform::Ubuntu {
println!("Skipping real package operations test on non-Ubuntu platform: {:?}", platform); println!(
"Skipping real package operations test on non-Ubuntu platform: {:?}",
platform
);
return; return;
} }
@ -753,7 +796,10 @@ mod tests {
} }
}; };
println!("Package {} is installed before test: {}", test_package, is_installed_before); println!(
"Package {} is installed before test: {}",
test_package, is_installed_before
);
// If the package is already installed, we'll remove it first // If the package is already installed, we'll remove it first
if is_installed_before { if is_installed_before {
@ -775,9 +821,12 @@ mod tests {
} else { } else {
println!("Verified package {} was removed", test_package); println!("Verified package {} was removed", test_package);
} }
}, }
Err(e) => { Err(e) => {
println!("Error checking if package is installed after removal: {}", e); println!(
"Error checking if package is installed after removal: {}",
e
);
return; return;
} }
} }
@ -802,9 +851,12 @@ mod tests {
} else { } else {
println!("Verified package {} was installed", test_package); println!("Verified package {} was installed", test_package);
} }
}, }
Err(e) => { Err(e) => {
println!("Error checking if package is installed after installation: {}", e); println!(
"Error checking if package is installed after installation: {}",
e
);
return; return;
} }
} }
@ -814,8 +866,11 @@ mod tests {
match hero.search("wget") { match hero.search("wget") {
Ok(results) => { Ok(results) => {
println!("Search results: {:?}", results); println!("Search results: {:?}", results);
assert!(results.iter().any(|r| r.contains("wget")), "Search results should contain wget"); assert!(
}, results.iter().any(|r| r.contains("wget")),
"Search results should contain wget"
);
}
Err(e) => { Err(e) => {
println!("Error searching for packages: {}", e); println!("Error searching for packages: {}", e);
return; return;
@ -828,9 +883,12 @@ mod tests {
Ok(packages) => { Ok(packages) => {
println!("Found {} installed packages", packages.len()); println!("Found {} installed packages", packages.len());
// Check if our test package is in the list // Check if our test package is in the list
assert!(packages.iter().any(|p| p == test_package), assert!(
"Installed packages list should contain {}", test_package); packages.iter().any(|p| p == test_package),
}, "Installed packages list should contain {}",
test_package
);
}
Err(e) => { Err(e) => {
println!("Error listing installed packages: {}", e); println!("Error listing installed packages: {}", e);
return; return;
@ -857,9 +915,12 @@ mod tests {
} else { } else {
println!("Verified package {} was removed", test_package); println!("Verified package {} was removed", test_package);
} }
}, }
Err(e) => { Err(e) => {
println!("Error checking if package is installed after removal: {}", e); println!(
"Error checking if package is installed after removal: {}",
e
);
return; return;
} }
} }

View File

@ -1,7 +1,6 @@
use regex::Regex; use regex::Regex;
use std::fs; use std::fs;
use std::io::{self, Read, Seek, SeekFrom}; use std::io::{self, Read};
use std::path::Path; use std::path::Path;
/// Represents the type of replacement to perform. /// Represents the type of replacement to perform.
@ -75,7 +74,7 @@ impl TextReplacer {
pub fn replace_file_to<P1: AsRef<Path>, P2: AsRef<Path>>( pub fn replace_file_to<P1: AsRef<Path>, P2: AsRef<Path>>(
&self, &self,
input_path: P1, input_path: P1,
output_path: P2 output_path: P2,
) -> io::Result<()> { ) -> io::Result<()> {
let content = self.replace_file(&input_path)?; let content = self.replace_file(&input_path)?;
fs::write(output_path, content)?; fs::write(output_path, content)?;
@ -157,10 +156,8 @@ impl TextReplacerBuilder {
ReplaceMode::Literal(pattern) ReplaceMode::Literal(pattern)
}; };
self.operations.push(ReplacementOperation { self.operations
mode, .push(ReplacementOperation { mode, replacement });
replacement,
});
true true
} else { } else {
@ -187,7 +184,7 @@ impl TextReplacerBuilder {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use std::io::Write; use std::io::{Seek, SeekFrom, Write};
use tempfile::NamedTempFile; use tempfile::NamedTempFile;
#[test] #[test]

View File

@ -1,9 +1,9 @@
use std::collections::HashMap;
use super::{ use super::{
error::RfsError,
cmd::execute_rfs_command, cmd::execute_rfs_command,
error::RfsError,
types::{Mount, MountType, StoreSpec}, types::{Mount, MountType, StoreSpec},
}; };
use std::collections::HashMap;
/// Builder for RFS mount operations /// Builder for RFS mount operations
#[derive(Clone)] #[derive(Clone)]
@ -17,6 +17,7 @@ pub struct RfsBuilder {
/// Mount options /// Mount options
options: HashMap<String, String>, options: HashMap<String, String>,
/// Mount ID /// Mount ID
#[allow(dead_code)]
mount_id: Option<String>, mount_id: Option<String>,
/// Debug mode /// Debug mode
debug: bool, debug: bool,
@ -139,7 +140,11 @@ impl RfsBuilder {
source: self.source, source: self.source,
target: self.target, target: self.target,
fs_type: self.mount_type.to_string(), fs_type: self.mount_type.to_string(),
options: self.options.iter().map(|(k, v)| format!("{}={}", k, v)).collect(), options: self
.options
.iter()
.map(|(k, v)| format!("{}={}", k, v))
.collect(),
}) })
} }
@ -154,7 +159,10 @@ impl RfsBuilder {
// Check for errors // Check for errors
if !result.success { if !result.success {
return Err(RfsError::UnmountFailed(format!("Failed to unmount {}: {}", self.target, result.stderr))); return Err(RfsError::UnmountFailed(format!(
"Failed to unmount {}: {}",
self.target, result.stderr
)));
} }
Ok(()) Ok(())
@ -272,7 +280,10 @@ impl PackBuilder {
// Check for errors // Check for errors
if !result.success { if !result.success {
return Err(RfsError::PackFailed(format!("Failed to pack {}: {}", self.directory, result.stderr))); return Err(RfsError::PackFailed(format!(
"Failed to pack {}: {}",
self.directory, result.stderr
)));
} }
Ok(()) Ok(())

View File

@ -1,7 +1,7 @@
use crate::process::{run_command, CommandResult};
use super::error::RfsError; use super::error::RfsError;
use std::thread_local; use crate::process::{run_command, CommandResult};
use std::cell::RefCell; use std::cell::RefCell;
use std::thread_local;
// Thread-local storage for debug flag // Thread-local storage for debug flag
thread_local! { thread_local! {
@ -9,6 +9,7 @@ thread_local! {
} }
/// Set the thread-local debug flag /// Set the thread-local debug flag
#[allow(dead_code)]
pub fn set_thread_local_debug(debug: bool) { pub fn set_thread_local_debug(debug: bool) {
DEBUG.with(|d| { DEBUG.with(|d| {
*d.borrow_mut() = debug; *d.borrow_mut() = debug;
@ -17,9 +18,7 @@ pub fn set_thread_local_debug(debug: bool) {
/// Get the current thread-local debug flag /// Get the current thread-local debug flag
pub fn thread_local_debug() -> bool { pub fn thread_local_debug() -> bool {
DEBUG.with(|d| { DEBUG.with(|d| *d.borrow())
*d.borrow()
})
} }
/// Execute an RFS command with the given arguments /// Execute an RFS command with the given arguments