Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
264 changes: 264 additions & 0 deletions src/platform.rs
Original file line number Diff line number Diff line change
@@ -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<Child>;

/// 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<Shell>;
}

/// 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<Child> {
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<Shell> {
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<Child> {
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<Shell> {
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<dyn PlatformExecutor> {
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<dyn PlatformExecutor> {
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();
}
}
2 changes: 2 additions & 0 deletions src/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use std::{
};

use crate::errors::BodoError;
use crate::platform::{get_platform_executor, PlatformExecutor};
use colored::{Color, Colorize};

pub struct ChildProcess {
Expand All @@ -20,6 +21,7 @@ pub struct ChildProcess {
pub struct ProcessManager {
pub children: Vec<ChildProcess>,
pub fail_fast: bool,
platform_executor: Box<dyn PlatformExecutor>,
}

impl ProcessManager {
Expand Down
2 changes: 1 addition & 1 deletion tests/added_coverage_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
5 changes: 1 addition & 4 deletions tests/cli_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
2 changes: 1 addition & 1 deletion tests/complete_coverage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
4 changes: 2 additions & 2 deletions tests/errors_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand All @@ -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(_)));
}
Expand Down
2 changes: 1 addition & 1 deletion tests/extra_coverage_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
2 changes: 1 addition & 1 deletion tests/new_cover_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
2 changes: 1 addition & 1 deletion tests/new_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down