diff --git a/Cargo.toml b/Cargo.toml index dcbcfd4..34002d6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ categories = ["os", "filesystem", "api-bindings"] readme = "README.md" [workspace] -members = [".", "vault", "git", "redisclient", "mycelium", "text", "os", "net", "zinit_client"] +members = [".", "vault", "git", "redisclient", "mycelium", "text", "os", "net", "zinit_client", "process"] [dependencies] hex = "0.4" @@ -66,6 +66,7 @@ sal-text = { path = "text" } sal-os = { path = "os" } sal-net = { path = "net" } sal-zinit-client = { path = "zinit_client" } +sal-process = { path = "process" } # Optional features for specific OS functionality [target.'cfg(unix)'.dependencies] diff --git a/MONOREPO_CONVERSION_PLAN.md b/MONOREPO_CONVERSION_PLAN.md index 49f24b9..4e348a4 100644 --- a/MONOREPO_CONVERSION_PLAN.md +++ b/MONOREPO_CONVERSION_PLAN.md @@ -168,7 +168,7 @@ Convert packages in dependency order (leaf packages first): - ✅ **Production features**: Global client management, async operations, comprehensive error handling - ✅ **Quality assurance**: All meaningless assertions replaced with meaningful validations - ✅ **Integration verified**: Herodo integration and test suite integration confirmed -- [ ] **process** → sal-process (depends on text) +- [x] **process** → sal-process (depends on text) #### 3.3 Higher-level Packages - [ ] **virt** → sal-virt (depends on process, os) diff --git a/mycelium/README.md b/mycelium/README.md index 610b8b8..d034b99 100644 --- a/mycelium/README.md +++ b/mycelium/README.md @@ -108,7 +108,3 @@ cargo test -- --nocapture - `base64` - Message encoding - `tokio` - Async runtime - `rhai` - Scripting support - -## License - -Apache-2.0 diff --git a/os/README.md b/os/README.md index 6f5afc6..b42c274 100644 --- a/os/README.md +++ b/os/README.md @@ -98,7 +98,3 @@ if is_linux() { print("Running on Linux"); } ``` - -## License - -Licensed under the Apache License, Version 2.0. diff --git a/process/Cargo.toml b/process/Cargo.toml new file mode 100644 index 0000000..dbe63d4 --- /dev/null +++ b/process/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "sal-process" +version = "0.1.0" +edition = "2021" +authors = ["PlanetFirst "] +description = "SAL Process - Cross-platform process management and command execution" +repository = "https://git.threefold.info/herocode/sal" +license = "Apache-2.0" + +[dependencies] +# Core dependencies for process management +tempfile = "3.5" +rhai = { version = "1.12.0", features = ["sync"] } +anyhow = "1.0.98" + +# SAL dependencies +sal-text = { path = "../text" } + +# Optional features for specific OS functionality +[target.'cfg(unix)'.dependencies] +nix = "0.30.1" + +[target.'cfg(windows)'.dependencies] +windows = { version = "0.61.1", features = [ + "Win32_Foundation", + "Win32_System_Threading", + "Win32_Storage_FileSystem", +] } + +[dev-dependencies] +tempfile = "3.5" diff --git a/process/README.md b/process/README.md new file mode 100644 index 0000000..f313587 --- /dev/null +++ b/process/README.md @@ -0,0 +1,178 @@ +# SAL Process Package + +The `sal-process` package provides comprehensive functionality for managing and interacting with system processes across different platforms (Windows, macOS, and Linux). + +## Features + +- **Command Execution**: Run commands and scripts with flexible options +- **Process Management**: List, find, and kill processes +- **Cross-Platform**: Works consistently across Windows, macOS, and Linux +- **Builder Pattern**: Fluent API for configuring command execution +- **Rhai Integration**: Full support for Rhai scripting language +- **Error Handling**: Comprehensive error types and handling + +## Installation + +Add this to your `Cargo.toml`: + +```toml +[dependencies] +sal-process = { path = "../process" } +``` + +## Usage + +### Basic Command Execution + +```rust +use sal_process::{run_command, run_silent}; + +// Run a command and capture output +let result = run_command("echo hello world")?; +println!("Output: {}", result.stdout); + +// Run a command silently +let result = run_silent("ls -la")?; +``` + +### Builder Pattern + +```rust +use sal_process::run; + +// Use the builder pattern for more control +let result = run("echo test") + .silent(true) + .die(false) + .log(true) + .execute()?; +``` + +### Process Management + +```rust +use sal_process::{which, process_list, process_get, kill}; + +// Check if a command exists +if let Some(path) = which("git") { + println!("Git found at: {}", path); +} + +// List all processes +let processes = process_list("")?; +println!("Found {} processes", processes.len()); + +// Find processes by pattern +let chrome_processes = process_list("chrome")?; + +// Get a single process (errors if 0 or >1 matches) +let process = process_get("unique_process_name")?; + +// Kill processes by pattern +kill("old_server")?; +``` + +### Multiline Scripts + +```rust +let script = r#" + echo "Starting script" + export VAR="test" + echo "Variable: $VAR" + echo "Script complete" +"#; + +let result = run_command(script)?; +``` + +## Rhai Integration + +The package provides full Rhai integration for scripting: + +```rhai +// Basic command execution +let result = run_command("echo hello"); +print(result.stdout); + +// Builder pattern +let result = run("echo test") + .silent() + .ignore_error() + .execute(); + +// Process management +let git_path = which("git"); +if git_path != () { + print(`Git found at: ${git_path}`); +} + +let processes = process_list("chrome"); +print(`Found ${processes.len()} Chrome processes`); +``` + +## Error Handling + +The package provides comprehensive error handling: + +```rust +use sal_process::{run, RunError}; + +match run("some_command").execute() { + Ok(result) => { + if result.success { + println!("Command succeeded: {}", result.stdout); + } else { + println!("Command failed with code: {}", result.code); + } + } + Err(RunError::CommandExecutionFailed(e)) => { + eprintln!("Failed to execute command: {}", e); + } + Err(e) => { + eprintln!("Other error: {}", e); + } +} +``` + +## Builder Options + +The `run()` function returns a builder with these options: + +- `.silent(bool)`: Suppress output to stdout/stderr +- `.die(bool)`: Return error if command fails (default: true) +- `.log(bool)`: Log command execution +- `.async_exec(bool)`: Run command asynchronously + +## Cross-Platform Support + +The package handles platform differences automatically: + +- **Windows**: Uses `cmd.exe` for script execution +- **Unix-like**: Uses `/bin/bash` with `-e` flag for error handling +- **Process listing**: Uses appropriate tools (`wmic` on Windows, `ps` on Unix) +- **Command detection**: Uses `where` on Windows, `which` on Unix + +## Testing + +Run the test suite: + +```bash +cargo test +``` + +The package includes comprehensive tests: +- Unit tests for all functionality +- Integration tests for real-world scenarios +- Rhai script tests for scripting integration +- Cross-platform compatibility tests + +## Dependencies + +- `tempfile`: For temporary script file creation +- `rhai`: For Rhai scripting integration +- `anyhow`: For error handling +- `sal-text`: For text processing utilities + +Platform-specific dependencies: +- `nix` (Unix): For Unix-specific process operations +- `windows` (Windows): For Windows-specific process operations diff --git a/process/src/lib.rs b/process/src/lib.rs new file mode 100644 index 0000000..bf64493 --- /dev/null +++ b/process/src/lib.rs @@ -0,0 +1,22 @@ +//! # SAL Process Package +//! +//! The `sal-process` package provides functionality for managing and interacting with +//! system processes across different platforms. It includes capabilities for: +//! +//! - Running commands and scripts +//! - Listing and filtering processes +//! - Killing processes +//! - Checking for command existence +//! - Screen session management +//! +//! This package is designed to work consistently across Windows, macOS, and Linux. + +mod run; +mod mgmt; +mod screen; + +pub mod rhai; + +pub use run::*; +pub use mgmt::*; +pub use screen::{new as new_screen, kill as kill_screen}; diff --git a/src/process/mgmt.rs b/process/src/mgmt.rs similarity index 98% rename from src/process/mgmt.rs rename to process/src/mgmt.rs index a4e7a9e..e294a29 100644 --- a/src/process/mgmt.rs +++ b/process/src/mgmt.rs @@ -72,7 +72,7 @@ pub struct ProcessInfo { * # Examples * * ``` - * use sal::process::which; + * use sal_process::which; * * match which("git") { * Some(path) => println!("Git is installed at: {}", path), @@ -118,7 +118,7 @@ pub fn which(cmd: &str) -> Option { * * ``` * // Kill all processes with "server" in their name - * use sal::process::kill; + * use sal_process::kill; * * fn main() -> Result<(), Box> { * let result = kill("server")?; @@ -210,7 +210,7 @@ pub fn kill(pattern: &str) -> Result { * * ``` * // List all processes - * use sal::process::process_list; + * use sal_process::process_list; * * fn main() -> Result<(), Box> { * let processes = process_list("")?; @@ -328,7 +328,7 @@ pub fn process_list(pattern: &str) -> Result, ProcessError> { * # Examples * * ```no_run - * use sal::process::process_get; + * use sal_process::process_get; * * fn main() -> Result<(), Box> { * let process = process_get("unique-server-name")?; diff --git a/src/rhai/process.rs b/process/src/rhai.rs similarity index 98% rename from src/rhai/process.rs rename to process/src/rhai.rs index 7e25b23..441448f 100644 --- a/src/rhai/process.rs +++ b/process/src/rhai.rs @@ -2,7 +2,7 @@ //! //! This module provides Rhai wrappers for the functions in the Process module. -use crate::process::{self, CommandResult, ProcessError, ProcessInfo, RunError }; +use crate::{self as process, CommandResult, ProcessError, ProcessInfo, RunError}; use rhai::{Array, Dynamic, Engine, EvalAltResult, Map}; use std::clone::Clone; diff --git a/src/process/run.rs b/process/src/run.rs similarity index 90% rename from src/process/run.rs rename to process/src/run.rs index afd4782..ea68823 100644 --- a/src/process/run.rs +++ b/process/src/run.rs @@ -1,13 +1,13 @@ -use std::io::{BufRead, BufReader, Write}; +use std::error::Error; +use std::fmt; use std::fs::{self, File}; +use std::io; +use std::io::{BufRead, BufReader, Write}; use std::path::{Path, PathBuf}; use std::process::{Child, Command, Output, Stdio}; -use std::fmt; -use std::error::Error; -use std::io; use std::thread; -use crate::text; +use sal_text; /// Error type for command and script execution operations #[derive(Debug)] @@ -41,7 +41,9 @@ impl fmt::Display for RunError { RunError::CommandFailed(e) => write!(f, "{}", e), RunError::ScriptPreparationFailed(e) => write!(f, "{}", e), RunError::ChildProcessError(e) => write!(f, "{}", e), - RunError::TempDirCreationFailed(e) => write!(f, "Failed to create temporary directory: {}", e), + RunError::TempDirCreationFailed(e) => { + write!(f, "Failed to create temporary directory: {}", e) + } RunError::FileCreationFailed(e) => write!(f, "Failed to create script file: {}", e), RunError::FileWriteFailed(e) => write!(f, "Failed to write to script file: {}", e), RunError::PermissionError(e) => write!(f, "Failed to set file permissions: {}", e), @@ -73,38 +75,30 @@ pub struct CommandResult { } impl CommandResult { - /// Create a default failed result with an error message - fn _error(message: &str) -> Self { - Self { - stdout: String::new(), - stderr: message.to_string(), - success: false, - code: -1, - } - } + // Implementation methods can be added here as needed } /// Prepare a script file and return the path and interpreter -fn prepare_script_file(script_content: &str) -> Result<(PathBuf, String, tempfile::TempDir), RunError> { +fn prepare_script_file( + script_content: &str, +) -> Result<(PathBuf, String, tempfile::TempDir), RunError> { // Dedent the script - let dedented = text::dedent(script_content); - + let dedented = sal_text::dedent(script_content); + // Create a temporary directory - let temp_dir = tempfile::tempdir() - .map_err(RunError::TempDirCreationFailed)?; - + let temp_dir = tempfile::tempdir().map_err(RunError::TempDirCreationFailed)?; + // Determine script extension and interpreter #[cfg(target_os = "windows")] let (ext, interpreter) = (".bat", "cmd.exe".to_string()); - + #[cfg(any(target_os = "macos", target_os = "linux"))] let (ext, interpreter) = (".sh", "/bin/bash".to_string()); - + // Create the script file let script_path = temp_dir.path().join(format!("script{}", ext)); - let mut file = File::create(&script_path) - .map_err(RunError::FileCreationFailed)?; - + let mut file = File::create(&script_path).map_err(RunError::FileCreationFailed)?; + // For Unix systems, ensure the script has a shebang line with -e flag #[cfg(any(target_os = "macos", target_os = "linux"))] { @@ -115,19 +109,19 @@ fn prepare_script_file(script_content: &str) -> Result<(PathBuf, String, tempfil // Add shebang with -e flag to ensure script fails on errors format!("#!/bin/bash -e\n{}", dedented) }; - + // Write the script content with shebang file.write_all(script_with_shebang.as_bytes()) .map_err(RunError::FileWriteFailed)?; } - + // For Windows, just write the script as is #[cfg(target_os = "windows")] { file.write_all(dedented.as_bytes()) .map_err(RunError::FileWriteFailed)?; } - + // Make the script executable (Unix only) #[cfg(any(target_os = "macos", target_os = "linux"))] { @@ -136,10 +130,9 @@ fn prepare_script_file(script_content: &str) -> Result<(PathBuf, String, tempfil .map_err(|e| RunError::PermissionError(e))? .permissions(); perms.set_mode(0o755); // rwxr-xr-x - fs::set_permissions(&script_path, perms) - .map_err(RunError::PermissionError)?; + fs::set_permissions(&script_path, perms).map_err(RunError::PermissionError)?; } - + Ok((script_path, interpreter, temp_dir)) } @@ -148,7 +141,7 @@ fn handle_child_output(mut child: Child, silent: bool) -> Result Result Result Result Result) -> Result { +fn process_command_output( + output: Result, +) -> Result { match output { Ok(out) => { let stdout = String::from_utf8_lossy(&out.stdout).to_string(); let stderr = String::from_utf8_lossy(&out.stderr).to_string(); // We'll collect stderr but not print it here // It will be included in the error message if the command fails - + // If the command failed, print a clear error message if !out.status.success() { - eprintln!("\x1b[31mCommand failed with exit code: {}\x1b[0m", - out.status.code().unwrap_or(-1)); + eprintln!( + "\x1b[31mCommand failed with exit code: {}\x1b[0m", + out.status.code().unwrap_or(-1) + ); } - + Ok(CommandResult { stdout, stderr, success: out.status.success(), code: out.status.code().unwrap_or(-1), }) - }, + } Err(e) => Err(RunError::CommandExecutionFailed(e)), } } @@ -278,26 +276,28 @@ fn run_command_internal(command: &str, silent: bool) -> Result Result { +fn execute_script_internal( + interpreter: &str, + script_path: &Path, + silent: bool, +) -> Result { #[cfg(target_os = "windows")] let command_args = vec!["/c", script_path.to_str().unwrap_or("")]; - + #[cfg(any(target_os = "macos", target_os = "linux"))] let command_args = vec!["-e", script_path.to_str().unwrap_or("")]; - + if silent { // For silent execution, use output() which captures but doesn't display - let output = Command::new(interpreter) - .args(&command_args) - .output(); - + let output = Command::new(interpreter).args(&command_args).output(); + let result = process_command_output(output)?; - + // If the script failed, return an error if !result.success { return Err(RunError::CommandFailed(format!( @@ -306,7 +306,7 @@ fn execute_script_internal(interpreter: &str, script_path: &Path, silent: bool) result.stderr.trim() ))); } - + Ok(result) } else { // For normal execution, spawn and handle the output streams @@ -316,9 +316,9 @@ fn execute_script_internal(interpreter: &str, script_path: &Path, silent: bool) .stderr(Stdio::piped()) .spawn() .map_err(RunError::CommandExecutionFailed)?; - + let result = handle_child_output(child, false)?; - + // If the script failed, return an error if !result.success { return Err(RunError::CommandFailed(format!( @@ -327,7 +327,7 @@ fn execute_script_internal(interpreter: &str, script_path: &Path, silent: bool) result.stderr.trim() ))); } - + Ok(result) } } @@ -336,11 +336,11 @@ fn execute_script_internal(interpreter: &str, script_path: &Path, silent: bool) fn run_script_internal(script: &str, silent: bool) -> Result { // Prepare the script file first to get the content with shebang let (script_path, interpreter, _temp_dir) = prepare_script_file(script)?; - + // Print the script being executed if not silent if !silent { println!("\x1b[36mExecuting script:\x1b[0m"); - + // Read the script file to get the content with shebang if let Ok(script_content) = fs::read_to_string(&script_path) { for (i, line) in script_content.lines().enumerate() { @@ -352,16 +352,16 @@ fn run_script_internal(script: &str, silent: bool) -> Result Result RunBuilder<'a> { /// Execute the command or script with the configured options pub fn execute(self) -> Result { let trimmed = self.cmd.trim(); - + // Log command execution if enabled if self.log { println!("\x1b[36m[LOG] Executing command: {}\x1b[0m", trimmed); } - + // Handle async execution if self.async_exec { let cmd_copy = trimmed.to_string(); let silent = self.silent; let log = self.log; - + // Spawn a thread to run the command asynchronously thread::spawn(move || { if log { println!("\x1b[36m[ASYNC] Starting execution\x1b[0m"); } - + let result = if cmd_copy.contains('\n') { run_script_internal(&cmd_copy, silent) } else { run_command_internal(&cmd_copy, silent) }; - + if log { match &result { Ok(res) => { if res.success { println!("\x1b[32m[ASYNC] Command completed successfully\x1b[0m"); } else { - eprintln!("\x1b[31m[ASYNC] Command failed with exit code: {}\x1b[0m", res.code); + eprintln!( + "\x1b[31m[ASYNC] Command failed with exit code: {}\x1b[0m", + res.code + ); } - }, + } Err(e) => { eprintln!("\x1b[31m[ASYNC] Command failed with error: {}\x1b[0m", e); } } } }); - + // Return a placeholder result for async execution return Ok(CommandResult { stdout: String::new(), @@ -474,7 +477,7 @@ impl<'a> RunBuilder<'a> { code: 0, }); } - + // Execute the command or script let result = if trimmed.contains('\n') { // This is a multiline script @@ -483,7 +486,7 @@ impl<'a> RunBuilder<'a> { // This is a single command run_command_internal(trimmed, self.silent) }; - + // Handle die=false: convert errors to CommandResult with success=false match result { Ok(res) => { @@ -492,14 +495,14 @@ impl<'a> RunBuilder<'a> { eprintln!("\x1b[33mWarning: Command failed with exit code {} but 'die' is false\x1b[0m", res.code); } Ok(res) - }, + } Err(e) => { // Print the error only if it's not a CommandFailed error // (which would already have printed the stderr) if !matches!(e, RunError::CommandFailed(_)) { eprintln!("\x1b[31mCommand error: {}\x1b[0m", e); } - + if self.die { Err(e) } else { diff --git a/src/process/screen.rs b/process/src/screen.rs similarity index 97% rename from src/process/screen.rs rename to process/src/screen.rs index b8091e3..9a72214 100644 --- a/src/process/screen.rs +++ b/process/src/screen.rs @@ -1,4 +1,4 @@ -use crate::process::run_command; +use crate::run_command; use anyhow::Result; use std::fs; @@ -46,4 +46,4 @@ pub fn kill(name: &str) -> Result<()> { run_command(&cmd)?; std::thread::sleep(std::time::Duration::from_millis(500)); Ok(()) -} \ No newline at end of file +} diff --git a/process/tests/mgmt_tests.rs b/process/tests/mgmt_tests.rs new file mode 100644 index 0000000..1482d1a --- /dev/null +++ b/process/tests/mgmt_tests.rs @@ -0,0 +1,278 @@ +use sal_process::{kill, process_get, process_list, which, ProcessError}; + +#[test] +fn test_which_existing_command() { + // Test with a command that should exist on all systems + #[cfg(target_os = "windows")] + let cmd = "cmd"; + + #[cfg(not(target_os = "windows"))] + let cmd = "sh"; + + let result = which(cmd); + assert!(result.is_some()); + assert!(!result.unwrap().is_empty()); +} + +#[test] +fn test_which_nonexistent_command() { + let result = which("nonexistent_command_12345"); + assert!(result.is_none()); +} + +#[test] +fn test_which_common_commands() { + // Test common commands that should exist + let common_commands = if cfg!(target_os = "windows") { + vec!["cmd", "powershell"] + } else { + vec!["sh", "ls", "echo"] + }; + + for cmd in common_commands { + let result = which(cmd); + assert!(result.is_some(), "Command '{}' should be found", cmd); + assert!(!result.unwrap().is_empty()); + } +} + +#[test] +fn test_process_list_all() { + let result = process_list("").unwrap(); + assert!( + !result.is_empty(), + "Should find at least one running process" + ); + + // Verify process info structure + let first_process = &result[0]; + assert!(first_process.pid > 0, "Process PID should be positive"); + assert!( + !first_process.name.is_empty(), + "Process name should not be empty" + ); +} + +#[test] +fn test_process_list_with_pattern() { + // Try to find processes with common names + let patterns = if cfg!(target_os = "windows") { + vec!["explorer", "winlogon", "System"] + } else { + vec!["init", "kernel", "systemd"] + }; + + let mut found_any = false; + for pattern in patterns { + if let Ok(processes) = process_list(pattern) { + if !processes.is_empty() { + found_any = true; + for process in processes { + assert!( + process.name.contains(pattern) + || process + .name + .to_lowercase() + .contains(&pattern.to_lowercase()) + ); + assert!(process.pid > 0); + } + break; + } + } + } + + // At least one pattern should match some processes + assert!( + found_any, + "Should find at least one process with common patterns" + ); +} + +#[test] +fn test_process_list_nonexistent_pattern() { + let result = process_list("nonexistent_process_12345").unwrap(); + assert!( + result.is_empty(), + "Should not find any processes with nonexistent pattern" + ); +} + +#[test] +fn test_process_info_structure() { + let processes = process_list("").unwrap(); + assert!(!processes.is_empty()); + + let process = &processes[0]; + + // Test ProcessInfo fields + assert!(process.pid > 0); + assert!(!process.name.is_empty()); + // memory and cpu are placeholders, so we just check they exist + assert!(process.memory >= 0.0); + assert!(process.cpu >= 0.0); +} + +#[test] +fn test_process_get_single_match() { + // Find a process that should be unique + let processes = process_list("").unwrap(); + assert!(!processes.is_empty()); + + // Try to find a process with a unique enough name + let mut unique_process = None; + for process in &processes { + let matches = process_list(&process.name).unwrap(); + if matches.len() == 1 { + unique_process = Some(process.clone()); + break; + } + } + + if let Some(process) = unique_process { + let result = process_get(&process.name).unwrap(); + assert_eq!(result.pid, process.pid); + assert_eq!(result.name, process.name); + } +} + +#[test] +fn test_process_get_no_match() { + let result = process_get("nonexistent_process_12345"); + assert!(result.is_err()); + match result.unwrap_err() { + ProcessError::NoProcessFound(pattern) => { + assert_eq!(pattern, "nonexistent_process_12345"); + } + _ => panic!("Expected NoProcessFound error"), + } +} + +#[test] +fn test_process_get_multiple_matches() { + // Find a pattern that matches multiple processes + let all_processes = process_list("").unwrap(); + assert!(!all_processes.is_empty()); + + // Try common patterns that might match multiple processes + let patterns = if cfg!(target_os = "windows") { + vec!["svchost", "conhost"] + } else { + vec!["kthread", "ksoftirqd"] + }; + + let mut _found_multiple = false; + for pattern in patterns { + if let Ok(processes) = process_list(pattern) { + if processes.len() > 1 { + let result = process_get(pattern); + assert!(result.is_err()); + match result.unwrap_err() { + ProcessError::MultipleProcessesFound(p, count) => { + assert_eq!(p, pattern); + assert_eq!(count, processes.len()); + _found_multiple = true; + break; + } + _ => panic!("Expected MultipleProcessesFound error"), + } + } + } + } + + // If we can't find multiple matches with common patterns, that's okay + // The test validates the error handling works correctly +} + +#[test] +fn test_kill_nonexistent_process() { + let result = kill("nonexistent_process_12345").unwrap(); + assert!(result.contains("No matching processes") || result.contains("Successfully killed")); +} + +#[test] +fn test_process_list_performance() { + use std::time::Instant; + + let start = Instant::now(); + let _processes = process_list("").unwrap(); + let duration = start.elapsed(); + + // Process listing should complete within reasonable time (5 seconds) + assert!( + duration.as_secs() < 5, + "Process listing took too long: {:?}", + duration + ); +} + +#[test] +fn test_which_performance() { + use std::time::Instant; + + let start = Instant::now(); + let _result = which("echo"); + let duration = start.elapsed(); + + // Which command should be very fast (1 second) + assert!( + duration.as_secs() < 1, + "Which command took too long: {:?}", + duration + ); +} + +#[test] +fn test_process_list_filtering_accuracy() { + // Test that filtering actually works correctly + let all_processes = process_list("").unwrap(); + assert!(!all_processes.is_empty()); + + // Pick a process name and filter by it + let test_process = &all_processes[0]; + let filtered_processes = process_list(&test_process.name).unwrap(); + + // All filtered processes should contain the pattern + for process in filtered_processes { + assert!(process.name.contains(&test_process.name)); + } +} + +#[test] +fn test_process_error_display() { + let error = ProcessError::NoProcessFound("test".to_string()); + let error_string = format!("{}", error); + assert!(error_string.contains("No processes found matching 'test'")); + + let error = ProcessError::MultipleProcessesFound("test".to_string(), 5); + let error_string = format!("{}", error); + assert!(error_string.contains("Multiple processes (5) found matching 'test'")); +} + +#[test] +fn test_cross_platform_process_operations() { + // Test operations that should work on all platforms + + // Test which with platform-specific commands + #[cfg(target_os = "windows")] + { + assert!(which("cmd").is_some()); + assert!(which("notepad").is_some()); + } + + #[cfg(target_os = "macos")] + { + assert!(which("sh").is_some()); + assert!(which("ls").is_some()); + } + + #[cfg(target_os = "linux")] + { + assert!(which("sh").is_some()); + assert!(which("ls").is_some()); + } + + // Test process listing works on all platforms + let processes = process_list("").unwrap(); + assert!(!processes.is_empty()); +} diff --git a/process/tests/rhai/01_command_execution.rhai b/process/tests/rhai/01_command_execution.rhai new file mode 100644 index 0000000..94dead5 --- /dev/null +++ b/process/tests/rhai/01_command_execution.rhai @@ -0,0 +1,119 @@ +// Test script for process command execution functionality + +print("=== Process Command Execution Tests ==="); + +// Test 1: Basic command execution +print("\n--- Test 1: Basic Command Execution ---"); +let result = run_command("echo hello world"); +assert_true(result.success, "Command should succeed"); +assert_true(result.code == 0, "Exit code should be 0"); +assert_true(result.stdout.contains("hello world"), "Output should contain 'hello world'"); +print("✓ Basic command execution works"); + +// Test 2: Silent command execution +print("\n--- Test 2: Silent Command Execution ---"); +let silent_result = run_silent("echo silent test"); +assert_true(silent_result.success, "Silent command should succeed"); +assert_true(silent_result.stdout.contains("silent test"), "Silent output should be captured"); +print("✓ Silent command execution works"); + +// Test 3: Builder pattern +print("\n--- Test 3: Builder Pattern ---"); +let builder_result = run("echo builder pattern").silent().execute(); +assert_true(builder_result.success, "Builder command should succeed"); +assert_true(builder_result.stdout.contains("builder pattern"), "Builder output should be captured"); +print("✓ Builder pattern works"); + +// Test 4: Error handling with die=false +print("\n--- Test 4: Error Handling (ignore_error) ---"); +let error_result = run("false").ignore_error().silent().execute(); +assert_true(!error_result.success, "Command should fail"); +assert_true(error_result.code != 0, "Exit code should be non-zero"); +print("✓ Error handling with ignore_error works"); + +// Test 5: Multiline script execution +print("\n--- Test 5: Multiline Script Execution ---"); +let script = ` + echo "Line 1" + echo "Line 2" + echo "Line 3" +`; +let script_result = run_command(script); +assert_true(script_result.success, "Script should succeed"); +assert_true(script_result.stdout.contains("Line 1"), "Should contain Line 1"); +assert_true(script_result.stdout.contains("Line 2"), "Should contain Line 2"); +assert_true(script_result.stdout.contains("Line 3"), "Should contain Line 3"); +print("✓ Multiline script execution works"); + +// Test 6: Command with arguments +print("\n--- Test 6: Command with Arguments ---"); +let args_result = run_command("echo arg1 arg2 arg3"); +assert_true(args_result.success, "Command with args should succeed"); +assert_true(args_result.stdout.contains("arg1 arg2 arg3"), "Should contain all arguments"); +print("✓ Command with arguments works"); + +// Test 7: Builder with logging +print("\n--- Test 7: Builder with Logging ---"); +let log_result = run("echo log test").log().silent().execute(); +assert_true(log_result.success, "Logged command should succeed"); +assert_true(log_result.stdout.contains("log test"), "Logged output should be captured"); +print("✓ Builder with logging works"); + +// Test 8: Run with options map +print("\n--- Test 8: Run with Options Map ---"); +let options = #{ + silent: true, + die: false, + log: false +}; +let options_result = run("echo options test", options); +assert_true(options_result.success, "Options command should succeed"); +assert_true(options_result.stdout.contains("options test"), "Options output should be captured"); +print("✓ Run with options map works"); + +// Test 9: Complex script with variables +print("\n--- Test 9: Complex Script with Variables ---"); +let var_script = ` + VAR="test_variable" + echo "Variable value: $VAR" +`; +let var_result = run_command(var_script); +assert_true(var_result.success, "Variable script should succeed"); +assert_true(var_result.stdout.contains("Variable value: test_variable"), "Should expand variables"); +print("✓ Complex script with variables works"); + +// Test 10: Script with conditionals +print("\n--- Test 10: Script with Conditionals ---"); +let cond_script = ` + if [ "hello" = "hello" ]; then + echo "Condition passed" + else + echo "Condition failed" + fi +`; +let cond_result = run_command(cond_script); +assert_true(cond_result.success, "Conditional script should succeed"); +assert_true(cond_result.stdout.contains("Condition passed"), "Condition should pass"); +print("✓ Script with conditionals works"); + +// Test 11: Builder method chaining +print("\n--- Test 11: Builder Method Chaining ---"); +let chain_result = run("echo chaining test") + .silent() + .ignore_error() + .log() + .execute(); +assert_true(chain_result.success, "Chained command should succeed"); +assert_true(chain_result.stdout.contains("chaining test"), "Chained output should be captured"); +print("✓ Builder method chaining works"); + +// Test 12: CommandResult properties +print("\n--- Test 12: CommandResult Properties ---"); +let prop_result = run_command("echo property test"); +assert_true(prop_result.success, "Property test command should succeed"); +assert_true(prop_result.code == 0, "Exit code property should be 0"); +assert_true(prop_result.stdout.len() > 0, "Stdout property should not be empty"); +assert_true(prop_result.stderr.len() >= 0, "Stderr property should exist"); +print("✓ CommandResult properties work"); + +print("\n=== All Command Execution Tests Passed! ==="); diff --git a/process/tests/rhai/02_process_management.rhai b/process/tests/rhai/02_process_management.rhai new file mode 100644 index 0000000..1f7dbf8 --- /dev/null +++ b/process/tests/rhai/02_process_management.rhai @@ -0,0 +1,153 @@ +// Test script for process management functionality + +print("=== Process Management Tests ==="); + +// Test 1: which function with existing command +print("\n--- Test 1: Which Function (Existing Command) ---"); +let echo_path = which("echo"); +if echo_path != () { + assert_true(echo_path.len() > 0, "Echo path should not be empty"); + print(`✓ which("echo") found at: ${echo_path}`); +} else { + // Try platform-specific commands + let cmd_path = which("cmd"); + let sh_path = which("sh"); + assert_true(cmd_path != () || sh_path != (), "Should find either cmd or sh"); + print("✓ which() function works with platform-specific commands"); +} + +// Test 2: which function with nonexistent command +print("\n--- Test 2: Which Function (Nonexistent Command) ---"); +let nonexistent = which("nonexistent_command_12345"); +assert_true(nonexistent == (), "Nonexistent command should return ()"); +print("✓ which() correctly handles nonexistent commands"); + +// Test 3: process_list function +print("\n--- Test 3: Process List Function ---"); +let all_processes = process_list(""); +assert_true(all_processes.len() > 0, "Should find at least one running process"); +print(`✓ process_list("") found ${all_processes.len()} processes`); + +// Test 4: process info properties +print("\n--- Test 4: Process Info Properties ---"); +if all_processes.len() > 0 { + let first_process = all_processes[0]; + assert_true(first_process.pid > 0, "Process PID should be positive"); + assert_true(first_process.name.len() > 0, "Process name should not be empty"); + assert_true(first_process.memory >= 0.0, "Process memory should be non-negative"); + assert_true(first_process.cpu >= 0.0, "Process CPU should be non-negative"); + print(`✓ Process properties: PID=${first_process.pid}, Name=${first_process.name}`); +} + +// Test 5: process_list with pattern +print("\n--- Test 5: Process List with Pattern ---"); +if all_processes.len() > 0 { + let test_process = all_processes[0]; + let filtered_processes = process_list(test_process.name); + assert_true(filtered_processes.len() >= 1, "Should find at least the test process"); + + // Verify all filtered processes contain the pattern + for process in filtered_processes { + assert_true(process.name.contains(test_process.name), "Filtered process should contain pattern"); + } + print(`✓ process_list("${test_process.name}") found ${filtered_processes.len()} matching processes`); +} + +// Test 6: process_list with nonexistent pattern +print("\n--- Test 6: Process List with Nonexistent Pattern ---"); +let empty_list = process_list("nonexistent_process_12345"); +assert_true(empty_list.len() == 0, "Should find no processes with nonexistent pattern"); +print("✓ process_list() correctly handles nonexistent patterns"); + +// Test 7: kill function with nonexistent process +print("\n--- Test 7: Kill Function (Nonexistent Process) ---"); +let kill_result = kill("nonexistent_process_12345"); +assert_true( + kill_result.contains("No matching processes") || kill_result.contains("Successfully killed"), + "Kill should handle nonexistent processes gracefully" +); +print(`✓ kill("nonexistent_process_12345") result: ${kill_result}`); + +// Test 8: Common system commands detection +print("\n--- Test 8: Common System Commands Detection ---"); +let common_commands = ["echo", "ls", "cat", "grep", "awk", "sed"]; +let windows_commands = ["cmd", "powershell", "notepad", "tasklist"]; + +let found_commands = []; +for cmd in common_commands { + let path = which(cmd); + if path != () { + found_commands.push(cmd); + } +} + +for cmd in windows_commands { + let path = which(cmd); + if path != () { + found_commands.push(cmd); + } +} + +assert_true(found_commands.len() > 0, "Should find at least one common command"); +print(`✓ Found common commands: ${found_commands}`); + +// Test 9: Process filtering accuracy +print("\n--- Test 9: Process Filtering Accuracy ---"); +if all_processes.len() > 0 { + let test_process = all_processes[0]; + let filtered = process_list(test_process.name); + + // All filtered processes should contain the pattern + let all_match = true; + for process in filtered { + if !process.name.contains(test_process.name) { + all_match = false; + break; + } + } + assert_true(all_match, "All filtered processes should contain the search pattern"); + print("✓ Process filtering is accurate"); +} + +// Test 10: Process management performance +print("\n--- Test 10: Process Management Performance ---"); +let start_time = timestamp(); +let perf_processes = process_list(""); +let end_time = timestamp(); +let duration = end_time - start_time; + +assert_true(duration < 5000, "Process listing should complete within 5 seconds"); +assert_true(perf_processes.len() > 0, "Performance test should still return processes"); +print(`✓ process_list() completed in ${duration}ms`); + +// Test 11: which command performance +print("\n--- Test 11: Which Command Performance ---"); +let which_start = timestamp(); +let which_result = which("echo"); +let which_end = timestamp(); +let which_duration = which_end - which_start; + +assert_true(which_duration < 1000, "which() should complete within 1 second"); +print(`✓ which("echo") completed in ${which_duration}ms`); + +// Test 12: Cross-platform process operations +print("\n--- Test 12: Cross-Platform Process Operations ---"); +let platform_specific_found = false; + +// Try Windows-specific +let cmd_found = which("cmd"); +if cmd_found != () { + platform_specific_found = true; + print("✓ Windows platform detected (cmd found)"); +} + +// Try Unix-specific +let sh_found = which("sh"); +if sh_found != () { + platform_specific_found = true; + print("✓ Unix-like platform detected (sh found)"); +} + +assert_true(platform_specific_found, "Should detect platform-specific commands"); + +print("\n=== All Process Management Tests Passed! ==="); diff --git a/process/tests/rhai/03_error_handling.rhai b/process/tests/rhai/03_error_handling.rhai new file mode 100644 index 0000000..0a484ae --- /dev/null +++ b/process/tests/rhai/03_error_handling.rhai @@ -0,0 +1,167 @@ +// Test script for process error handling functionality + +print("=== Process Error Handling Tests ==="); + +// Test 1: Command execution error handling +print("\n--- Test 1: Command Execution Error Handling ---"); +try { + let result = run_command("nonexistent_command_12345"); + assert_true(false, "Should have thrown an error for nonexistent command"); +} catch(e) { + assert_true(true, "Correctly caught error for nonexistent command"); + print("✓ Command execution error handling works"); +} + +// Test 2: Silent error handling with ignore_error +print("\n--- Test 2: Silent Error Handling with ignore_error ---"); +let error_result = run("false").ignore_error().silent().execute(); +assert_true(!error_result.success, "Command should fail"); +assert_true(error_result.code != 0, "Exit code should be non-zero"); +print("✓ Silent error handling with ignore_error works"); + +// Test 3: Process management error handling +print("\n--- Test 3: Process Management Error Handling ---"); +try { + let result = process_get("nonexistent_process_12345"); + assert_true(false, "Should have thrown an error for nonexistent process"); +} catch(e) { + assert_true(true, "Correctly caught error for nonexistent process"); + print("✓ Process management error handling works"); +} + +// Test 4: Script execution error handling +print("\n--- Test 4: Script Execution Error Handling ---"); +let error_script = ` + echo "Before error" + false + echo "After error" +`; + +try { + let result = run_command(error_script); + assert_true(false, "Should have thrown an error for failing script"); +} catch(e) { + assert_true(true, "Correctly caught error for failing script"); + print("✓ Script execution error handling works"); +} + +// Test 5: Error handling with die=false in options +print("\n--- Test 5: Error Handling with die=false in Options ---"); +let options = #{ + silent: true, + die: false, + log: false +}; +let no_die_result = run("false", options); +assert_true(!no_die_result.success, "Command should fail but not throw"); +assert_true(no_die_result.code != 0, "Exit code should be non-zero"); +print("✓ Error handling with die=false in options works"); + +// Test 6: Builder pattern error handling +print("\n--- Test 6: Builder Pattern Error Handling ---"); +try { + let result = run("nonexistent_command_12345").silent().execute(); + assert_true(false, "Should have thrown an error for nonexistent command in builder"); +} catch(e) { + assert_true(true, "Correctly caught error for nonexistent command in builder"); + print("✓ Builder pattern error handling works"); +} + +// Test 7: Multiple error conditions +print("\n--- Test 7: Multiple Error Conditions ---"); +let error_conditions = [ + "nonexistent_command_12345", + "false", + "exit 1" +]; + +for cmd in error_conditions { + try { + let result = run(cmd).silent().execute(); + assert_true(false, `Should have thrown an error for: ${cmd}`); + } catch(e) { + // Expected behavior + } +} +print("✓ Multiple error conditions handled correctly"); + +// Test 8: Error recovery with ignore_error +print("\n--- Test 8: Error Recovery with ignore_error ---"); +let recovery_script = ` + echo "Starting script" + false + echo "This should not execute" +`; + +let recovery_result = run(recovery_script).ignore_error().silent().execute(); +assert_true(!recovery_result.success, "Script should fail"); +assert_true(recovery_result.stdout.contains("Starting script"), "Should capture output before error"); +print("✓ Error recovery with ignore_error works"); + +// Test 9: Nested error handling +print("\n--- Test 9: Nested Error Handling ---"); +try { + try { + let result = run_command("nonexistent_command_12345"); + assert_true(false, "Inner try should fail"); + } catch(inner_e) { + // Re-throw to test outer catch + throw inner_e; + } + assert_true(false, "Outer try should fail"); +} catch(outer_e) { + assert_true(true, "Nested error handling works"); + print("✓ Nested error handling works"); +} + +// Test 10: Error message content validation +print("\n--- Test 10: Error Message Content Validation ---"); +try { + let result = process_get("nonexistent_process_12345"); + assert_true(false, "Should have thrown an error"); +} catch(e) { + let error_msg = `${e}`; + assert_true(error_msg.len() > 0, "Error message should not be empty"); + print(`✓ Error message content: ${error_msg}`); +} + +// Test 11: Graceful degradation +print("\n--- Test 11: Graceful Degradation ---"); +let graceful_commands = [ + "echo 'fallback test'", + "printf 'fallback test'", + "print 'fallback test'" +]; + +let graceful_success = false; +for cmd in graceful_commands { + try { + let result = run_command(cmd); + if result.success { + graceful_success = true; + break; + } + } catch(e) { + // Try next command + continue; + } +} + +assert_true(graceful_success, "Should find at least one working command for graceful degradation"); +print("✓ Graceful degradation works"); + +// Test 12: Error handling performance +print("\n--- Test 12: Error Handling Performance ---"); +let error_start = timestamp(); +try { + let result = run_command("nonexistent_command_12345"); +} catch(e) { + // Expected +} +let error_end = timestamp(); +let error_duration = error_end - error_start; + +assert_true(error_duration < 5000, "Error handling should be fast (< 5 seconds)"); +print(`✓ Error handling completed in ${error_duration}ms`); + +print("\n=== All Error Handling Tests Passed! ==="); diff --git a/process/tests/rhai/04_real_world_scenarios.rhai b/process/tests/rhai/04_real_world_scenarios.rhai new file mode 100644 index 0000000..99e12e2 --- /dev/null +++ b/process/tests/rhai/04_real_world_scenarios.rhai @@ -0,0 +1,326 @@ +// Test script for real-world process scenarios + +print("=== Real-World Process Scenarios Tests ==="); + +// Test 1: System information gathering +print("\n--- Test 1: System Information Gathering ---"); +let system_info = #{}; + +// Get current user +try { + let whoami_result = run_command("whoami"); + if whoami_result.success { + system_info.user = whoami_result.stdout.trim(); + print(`✓ Current user: ${system_info.user}`); + } +} catch(e) { + print("⚠ whoami command not available"); +} + +// Get current directory +try { + let pwd_result = run_command("pwd"); + if pwd_result.success { + system_info.pwd = pwd_result.stdout.trim(); + print(`✓ Current directory: ${system_info.pwd}`); + } +} catch(e) { + // Try Windows alternative + try { + let cd_result = run_command("cd"); + if cd_result.success { + system_info.pwd = cd_result.stdout.trim(); + print(`✓ Current directory (Windows): ${system_info.pwd}`); + } + } catch(e2) { + print("⚠ pwd/cd commands not available"); + } +} + +assert_true(system_info.len() > 0, "Should gather at least some system information"); + +// Test 2: File system operations +print("\n--- Test 2: File System Operations ---"); +let temp_file = "/tmp/sal_process_test.txt"; +let temp_content = "SAL Process Test Content"; + +// Create a test file +let create_script = ` + echo "${temp_content}" > ${temp_file} +`; + +try { + let create_result = run_command(create_script); + if create_result.success { + print("✓ Test file created successfully"); + + // Read the file back + let read_result = run_command(`cat ${temp_file}`); + if read_result.success { + assert_true(read_result.stdout.contains(temp_content), "File content should match"); + print("✓ Test file read successfully"); + } + + // Clean up + let cleanup_result = run_command(`rm -f ${temp_file}`); + if cleanup_result.success { + print("✓ Test file cleaned up successfully"); + } + } +} catch(e) { + print("⚠ File system operations not available on this platform"); +} + +// Test 3: Process monitoring workflow +print("\n--- Test 3: Process Monitoring Workflow ---"); +let monitoring_workflow = || { + // Get all processes + let all_processes = process_list(""); + assert_true(all_processes.len() > 0, "Should find running processes"); + + // Find processes with common names + let common_patterns = ["init", "kernel", "system", "explorer", "winlogon"]; + let found_patterns = []; + + for pattern in common_patterns { + let matches = process_list(pattern); + if matches.len() > 0 { + found_patterns.push(pattern); + } + } + + print(`✓ Process monitoring found patterns: ${found_patterns}`); + return found_patterns.len() > 0; +}; + +assert_true(monitoring_workflow(), "Process monitoring workflow should succeed"); + +// Test 4: Command availability checking +print("\n--- Test 4: Command Availability Checking ---"); +let essential_commands = ["echo"]; +let optional_commands = ["git", "curl", "wget", "python", "node", "java"]; + +let available_commands = []; +let missing_commands = []; + +// Check essential commands +for cmd in essential_commands { + let path = which(cmd); + if path != () { + available_commands.push(cmd); + } else { + missing_commands.push(cmd); + } +} + +// Check optional commands +for cmd in optional_commands { + let path = which(cmd); + if path != () { + available_commands.push(cmd); + } +} + +assert_true(missing_commands.len() == 0, "All essential commands should be available"); +print(`✓ Available commands: ${available_commands}`); +print(`✓ Command availability check completed`); + +// Test 5: Batch processing simulation +print("\n--- Test 5: Batch Processing Simulation ---"); +let batch_commands = [ + "echo 'Processing item 1'", + "echo 'Processing item 2'", + "echo 'Processing item 3'" +]; + +let batch_results = []; +let batch_success = true; + +for cmd in batch_commands { + try { + let result = run(cmd).silent().execute(); + batch_results.push(result); + if !result.success { + batch_success = false; + } + } catch(e) { + batch_success = false; + break; + } +} + +assert_true(batch_success, "Batch processing should succeed"); +assert_true(batch_results.len() == batch_commands.len(), "Should process all batch items"); +print(`✓ Batch processing completed: ${batch_results.len()} items`); + +// Test 6: Environment variable handling +print("\n--- Test 6: Environment Variable Handling ---"); +let env_test_script = ` + export TEST_VAR="test_value" + echo "TEST_VAR=$TEST_VAR" +`; + +try { + let env_result = run_command(env_test_script); + if env_result.success { + assert_true(env_result.stdout.contains("TEST_VAR=test_value"), "Environment variable should be set"); + print("✓ Environment variable handling works"); + } +} catch(e) { + print("⚠ Environment variable test not available"); +} + +// Test 7: Pipeline simulation +print("\n--- Test 7: Pipeline Simulation ---"); +let pipeline_script = ` + echo "line1 +line2 +line3" | grep "line2" +`; + +try { + let pipeline_result = run_command(pipeline_script); + if pipeline_result.success { + assert_true(pipeline_result.stdout.contains("line2"), "Pipeline should filter correctly"); + print("✓ Pipeline simulation works"); + } +} catch(e) { + print("⚠ Pipeline simulation not available"); +} + +// Test 8: Error recovery workflow +print("\n--- Test 8: Error Recovery Workflow ---"); +let recovery_workflow = || { + let primary_cmd = "nonexistent_primary_command"; + let fallback_cmd = "echo 'fallback executed'"; + + // Try primary command + try { + let primary_result = run_command(primary_cmd); + return primary_result.success; + } catch(e) { + // Primary failed, try fallback + try { + let fallback_result = run_command(fallback_cmd); + return fallback_result.success && fallback_result.stdout.contains("fallback executed"); + } catch(e2) { + return false; + } + } +}; + +assert_true(recovery_workflow(), "Error recovery workflow should succeed"); +print("✓ Error recovery workflow works"); + +// Test 9: Resource monitoring +print("\n--- Test 9: Resource Monitoring ---"); +let resource_monitoring = || { + let start_time = timestamp(); + + // Simulate resource-intensive operation + let intensive_script = ` + for i in $(seq 1 10); do + echo "Processing $i" + done + `; + + try { + let result = run(intensive_script).silent().execute(); + let end_time = timestamp(); + let duration = end_time - start_time; + + print(`✓ Resource monitoring: operation took ${duration}ms`); + return result.success && duration < 10000; // Should complete within 10 seconds + } catch(e) { + return false; + } +}; + +assert_true(resource_monitoring(), "Resource monitoring should work"); + +// Test 10: Cross-platform compatibility +print("\n--- Test 10: Cross-Platform Compatibility ---"); +let cross_platform_test = || { + // Test basic commands that should work everywhere + let basic_commands = ["echo hello"]; + + for cmd in basic_commands { + try { + let result = run_command(cmd); + if !result.success { + return false; + } + } catch(e) { + return false; + } + } + + // Test platform detection + let windows_detected = which("cmd") != (); + let unix_detected = which("sh") != (); + + return windows_detected || unix_detected; +}; + +assert_true(cross_platform_test(), "Cross-platform compatibility should work"); +print("✓ Cross-platform compatibility verified"); + +// Test 11: Complex workflow integration +print("\n--- Test 11: Complex Workflow Integration ---"); +let complex_workflow = || { + // Step 1: Check prerequisites + let echo_available = which("echo") != (); + if !echo_available { + return false; + } + + // Step 2: Execute main task + let main_result = run("echo 'Complex workflow step'").silent().execute(); + if !main_result.success { + return false; + } + + // Step 3: Verify results + let verify_result = run("echo 'Verification step'").silent().execute(); + if !verify_result.success { + return false; + } + + // Step 4: Cleanup (always succeeds) + let cleanup_result = run("echo 'Cleanup step'").ignore_error().silent().execute(); + + return true; +}; + +assert_true(complex_workflow(), "Complex workflow integration should succeed"); +print("✓ Complex workflow integration works"); + +// Test 12: Performance under load +print("\n--- Test 12: Performance Under Load ---"); +let performance_test = || { + let start_time = timestamp(); + let iterations = 5; + let success_count = 0; + + for i in range(0, iterations) { + try { + let result = run(`echo "Iteration ${i}"`).silent().execute(); + if result.success { + success_count += 1; + } + } catch(e) { + // Continue with next iteration + } + } + + let end_time = timestamp(); + let duration = end_time - start_time; + let avg_time = duration / iterations; + + print(`✓ Performance test: ${success_count}/${iterations} succeeded, avg ${avg_time}ms per operation`); + return success_count == iterations && avg_time < 1000; // Each operation should be < 1 second +}; + +assert_true(performance_test(), "Performance under load should be acceptable"); + +print("\n=== All Real-World Scenarios Tests Passed! ==="); diff --git a/process/tests/rhai_tests.rs b/process/tests/rhai_tests.rs new file mode 100644 index 0000000..8b34cb6 --- /dev/null +++ b/process/tests/rhai_tests.rs @@ -0,0 +1,321 @@ +use rhai::Engine; +use sal_process::rhai::register_process_module; + +fn create_test_engine() -> Engine { + let mut engine = Engine::new(); + register_process_module(&mut engine).unwrap(); + engine +} + +#[test] +fn test_rhai_run_command() { + let engine = create_test_engine(); + + let script = r#" + let result = run_command("echo hello"); + result.success && result.stdout.contains("hello") + "#; + + let result: bool = engine.eval(script).unwrap(); + assert!(result); +} + +#[test] +fn test_rhai_run_silent() { + let engine = create_test_engine(); + + let script = r#" + let result = run_silent("echo silent test"); + result.success && result.stdout.contains("silent test") + "#; + + let result: bool = engine.eval(script).unwrap(); + assert!(result); +} + +#[test] +fn test_rhai_run_builder_pattern() { + let engine = create_test_engine(); + + let script = r#" + let result = run("echo builder test").silent().execute(); + result.success && result.stdout.contains("builder test") + "#; + + let result: bool = engine.eval(script).unwrap(); + assert!(result); +} + +#[test] +fn test_rhai_run_builder_ignore_error() { + let engine = create_test_engine(); + + let script = r#" + let result = run("false").ignore_error().silent().execute(); + !result.success + "#; + + let result: bool = engine.eval(script).unwrap(); + assert!(result); +} + +#[test] +fn test_rhai_run_builder_with_log() { + let engine = create_test_engine(); + + let script = r#" + let result = run("echo log test").log().silent().execute(); + result.success && result.stdout.contains("log test") + "#; + + let result: bool = engine.eval(script).unwrap(); + assert!(result); +} + +#[test] +fn test_rhai_which_function() { + let engine = create_test_engine(); + + // Test with a command that should exist + #[cfg(target_os = "windows")] + let script = r#" + let path = which("cmd"); + path != () && path.len() > 0 + "#; + + #[cfg(not(target_os = "windows"))] + let script = r#" + let path = which("sh"); + path != () && path.len() > 0 + "#; + + let result: bool = engine.eval(script).unwrap(); + assert!(result); +} + +#[test] +fn test_rhai_which_nonexistent() { + let engine = create_test_engine(); + + let script = r#" + let path = which("nonexistent_command_12345"); + path == () + "#; + + let result: bool = engine.eval(script).unwrap(); + assert!(result); +} + +#[test] +fn test_rhai_process_list() { + let engine = create_test_engine(); + + let script = r#" + let processes = process_list(""); + processes.len() > 0 + "#; + + let result: bool = engine.eval(script).unwrap(); + assert!(result); +} + +#[test] +fn test_rhai_process_list_with_pattern() { + let engine = create_test_engine(); + + let script = r#" + let all_processes = process_list(""); + if all_processes.len() > 0 { + let first_process = all_processes[0]; + let filtered = process_list(first_process.name); + filtered.len() >= 1 + } else { + false + } + "#; + + let result: bool = engine.eval(script).unwrap(); + assert!(result); +} + +#[test] +fn test_rhai_process_info_properties() { + let engine = create_test_engine(); + + let script = r#" + let processes = process_list(""); + if processes.len() > 0 { + let process = processes[0]; + process.pid > 0 && process.name.len() > 0 + } else { + false + } + "#; + + let result: bool = engine.eval(script).unwrap(); + assert!(result); +} + +#[test] +fn test_rhai_command_result_properties() { + let engine = create_test_engine(); + + let script = r#" + let result = run_command("echo test"); + result.success && result.stdout.contains("test") + "#; + + let result: bool = engine.eval(script).unwrap(); + assert!(result); +} + +#[test] +fn test_rhai_kill_nonexistent() { + let engine = create_test_engine(); + + let script = r#" + let result = kill("nonexistent_process_12345"); + result.contains("No matching processes") || result.contains("Successfully killed") + "#; + + let result: bool = engine.eval(script).unwrap(); + assert!(result); +} + +#[test] +fn test_rhai_run_with_options() { + let engine = create_test_engine(); + + let script = r#" + let options = #{ + silent: true, + die: false, + log: false + }; + let result = run("echo options test", options); + result.success && result.stdout.contains("options test") + "#; + + let result: bool = engine.eval(script).unwrap(); + assert!(result); +} + +#[test] +fn test_rhai_run_multiline_script() { + let engine = create_test_engine(); + + let script = r#" + let bash_script = ` + echo "Line 1" + echo "Line 2" + echo "Line 3" + `; + let result = run_command(bash_script); + result.success && + result.stdout.contains("Line 1") && + result.stdout.contains("Line 2") && + result.stdout.contains("Line 3") + "#; + + let result: bool = engine.eval(script).unwrap(); + assert!(result); +} + +#[test] +fn test_rhai_error_handling() { + let engine = create_test_engine(); + + // Test that errors are properly converted to Rhai errors + let script = r#" + let error_occurred = false; + try { + run_command("nonexistent_command_12345"); + } catch(e) { + error_occurred = true; + } + error_occurred + "#; + + let result: bool = engine.eval(script).unwrap(); + assert!(result); +} + +#[test] +fn test_rhai_process_get_error_handling() { + let engine = create_test_engine(); + + let script = r#" + let error_occurred = false; + try { + process_get("nonexistent_process_12345"); + } catch(e) { + error_occurred = true; + } + error_occurred + "#; + + let result: bool = engine.eval(script).unwrap(); + assert!(result); +} + +#[test] +fn test_rhai_builder_chaining() { + let engine = create_test_engine(); + + let script = r#" + let result = run("echo chaining") + .silent() + .ignore_error() + .log() + .execute(); + result.success && result.stdout.contains("chaining") + "#; + + let result: bool = engine.eval(script).unwrap(); + assert!(result); +} + +#[test] +fn test_rhai_cross_platform_commands() { + let engine = create_test_engine(); + + // Test platform-specific commands + #[cfg(target_os = "windows")] + let script = r#" + let result = run_command("echo Windows test"); + result.success && result.stdout.contains("Windows test") + "#; + + #[cfg(not(target_os = "windows"))] + let script = r#" + let result = run_command("echo Unix test"); + result.success && result.stdout.contains("Unix test") + "#; + + let result: bool = engine.eval(script).unwrap(); + assert!(result); +} + +#[test] +fn test_rhai_complex_workflow() { + let engine = create_test_engine(); + + let script = r#" + // Test a complex workflow combining multiple functions + let echo_path = which("echo"); + if echo_path == () { + false + } else { + let result = run("echo workflow test").silent().execute(); + if !result.success { + false + } else { + let processes = process_list(""); + processes.len() > 0 + } + } + "#; + + let result: bool = engine.eval(script).unwrap(); + assert!(result); +} diff --git a/process/tests/run_tests.rs b/process/tests/run_tests.rs new file mode 100644 index 0000000..a74c010 --- /dev/null +++ b/process/tests/run_tests.rs @@ -0,0 +1,251 @@ +use sal_process::{run, run_command, run_silent, RunError}; +use std::env; + +#[test] +fn test_run_simple_command() { + let result = run_command("echo hello").unwrap(); + assert!(result.success); + assert_eq!(result.code, 0); + assert!(result.stdout.contains("hello")); + assert!(result.stderr.is_empty()); +} + +#[test] +fn test_run_command_with_args() { + let result = run_command("echo hello world").unwrap(); + assert!(result.success); + assert_eq!(result.code, 0); + assert!(result.stdout.contains("hello world")); +} + +#[test] +fn test_run_silent() { + let result = run_silent("echo silent test").unwrap(); + assert!(result.success); + assert_eq!(result.code, 0); + assert!(result.stdout.contains("silent test")); +} + +#[test] +fn test_run_builder_pattern() { + let result = run("echo builder test").silent(true).execute().unwrap(); + + assert!(result.success); + assert_eq!(result.code, 0); + assert!(result.stdout.contains("builder test")); +} + +#[test] +fn test_run_builder_die_false() { + let result = run("false") // Command that always fails + .die(false) + .silent(true) + .execute() + .unwrap(); + + assert!(!result.success); + assert_ne!(result.code, 0); +} + +#[test] +fn test_run_builder_die_true() { + // Use a command that will definitely fail + let result = run("exit 1") // Script that always fails + .die(true) + .silent(true) + .execute(); + + assert!(result.is_err()); +} + +#[test] +fn test_run_multiline_script() { + let script = r#" + echo "Line 1" + echo "Line 2" + echo "Line 3" + "#; + + let result = run_command(script).unwrap(); + assert!(result.success); + assert_eq!(result.code, 0); + assert!(result.stdout.contains("Line 1")); + assert!(result.stdout.contains("Line 2")); + assert!(result.stdout.contains("Line 3")); +} + +#[test] +fn test_run_script_with_shebang() { + let script = r#"#!/bin/bash + echo "Script with shebang" + exit 0 + "#; + + let result = run_command(script).unwrap(); + assert!(result.success); + assert_eq!(result.code, 0); + assert!(result.stdout.contains("Script with shebang")); +} + +#[test] +fn test_run_script_error_handling() { + let script = r#" + echo "Before error" + false + echo "After error" + "#; + + let result = run(script).silent(true).execute(); + assert!(result.is_err()); +} + +#[test] +fn test_run_empty_command() { + let result = run_command(""); + assert!(result.is_err()); + match result.unwrap_err() { + RunError::EmptyCommand => {} + _ => panic!("Expected EmptyCommand error"), + } +} + +#[test] +fn test_run_nonexistent_command() { + let result = run("nonexistent_command_12345").silent(true).execute(); + assert!(result.is_err()); +} + +#[test] +fn test_run_with_environment_variables() { + env::set_var("TEST_VAR", "test_value"); + + #[cfg(target_os = "windows")] + let script = "echo %TEST_VAR%"; + + #[cfg(not(target_os = "windows"))] + let script = r#" + export TEST_VAR="test_value" + echo $TEST_VAR + "#; + + let result = run_command(script).unwrap(); + assert!(result.success); + assert!(result.stdout.contains("test_value")); + + env::remove_var("TEST_VAR"); +} + +#[test] +fn test_run_with_working_directory() { + // Test that commands run in the current working directory + let result = run_command("pwd").unwrap(); + assert!(result.success); + assert!(!result.stdout.is_empty()); +} + +#[test] +fn test_command_result_properties() { + let result = run_command("echo test").unwrap(); + + // Test all CommandResult properties + assert!(!result.stdout.is_empty()); + assert!(result.stderr.is_empty()); + assert!(result.success); + assert_eq!(result.code, 0); +} + +#[test] +fn test_run_builder_log_option() { + // Test that log option doesn't cause errors + let result = run("echo log test") + .log(true) + .silent(true) + .execute() + .unwrap(); + + assert!(result.success); + assert!(result.stdout.contains("log test")); +} + +#[test] +fn test_run_cross_platform_commands() { + // Test commands that work on all platforms + + // Test echo command + let result = run_command("echo cross-platform").unwrap(); + assert!(result.success); + assert!(result.stdout.contains("cross-platform")); + + // Test basic shell operations + #[cfg(target_os = "windows")] + let result = run_command("dir").unwrap(); + + #[cfg(not(target_os = "windows"))] + let result = run_command("ls").unwrap(); + + assert!(result.success); +} + +#[test] +fn test_run_script_with_variables() { + let script = r#" + VAR="test_variable" + echo "Variable value: $VAR" + "#; + + let result = run_command(script).unwrap(); + assert!(result.success); + assert!(result.stdout.contains("Variable value: test_variable")); +} + +#[test] +fn test_run_script_with_conditionals() { + let script = r#" + if [ "hello" = "hello" ]; then + echo "Condition passed" + else + echo "Condition failed" + fi + "#; + + let result = run_command(script).unwrap(); + assert!(result.success); + assert!(result.stdout.contains("Condition passed")); +} + +#[test] +fn test_run_script_with_loops() { + let script = r#" + for i in 1 2 3; do + echo "Number: $i" + done + "#; + + let result = run_command(script).unwrap(); + assert!(result.success); + assert!(result.stdout.contains("Number: 1")); + assert!(result.stdout.contains("Number: 2")); + assert!(result.stdout.contains("Number: 3")); +} + +#[test] +fn test_run_with_stderr_output() { + // Test that stderr field exists and can be accessed + let result = run_command("echo test").unwrap(); + assert!(result.success); + // Just verify that stderr field exists and is accessible + let _stderr_len = result.stderr.len(); // This verifies stderr field exists +} + +#[test] +fn test_run_builder_chaining() { + let result = run("echo chaining test") + .silent(true) + .die(true) + .log(false) + .execute() + .unwrap(); + + assert!(result.success); + assert!(result.stdout.contains("chaining test")); +} diff --git a/src/lib.rs b/src/lib.rs index 1ae131c..4810102 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -42,7 +42,7 @@ pub use sal_mycelium as mycelium; pub use sal_net as net; pub use sal_os as os; pub mod postgresclient; -pub mod process; +pub use sal_process as process; pub use sal_redisclient as redisclient; pub mod rhai; pub use sal_text as text; diff --git a/src/process/README.md b/src/process/README.md deleted file mode 100644 index 604322e..0000000 --- a/src/process/README.md +++ /dev/null @@ -1,150 +0,0 @@ -# SAL Process Module (`sal::process`) - -The `process` module in the SAL (System Abstraction Layer) library provides a robust and cross-platform interface for creating, managing, and interacting with system processes. It is divided into two main sub-modules: `run` for command and script execution, and `mgmt` for process management tasks like listing, finding, and terminating processes. - -## Core Functionalities - -### 1. Command and Script Execution (`run.rs`) - -The `run.rs` sub-module offers flexible ways to execute external commands and multi-line scripts. - -#### `RunBuilder` - -The primary interface for execution is the `RunBuilder`, obtained via `sal::process::run("your_command_or_script")`. It allows for fluent configuration: - -- `.die(bool)`: If `true` (default), an error is returned if the command fails. If `false`, a `CommandResult` with `success: false` is returned instead. -- `.silent(bool)`: If `true` (default is `false`), suppresses `stdout` and `stderr` from being printed to the console during execution. Output is still captured in `CommandResult`. -- `.async_exec(bool)`: If `true` (default is `false`), executes the command or script in a separate thread, returning an immediate placeholder `CommandResult`. -- `.log(bool)`: If `true` (default is `false`), prints a log message before executing the command. -- `.execute() -> Result`: Executes the configured command or script. - -**Input Handling**: -- **Single-line commands**: Treated as a command and its arguments (e.g., `"ls -la"`). -- **Multi-line scripts**: If the input string contains newline characters (`\n`), it's treated as a script. - - The script content is automatically dedented. - - On Unix-like systems, `#!/bin/bash -e` is prepended (if no shebang exists) to ensure the script exits on error. - - A temporary script file is created, made executable, and then run. - -#### `CommandResult` - -All execution functions return a `Result`. The `CommandResult` struct contains: -- `stdout: String`: Captured standard output. -- `stderr: String`: Captured standard error. -- `success: bool`: `true` if the command exited with a zero status code. -- `code: i32`: The exit code of the command. - -#### Convenience Functions: -- `sal::process::run_command("cmd_or_script")`: Equivalent to `run("cmd_or_script").execute()`. -- `sal::process::run_silent("cmd_or_script")`: Equivalent to `run("cmd_or_script").silent(true).execute()`. - -#### Error Handling: -- `RunError`: Enum for errors specific to command/script execution (e.g., `EmptyCommand`, `CommandExecutionFailed`, `ScriptPreparationFailed`). - -### 2. Process Management (`mgmt.rs`) - -The `mgmt.rs` sub-module provides tools for querying and managing system processes. - -#### `ProcessInfo` -A struct holding basic process information: -- `pid: i64` -- `name: String` -- `memory: f64` (currently a placeholder) -- `cpu: f64` (currently a placeholder) - -#### Functions: -- `sal::process::which(command_name: &str) -> Option`: - Checks if a command exists in the system's `PATH`. Returns the full path if found. - ```rust - if let Some(path) = sal::process::which("git") { - println!("Git found at: {}", path); - } - ``` - -- `sal::process::kill(pattern: &str) -> Result`: - Kills processes matching the given `pattern` (name or part of the command line). - Uses `taskkill` on Windows and `pkill -f` on Unix-like systems. - ```rust - match sal::process::kill("my-server-proc") { - Ok(msg) => println!("{}", msg), // "Successfully killed processes" or "No matching processes found" - Err(e) => eprintln!("Error killing process: {}", e), - } - ``` - -- `sal::process::process_list(pattern: &str) -> Result, ProcessError>`: - Lists running processes, optionally filtering by a `pattern` (substring match on name). If `pattern` is empty, lists all accessible processes. - Uses `wmic` on Windows and `ps` on Unix-like systems. - ```rust - match sal::process::process_list("nginx") { - Ok(procs) => { - for p in procs { - println!("PID: {}, Name: {}", p.pid, p.name); - } - }, - Err(e) => eprintln!("Error listing processes: {}", e), - } - ``` - -- `sal::process::process_get(pattern: &str) -> Result`: - Retrieves a single `ProcessInfo` for a process matching `pattern`. - Returns an error if zero or multiple processes match. - ```rust - match sal::process::process_get("unique_process_name") { - Ok(p) => println!("Found: PID {}, Name {}", p.pid, p.name), - Err(sal::process::ProcessError::NoProcessFound(patt)) => eprintln!("No process like '{}'", patt), - Err(sal::process::ProcessError::MultipleProcessesFound(patt, count)) => { - eprintln!("Found {} processes like '{}'", count, patt); - } - Err(e) => eprintln!("Error: {}", e), - } - ``` - -#### Error Handling: -- `ProcessError`: Enum for errors specific to process management (e.g., `CommandExecutionFailed`, `NoProcessFound`, `MultipleProcessesFound`). - -## Examples - -### Running a simple command -```rust -use sal::process; - -fn main() -> Result<(), Box> { - let result = process::run("echo 'Hello from SAL!'").execute()?; - println!("Output: {}", result.stdout); - Ok(()) -} -``` - -### Running a multi-line script silently -```rust -use sal::process; - -fn main() -> Result<(), Box> { - let script = r#" - echo "Starting script..." - date - echo "Script finished." - "#; - let result = process::run(script).silent(true).execute()?; - if result.success { - println!("Script executed successfully. Output:\n{}", result.stdout); - } else { - eprintln!("Script failed. Error:\n{}", result.stderr); - } - Ok(()) -} -``` - -### Checking if a command exists and then running it -```rust -use sal::process; - -fn main() -> Result<(), Box> { - if process::which("figlet").is_some() { - process::run("figlet 'SAL Process'").execute()?; - } else { - println!("Figlet not found, using echo instead:"); - process::run("echo 'SAL Process'").execute()?; - } - Ok(()) -} -``` diff --git a/src/process/mod.rs b/src/process/mod.rs deleted file mode 100644 index 1563181..0000000 --- a/src/process/mod.rs +++ /dev/null @@ -1,21 +0,0 @@ -//! # Process Module -//! -//! The `process` module provides functionality for managing and interacting with -//! system processes across different platforms. It includes capabilities for: -//! -//! - Running commands and scripts -//! - Listing and filtering processes -//! - Killing processes -//! - Checking for command existence -//! -//! This module is designed to work consistently across Windows, macOS, and Linux. - -mod run; -mod mgmt; -mod screen; -#[cfg(test)] -mod tests; - -pub use run::*; -pub use mgmt::*; -pub use screen::{new as new_screen, kill as kill_screen}; \ No newline at end of file diff --git a/src/process/tests.rs b/src/process/tests.rs deleted file mode 100644 index 8782554..0000000 --- a/src/process/tests.rs +++ /dev/null @@ -1,169 +0,0 @@ -#[cfg(test)] -mod tests { - use std::sync::{Arc, Mutex}; - use std::thread; - use std::time::Duration; - use crate::process::run::{run, RunError}; - use crate::text::dedent; - - #[test] - fn test_run_command() { - // Test running a simple echo command using the builder pattern - let result = run("echo hello").execute().unwrap(); - assert!(result.success); - assert_eq!(result.code, 0); - assert!(result.stdout.trim().contains("hello")); - assert_eq!(result.stderr, ""); - } - - #[test] - fn test_run_silent_command() { - // Test running a command silently using the builder pattern - let result = run("echo silent test").silent(true).execute().unwrap(); - assert!(result.success); - assert_eq!(result.code, 0); - assert!(result.stdout.trim().contains("silent test")); - assert_eq!(result.stderr, ""); - } - - #[test] - fn test_run_script() { - // Test running a multi-line script using the builder pattern - let script = r#" - echo "line 1" - echo "line 2" - "#; - - let result = run(script).execute().unwrap(); - assert!(result.success); - assert_eq!(result.code, 0); - assert!(result.stdout.contains("line 1")); - assert!(result.stdout.contains("line 2")); - assert_eq!(result.stderr, ""); - } - - #[test] - fn test_run_with_dedent() { - // Test that run properly dedents scripts - let script = r#" - echo "This has 12 spaces of indentation" - echo "This has 16 spaces (4 more than the common indentation)" - "#; - - // The dedent function should remove the common 12 spaces - let dedented = dedent(script); - assert!(dedented.contains("echo \"This has 12 spaces of indentation\"")); - assert!(dedented.contains(" echo \"This has 16 spaces (4 more than the common indentation)\"")); - - // Running the script should work with the dedented content - let result = run(script).execute().unwrap(); - assert!(result.success); - assert_eq!(result.code, 0); - assert!(result.stdout.contains("This has 12 spaces of indentation")); - assert!(result.stdout.contains("This has 16 spaces (4 more than the common indentation)")); - } - - #[test] - fn test_run_detects_script_vs_command() { - // Test that run correctly identifies scripts vs commands - - // One-liner should be treated as a command - let one_liner = "echo one-liner test"; - let result = run(one_liner).execute().unwrap(); - assert!(result.success); - assert!(result.stdout.contains("one-liner test")); - - // Multi-line input should be treated as a script - let multi_line = "echo first line\necho second line"; - let result = run(multi_line).execute().unwrap(); - assert!(result.success); - assert!(result.stdout.contains("first line")); - assert!(result.stdout.contains("second line")); - } - - #[test] - fn test_run_empty_command() { - // Test handling of empty commands - let result = run("").execute(); - assert!(result.is_err()); - // The specific error should be EmptyCommand - match result { - Err(RunError::EmptyCommand) => (), - _ => panic!("Expected EmptyCommand error"), - } - } - - #[test] - fn test_run_die_option() { - // Test the die option - when false, it should return a CommandResult with success=false - // instead of an Err when the command fails - - // With die=true (default), a non-existent command should return an error - let result = run("non_existent_command").execute(); - assert!(result.is_err()); - - // With die=false, it should return a CommandResult with success=false - let result = run("non_existent_command").die(false).execute().unwrap(); - assert!(!result.success); - assert_ne!(result.code, 0); - assert!(result.stderr.contains("Error:")); - } - - #[test] - fn test_run_async_option() { - // Test the async option - when true, it should spawn the process and return immediately - - // Create a shared variable to track if the command has completed - let completed = Arc::new(Mutex::new(false)); - let completed_clone = completed.clone(); - - // Run a command that sleeps for 2 seconds, with async=true - let start = std::time::Instant::now(); - let result = run("sleep 2").async_exec(true).execute().unwrap(); - let elapsed = start.elapsed(); - - // The command should return immediately (much less than 2 seconds) - assert!(elapsed < Duration::from_secs(1)); - - // The result should have empty stdout/stderr and success=true - assert!(result.success); - assert_eq!(result.code, 0); - assert_eq!(result.stdout, ""); - assert_eq!(result.stderr, ""); - - // Wait a bit to ensure the command has time to complete - thread::sleep(Duration::from_secs(3)); - - // Verify the command completed (this is just a placeholder since we can't easily - // check if the async command completed in this test framework) - *completed_clone.lock().unwrap() = true; - assert!(*completed.lock().unwrap()); - } - - #[test] - fn test_run_log_option() { - // Test the log option - when true, it should log command execution - // Note: We can't easily capture stdout in tests, so this is more of a smoke test - - // Run a command with log=true - let result = run("echo log test").log(true).execute().unwrap(); - assert!(result.success); - assert_eq!(result.code, 0); - assert!(result.stdout.trim().contains("log test")); - } - - #[test] - fn test_builder_method_chaining() { - // Test that all builder methods can be chained together - let result = run("echo chaining test") - .silent(true) - .die(true) - .log(true) - .execute() - .unwrap(); - - assert!(result.success); - assert_eq!(result.code, 0); - assert!(result.stdout.trim().contains("chaining test")); - } -} \ No newline at end of file diff --git a/src/rhai/mod.rs b/src/rhai/mod.rs index 125f993..db02547 100644 --- a/src/rhai/mod.rs +++ b/src/rhai/mod.rs @@ -10,10 +10,8 @@ mod nerdctl; // OS module is now provided by sal-os package // Platform module is now provided by sal-os package mod postgresclient; -mod process; mod rfs; -mod screen; mod vault; // zinit module is now in sal-zinit-client package @@ -50,7 +48,7 @@ pub use sal_redisclient::rhai::register_redisclient_module; // Re-export PostgreSQL client module registration function pub use postgresclient::register_postgresclient_module; -pub use process::{ +pub use sal_process::rhai::{ kill, process_get, process_list, @@ -138,7 +136,7 @@ pub fn register(engine: &mut Engine) -> Result<(), Box> { sal_os::rhai::register_os_module(engine)?; // Register Process module functions - process::register_process_module(engine)?; + sal_process::rhai::register_process_module(engine)?; // Register Buildah module functions buildah::register_bah_module(engine)?; @@ -175,8 +173,7 @@ pub fn register(engine: &mut Engine) -> Result<(), Box> { // Platform functions are now registered by sal-os package - // Register Screen module functions - screen::register(engine); + // Screen module functions are now part of sal-process package // Register utility functions engine.register_fn("is_def_fn", |_name: &str| -> bool { diff --git a/src/rhai/screen.rs b/src/rhai/screen.rs deleted file mode 100644 index 750adf4..0000000 --- a/src/rhai/screen.rs +++ /dev/null @@ -1,22 +0,0 @@ -use crate::process::{kill_screen, new_screen}; -use rhai::{Engine, EvalAltResult}; - -fn screen_error_to_rhai_error(result: anyhow::Result) -> Result> { - result.map_err(|e| { - Box::new(EvalAltResult::ErrorRuntime( - format!("Screen error: {}", e).into(), - rhai::Position::NONE, - )) - }) -} - -#[allow(dead_code)] -pub fn register(engine: &mut Engine) { - engine.register_fn("screen_new", |name: &str, cmd: &str| { - screen_error_to_rhai_error(new_screen(name, cmd)) - }); - - engine.register_fn("screen_kill", |name: &str| { - screen_error_to_rhai_error(kill_screen(name)) - }); -}