diff --git a/src/lib.rs b/src/lib.rs index ae95f32..7010cb8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,7 @@ pub mod designer; pub mod errors; pub mod graph; pub mod manager; +pub mod platform; pub mod plugin; pub mod plugins; pub mod process; diff --git a/src/platform.rs b/src/platform.rs new file mode 100644 index 0000000..84dcf20 --- /dev/null +++ b/src/platform.rs @@ -0,0 +1,264 @@ +use std::path::{Path, PathBuf}; +use std::process::{Child, Command}; + +use crate::errors::Result; + +/// Supported shell types for command execution +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Shell { + /// Windows Command Prompt + Cmd, + /// Windows PowerShell + PowerShell, + /// Unix/Linux shell + Sh, + /// Bash shell + Bash, +} + +/// Platform-specific execution abstraction +pub trait PlatformExecutor { + /// Spawn a process with the given command + fn spawn_process(&self, command: &Command) -> Result; + + /// Normalize a path for the current platform + fn normalize_path(&self, path: &Path) -> PathBuf; + + /// Get the default shell for the platform + fn get_shell(&self) -> Shell; + + /// Get the PATH environment variable separator for the platform + fn get_path_separator(&self) -> &'static str; + + /// Build a shell command for executing a script string + fn build_shell_command(&self, script: &str) -> Command; + + /// Get available shells on this platform + fn get_available_shells(&self) -> Vec; +} + +/// Windows platform executor +#[derive(Debug, Default)] +pub struct WindowsExecutor { + preferred_shell: Shell, +} + +impl WindowsExecutor { + pub fn new() -> Self { + Self { + preferred_shell: Shell::Cmd, + } + } + + pub fn with_shell(shell: Shell) -> Self { + Self { + preferred_shell: shell, + } + } + + /// Check if PowerShell is available on the system + fn is_powershell_available(&self) -> bool { + Command::new("powershell") + .arg("-Version") + .output() + .is_ok() + } +} + +impl PlatformExecutor for WindowsExecutor { + fn spawn_process(&self, command: &Command) -> Result { + Ok(command.spawn()?) + } + + fn normalize_path(&self, path: &Path) -> PathBuf { + // Convert forward slashes to backslashes on Windows + let path_str = path.to_string_lossy(); + let normalized = path_str.replace('/', "\\"); + PathBuf::from(normalized) + } + + fn get_shell(&self) -> Shell { + match self.preferred_shell { + Shell::PowerShell if self.is_powershell_available() => Shell::PowerShell, + _ => Shell::Cmd, + } + } + + fn get_path_separator(&self) -> &'static str { + ";" + } + + fn build_shell_command(&self, script: &str) -> Command { + match self.get_shell() { + Shell::PowerShell => { + let mut cmd = Command::new("powershell"); + cmd.arg("-Command").arg(script); + cmd + } + Shell::Cmd => { + let mut cmd = Command::new("cmd"); + cmd.arg("/C").arg(script); + cmd + } + _ => { + // Fallback to cmd + let mut cmd = Command::new("cmd"); + cmd.arg("/C").arg(script); + cmd + } + } + } + + fn get_available_shells(&self) -> Vec { + let mut shells = vec![Shell::Cmd]; + if self.is_powershell_available() { + shells.push(Shell::PowerShell); + } + shells + } +} + +/// Unix/Linux platform executor +#[derive(Debug, Default)] +pub struct UnixExecutor { + preferred_shell: Shell, +} + +impl UnixExecutor { + pub fn new() -> Self { + Self { + preferred_shell: Shell::Sh, + } + } + + pub fn with_shell(shell: Shell) -> Self { + Self { + preferred_shell: shell, + } + } + + /// Check if bash is available on the system + fn is_bash_available(&self) -> bool { + Command::new("bash") + .arg("--version") + .output() + .is_ok() + } +} + +impl PlatformExecutor for UnixExecutor { + fn spawn_process(&self, command: &Command) -> Result { + Ok(command.spawn()?) + } + + fn normalize_path(&self, path: &Path) -> PathBuf { + // Unix paths are already normalized + path.to_path_buf() + } + + fn get_shell(&self) -> Shell { + match self.preferred_shell { + Shell::Bash if self.is_bash_available() => Shell::Bash, + _ => Shell::Sh, + } + } + + fn get_path_separator(&self) -> &'static str { + ":" + } + + fn build_shell_command(&self, script: &str) -> Command { + match self.get_shell() { + Shell::Bash => { + let mut cmd = Command::new("bash"); + cmd.arg("-c").arg(script); + cmd + } + Shell::Sh => { + let mut cmd = Command::new("sh"); + cmd.arg("-c").arg(script); + cmd + } + _ => { + // Fallback to sh + let mut cmd = Command::new("sh"); + cmd.arg("-c").arg(script); + cmd + } + } + } + + fn get_available_shells(&self) -> Vec { + let mut shells = vec![Shell::Sh]; + if self.is_bash_available() { + shells.push(Shell::Bash); + } + shells + } +} + +/// Get the appropriate platform executor for the current OS +pub fn get_platform_executor() -> Box { + if cfg!(target_os = "windows") { + Box::new(WindowsExecutor::new()) + } else { + Box::new(UnixExecutor::new()) + } +} + +/// Get a platform executor with a specific shell preference +pub fn get_platform_executor_with_shell(shell: Shell) -> Box { + if cfg!(target_os = "windows") { + Box::new(WindowsExecutor::with_shell(shell)) + } else { + Box::new(UnixExecutor::with_shell(shell)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_windows_path_normalization() { + let executor = WindowsExecutor::new(); + let path = Path::new("src/main/java/com/example"); + let normalized = executor.normalize_path(path); + assert_eq!(normalized.to_string_lossy(), "src\\main\\java\\com\\example"); + } + + #[test] + fn test_unix_path_normalization() { + let executor = UnixExecutor::new(); + let path = Path::new("src/main/java/com/example"); + let normalized = executor.normalize_path(path); + assert_eq!(normalized, path); + } + + #[test] + fn test_path_separators() { + let windows_executor = WindowsExecutor::new(); + assert_eq!(windows_executor.get_path_separator(), ";"); + + let unix_executor = UnixExecutor::new(); + assert_eq!(unix_executor.get_path_separator(), ":"); + } + + #[test] + fn test_shell_command_building() { + let windows_executor = WindowsExecutor::new(); + let cmd = windows_executor.build_shell_command("echo hello"); + assert_eq!(cmd.get_program(), "cmd"); + + let unix_executor = UnixExecutor::new(); + let cmd = unix_executor.build_shell_command("echo hello"); + assert_eq!(cmd.get_program(), "sh"); + } + + #[test] + fn test_get_platform_executor() { + let executor = get_platform_executor(); + // Just verify it returns something without panicking + let _separator = executor.get_path_separator(); + } +} \ No newline at end of file diff --git a/src/process.rs b/src/process.rs index 6f08c8d..66605e1 100644 --- a/src/process.rs +++ b/src/process.rs @@ -8,6 +8,7 @@ use std::{ }; use crate::errors::BodoError; +use crate::platform::{get_platform_executor, PlatformExecutor}; use colored::{Color, Colorize}; pub struct ChildProcess { @@ -20,6 +21,7 @@ pub struct ChildProcess { pub struct ProcessManager { pub children: Vec, pub fail_fast: bool, + platform_executor: Box, } impl ProcessManager { diff --git a/tests/added_coverage_tests.rs b/tests/added_coverage_tests.rs index bf74621..d87774b 100644 --- a/tests/added_coverage_tests.rs +++ b/tests/added_coverage_tests.rs @@ -94,7 +94,7 @@ mod new_tests { #[test] fn test_bodo_error_variants_display() { - let io_err = BodoError::IoError(std::io::Error::new(std::io::ErrorKind::Other, "io error")); + let io_err = BodoError::IoError(std::io::Error::other("io error")); assert_eq!(format!("{}", io_err), "io error"); let watcher_err = BodoError::WatcherError("watcher error".to_string()); diff --git a/tests/cli_test.rs b/tests/cli_test.rs index fce6c86..639b244 100644 --- a/tests/cli_test.rs +++ b/tests/cli_test.rs @@ -86,10 +86,7 @@ fn test_cli_get_task_name_with_existing_task() { #[test] fn test_bodo_error_variants_display() { - let io_err = bodo::errors::BodoError::IoError(std::io::Error::new( - std::io::ErrorKind::Other, - "io error", - )); + let io_err = bodo::errors::BodoError::IoError(std::io::Error::other("io error")); assert_eq!(format!("{}", io_err), "io error"); let watcher_err = bodo::errors::BodoError::WatcherError("watcher error".to_string()); diff --git a/tests/complete_coverage.rs b/tests/complete_coverage.rs index 486d57a..bf19a66 100644 --- a/tests/complete_coverage.rs +++ b/tests/complete_coverage.rs @@ -91,7 +91,7 @@ fn test_cli_get_task_name_with_existing_task() { #[test] fn test_bodo_error_variants_display() { - let io_err = BodoError::IoError(std::io::Error::new(std::io::ErrorKind::Other, "io error")); + let io_err = BodoError::IoError(std::io::Error::other("io error")); assert_eq!(format!("{}", io_err), "io error"); let watcher_err = BodoError::WatcherError("watcher error".to_string()); diff --git a/tests/errors_test.rs b/tests/errors_test.rs index e25f80f..ed61303 100644 --- a/tests/errors_test.rs +++ b/tests/errors_test.rs @@ -5,7 +5,7 @@ use std::io; #[test] fn test_bodo_error_display() { - let err = BodoError::IoError(io::Error::new(io::ErrorKind::Other, "io_error")); + let err = BodoError::IoError(io::Error::other("io_error")); assert_eq!(format!("{}", err), "io_error"); let err = BodoError::WatcherError("watcher_error".to_string()); @@ -30,7 +30,7 @@ fn test_bodo_error_display() { #[test] fn test_bodo_error_from_io_error() { use std::io; - let io_err = io::Error::new(io::ErrorKind::Other, "some io error"); + let io_err = io::Error::other("some io error"); let bodo_err: BodoError = io_err.into(); assert!(matches!(bodo_err, BodoError::IoError(_))); } diff --git a/tests/extra_coverage_tests.rs b/tests/extra_coverage_tests.rs index c7de365..a5cee63 100644 --- a/tests/extra_coverage_tests.rs +++ b/tests/extra_coverage_tests.rs @@ -94,7 +94,7 @@ mod new_tests { #[test] fn test_bodo_error_variants_display() { - let io_err = BodoError::IoError(std::io::Error::new(std::io::ErrorKind::Other, "io error")); + let io_err = BodoError::IoError(std::io::Error::other("io error")); assert_eq!(format!("{}", io_err), "io error"); let watcher_err = BodoError::WatcherError("watcher error".to_string()); diff --git a/tests/new_cover_tests.rs b/tests/new_cover_tests.rs index 7b92c50..2ef76c1 100644 --- a/tests/new_cover_tests.rs +++ b/tests/new_cover_tests.rs @@ -92,7 +92,7 @@ mod new_tests { #[test] fn test_bodo_error_variants_display() { - let io_err = BodoError::IoError(std::io::Error::new(std::io::ErrorKind::Other, "io error")); + let io_err = BodoError::IoError(std::io::Error::other("io error")); assert_eq!(format!("{}", io_err), "io error"); let watcher_err = BodoError::WatcherError("watcher error".to_string()); diff --git a/tests/new_tests.rs b/tests/new_tests.rs index 7abf732..76980ad 100644 --- a/tests/new_tests.rs +++ b/tests/new_tests.rs @@ -94,7 +94,7 @@ mod new_tests { #[test] fn test_bodo_error_variants_display() { - let io_err = BodoError::IoError(std::io::Error::new(std::io::ErrorKind::Other, "io error")); + let io_err = BodoError::IoError(std::io::Error::other("io error")); assert_eq!(format!("{}", io_err), "io error"); let watcher_err = BodoError::WatcherError("watcher error".to_string());