feat: Add process package to monorepo
Some checks are pending
Rhai Tests / Run Rhai Tests (push) Waiting to run
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:
parent
511729c477
commit
3e3d0a1d45
@ -11,7 +11,7 @@ categories = ["os", "filesystem", "api-bindings"]
|
|||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = [".", "vault", "git", "redisclient", "mycelium", "text", "os", "net", "zinit_client"]
|
members = [".", "vault", "git", "redisclient", "mycelium", "text", "os", "net", "zinit_client", "process"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
hex = "0.4"
|
hex = "0.4"
|
||||||
@ -66,6 +66,7 @@ sal-text = { path = "text" }
|
|||||||
sal-os = { path = "os" }
|
sal-os = { path = "os" }
|
||||||
sal-net = { path = "net" }
|
sal-net = { path = "net" }
|
||||||
sal-zinit-client = { path = "zinit_client" }
|
sal-zinit-client = { path = "zinit_client" }
|
||||||
|
sal-process = { path = "process" }
|
||||||
|
|
||||||
# Optional features for specific OS functionality
|
# Optional features for specific OS functionality
|
||||||
[target.'cfg(unix)'.dependencies]
|
[target.'cfg(unix)'.dependencies]
|
||||||
|
@ -168,7 +168,7 @@ Convert packages in dependency order (leaf packages first):
|
|||||||
- ✅ **Production features**: Global client management, async operations, comprehensive error handling
|
- ✅ **Production features**: Global client management, async operations, comprehensive error handling
|
||||||
- ✅ **Quality assurance**: All meaningless assertions replaced with meaningful validations
|
- ✅ **Quality assurance**: All meaningless assertions replaced with meaningful validations
|
||||||
- ✅ **Integration verified**: Herodo integration and test suite integration confirmed
|
- ✅ **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
|
#### 3.3 Higher-level Packages
|
||||||
- [ ] **virt** → sal-virt (depends on process, os)
|
- [ ] **virt** → sal-virt (depends on process, os)
|
||||||
|
@ -108,7 +108,3 @@ cargo test -- --nocapture
|
|||||||
- `base64` - Message encoding
|
- `base64` - Message encoding
|
||||||
- `tokio` - Async runtime
|
- `tokio` - Async runtime
|
||||||
- `rhai` - Scripting support
|
- `rhai` - Scripting support
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
Apache-2.0
|
|
||||||
|
@ -98,7 +98,3 @@ if is_linux() {
|
|||||||
print("Running on Linux");
|
print("Running on Linux");
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0.
|
|
||||||
|
31
process/Cargo.toml
Normal file
31
process/Cargo.toml
Normal 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
178
process/README.md
Normal 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
22
process/src/lib.rs
Normal 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};
|
@ -72,7 +72,7 @@ pub struct ProcessInfo {
|
|||||||
* # Examples
|
* # Examples
|
||||||
*
|
*
|
||||||
* ```
|
* ```
|
||||||
* use sal::process::which;
|
* use sal_process::which;
|
||||||
*
|
*
|
||||||
* match which("git") {
|
* match which("git") {
|
||||||
* Some(path) => println!("Git is installed at: {}", path),
|
* Some(path) => println!("Git is installed at: {}", path),
|
||||||
@ -118,7 +118,7 @@ pub fn which(cmd: &str) -> Option<String> {
|
|||||||
*
|
*
|
||||||
* ```
|
* ```
|
||||||
* // Kill all processes with "server" in their name
|
* // Kill all processes with "server" in their name
|
||||||
* use sal::process::kill;
|
* use sal_process::kill;
|
||||||
*
|
*
|
||||||
* fn main() -> Result<(), Box<dyn std::error::Error>> {
|
* fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
* let result = kill("server")?;
|
* let result = kill("server")?;
|
||||||
@ -210,7 +210,7 @@ pub fn kill(pattern: &str) -> Result<String, ProcessError> {
|
|||||||
*
|
*
|
||||||
* ```
|
* ```
|
||||||
* // List all processes
|
* // List all processes
|
||||||
* use sal::process::process_list;
|
* use sal_process::process_list;
|
||||||
*
|
*
|
||||||
* fn main() -> Result<(), Box<dyn std::error::Error>> {
|
* fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
* let processes = process_list("")?;
|
* let processes = process_list("")?;
|
||||||
@ -328,7 +328,7 @@ pub fn process_list(pattern: &str) -> Result<Vec<ProcessInfo>, ProcessError> {
|
|||||||
* # Examples
|
* # Examples
|
||||||
*
|
*
|
||||||
* ```no_run
|
* ```no_run
|
||||||
* use sal::process::process_get;
|
* use sal_process::process_get;
|
||||||
*
|
*
|
||||||
* fn main() -> Result<(), Box<dyn std::error::Error>> {
|
* fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
* let process = process_get("unique-server-name")?;
|
* let process = process_get("unique-server-name")?;
|
@ -2,7 +2,7 @@
|
|||||||
//!
|
//!
|
||||||
//! This module provides Rhai wrappers for the functions in the Process module.
|
//! 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 rhai::{Array, Dynamic, Engine, EvalAltResult, Map};
|
||||||
use std::clone::Clone;
|
use std::clone::Clone;
|
||||||
|
|
@ -1,13 +1,13 @@
|
|||||||
use std::io::{BufRead, BufReader, Write};
|
use std::error::Error;
|
||||||
|
use std::fmt;
|
||||||
use std::fs::{self, File};
|
use std::fs::{self, File};
|
||||||
|
use std::io;
|
||||||
|
use std::io::{BufRead, BufReader, Write};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::{Child, Command, Output, Stdio};
|
use std::process::{Child, Command, Output, Stdio};
|
||||||
use std::fmt;
|
|
||||||
use std::error::Error;
|
|
||||||
use std::io;
|
|
||||||
use std::thread;
|
use std::thread;
|
||||||
|
|
||||||
use crate::text;
|
use sal_text;
|
||||||
|
|
||||||
/// Error type for command and script execution operations
|
/// Error type for command and script execution operations
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@ -41,7 +41,9 @@ impl fmt::Display for RunError {
|
|||||||
RunError::CommandFailed(e) => write!(f, "{}", e),
|
RunError::CommandFailed(e) => write!(f, "{}", e),
|
||||||
RunError::ScriptPreparationFailed(e) => write!(f, "{}", e),
|
RunError::ScriptPreparationFailed(e) => write!(f, "{}", e),
|
||||||
RunError::ChildProcessError(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::FileCreationFailed(e) => write!(f, "Failed to create script file: {}", e),
|
||||||
RunError::FileWriteFailed(e) => write!(f, "Failed to write to 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),
|
RunError::PermissionError(e) => write!(f, "Failed to set file permissions: {}", e),
|
||||||
@ -73,25 +75,18 @@ pub struct CommandResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl CommandResult {
|
impl CommandResult {
|
||||||
/// Create a default failed result with an error message
|
// Implementation methods can be added here as needed
|
||||||
fn _error(message: &str) -> Self {
|
|
||||||
Self {
|
|
||||||
stdout: String::new(),
|
|
||||||
stderr: message.to_string(),
|
|
||||||
success: false,
|
|
||||||
code: -1,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Prepare a script file and return the path and interpreter
|
/// 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
|
// Dedent the script
|
||||||
let dedented = text::dedent(script_content);
|
let dedented = sal_text::dedent(script_content);
|
||||||
|
|
||||||
// Create a temporary directory
|
// Create a temporary directory
|
||||||
let temp_dir = tempfile::tempdir()
|
let temp_dir = tempfile::tempdir().map_err(RunError::TempDirCreationFailed)?;
|
||||||
.map_err(RunError::TempDirCreationFailed)?;
|
|
||||||
|
|
||||||
// Determine script extension and interpreter
|
// Determine script extension and interpreter
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
@ -102,8 +97,7 @@ fn prepare_script_file(script_content: &str) -> Result<(PathBuf, String, tempfil
|
|||||||
|
|
||||||
// Create the script file
|
// Create the script file
|
||||||
let script_path = temp_dir.path().join(format!("script{}", ext));
|
let script_path = temp_dir.path().join(format!("script{}", ext));
|
||||||
let mut file = File::create(&script_path)
|
let mut file = File::create(&script_path).map_err(RunError::FileCreationFailed)?;
|
||||||
.map_err(RunError::FileCreationFailed)?;
|
|
||||||
|
|
||||||
// For Unix systems, ensure the script has a shebang line with -e flag
|
// For Unix systems, ensure the script has a shebang line with -e flag
|
||||||
#[cfg(any(target_os = "macos", target_os = "linux"))]
|
#[cfg(any(target_os = "macos", target_os = "linux"))]
|
||||||
@ -136,8 +130,7 @@ fn prepare_script_file(script_content: &str) -> Result<(PathBuf, String, tempfil
|
|||||||
.map_err(|e| RunError::PermissionError(e))?
|
.map_err(|e| RunError::PermissionError(e))?
|
||||||
.permissions();
|
.permissions();
|
||||||
perms.set_mode(0o755); // rwxr-xr-x
|
perms.set_mode(0o755); // rwxr-xr-x
|
||||||
fs::set_permissions(&script_path, perms)
|
fs::set_permissions(&script_path, perms).map_err(RunError::PermissionError)?;
|
||||||
.map_err(RunError::PermissionError)?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok((script_path, interpreter, temp_dir))
|
Ok((script_path, interpreter, temp_dir))
|
||||||
@ -201,8 +194,9 @@ fn handle_child_output(mut child: Child, silent: bool) -> Result<CommandResult,
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Wait for the child process to exit
|
// Wait for the child process to exit
|
||||||
let status = child.wait()
|
let status = child.wait().map_err(|e| {
|
||||||
.map_err(|e| RunError::ChildProcessError(format!("Failed to wait on child process: {}", e)))?;
|
RunError::ChildProcessError(format!("Failed to wait on child process: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
// Join our stdout thread if it exists
|
// Join our stdout thread if it exists
|
||||||
let captured_stdout = if let Some(handle) = stdout_handle {
|
let captured_stdout = if let Some(handle) = stdout_handle {
|
||||||
@ -236,7 +230,9 @@ fn handle_child_output(mut child: Child, silent: bool) -> Result<CommandResult,
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Processes Output structure from Command::output() into CommandResult
|
/// Processes Output structure from Command::output() into CommandResult
|
||||||
fn process_command_output(output: Result<Output, std::io::Error>) -> Result<CommandResult, RunError> {
|
fn process_command_output(
|
||||||
|
output: Result<Output, std::io::Error>,
|
||||||
|
) -> Result<CommandResult, RunError> {
|
||||||
match output {
|
match output {
|
||||||
Ok(out) => {
|
Ok(out) => {
|
||||||
let stdout = String::from_utf8_lossy(&out.stdout).to_string();
|
let stdout = String::from_utf8_lossy(&out.stdout).to_string();
|
||||||
@ -246,8 +242,10 @@ fn process_command_output(output: Result<Output, std::io::Error>) -> Result<Comm
|
|||||||
|
|
||||||
// If the command failed, print a clear error message
|
// If the command failed, print a clear error message
|
||||||
if !out.status.success() {
|
if !out.status.success() {
|
||||||
eprintln!("\x1b[31mCommand failed with exit code: {}\x1b[0m",
|
eprintln!(
|
||||||
out.status.code().unwrap_or(-1));
|
"\x1b[31mCommand failed with exit code: {}\x1b[0m",
|
||||||
|
out.status.code().unwrap_or(-1)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(CommandResult {
|
Ok(CommandResult {
|
||||||
@ -256,7 +254,7 @@ fn process_command_output(output: Result<Output, std::io::Error>) -> Result<Comm
|
|||||||
success: out.status.success(),
|
success: out.status.success(),
|
||||||
code: out.status.code().unwrap_or(-1),
|
code: out.status.code().unwrap_or(-1),
|
||||||
})
|
})
|
||||||
},
|
}
|
||||||
Err(e) => Err(RunError::CommandExecutionFailed(e)),
|
Err(e) => Err(RunError::CommandExecutionFailed(e)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -283,7 +281,11 @@ fn run_command_internal(command: &str, silent: bool) -> Result<CommandResult, Ru
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Execute a script with the given interpreter and path
|
/// Execute a script with the given interpreter and path
|
||||||
fn execute_script_internal(interpreter: &str, script_path: &Path, silent: bool) -> Result<CommandResult, RunError> {
|
fn execute_script_internal(
|
||||||
|
interpreter: &str,
|
||||||
|
script_path: &Path,
|
||||||
|
silent: bool,
|
||||||
|
) -> Result<CommandResult, RunError> {
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
let command_args = vec!["/c", script_path.to_str().unwrap_or("")];
|
let command_args = vec!["/c", script_path.to_str().unwrap_or("")];
|
||||||
|
|
||||||
@ -292,9 +294,7 @@ fn execute_script_internal(interpreter: &str, script_path: &Path, silent: bool)
|
|||||||
|
|
||||||
if silent {
|
if silent {
|
||||||
// For silent execution, use output() which captures but doesn't display
|
// For silent execution, use output() which captures but doesn't display
|
||||||
let output = Command::new(interpreter)
|
let output = Command::new(interpreter).args(&command_args).output();
|
||||||
.args(&command_args)
|
|
||||||
.output();
|
|
||||||
|
|
||||||
let result = process_command_output(output)?;
|
let result = process_command_output(output)?;
|
||||||
|
|
||||||
@ -456,9 +456,12 @@ impl<'a> RunBuilder<'a> {
|
|||||||
if res.success {
|
if res.success {
|
||||||
println!("\x1b[32m[ASYNC] Command completed successfully\x1b[0m");
|
println!("\x1b[32m[ASYNC] Command completed successfully\x1b[0m");
|
||||||
} else {
|
} 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) => {
|
Err(e) => {
|
||||||
eprintln!("\x1b[31m[ASYNC] Command failed with error: {}\x1b[0m", e);
|
eprintln!("\x1b[31m[ASYNC] Command failed with error: {}\x1b[0m", e);
|
||||||
}
|
}
|
||||||
@ -492,7 +495,7 @@ impl<'a> RunBuilder<'a> {
|
|||||||
eprintln!("\x1b[33mWarning: Command failed with exit code {} but 'die' is false\x1b[0m", res.code);
|
eprintln!("\x1b[33mWarning: Command failed with exit code {} but 'die' is false\x1b[0m", res.code);
|
||||||
}
|
}
|
||||||
Ok(res)
|
Ok(res)
|
||||||
},
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
// Print the error only if it's not a CommandFailed error
|
// Print the error only if it's not a CommandFailed error
|
||||||
// (which would already have printed the stderr)
|
// (which would already have printed the stderr)
|
@ -1,4 +1,4 @@
|
|||||||
use crate::process::run_command;
|
use crate::run_command;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
|
278
process/tests/mgmt_tests.rs
Normal file
278
process/tests/mgmt_tests.rs
Normal 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());
|
||||||
|
}
|
119
process/tests/rhai/01_command_execution.rhai
Normal file
119
process/tests/rhai/01_command_execution.rhai
Normal 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! ===");
|
153
process/tests/rhai/02_process_management.rhai
Normal file
153
process/tests/rhai/02_process_management.rhai
Normal 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! ===");
|
167
process/tests/rhai/03_error_handling.rhai
Normal file
167
process/tests/rhai/03_error_handling.rhai
Normal 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! ===");
|
326
process/tests/rhai/04_real_world_scenarios.rhai
Normal file
326
process/tests/rhai/04_real_world_scenarios.rhai
Normal 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
321
process/tests/rhai_tests.rs
Normal 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
251
process/tests/run_tests.rs
Normal 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"));
|
||||||
|
}
|
@ -42,7 +42,7 @@ pub use sal_mycelium as mycelium;
|
|||||||
pub use sal_net as net;
|
pub use sal_net as net;
|
||||||
pub use sal_os as os;
|
pub use sal_os as os;
|
||||||
pub mod postgresclient;
|
pub mod postgresclient;
|
||||||
pub mod process;
|
pub use sal_process as process;
|
||||||
pub use sal_redisclient as redisclient;
|
pub use sal_redisclient as redisclient;
|
||||||
pub mod rhai;
|
pub mod rhai;
|
||||||
pub use sal_text as text;
|
pub use sal_text as text;
|
||||||
|
@ -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<CommandResult, RunError>`: 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<CommandResult, RunError>`. 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<String>`:
|
|
||||||
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<String, ProcessError>`:
|
|
||||||
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<Vec<ProcessInfo>, 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<ProcessInfo, ProcessError>`:
|
|
||||||
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<dyn std::error::Error>> {
|
|
||||||
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<dyn std::error::Error>> {
|
|
||||||
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<dyn std::error::Error>> {
|
|
||||||
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(())
|
|
||||||
}
|
|
||||||
```
|
|
@ -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};
|
|
@ -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"));
|
|
||||||
}
|
|
||||||
}
|
|
@ -10,10 +10,8 @@ mod nerdctl;
|
|||||||
// OS module is now provided by sal-os package
|
// OS module is now provided by sal-os package
|
||||||
// Platform module is now provided by sal-os package
|
// Platform module is now provided by sal-os package
|
||||||
mod postgresclient;
|
mod postgresclient;
|
||||||
mod process;
|
|
||||||
|
|
||||||
mod rfs;
|
mod rfs;
|
||||||
mod screen;
|
|
||||||
mod vault;
|
mod vault;
|
||||||
// zinit module is now in sal-zinit-client package
|
// 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
|
// Re-export PostgreSQL client module registration function
|
||||||
pub use postgresclient::register_postgresclient_module;
|
pub use postgresclient::register_postgresclient_module;
|
||||||
|
|
||||||
pub use process::{
|
pub use sal_process::rhai::{
|
||||||
kill,
|
kill,
|
||||||
process_get,
|
process_get,
|
||||||
process_list,
|
process_list,
|
||||||
@ -138,7 +136,7 @@ pub fn register(engine: &mut Engine) -> Result<(), Box<rhai::EvalAltResult>> {
|
|||||||
sal_os::rhai::register_os_module(engine)?;
|
sal_os::rhai::register_os_module(engine)?;
|
||||||
|
|
||||||
// Register Process module functions
|
// Register Process module functions
|
||||||
process::register_process_module(engine)?;
|
sal_process::rhai::register_process_module(engine)?;
|
||||||
|
|
||||||
// Register Buildah module functions
|
// Register Buildah module functions
|
||||||
buildah::register_bah_module(engine)?;
|
buildah::register_bah_module(engine)?;
|
||||||
@ -175,8 +173,7 @@ pub fn register(engine: &mut Engine) -> Result<(), Box<rhai::EvalAltResult>> {
|
|||||||
|
|
||||||
// Platform functions are now registered by sal-os package
|
// Platform functions are now registered by sal-os package
|
||||||
|
|
||||||
// Register Screen module functions
|
// Screen module functions are now part of sal-process package
|
||||||
screen::register(engine);
|
|
||||||
|
|
||||||
// Register utility functions
|
// Register utility functions
|
||||||
engine.register_fn("is_def_fn", |_name: &str| -> bool {
|
engine.register_fn("is_def_fn", |_name: &str| -> bool {
|
||||||
|
@ -1,22 +0,0 @@
|
|||||||
use crate::process::{kill_screen, new_screen};
|
|
||||||
use rhai::{Engine, EvalAltResult};
|
|
||||||
|
|
||||||
fn screen_error_to_rhai_error<T>(result: anyhow::Result<T>) -> Result<T, Box<EvalAltResult>> {
|
|
||||||
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))
|
|
||||||
});
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user