feat: Add process package to monorepo
Some checks are pending
Rhai Tests / Run Rhai Tests (push) Waiting to run

- Add `sal-process` package for cross-platform process management.
- Update workspace members in `Cargo.toml`.
- Mark process package as complete in MONOREPO_CONVERSION_PLAN.md
- Remove license information from `mycelium` and `os` READMEs.
This commit is contained in:
Mahmoud-Emad
2025-06-22 11:41:10 +03:00
parent 511729c477
commit 3e3d0a1d45
24 changed files with 1942 additions and 465 deletions

31
process/Cargo.toml Normal file
View File

@@ -0,0 +1,31 @@
[package]
name = "sal-process"
version = "0.1.0"
edition = "2021"
authors = ["PlanetFirst <info@incubaid.com>"]
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"

178
process/README.md Normal file
View File

@@ -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

22
process/src/lib.rs Normal file
View File

@@ -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};

351
process/src/mgmt.rs Normal file
View File

@@ -0,0 +1,351 @@
use std::error::Error;
use std::fmt;
use std::io;
use std::process::Command;
/// Error type for process management operations
///
/// This enum represents various errors that can occur during process management
/// operations such as listing, finding, or killing processes.
#[derive(Debug)]
pub enum ProcessError {
/// An error occurred while executing a command
CommandExecutionFailed(io::Error),
/// A command executed successfully but returned an error
CommandFailed(String),
/// No process was found matching the specified pattern
NoProcessFound(String),
/// Multiple processes were found matching the specified pattern
MultipleProcessesFound(String, usize),
}
/// Implement Display for ProcessError to provide human-readable error messages
impl fmt::Display for ProcessError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ProcessError::CommandExecutionFailed(e) => {
write!(f, "Failed to execute command: {}", e)
}
ProcessError::CommandFailed(e) => write!(f, "{}", e),
ProcessError::NoProcessFound(pattern) => {
write!(f, "No processes found matching '{}'", pattern)
}
ProcessError::MultipleProcessesFound(pattern, count) => write!(
f,
"Multiple processes ({}) found matching '{}'",
count, pattern
),
}
}
}
// Implement Error trait for ProcessError
impl Error for ProcessError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
ProcessError::CommandExecutionFailed(e) => Some(e),
_ => None,
}
}
}
// Define a struct to represent process information
#[derive(Debug, Clone)]
pub struct ProcessInfo {
pub pid: i64,
pub name: String,
pub memory: f64,
pub cpu: f64,
}
/**
* Check if a command exists in PATH.
*
* # Arguments
*
* * `cmd` - The command to check
*
* # Returns
*
* * `Option<String>` - The full path to the command if found, None otherwise
*
* # Examples
*
* ```
* use sal_process::which;
*
* match which("git") {
* Some(path) => println!("Git is installed at: {}", path),
* None => println!("Git is not installed"),
* }
* ```
*/
pub fn which(cmd: &str) -> Option<String> {
#[cfg(target_os = "windows")]
let which_cmd = "where";
#[cfg(any(target_os = "macos", target_os = "linux"))]
let which_cmd = "which";
let output = Command::new(which_cmd).arg(cmd).output();
match output {
Ok(out) => {
if out.status.success() {
let path = String::from_utf8_lossy(&out.stdout).trim().to_string();
Some(path)
} else {
None
}
}
Err(_) => None,
}
}
/**
* Kill processes matching a pattern.
*
* # Arguments
*
* * `pattern` - The pattern to match against process names
*
* # Returns
*
* * `Ok(String)` - A success message indicating processes were killed or none were found
* * `Err(ProcessError)` - An error if the kill operation failed
*
* # Examples
*
* ```
* // Kill all processes with "server" in their name
* use sal_process::kill;
*
* fn main() -> Result<(), Box<dyn std::error::Error>> {
* let result = kill("server")?;
* println!("{}", result);
* Ok(())
* }
* ```
*/
pub fn kill(pattern: &str) -> Result<String, ProcessError> {
// Platform specific implementation
#[cfg(target_os = "windows")]
{
// On Windows, use taskkill with wildcard support
let mut args = vec!["/F"]; // Force kill
if pattern.contains('*') {
// If it contains wildcards, use filter
args.extend(&["/FI", &format!("IMAGENAME eq {}", pattern)]);
} else {
// Otherwise use image name directly
args.extend(&["/IM", pattern]);
}
let output = Command::new("taskkill")
.args(&args)
.output()
.map_err(ProcessError::CommandExecutionFailed)?;
if output.status.success() {
Ok("Successfully killed processes".to_string())
} else {
let error = String::from_utf8_lossy(&output.stderr);
if error.is_empty() {
let stdout = String::from_utf8_lossy(&output.stdout);
if stdout.contains("No tasks") {
Ok("No matching processes found".to_string())
} else {
Err(ProcessError::CommandFailed(format!(
"Failed to kill processes: {}",
stdout
)))
}
} else {
Err(ProcessError::CommandFailed(format!(
"Failed to kill processes: {}",
error
)))
}
}
}
#[cfg(any(target_os = "macos", target_os = "linux"))]
{
// On Unix-like systems, use pkill which has built-in pattern matching
let output = Command::new("pkill")
.arg("-f") // Match against full process name/args
.arg(pattern)
.output()
.map_err(ProcessError::CommandExecutionFailed)?;
// pkill returns 0 if processes were killed, 1 if none matched
if output.status.success() {
Ok("Successfully killed processes".to_string())
} else if output.status.code() == Some(1) {
Ok("No matching processes found".to_string())
} else {
let error = String::from_utf8_lossy(&output.stderr);
Err(ProcessError::CommandFailed(format!(
"Failed to kill processes: {}",
error
)))
}
}
}
/**
* List processes matching a pattern (or all if pattern is empty).
*
* # Arguments
*
* * `pattern` - The pattern to match against process names (empty string for all processes)
*
* # Returns
*
* * `Ok(Vec<ProcessInfo>)` - A vector of process information for matching processes
* * `Err(ProcessError)` - An error if the list operation failed
*
* # Examples
*
* ```
* // List all processes
* use sal_process::process_list;
*
* fn main() -> Result<(), Box<dyn std::error::Error>> {
* let processes = process_list("")?;
*
* // List processes with "server" in their name
* let processes = process_list("server")?;
* for proc in processes {
* println!("PID: {}, Name: {}", proc.pid, proc.name);
* }
* Ok(())
* }
* ```
*/
pub fn process_list(pattern: &str) -> Result<Vec<ProcessInfo>, ProcessError> {
let mut processes = Vec::new();
// Platform specific implementations
#[cfg(target_os = "windows")]
{
// Windows implementation using wmic
let output = Command::new("wmic")
.args(&["process", "list", "brief"])
.output()
.map_err(ProcessError::CommandExecutionFailed)?;
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
// Parse output (assuming format: Handle Name Priority)
for line in stdout.lines().skip(1) {
// Skip header
let parts: Vec<&str> = line.trim().split_whitespace().collect();
if parts.len() >= 2 {
let pid = parts[0].parse::<i64>().unwrap_or(0);
let name = parts[1].to_string();
// Filter by pattern if provided
if !pattern.is_empty() && !name.contains(pattern) {
continue;
}
processes.push(ProcessInfo {
pid,
name,
memory: 0.0, // Placeholder
cpu: 0.0, // Placeholder
});
}
}
} else {
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
return Err(ProcessError::CommandFailed(format!(
"Failed to list processes: {}",
stderr
)));
}
}
#[cfg(any(target_os = "macos", target_os = "linux"))]
{
// Unix implementation using ps
let output = Command::new("ps")
.args(&["-eo", "pid,comm"])
.output()
.map_err(ProcessError::CommandExecutionFailed)?;
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
// Parse output (assuming format: PID COMMAND)
for line in stdout.lines().skip(1) {
// Skip header
let parts: Vec<&str> = line.trim().split_whitespace().collect();
if parts.len() >= 2 {
let pid = parts[0].parse::<i64>().unwrap_or(0);
let name = parts[1].to_string();
// Filter by pattern if provided
if !pattern.is_empty() && !name.contains(pattern) {
continue;
}
processes.push(ProcessInfo {
pid,
name,
memory: 0.0, // Placeholder
cpu: 0.0, // Placeholder
});
}
}
} else {
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
return Err(ProcessError::CommandFailed(format!(
"Failed to list processes: {}",
stderr
)));
}
}
Ok(processes)
}
/**
* Get a single process matching the pattern (error if 0 or more than 1 match).
*
* # Arguments
*
* * `pattern` - The pattern to match against process names
*
* # Returns
*
* * `Ok(ProcessInfo)` - Information about the matching process
* * `Err(ProcessError)` - An error if no process or multiple processes match
*
* # Examples
*
* ```no_run
* use sal_process::process_get;
*
* fn main() -> Result<(), Box<dyn std::error::Error>> {
* let process = process_get("unique-server-name")?;
* println!("Found process: {} (PID: {})", process.name, process.pid);
* Ok(())
* }
* ```
*/
pub fn process_get(pattern: &str) -> Result<ProcessInfo, ProcessError> {
let processes = process_list(pattern)?;
match processes.len() {
0 => Err(ProcessError::NoProcessFound(pattern.to_string())),
1 => Ok(processes[0].clone()),
_ => Err(ProcessError::MultipleProcessesFound(
pattern.to_string(),
processes.len(),
)),
}
}

212
process/src/rhai.rs Normal file
View File

@@ -0,0 +1,212 @@
//! Rhai wrappers for Process module functions
//!
//! This module provides Rhai wrappers for the functions in the Process module.
use crate::{self as process, CommandResult, ProcessError, ProcessInfo, RunError};
use rhai::{Array, Dynamic, Engine, EvalAltResult, Map};
use std::clone::Clone;
/// Register Process module functions with the Rhai engine
///
/// # Arguments
///
/// * `engine` - The Rhai engine to register the functions with
///
/// # Returns
///
/// * `Result<(), Box<EvalAltResult>>` - Ok if registration was successful, Err otherwise
pub fn register_process_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
// Register types
// register_process_types(engine)?; // Removed
// Register CommandResult type and its methods
engine.register_type_with_name::<CommandResult>("CommandResult");
engine.register_get("stdout", |r: &mut CommandResult| r.stdout.clone());
engine.register_get("stderr", |r: &mut CommandResult| r.stderr.clone());
engine.register_get("success", |r: &mut CommandResult| r.success);
engine.register_get("code", |r: &mut CommandResult| r.code);
// Register ProcessInfo type and its methods
engine.register_type_with_name::<ProcessInfo>("ProcessInfo");
engine.register_get("pid", |p: &mut ProcessInfo| p.pid);
engine.register_get("name", |p: &mut ProcessInfo| p.name.clone());
engine.register_get("memory", |p: &mut ProcessInfo| p.memory);
engine.register_get("cpu", |p: &mut ProcessInfo| p.cpu);
// Register CommandBuilder type and its methods
engine.register_type_with_name::<RhaiCommandBuilder>("CommandBuilder");
engine.register_fn("run", RhaiCommandBuilder::new_rhai); // This is the builder entry point
engine.register_fn("silent", RhaiCommandBuilder::silent); // Method on CommandBuilder
engine.register_fn("ignore_error", RhaiCommandBuilder::ignore_error); // Method on CommandBuilder
engine.register_fn("log", RhaiCommandBuilder::log); // Method on CommandBuilder
engine.register_fn("execute", RhaiCommandBuilder::execute_command); // Method on CommandBuilder
// Register other process management functions
engine.register_fn("which", which);
engine.register_fn("kill", kill);
engine.register_fn("process_list", process_list);
engine.register_fn("process_get", process_get);
// Register legacy functions for backward compatibility
engine.register_fn("run_command", run_command);
engine.register_fn("run_silent", run_silent);
engine.register_fn("run", run_with_options);
Ok(())
}
// Helper functions for error conversion
fn run_error_to_rhai_error<T>(result: Result<T, RunError>) -> Result<T, Box<EvalAltResult>> {
result.map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime(
format!("Run error: {}", e).into(),
rhai::Position::NONE,
))
})
}
// Define a Rhai-facing builder struct
#[derive(Clone)]
struct RhaiCommandBuilder {
command: String,
die_on_error: bool,
is_silent: bool,
enable_log: bool,
}
impl RhaiCommandBuilder {
// Constructor function for Rhai (registered as `run`)
pub fn new_rhai(command: &str) -> Self {
Self {
command: command.to_string(),
die_on_error: true, // Default: die on error
is_silent: false,
enable_log: false,
}
}
// Rhai method: .silent()
pub fn silent(mut self) -> Self {
self.is_silent = true;
self
}
// Rhai method: .ignore_error()
pub fn ignore_error(mut self) -> Self {
self.die_on_error = false;
self
}
// Rhai method: .log()
pub fn log(mut self) -> Self {
self.enable_log = true;
self
}
// Rhai method: .execute() - Execute the command
pub fn execute_command(self) -> Result<CommandResult, Box<EvalAltResult>> {
let builder = process::run(&self.command)
.die(self.die_on_error)
.silent(self.is_silent)
.log(self.enable_log);
// Execute the command
run_error_to_rhai_error(builder.execute())
}
}
fn process_error_to_rhai_error<T>(
result: Result<T, ProcessError>,
) -> Result<T, Box<EvalAltResult>> {
result.map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime(
format!("Process error: {}", e).into(),
rhai::Position::NONE,
))
})
}
//
// Process Management Function Wrappers
//
/// Wrapper for process::which
///
/// Check if a command exists in PATH.
pub fn which(cmd: &str) -> Dynamic {
match process::which(cmd) {
Some(path) => path.into(),
None => Dynamic::UNIT,
}
}
/// Wrapper for process::kill
///
/// Kill processes matching a pattern.
pub fn kill(pattern: &str) -> Result<String, Box<EvalAltResult>> {
process_error_to_rhai_error(process::kill(pattern))
}
/// Wrapper for process::process_list
///
/// List processes matching a pattern (or all if pattern is empty).
pub fn process_list(pattern: &str) -> Result<Array, Box<EvalAltResult>> {
let processes = process_error_to_rhai_error(process::process_list(pattern))?;
// Convert Vec<ProcessInfo> to Rhai Array
let mut array = Array::new();
for process in processes {
array.push(Dynamic::from(process));
}
Ok(array)
}
/// Wrapper for process::process_get
///
/// Get a single process matching the pattern (error if 0 or more than 1 match).
pub fn process_get(pattern: &str) -> Result<ProcessInfo, Box<EvalAltResult>> {
process_error_to_rhai_error(process::process_get(pattern))
}
/// Legacy wrapper for process::run
///
/// Run a command and return the result.
pub fn run_command(cmd: &str) -> Result<CommandResult, Box<EvalAltResult>> {
run_error_to_rhai_error(process::run(cmd).execute())
}
/// Legacy wrapper for process::run with silent option
///
/// Run a command silently and return the result.
pub fn run_silent(cmd: &str) -> Result<CommandResult, Box<EvalAltResult>> {
run_error_to_rhai_error(process::run(cmd).silent(true).execute())
}
/// Legacy wrapper for process::run with options
///
/// Run a command with options and return the result.
pub fn run_with_options(cmd: &str, options: Map) -> Result<CommandResult, Box<EvalAltResult>> {
let mut builder = process::run(cmd);
// Apply options
if let Some(silent) = options.get("silent") {
if let Ok(silent_bool) = silent.as_bool() {
builder = builder.silent(silent_bool);
}
}
if let Some(die) = options.get("die") {
if let Ok(die_bool) = die.as_bool() {
builder = builder.die(die_bool);
}
}
if let Some(log) = options.get("log") {
if let Ok(log_bool) = log.as_bool() {
builder = builder.log(log_bool);
}
}
run_error_to_rhai_error(builder.execute())
}

535
process/src/run.rs Normal file
View File

@@ -0,0 +1,535 @@
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::thread;
use sal_text;
/// Error type for command and script execution operations
#[derive(Debug)]
pub enum RunError {
/// The command string was empty
EmptyCommand,
/// An error occurred while executing a command
CommandExecutionFailed(io::Error),
/// A command executed successfully but returned an error
CommandFailed(String),
/// An error occurred while preparing a script for execution
ScriptPreparationFailed(String),
/// An error occurred in a child process
ChildProcessError(String),
/// Failed to create a temporary directory
TempDirCreationFailed(io::Error),
/// Failed to create a script file
FileCreationFailed(io::Error),
/// Failed to write to a script file
FileWriteFailed(io::Error),
/// Failed to set file permissions
PermissionError(io::Error),
}
/// Implement Display for RunError to provide human-readable error messages
impl fmt::Display for RunError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
RunError::EmptyCommand => write!(f, "Empty command"),
RunError::CommandExecutionFailed(e) => write!(f, "Failed to execute command: {}", e),
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::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),
}
}
}
// Implement Error trait for RunError
impl Error for RunError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
RunError::CommandExecutionFailed(e) => Some(e),
RunError::TempDirCreationFailed(e) => Some(e),
RunError::FileCreationFailed(e) => Some(e),
RunError::FileWriteFailed(e) => Some(e),
RunError::PermissionError(e) => Some(e),
_ => None,
}
}
}
/// A structure to hold command execution results
#[derive(Debug, Clone)]
pub struct CommandResult {
pub stdout: String,
pub stderr: String,
pub success: bool,
pub code: i32,
}
impl CommandResult {
// 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> {
// Dedent the script
let dedented = sal_text::dedent(script_content);
// Create a temporary directory
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)?;
// For Unix systems, ensure the script has a shebang line with -e flag
#[cfg(any(target_os = "macos", target_os = "linux"))]
{
let script_with_shebang = if dedented.trim_start().starts_with("#!") {
// Script already has a shebang, use it as is
dedented
} else {
// 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"))]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(&script_path)
.map_err(|e| RunError::PermissionError(e))?
.permissions();
perms.set_mode(0o755); // rwxr-xr-x
fs::set_permissions(&script_path, perms).map_err(RunError::PermissionError)?;
}
Ok((script_path, interpreter, temp_dir))
}
/// Capture output from Child's stdio streams with optional printing
fn handle_child_output(mut child: Child, silent: bool) -> Result<CommandResult, RunError> {
// Prepare to read stdout & stderr line-by-line
let stdout = child.stdout.take();
let stderr = child.stderr.take();
// Process stdout
let stdout_handle = if let Some(out) = stdout {
let reader = BufReader::new(out);
let silent_clone = silent;
// Spawn a thread to capture and optionally print stdout
Some(std::thread::spawn(move || {
let mut local_buffer = String::new();
for line in reader.lines() {
if let Ok(l) = line {
// Print the line if not silent and flush immediately
if !silent_clone {
println!("{}", l);
std::io::stdout().flush().unwrap_or(());
}
// Store it in our captured buffer
local_buffer.push_str(&l);
local_buffer.push('\n');
}
}
local_buffer
}))
} else {
None
};
// Process stderr
let stderr_handle = if let Some(err) = stderr {
let reader = BufReader::new(err);
let silent_clone = silent;
// Spawn a thread to capture and optionally print stderr
Some(std::thread::spawn(move || {
let mut local_buffer = String::new();
for line in reader.lines() {
if let Ok(l) = line {
// Print the line if not silent and flush immediately
if !silent_clone {
// Print all stderr messages
eprintln!("\x1b[31mERROR: {}\x1b[0m", l); // Red color for errors
std::io::stderr().flush().unwrap_or(());
}
// Store it in our captured buffer
local_buffer.push_str(&l);
local_buffer.push('\n');
}
}
local_buffer
}))
} else {
None
};
// Wait for the child process to exit
let status = child.wait().map_err(|e| {
RunError::ChildProcessError(format!("Failed to wait on child process: {}", e))
})?;
// Join our stdout thread if it exists
let captured_stdout = if let Some(handle) = stdout_handle {
handle.join().unwrap_or_default()
} else {
"Failed to capture stdout".to_string()
};
// Join our stderr thread if it exists
let captured_stderr = if let Some(handle) = stderr_handle {
handle.join().unwrap_or_default()
} else {
"Failed to capture stderr".to_string()
};
// If the command failed, print the stderr if it wasn't already printed
if !status.success() && silent && !captured_stderr.is_empty() {
eprintln!("\x1b[31mCommand failed with error:\x1b[0m");
for line in captured_stderr.lines() {
eprintln!("\x1b[31m{}\x1b[0m", line);
}
}
// Return the command result
Ok(CommandResult {
stdout: captured_stdout,
stderr: captured_stderr,
success: status.success(),
code: status.code().unwrap_or(-1),
})
}
/// Processes Output structure from Command::output() into CommandResult
fn process_command_output(
output: Result<Output, std::io::Error>,
) -> Result<CommandResult, RunError> {
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)
);
}
Ok(CommandResult {
stdout,
stderr,
success: out.status.success(),
code: out.status.code().unwrap_or(-1),
})
}
Err(e) => Err(RunError::CommandExecutionFailed(e)),
}
}
/// Common logic for running a command with optional silent mode
fn run_command_internal(command: &str, silent: bool) -> Result<CommandResult, RunError> {
let mut parts = command.split_whitespace();
let cmd = match parts.next() {
Some(c) => c,
None => return Err(RunError::EmptyCommand),
};
let args: Vec<&str> = parts.collect();
// Spawn the child process with piped stdout & stderr
let child = Command::new(cmd)
.args(&args)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(RunError::CommandExecutionFailed)?;
handle_child_output(child, silent)
}
/// Execute a script with the given interpreter and path
fn execute_script_internal(
interpreter: &str,
script_path: &Path,
silent: bool,
) -> Result<CommandResult, RunError> {
#[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 result = process_command_output(output)?;
// If the script failed, return an error
if !result.success {
return Err(RunError::CommandFailed(format!(
"Script execution failed with exit code {}: {}",
result.code,
result.stderr.trim()
)));
}
Ok(result)
} else {
// For normal execution, spawn and handle the output streams
let child = Command::new(interpreter)
.args(&command_args)
.stdout(Stdio::piped())
.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!(
"Script execution failed with exit code {}: {}",
result.code,
result.stderr.trim()
)));
}
Ok(result)
}
}
/// Run a multiline script with optional silent mode
fn run_script_internal(script: &str, silent: bool) -> Result<CommandResult, RunError> {
// 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() {
println!("\x1b[36m{:3}: {}\x1b[0m", i + 1, line);
}
} else {
// Fallback to original script if reading fails
for (i, line) in script.lines().enumerate() {
println!("\x1b[36m{:3}: {}\x1b[0m", i + 1, line);
}
}
println!("\x1b[36m---\x1b[0m");
}
// _temp_dir is kept in scope until the end of this function to ensure
// it's not dropped prematurely, which would clean up the directory
// Execute the script and handle the result
let result = execute_script_internal(&interpreter, &script_path, silent);
// If there was an error, print a clear error message only if it's not a CommandFailed error
// (which would already have printed the stderr)
if let Err(ref e) = result {
if !matches!(e, RunError::CommandFailed(_)) {
eprintln!("\x1b[31mScript execution failed: {}\x1b[0m", e);
}
}
result
}
/// A builder for configuring and executing commands or scripts
pub struct RunBuilder<'a> {
/// The command or script to run
cmd: &'a str,
/// Whether to return an error if the command fails (default: true)
die: bool,
/// Whether to suppress output to stdout/stderr (default: false)
silent: bool,
/// Whether to run the command asynchronously (default: false)
async_exec: bool,
/// Whether to log command execution (default: false)
log: bool,
}
impl<'a> RunBuilder<'a> {
/// Create a new RunBuilder with default settings
pub fn new(cmd: &'a str) -> Self {
Self {
cmd,
die: true,
silent: false,
async_exec: false,
log: false,
}
}
/// Set whether to return an error if the command fails
pub fn die(mut self, die: bool) -> Self {
self.die = die;
self
}
/// Set whether to suppress output to stdout/stderr
pub fn silent(mut self, silent: bool) -> Self {
self.silent = silent;
self
}
/// Set whether to run the command asynchronously
pub fn async_exec(mut self, async_exec: bool) -> Self {
self.async_exec = async_exec;
self
}
/// Set whether to log command execution
pub fn log(mut self, log: bool) -> Self {
self.log = log;
self
}
/// Execute the command or script with the configured options
pub fn execute(self) -> Result<CommandResult, RunError> {
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
);
}
}
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(),
stderr: String::new(),
success: true,
code: 0,
});
}
// Execute the command or script
let result = if trimmed.contains('\n') {
// This is a multiline script
run_script_internal(trimmed, self.silent)
} else {
// 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) => {
// If the command failed but die is false, print a warning
if !res.success && !self.die && !self.silent {
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 {
// Convert error to CommandResult with success=false
Ok(CommandResult {
stdout: String::new(),
stderr: format!("Error: {}", e),
success: false,
code: -1,
})
}
}
}
}
}
/// Create a new RunBuilder for executing a command or script
pub fn run(cmd: &str) -> RunBuilder {
RunBuilder::new(cmd)
}
/// Run a command or multiline script with arguments
pub fn run_command(command: &str) -> Result<CommandResult, RunError> {
run(command).execute()
}
/// Run a command or multiline script with arguments silently
pub fn run_silent(command: &str) -> Result<CommandResult, RunError> {
run(command).silent(true).execute()
}

49
process/src/screen.rs Normal file
View File

@@ -0,0 +1,49 @@
use crate::run_command;
use anyhow::Result;
use std::fs;
/// Executes a command in a new screen session.
///
/// # Arguments
///
/// * `name` - The name of the screen session.
/// * `cmd` - The command to execute.
///
/// # Returns
///
/// * `Result<()>` - Ok if the command was executed successfully, otherwise an error.
pub fn new(name: &str, cmd: &str) -> Result<()> {
let script_path = format!("/tmp/cmd_{}.sh", name);
let mut script_content = String::new();
if !cmd.starts_with("#!") {
script_content.push_str("#!/bin/bash\n");
}
script_content.push_str("set -e\n");
script_content.push_str(cmd);
fs::write(&script_path, script_content)?;
fs::set_permissions(&script_path, std::os::unix::fs::PermissionsExt::from_mode(0o755))?;
let screen_cmd = format!("screen -d -m -S {} {}", name, script_path);
run_command(&screen_cmd)?;
Ok(())
}
/// Kills a screen session.
///
/// # Arguments
///
/// * `name` - The name of the screen session to kill.
///
/// # Returns
///
/// * `Result<()>` - Ok if the session was killed successfully, otherwise an error.
pub fn kill(name: &str) -> Result<()> {
let cmd = format!("screen -S {} -X quit", name);
run_command(&cmd)?;
std::thread::sleep(std::time::Duration::from_millis(500));
Ok(())
}

278
process/tests/mgmt_tests.rs Normal file
View File

@@ -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());
}

View File

@@ -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! ===");

View File

@@ -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! ===");

View File

@@ -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! ===");

View File

@@ -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! ===");

321
process/tests/rhai_tests.rs Normal file
View File

@@ -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);
}

251
process/tests/run_tests.rs Normal file
View File

@@ -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"));
}