development_monorepo #13
| @@ -11,7 +11,7 @@ categories = ["os", "filesystem", "api-bindings"] | ||||
| readme = "README.md" | ||||
|  | ||||
| [workspace] | ||||
| members = [".", "vault", "git", "redisclient", "mycelium", "text"] | ||||
| members = [".", "vault", "git", "redisclient", "mycelium", "text", "os"] | ||||
|  | ||||
| [dependencies] | ||||
| hex = "0.4" | ||||
| @@ -64,6 +64,7 @@ sal-git = { path = "git" } | ||||
| sal-redisclient = { path = "redisclient" } | ||||
| sal-mycelium = { path = "mycelium" } | ||||
| sal-text = { path = "text" } | ||||
| sal-os = { path = "os" } | ||||
|  | ||||
| # Optional features for specific OS functionality | ||||
| [target.'cfg(unix)'.dependencies] | ||||
|   | ||||
							
								
								
									
										36
									
								
								os/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								os/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | ||||
| [package] | ||||
| name = "sal-os" | ||||
| version = "0.1.0" | ||||
| edition = "2021" | ||||
| authors = ["PlanetFirst <info@incubaid.com>"] | ||||
| description = "SAL OS - Operating system interaction utilities with cross-platform abstraction" | ||||
| repository = "https://git.threefold.info/herocode/sal" | ||||
| license = "Apache-2.0" | ||||
| keywords = ["system", "os", "filesystem", "download", "package-management"] | ||||
| categories = ["os", "filesystem", "api-bindings"] | ||||
|  | ||||
| [dependencies] | ||||
| # Core dependencies for file system operations | ||||
| dirs = "6.0.0" | ||||
| glob = "0.3.1" | ||||
| libc = "0.2" | ||||
|  | ||||
| # Error handling | ||||
| thiserror = "2.0.12" | ||||
|  | ||||
| # Rhai scripting support | ||||
| rhai = { version = "1.12.0", features = ["sync"] } | ||||
|  | ||||
| # 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" | ||||
							
								
								
									
										104
									
								
								os/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										104
									
								
								os/README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,104 @@ | ||||
| # SAL OS Package (`sal-os`) | ||||
|  | ||||
| The `sal-os` package provides a comprehensive suite of operating system interaction utilities. It offers a cross-platform abstraction layer for common OS-level tasks, simplifying system programming in Rust. | ||||
|  | ||||
| ## Features | ||||
|  | ||||
| - **File System Operations**: Comprehensive file and directory manipulation | ||||
| - **Download Utilities**: File downloading with automatic extraction support | ||||
| - **Package Management**: System package manager integration | ||||
| - **Platform Detection**: Cross-platform OS and architecture detection | ||||
| - **Rhai Integration**: Full scripting support for all OS operations | ||||
|  | ||||
| ## Modules | ||||
|  | ||||
| - `fs`: File system operations (create, copy, delete, find, etc.) | ||||
| - `download`: File downloading and basic installation | ||||
| - `package`: System package management | ||||
| - `platform`: Platform and architecture detection | ||||
|  | ||||
| ## Usage | ||||
|  | ||||
| Add this to your `Cargo.toml`: | ||||
|  | ||||
| ```toml | ||||
| [dependencies] | ||||
| sal-os = "0.1.0" | ||||
| ``` | ||||
|  | ||||
| ### File System Operations | ||||
|  | ||||
| ```rust | ||||
| use sal_os::fs; | ||||
|  | ||||
| fn main() -> Result<(), Box<dyn std::error::Error>> { | ||||
|     // Create directory | ||||
|     fs::mkdir("my_dir")?; | ||||
|      | ||||
|     // Write and read files | ||||
|     fs::file_write("my_dir/example.txt", "Hello from SAL!")?; | ||||
|     let content = fs::file_read("my_dir/example.txt")?; | ||||
|      | ||||
|     // Find files | ||||
|     let files = fs::find_files(".", "*.txt")?; | ||||
|      | ||||
|     Ok(()) | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ### Download Operations | ||||
|  | ||||
| ```rust | ||||
| use sal_os::download; | ||||
|  | ||||
| fn main() -> Result<(), Box<dyn std::error::Error>> { | ||||
|     // Download and extract archive | ||||
|     let path = download::download("https://example.com/archive.tar.gz", "/tmp", 1024)?; | ||||
|      | ||||
|     // Download specific file | ||||
|     download::download_file("https://example.com/script.sh", "/tmp/script.sh", 0)?; | ||||
|     download::chmod_exec("/tmp/script.sh")?; | ||||
|      | ||||
|     Ok(()) | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ### Platform Detection | ||||
|  | ||||
| ```rust | ||||
| use sal_os::platform; | ||||
|  | ||||
| fn main() { | ||||
|     if platform::is_linux() { | ||||
|         println!("Running on Linux"); | ||||
|     } | ||||
|      | ||||
|     if platform::is_arm() { | ||||
|         println!("ARM architecture detected"); | ||||
|     } | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ## Rhai Integration | ||||
|  | ||||
| The package provides full Rhai scripting support: | ||||
|  | ||||
| ```rhai | ||||
| // File operations | ||||
| mkdir("test_dir"); | ||||
| file_write("test_dir/hello.txt", "Hello World!"); | ||||
| let content = file_read("test_dir/hello.txt"); | ||||
|  | ||||
| // Download operations | ||||
| download("https://example.com/file.zip", "/tmp", 0); | ||||
| chmod_exec("/tmp/script.sh"); | ||||
|  | ||||
| // Platform detection | ||||
| if is_linux() { | ||||
|     print("Running on Linux"); | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ## License | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0. | ||||
							
								
								
									
										13
									
								
								os/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								os/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| pub mod download; | ||||
| pub mod fs; | ||||
| pub mod package; | ||||
| pub mod platform; | ||||
|  | ||||
| // Re-export all public functions and types | ||||
| pub use download::*; | ||||
| pub use fs::*; | ||||
| pub use package::*; | ||||
| pub use platform::*; | ||||
|  | ||||
| // Rhai integration module | ||||
| pub mod rhai; | ||||
| @@ -1,6 +1,14 @@ | ||||
| use crate::process::CommandResult; | ||||
| use std::process::Command; | ||||
| 
 | ||||
| /// A structure to hold command execution results
 | ||||
| #[derive(Debug, Clone)] | ||||
| pub struct CommandResult { | ||||
|     pub stdout: String, | ||||
|     pub stderr: String, | ||||
|     pub success: bool, | ||||
|     pub code: i32, | ||||
| } | ||||
| 
 | ||||
| /// Error type for package management operations
 | ||||
| #[derive(Debug)] | ||||
| pub enum PackageError { | ||||
| @@ -1,4 +1,16 @@ | ||||
| use crate::rhai::error::SalError; | ||||
| use thiserror::Error; | ||||
| 
 | ||||
| #[derive(Debug, Error)] | ||||
| pub enum PlatformError { | ||||
|     #[error("{0}: {1}")] | ||||
|     Generic(String, String), | ||||
| } | ||||
| 
 | ||||
| impl PlatformError { | ||||
|     pub fn new(kind: &str, message: &str) -> Self { | ||||
|         PlatformError::Generic(kind.to_string(), message.to_string()) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[cfg(target_os = "macos")] | ||||
| pub fn is_osx() -> bool { | ||||
| @@ -40,24 +52,24 @@ pub fn is_x86() -> bool { | ||||
|     false | ||||
| } | ||||
| 
 | ||||
| pub fn check_linux_x86() -> Result<(), SalError> { | ||||
| pub fn check_linux_x86() -> Result<(), PlatformError> { | ||||
|     if is_linux() && is_x86() { | ||||
|         Ok(()) | ||||
|     } else { | ||||
|         Err(SalError::Generic( | ||||
|             "Platform Check Error".to_string(), | ||||
|             "This operation is only supported on Linux x86_64.".to_string(), | ||||
|         Err(PlatformError::new( | ||||
|             "Platform Check Error", | ||||
|             "This operation is only supported on Linux x86_64.", | ||||
|         )) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| pub fn check_macos_arm() -> Result<(), SalError> { | ||||
| pub fn check_macos_arm() -> Result<(), PlatformError> { | ||||
|     if is_osx() && is_arm() { | ||||
|         Ok(()) | ||||
|     } else { | ||||
|         Err(SalError::Generic( | ||||
|             "Platform Check Error".to_string(), | ||||
|             "This operation is only supported on macOS ARM.".to_string(), | ||||
|         Err(PlatformError::new( | ||||
|             "Platform Check Error", | ||||
|             "This operation is only supported on macOS ARM.", | ||||
|         )) | ||||
|     } | ||||
| } | ||||
| @@ -2,10 +2,25 @@ | ||||
| //!
 | ||||
| //! This module provides Rhai wrappers for the functions in the OS module.
 | ||||
| 
 | ||||
| use rhai::{Engine, EvalAltResult, Array}; | ||||
| use crate::os; | ||||
| use crate::os::package::PackHero; | ||||
| use super::error::{ToRhaiError, register_error_types}; | ||||
| use crate::package::PackHero; | ||||
| use crate::{download as dl, fs, package}; | ||||
| use rhai::{Array, Engine, EvalAltResult, Position}; | ||||
| 
 | ||||
| /// A trait for converting a Result to a Rhai-compatible error
 | ||||
| pub trait ToRhaiError<T> { | ||||
|     fn to_rhai_error(self) -> Result<T, Box<EvalAltResult>>; | ||||
| } | ||||
| 
 | ||||
| impl<T, E: std::error::Error> ToRhaiError<T> for Result<T, E> { | ||||
|     fn to_rhai_error(self) -> Result<T, Box<EvalAltResult>> { | ||||
|         self.map_err(|e| { | ||||
|             Box::new(EvalAltResult::ErrorRuntime( | ||||
|                 e.to_string().into(), | ||||
|                 Position::NONE, | ||||
|             )) | ||||
|         }) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /// Register OS module functions with the Rhai engine
 | ||||
| ///
 | ||||
| @@ -17,9 +32,6 @@ use super::error::{ToRhaiError, register_error_types}; | ||||
| ///
 | ||||
| /// * `Result<(), Box<EvalAltResult>>` - Ok if registration was successful, Err otherwise
 | ||||
| pub fn register_os_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> { | ||||
|     // Register error types
 | ||||
|     register_error_types(engine)?; | ||||
|     
 | ||||
|     // Register file system functions
 | ||||
|     engine.register_fn("copy", copy); | ||||
|     engine.register_fn("copy_bin", copy_bin); | ||||
| @@ -61,6 +73,14 @@ pub fn register_os_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> | ||||
|     engine.register_fn("package_set_debug", package_set_debug); | ||||
|     engine.register_fn("package_platform", package_platform); | ||||
| 
 | ||||
|     // Register platform detection functions
 | ||||
|     engine.register_fn("platform_is_osx", platform_is_osx); | ||||
|     engine.register_fn("platform_is_linux", platform_is_linux); | ||||
|     engine.register_fn("platform_is_arm", platform_is_arm); | ||||
|     engine.register_fn("platform_is_x86", platform_is_x86); | ||||
|     engine.register_fn("platform_check_linux_x86", platform_check_linux_x86); | ||||
|     engine.register_fn("platform_check_macos_arm", platform_check_macos_arm); | ||||
| 
 | ||||
|     Ok(()) | ||||
| } | ||||
| 
 | ||||
| @@ -68,39 +88,39 @@ pub fn register_os_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> | ||||
| // File System Function Wrappers
 | ||||
| //
 | ||||
| 
 | ||||
| /// Wrapper for os::copy
 | ||||
| /// Wrapper for fs::copy
 | ||||
| ///
 | ||||
| /// Recursively copy a file or directory from source to destination.
 | ||||
| pub fn copy(src: &str, dest: &str) -> Result<String, Box<EvalAltResult>> { | ||||
|     os::copy(src, dest).to_rhai_error() | ||||
|     fs::copy(src, dest).to_rhai_error() | ||||
| } | ||||
| 
 | ||||
| /// Wrapper for os::copy_bin
 | ||||
| /// Wrapper for fs::copy_bin
 | ||||
| ///
 | ||||
| /// Copy a binary to the correct location based on OS and user privileges.
 | ||||
| pub fn copy_bin(src: &str) -> Result<String, Box<EvalAltResult>> { | ||||
|     os::copy_bin(src).to_rhai_error() | ||||
|     fs::copy_bin(src).to_rhai_error() | ||||
| } | ||||
| 
 | ||||
| /// Wrapper for os::exist
 | ||||
| /// Wrapper for fs::exist
 | ||||
| ///
 | ||||
| /// Check if a file or directory exists.
 | ||||
| pub fn exist(path: &str) -> bool { | ||||
|     os::exist(path) | ||||
|     fs::exist(path) | ||||
| } | ||||
| 
 | ||||
| /// Wrapper for os::find_file
 | ||||
| /// Wrapper for fs::find_file
 | ||||
| ///
 | ||||
| /// Find a file in a directory (with support for wildcards).
 | ||||
| pub fn find_file(dir: &str, filename: &str) -> Result<String, Box<EvalAltResult>> { | ||||
|     os::find_file(dir, filename).to_rhai_error() | ||||
|     fs::find_file(dir, filename).to_rhai_error() | ||||
| } | ||||
| 
 | ||||
| /// Wrapper for os::find_files
 | ||||
| /// Wrapper for fs::find_files
 | ||||
| ///
 | ||||
| /// Find multiple files in a directory (recursive, with support for wildcards).
 | ||||
| pub fn find_files(dir: &str, filename: &str) -> Result<Array, Box<EvalAltResult>> { | ||||
|     let files = os::find_files(dir, filename).to_rhai_error()?; | ||||
|     let files = fs::find_files(dir, filename).to_rhai_error()?; | ||||
| 
 | ||||
|     // Convert Vec<String> to Rhai Array
 | ||||
|     let mut array = Array::new(); | ||||
| @@ -111,18 +131,18 @@ pub fn find_files(dir: &str, filename: &str) -> Result<Array, Box<EvalAltResult> | ||||
|     Ok(array) | ||||
| } | ||||
| 
 | ||||
| /// Wrapper for os::find_dir
 | ||||
| /// Wrapper for fs::find_dir
 | ||||
| ///
 | ||||
| /// Find a directory in a parent directory (with support for wildcards).
 | ||||
| pub fn find_dir(dir: &str, dirname: &str) -> Result<String, Box<EvalAltResult>> { | ||||
|     os::find_dir(dir, dirname).to_rhai_error() | ||||
|     fs::find_dir(dir, dirname).to_rhai_error() | ||||
| } | ||||
| 
 | ||||
| /// Wrapper for os::find_dirs
 | ||||
| /// Wrapper for fs::find_dirs
 | ||||
| ///
 | ||||
| /// Find multiple directories in a parent directory (recursive, with support for wildcards).
 | ||||
| pub fn find_dirs(dir: &str, dirname: &str) -> Result<Array, Box<EvalAltResult>> { | ||||
|     let dirs = os::find_dirs(dir, dirname).to_rhai_error()?; | ||||
|     let dirs = fs::find_dirs(dir, dirname).to_rhai_error()?; | ||||
| 
 | ||||
|     // Convert Vec<String> to Rhai Array
 | ||||
|     let mut array = Array::new(); | ||||
| @@ -133,67 +153,67 @@ pub fn find_dirs(dir: &str, dirname: &str) -> Result<Array, Box<EvalAltResult>> | ||||
|     Ok(array) | ||||
| } | ||||
| 
 | ||||
| /// Wrapper for os::delete
 | ||||
| /// Wrapper for fs::delete
 | ||||
| ///
 | ||||
| /// Delete a file or directory (defensive - doesn't error if file doesn't exist).
 | ||||
| pub fn delete(path: &str) -> Result<String, Box<EvalAltResult>> { | ||||
|     os::delete(path).to_rhai_error() | ||||
|     fs::delete(path).to_rhai_error() | ||||
| } | ||||
| 
 | ||||
| /// Wrapper for os::mkdir
 | ||||
| /// Wrapper for fs::mkdir
 | ||||
| ///
 | ||||
| /// Create a directory and all parent directories (defensive - doesn't error if directory exists).
 | ||||
| pub fn mkdir(path: &str) -> Result<String, Box<EvalAltResult>> { | ||||
|     os::mkdir(path).to_rhai_error() | ||||
|     fs::mkdir(path).to_rhai_error() | ||||
| } | ||||
| 
 | ||||
| /// Wrapper for os::file_size
 | ||||
| /// Wrapper for fs::file_size
 | ||||
| ///
 | ||||
| /// Get the size of a file in bytes.
 | ||||
| pub fn file_size(path: &str) -> Result<i64, Box<EvalAltResult>> { | ||||
|     os::file_size(path).to_rhai_error() | ||||
|     fs::file_size(path).to_rhai_error() | ||||
| } | ||||
| 
 | ||||
| /// Wrapper for os::rsync
 | ||||
| /// Wrapper for fs::rsync
 | ||||
| ///
 | ||||
| /// Sync directories using rsync (or platform equivalent).
 | ||||
| pub fn rsync(src: &str, dest: &str) -> Result<String, Box<EvalAltResult>> { | ||||
|     os::rsync(src, dest).to_rhai_error() | ||||
|     fs::rsync(src, dest).to_rhai_error() | ||||
| } | ||||
| 
 | ||||
| /// Wrapper for os::chdir
 | ||||
| /// Wrapper for fs::chdir
 | ||||
| ///
 | ||||
| /// Change the current working directory.
 | ||||
| pub fn chdir(path: &str) -> Result<String, Box<EvalAltResult>> { | ||||
|     os::chdir(path).to_rhai_error() | ||||
|     fs::chdir(path).to_rhai_error() | ||||
| } | ||||
| 
 | ||||
| /// Wrapper for os::file_read
 | ||||
| /// Wrapper for fs::file_read
 | ||||
| ///
 | ||||
| /// Read the contents of a file.
 | ||||
| pub fn file_read(path: &str) -> Result<String, Box<EvalAltResult>> { | ||||
|     os::file_read(path).to_rhai_error() | ||||
|     fs::file_read(path).to_rhai_error() | ||||
| } | ||||
| 
 | ||||
| /// Wrapper for os::file_write
 | ||||
| /// Wrapper for fs::file_write
 | ||||
| ///
 | ||||
| /// Write content to a file (creates the file if it doesn't exist, overwrites if it does).
 | ||||
| pub fn file_write(path: &str, content: &str) -> Result<String, Box<EvalAltResult>> { | ||||
|     os::file_write(path, content).to_rhai_error() | ||||
|     fs::file_write(path, content).to_rhai_error() | ||||
| } | ||||
| 
 | ||||
| /// Wrapper for os::file_write_append
 | ||||
| /// Wrapper for fs::file_write_append
 | ||||
| ///
 | ||||
| /// Append content to a file (creates the file if it doesn't exist).
 | ||||
| pub fn file_write_append(path: &str, content: &str) -> Result<String, Box<EvalAltResult>> { | ||||
|     os::file_write_append(path, content).to_rhai_error() | ||||
|     fs::file_write_append(path, content).to_rhai_error() | ||||
| } | ||||
| 
 | ||||
| /// Wrapper for os::mv
 | ||||
| /// Wrapper for fs::mv
 | ||||
| ///
 | ||||
| /// Move a file or directory from source to destination.
 | ||||
| pub fn mv(src: &str, dest: &str) -> Result<String, Box<EvalAltResult>> { | ||||
|     os::mv(src, dest).to_rhai_error() | ||||
|     fs::mv(src, dest).to_rhai_error() | ||||
| } | ||||
| 
 | ||||
| //
 | ||||
| @@ -204,35 +224,39 @@ pub fn mv(src: &str, dest: &str) -> Result<String, Box<EvalAltResult>> { | ||||
| ///
 | ||||
| /// Download a file from URL to destination using the curl command.
 | ||||
| pub fn download(url: &str, dest: &str, min_size_kb: i64) -> Result<String, Box<EvalAltResult>> { | ||||
|     os::download(url, dest, min_size_kb).to_rhai_error() | ||||
|     dl::download(url, dest, min_size_kb).to_rhai_error() | ||||
| } | ||||
| 
 | ||||
| /// Wrapper for os::download_file
 | ||||
| ///
 | ||||
| /// Download a file from URL to a specific file destination using the curl command.
 | ||||
| pub fn download_file(url: &str, dest: &str, min_size_kb: i64) -> Result<String, Box<EvalAltResult>> { | ||||
|     os::download_file(url, dest, min_size_kb).to_rhai_error() | ||||
| pub fn download_file( | ||||
|     url: &str, | ||||
|     dest: &str, | ||||
|     min_size_kb: i64, | ||||
| ) -> Result<String, Box<EvalAltResult>> { | ||||
|     dl::download_file(url, dest, min_size_kb).to_rhai_error() | ||||
| } | ||||
| 
 | ||||
| /// Wrapper for os::download_install
 | ||||
| ///
 | ||||
| /// Download a file and install it if it's a supported package format.
 | ||||
| pub fn download_install(url: &str, min_size_kb: i64) -> Result<String, Box<EvalAltResult>> { | ||||
|     os::download_install(url, min_size_kb).to_rhai_error() | ||||
|     dl::download_install(url, min_size_kb).to_rhai_error() | ||||
| } | ||||
| 
 | ||||
| /// Wrapper for os::chmod_exec
 | ||||
| ///
 | ||||
| /// Make a file executable (equivalent to chmod +x).
 | ||||
| pub fn chmod_exec(path: &str) -> Result<String, Box<EvalAltResult>> { | ||||
|     os::chmod_exec(path).to_rhai_error() | ||||
|     dl::chmod_exec(path).to_rhai_error() | ||||
| } | ||||
| 
 | ||||
| /// Wrapper for os::which
 | ||||
| ///
 | ||||
| /// Check if a command exists in the system PATH.
 | ||||
| pub fn which(command: &str) -> String { | ||||
|     os::which(command) | ||||
|     fs::which(command) | ||||
| } | ||||
| 
 | ||||
| /// Wrapper for os::cmd_ensure_exists
 | ||||
| @@ -240,7 +264,7 @@ pub fn which(command: &str) -> String { | ||||
| /// Ensure that one or more commands exist in the system PATH.
 | ||||
| /// If any command doesn't exist, an error is thrown.
 | ||||
| pub fn cmd_ensure_exists(commands: &str) -> Result<String, Box<EvalAltResult>> { | ||||
|     os::cmd_ensure_exists(commands).to_rhai_error() | ||||
|     fs::cmd_ensure_exists(commands).to_rhai_error() | ||||
| } | ||||
| 
 | ||||
| //
 | ||||
| @@ -349,8 +373,52 @@ pub fn package_set_debug(debug: bool) -> bool { | ||||
| pub fn package_platform() -> String { | ||||
|     let hero = PackHero::new(); | ||||
|     match hero.platform() { | ||||
|         os::package::Platform::Ubuntu => "Ubuntu".to_string(), | ||||
|         os::package::Platform::MacOS => "MacOS".to_string(), | ||||
|         os::package::Platform::Unknown => "Unknown".to_string(), | ||||
|         package::Platform::Ubuntu => "Ubuntu".to_string(), | ||||
|         package::Platform::MacOS => "MacOS".to_string(), | ||||
|         package::Platform::Unknown => "Unknown".to_string(), | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| //
 | ||||
| // Platform Detection Function Wrappers
 | ||||
| //
 | ||||
| 
 | ||||
| /// Wrapper for platform::is_osx
 | ||||
| pub fn platform_is_osx() -> bool { | ||||
|     crate::platform::is_osx() | ||||
| } | ||||
| 
 | ||||
| /// Wrapper for platform::is_linux
 | ||||
| pub fn platform_is_linux() -> bool { | ||||
|     crate::platform::is_linux() | ||||
| } | ||||
| 
 | ||||
| /// Wrapper for platform::is_arm
 | ||||
| pub fn platform_is_arm() -> bool { | ||||
|     crate::platform::is_arm() | ||||
| } | ||||
| 
 | ||||
| /// Wrapper for platform::is_x86
 | ||||
| pub fn platform_is_x86() -> bool { | ||||
|     crate::platform::is_x86() | ||||
| } | ||||
| 
 | ||||
| /// Wrapper for platform::check_linux_x86
 | ||||
| pub fn platform_check_linux_x86() -> Result<(), Box<EvalAltResult>> { | ||||
|     crate::platform::check_linux_x86().map_err(|e| { | ||||
|         Box::new(EvalAltResult::ErrorRuntime( | ||||
|             format!("Platform Check Error: {}", e).into(), | ||||
|             Position::NONE, | ||||
|         )) | ||||
|     }) | ||||
| } | ||||
| 
 | ||||
| /// Wrapper for platform::check_macos_arm
 | ||||
| pub fn platform_check_macos_arm() -> Result<(), Box<EvalAltResult>> { | ||||
|     crate::platform::check_macos_arm().map_err(|e| { | ||||
|         Box::new(EvalAltResult::ErrorRuntime( | ||||
|             format!("Platform Check Error: {}", e).into(), | ||||
|             Position::NONE, | ||||
|         )) | ||||
|     }) | ||||
| } | ||||
							
								
								
									
										208
									
								
								os/tests/download_tests.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										208
									
								
								os/tests/download_tests.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,208 @@ | ||||
| use sal_os::{download, DownloadError}; | ||||
| use std::fs; | ||||
| use tempfile::TempDir; | ||||
|  | ||||
| #[test] | ||||
| fn test_chmod_exec() { | ||||
|     let temp_dir = TempDir::new().unwrap(); | ||||
|     let test_file = temp_dir.path().join("test_script.sh"); | ||||
|  | ||||
|     // Create a test file | ||||
|     fs::write(&test_file, "#!/bin/bash\necho 'test'").unwrap(); | ||||
|  | ||||
|     // Make it executable | ||||
|     let result = download::chmod_exec(test_file.to_str().unwrap()); | ||||
|     assert!(result.is_ok()); | ||||
|  | ||||
|     // Check if file is executable (Unix only) | ||||
|     #[cfg(unix)] | ||||
|     { | ||||
|         use std::os::unix::fs::PermissionsExt; | ||||
|         let metadata = fs::metadata(&test_file).unwrap(); | ||||
|         let permissions = metadata.permissions(); | ||||
|         assert!(permissions.mode() & 0o111 != 0); // Check if any execute bit is set | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_download_error_handling() { | ||||
|     let temp_dir = TempDir::new().unwrap(); | ||||
|  | ||||
|     // Test with invalid URL | ||||
|     let result = download::download("invalid-url", temp_dir.path().to_str().unwrap(), 0); | ||||
|     assert!(result.is_err()); | ||||
|  | ||||
|     // Test with non-existent domain | ||||
|     let result = download::download( | ||||
|         "https://nonexistentdomain12345.com/file.txt", | ||||
|         temp_dir.path().to_str().unwrap(), | ||||
|         0, | ||||
|     ); | ||||
|     assert!(result.is_err()); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_download_file_error_handling() { | ||||
|     let temp_dir = TempDir::new().unwrap(); | ||||
|     let dest_file = temp_dir.path().join("downloaded_file.txt"); | ||||
|  | ||||
|     // Test with invalid URL | ||||
|     let result = download::download_file("invalid-url", dest_file.to_str().unwrap(), 0); | ||||
|     assert!(result.is_err()); | ||||
|  | ||||
|     // Test with non-existent domain | ||||
|     let result = download::download_file( | ||||
|         "https://nonexistentdomain12345.com/file.txt", | ||||
|         dest_file.to_str().unwrap(), | ||||
|         0, | ||||
|     ); | ||||
|     assert!(result.is_err()); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_download_install_error_handling() { | ||||
|     // Test with invalid URL | ||||
|     let result = download::download_install("invalid-url", 0); | ||||
|     assert!(result.is_err()); | ||||
|  | ||||
|     // Test with non-existent domain | ||||
|     let result = download::download_install("https://nonexistentdomain12345.com/package.deb", 0); | ||||
|     assert!(result.is_err()); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_download_minimum_size_validation() { | ||||
|     let temp_dir = TempDir::new().unwrap(); | ||||
|  | ||||
|     // Test with a very high minimum size requirement that won't be met | ||||
|     // This should fail even if the URL exists | ||||
|     let result = download::download( | ||||
|         "https://httpbin.org/bytes/10", // This returns only 10 bytes | ||||
|         temp_dir.path().to_str().unwrap(), | ||||
|         1000, // Require 1000KB minimum | ||||
|     ); | ||||
|     // This might succeed or fail depending on network, but we're testing the interface | ||||
|     // The important thing is that it doesn't panic | ||||
|     let _ = result; | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_download_to_nonexistent_directory() { | ||||
|     // Test downloading to a directory that doesn't exist | ||||
|     // The download function should create parent directories | ||||
|     let temp_dir = TempDir::new().unwrap(); | ||||
|     let nonexistent_dir = temp_dir.path().join("nonexistent").join("nested"); | ||||
|  | ||||
|     let _ = download::download( | ||||
|         "https://httpbin.org/status/404", // This will fail, but directory creation should work | ||||
|         nonexistent_dir.to_str().unwrap(), | ||||
|         0, | ||||
|     ); | ||||
|  | ||||
|     // The directory should be created even if download fails | ||||
|     assert!(nonexistent_dir.exists()); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_chmod_exec_nonexistent_file() { | ||||
|     // Test chmod_exec on a file that doesn't exist | ||||
|     let result = download::chmod_exec("/nonexistent/path/file.sh"); | ||||
|     assert!(result.is_err()); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_download_file_path_validation() { | ||||
|     let _ = TempDir::new().unwrap(); | ||||
|  | ||||
|     // Test with invalid destination path | ||||
|     let result = download::download_file( | ||||
|         "https://httpbin.org/status/404", | ||||
|         "/invalid/path/that/does/not/exist/file.txt", | ||||
|         0, | ||||
|     ); | ||||
|     assert!(result.is_err()); | ||||
| } | ||||
|  | ||||
| // Integration test that requires network access | ||||
| // This test is marked with ignore so it doesn't run by default | ||||
| #[test] | ||||
| #[ignore] | ||||
| fn test_download_real_file() { | ||||
|     let temp_dir = TempDir::new().unwrap(); | ||||
|  | ||||
|     // Download a small file from httpbin (a testing service) | ||||
|     let result = download::download( | ||||
|         "https://httpbin.org/bytes/100", // Returns 100 random bytes | ||||
|         temp_dir.path().to_str().unwrap(), | ||||
|         0, | ||||
|     ); | ||||
|  | ||||
|     if result.is_ok() { | ||||
|         // If download succeeded, verify the file exists | ||||
|         let downloaded_path = result.unwrap(); | ||||
|         assert!(fs::metadata(&downloaded_path).is_ok()); | ||||
|  | ||||
|         // Verify file size is approximately correct | ||||
|         let metadata = fs::metadata(&downloaded_path).unwrap(); | ||||
|         assert!(metadata.len() >= 90 && metadata.len() <= 110); // Allow some variance | ||||
|     } | ||||
|     // If download failed (network issues), that's okay for this test | ||||
| } | ||||
|  | ||||
| // Integration test for download_file | ||||
| #[test] | ||||
| #[ignore] | ||||
| fn test_download_file_real() { | ||||
|     let temp_dir = TempDir::new().unwrap(); | ||||
|     let dest_file = temp_dir.path().join("test_download.bin"); | ||||
|  | ||||
|     // Download a small file to specific location | ||||
|     let result = download::download_file( | ||||
|         "https://httpbin.org/bytes/50", | ||||
|         dest_file.to_str().unwrap(), | ||||
|         0, | ||||
|     ); | ||||
|  | ||||
|     if result.is_ok() { | ||||
|         // Verify the file was created at the specified location | ||||
|         assert!(dest_file.exists()); | ||||
|  | ||||
|         // Verify file size | ||||
|         let metadata = fs::metadata(&dest_file).unwrap(); | ||||
|         assert!(metadata.len() >= 40 && metadata.len() <= 60); // Allow some variance | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_download_error_types() { | ||||
|     // DownloadError is already imported at the top | ||||
|  | ||||
|     // Test that our error types can be created and displayed | ||||
|     let error = DownloadError::InvalidUrl("test".to_string()); | ||||
|     assert!(!error.to_string().is_empty()); | ||||
|  | ||||
|     let error = DownloadError::DownloadFailed("test".to_string()); | ||||
|     assert!(!error.to_string().is_empty()); | ||||
|  | ||||
|     let error = DownloadError::FileTooSmall(50, 100); | ||||
|     assert!(!error.to_string().is_empty()); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_download_url_parsing() { | ||||
|     let temp_dir = TempDir::new().unwrap(); | ||||
|  | ||||
|     // Test with URL that has no filename | ||||
|     let result = download::download("https://example.com/", temp_dir.path().to_str().unwrap(), 0); | ||||
|     // Should fail with invalid URL error | ||||
|     assert!(result.is_err()); | ||||
|  | ||||
|     // Test with URL that has query parameters | ||||
|     let result = download::download( | ||||
|         "https://httpbin.org/get?param=value", | ||||
|         temp_dir.path().to_str().unwrap(), | ||||
|         0, | ||||
|     ); | ||||
|     // This might succeed or fail depending on network, but shouldn't panic | ||||
|     let _ = result; | ||||
| } | ||||
							
								
								
									
										212
									
								
								os/tests/fs_tests.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										212
									
								
								os/tests/fs_tests.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,212 @@ | ||||
| use sal_os::fs; | ||||
| use std::fs as std_fs; | ||||
| use tempfile::TempDir; | ||||
|  | ||||
| #[test] | ||||
| fn test_exist() { | ||||
|     let temp_dir = TempDir::new().unwrap(); | ||||
|     let temp_path = temp_dir.path(); | ||||
|      | ||||
|     // Test directory exists | ||||
|     assert!(fs::exist(temp_path.to_str().unwrap())); | ||||
|      | ||||
|     // Test file doesn't exist | ||||
|     let non_existent = temp_path.join("non_existent.txt"); | ||||
|     assert!(!fs::exist(non_existent.to_str().unwrap())); | ||||
|      | ||||
|     // Create a file and test it exists | ||||
|     let test_file = temp_path.join("test.txt"); | ||||
|     std_fs::write(&test_file, "test content").unwrap(); | ||||
|     assert!(fs::exist(test_file.to_str().unwrap())); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_mkdir() { | ||||
|     let temp_dir = TempDir::new().unwrap(); | ||||
|     let new_dir = temp_dir.path().join("new_directory"); | ||||
|      | ||||
|     // Directory shouldn't exist initially | ||||
|     assert!(!fs::exist(new_dir.to_str().unwrap())); | ||||
|      | ||||
|     // Create directory | ||||
|     let result = fs::mkdir(new_dir.to_str().unwrap()); | ||||
|     assert!(result.is_ok()); | ||||
|      | ||||
|     // Directory should now exist | ||||
|     assert!(fs::exist(new_dir.to_str().unwrap())); | ||||
|      | ||||
|     // Creating existing directory should not error (defensive) | ||||
|     let result2 = fs::mkdir(new_dir.to_str().unwrap()); | ||||
|     assert!(result2.is_ok()); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_file_write_and_read() { | ||||
|     let temp_dir = TempDir::new().unwrap(); | ||||
|     let test_file = temp_dir.path().join("test_write.txt"); | ||||
|     let content = "Hello, World!"; | ||||
|      | ||||
|     // Write file | ||||
|     let write_result = fs::file_write(test_file.to_str().unwrap(), content); | ||||
|     assert!(write_result.is_ok()); | ||||
|      | ||||
|     // File should exist | ||||
|     assert!(fs::exist(test_file.to_str().unwrap())); | ||||
|      | ||||
|     // Read file | ||||
|     let read_result = fs::file_read(test_file.to_str().unwrap()); | ||||
|     assert!(read_result.is_ok()); | ||||
|     assert_eq!(read_result.unwrap(), content); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_file_write_append() { | ||||
|     let temp_dir = TempDir::new().unwrap(); | ||||
|     let test_file = temp_dir.path().join("test_append.txt"); | ||||
|      | ||||
|     // Write initial content | ||||
|     let initial_content = "Line 1\n"; | ||||
|     let append_content = "Line 2\n"; | ||||
|      | ||||
|     let write_result = fs::file_write(test_file.to_str().unwrap(), initial_content); | ||||
|     assert!(write_result.is_ok()); | ||||
|      | ||||
|     // Append content | ||||
|     let append_result = fs::file_write_append(test_file.to_str().unwrap(), append_content); | ||||
|     assert!(append_result.is_ok()); | ||||
|      | ||||
|     // Read and verify | ||||
|     let read_result = fs::file_read(test_file.to_str().unwrap()); | ||||
|     assert!(read_result.is_ok()); | ||||
|     assert_eq!(read_result.unwrap(), format!("{}{}", initial_content, append_content)); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_file_size() { | ||||
|     let temp_dir = TempDir::new().unwrap(); | ||||
|     let test_file = temp_dir.path().join("test_size.txt"); | ||||
|     let content = "Hello, World!"; // 13 bytes | ||||
|      | ||||
|     // Write file | ||||
|     fs::file_write(test_file.to_str().unwrap(), content).unwrap(); | ||||
|      | ||||
|     // Check size | ||||
|     let size_result = fs::file_size(test_file.to_str().unwrap()); | ||||
|     assert!(size_result.is_ok()); | ||||
|     assert_eq!(size_result.unwrap(), 13); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_delete() { | ||||
|     let temp_dir = TempDir::new().unwrap(); | ||||
|     let test_file = temp_dir.path().join("test_delete.txt"); | ||||
|      | ||||
|     // Create file | ||||
|     fs::file_write(test_file.to_str().unwrap(), "test").unwrap(); | ||||
|     assert!(fs::exist(test_file.to_str().unwrap())); | ||||
|      | ||||
|     // Delete file | ||||
|     let delete_result = fs::delete(test_file.to_str().unwrap()); | ||||
|     assert!(delete_result.is_ok()); | ||||
|      | ||||
|     // File should no longer exist | ||||
|     assert!(!fs::exist(test_file.to_str().unwrap())); | ||||
|      | ||||
|     // Deleting non-existent file should not error (defensive) | ||||
|     let delete_result2 = fs::delete(test_file.to_str().unwrap()); | ||||
|     assert!(delete_result2.is_ok()); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_copy() { | ||||
|     let temp_dir = TempDir::new().unwrap(); | ||||
|     let source_file = temp_dir.path().join("source.txt"); | ||||
|     let dest_file = temp_dir.path().join("dest.txt"); | ||||
|     let content = "Copy test content"; | ||||
|      | ||||
|     // Create source file | ||||
|     fs::file_write(source_file.to_str().unwrap(), content).unwrap(); | ||||
|      | ||||
|     // Copy file | ||||
|     let copy_result = fs::copy(source_file.to_str().unwrap(), dest_file.to_str().unwrap()); | ||||
|     assert!(copy_result.is_ok()); | ||||
|      | ||||
|     // Destination should exist and have same content | ||||
|     assert!(fs::exist(dest_file.to_str().unwrap())); | ||||
|     let dest_content = fs::file_read(dest_file.to_str().unwrap()).unwrap(); | ||||
|     assert_eq!(dest_content, content); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_mv() { | ||||
|     let temp_dir = TempDir::new().unwrap(); | ||||
|     let source_file = temp_dir.path().join("source_mv.txt"); | ||||
|     let dest_file = temp_dir.path().join("dest_mv.txt"); | ||||
|     let content = "Move test content"; | ||||
|      | ||||
|     // Create source file | ||||
|     fs::file_write(source_file.to_str().unwrap(), content).unwrap(); | ||||
|      | ||||
|     // Move file | ||||
|     let mv_result = fs::mv(source_file.to_str().unwrap(), dest_file.to_str().unwrap()); | ||||
|     assert!(mv_result.is_ok()); | ||||
|      | ||||
|     // Source should no longer exist, destination should exist | ||||
|     assert!(!fs::exist(source_file.to_str().unwrap())); | ||||
|     assert!(fs::exist(dest_file.to_str().unwrap())); | ||||
|      | ||||
|     // Destination should have same content | ||||
|     let dest_content = fs::file_read(dest_file.to_str().unwrap()).unwrap(); | ||||
|     assert_eq!(dest_content, content); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_which() { | ||||
|     // Test with a command that should exist on most systems | ||||
|     let result = fs::which("ls"); | ||||
|     assert!(!result.is_empty()); | ||||
|      | ||||
|     // Test with a command that shouldn't exist | ||||
|     let result = fs::which("nonexistentcommand12345"); | ||||
|     assert!(result.is_empty()); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_find_files() { | ||||
|     let temp_dir = TempDir::new().unwrap(); | ||||
|     let temp_path = temp_dir.path(); | ||||
|      | ||||
|     // Create test files | ||||
|     fs::file_write(&temp_path.join("test1.txt").to_string_lossy(), "content1").unwrap(); | ||||
|     fs::file_write(&temp_path.join("test2.txt").to_string_lossy(), "content2").unwrap(); | ||||
|     fs::file_write(&temp_path.join("other.log").to_string_lossy(), "log content").unwrap(); | ||||
|      | ||||
|     // Find .txt files | ||||
|     let txt_files = fs::find_files(temp_path.to_str().unwrap(), "*.txt"); | ||||
|     assert!(txt_files.is_ok()); | ||||
|     let files = txt_files.unwrap(); | ||||
|     assert_eq!(files.len(), 2); | ||||
|      | ||||
|     // Find all files | ||||
|     let all_files = fs::find_files(temp_path.to_str().unwrap(), "*"); | ||||
|     assert!(all_files.is_ok()); | ||||
|     let files = all_files.unwrap(); | ||||
|     assert!(files.len() >= 3); // At least our 3 files | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_find_dirs() { | ||||
|     let temp_dir = TempDir::new().unwrap(); | ||||
|     let temp_path = temp_dir.path(); | ||||
|      | ||||
|     // Create test directories | ||||
|     fs::mkdir(&temp_path.join("dir1").to_string_lossy()).unwrap(); | ||||
|     fs::mkdir(&temp_path.join("dir2").to_string_lossy()).unwrap(); | ||||
|     fs::mkdir(&temp_path.join("subdir").to_string_lossy()).unwrap(); | ||||
|      | ||||
|     // Find directories | ||||
|     let dirs = fs::find_dirs(temp_path.to_str().unwrap(), "dir*"); | ||||
|     assert!(dirs.is_ok()); | ||||
|     let found_dirs = dirs.unwrap(); | ||||
|     assert!(found_dirs.len() >= 2); // At least dir1 and dir2 | ||||
| } | ||||
							
								
								
									
										366
									
								
								os/tests/package_tests.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										366
									
								
								os/tests/package_tests.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,366 @@ | ||||
| use sal_os::package::{PackHero, Platform}; | ||||
|  | ||||
| #[test] | ||||
| fn test_pack_hero_creation() { | ||||
|     // Test that we can create a PackHero instance | ||||
|     let hero = PackHero::new(); | ||||
|  | ||||
|     // Test that platform detection works | ||||
|     let platform = hero.platform(); | ||||
|     match platform { | ||||
|         Platform::Ubuntu | Platform::MacOS | Platform::Unknown => { | ||||
|             // All valid platforms | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_platform_detection() { | ||||
|     let hero = PackHero::new(); | ||||
|     let platform = hero.platform(); | ||||
|  | ||||
|     // Platform should be deterministic | ||||
|     let platform2 = hero.platform(); | ||||
|     assert_eq!(format!("{:?}", platform), format!("{:?}", platform2)); | ||||
|  | ||||
|     // Test platform display | ||||
|     match platform { | ||||
|         Platform::Ubuntu => { | ||||
|             assert_eq!(format!("{:?}", platform), "Ubuntu"); | ||||
|         } | ||||
|         Platform::MacOS => { | ||||
|             assert_eq!(format!("{:?}", platform), "MacOS"); | ||||
|         } | ||||
|         Platform::Unknown => { | ||||
|             assert_eq!(format!("{:?}", platform), "Unknown"); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_debug_mode() { | ||||
|     let mut hero = PackHero::new(); | ||||
|  | ||||
|     // Test setting debug mode | ||||
|     hero.set_debug(true); | ||||
|     hero.set_debug(false); | ||||
|  | ||||
|     // Debug mode setting should not panic | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_package_operations_error_handling() { | ||||
|     let hero = PackHero::new(); | ||||
|  | ||||
|     // Test with invalid package name | ||||
|     let result = hero.is_installed("nonexistent-package-12345-xyz"); | ||||
|     // This should return a result (either Ok(false) or Err) | ||||
|     // Validate that we get a proper result type | ||||
|     match result { | ||||
|         Ok(is_installed) => { | ||||
|             // Should return false for non-existent package | ||||
|             assert!( | ||||
|                 !is_installed, | ||||
|                 "Non-existent package should not be reported as installed" | ||||
|             ); | ||||
|         } | ||||
|         Err(_) => { | ||||
|             // Error is also acceptable (e.g., no package manager available) | ||||
|             // The important thing is it doesn't panic | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // Test install with invalid package | ||||
|     let result = hero.install("nonexistent-package-12345-xyz"); | ||||
|     // This should return an error | ||||
|     assert!(result.is_err()); | ||||
|  | ||||
|     // Test remove with invalid package | ||||
|     let result = hero.remove("nonexistent-package-12345-xyz"); | ||||
|     // This might succeed (if package wasn't installed) or fail | ||||
|     // Validate that we get a proper result type | ||||
|     match result { | ||||
|         Ok(_) => { | ||||
|             // Success is acceptable (package wasn't installed) | ||||
|         } | ||||
|         Err(err) => { | ||||
|             // Error is also acceptable | ||||
|             // Verify error message is meaningful | ||||
|             let error_msg = err.to_string(); | ||||
|             assert!(!error_msg.is_empty(), "Error message should not be empty"); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_package_search_basic() { | ||||
|     let hero = PackHero::new(); | ||||
|  | ||||
|     // Test search with empty query | ||||
|     let result = hero.search(""); | ||||
|     // Should handle empty query gracefully | ||||
|     // Validate that we get a proper result type | ||||
|     match result { | ||||
|         Ok(packages) => { | ||||
|             // Empty search might return all packages or empty list | ||||
|             // Verify the result is a valid vector | ||||
|             assert!( | ||||
|                 packages.len() < 50000, | ||||
|                 "Empty search returned unreasonably large result" | ||||
|             ); | ||||
|         } | ||||
|         Err(err) => { | ||||
|             // Error is acceptable for empty query | ||||
|             let error_msg = err.to_string(); | ||||
|             assert!(!error_msg.is_empty(), "Error message should not be empty"); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // Test search with very specific query that likely won't match | ||||
|     let result = hero.search("nonexistent-package-xyz-12345"); | ||||
|     if let Ok(packages) = result { | ||||
|         // If search succeeded, it should return a vector | ||||
|         // The vector should be valid (we can get its length) | ||||
|         let _count = packages.len(); | ||||
|         // Search results should be reasonable (not absurdly large) | ||||
|         assert!( | ||||
|             packages.len() < 10000, | ||||
|             "Search returned unreasonably large result set" | ||||
|         ); | ||||
|     } | ||||
|     // If search failed, that's also acceptable | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_package_list_basic() { | ||||
|     let hero = PackHero::new(); | ||||
|  | ||||
|     // Test listing installed packages | ||||
|     let result = hero.list_installed(); | ||||
|     if let Ok(packages) = result { | ||||
|         // If listing succeeded, it should return a vector | ||||
|         // On most systems, there should be at least some packages installed | ||||
|         println!("Found {} installed packages", packages.len()); | ||||
|     } | ||||
|     // If listing failed (e.g., no package manager available), that's acceptable | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_package_update_basic() { | ||||
|     let hero = PackHero::new(); | ||||
|  | ||||
|     // Test package list update | ||||
|     let result = hero.update(); | ||||
|     // This might succeed or fail depending on permissions and network | ||||
|     // Validate that we get a proper result type | ||||
|     match result { | ||||
|         Ok(_) => { | ||||
|             // Success is good - package list was updated | ||||
|         } | ||||
|         Err(err) => { | ||||
|             // Error is acceptable (no permissions, no network, etc.) | ||||
|             let error_msg = err.to_string(); | ||||
|             assert!(!error_msg.is_empty(), "Error message should not be empty"); | ||||
|             // Common error patterns we expect | ||||
|             let error_lower = error_msg.to_lowercase(); | ||||
|             assert!( | ||||
|                 error_lower.contains("permission") | ||||
|                     || error_lower.contains("network") | ||||
|                     || error_lower.contains("command") | ||||
|                     || error_lower.contains("not found") | ||||
|                     || error_lower.contains("failed"), | ||||
|                 "Error message should indicate a reasonable failure cause: {}", | ||||
|                 error_msg | ||||
|             ); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| #[ignore] // Skip by default as this can take a very long time and modify the system | ||||
| fn test_package_upgrade_basic() { | ||||
|     let hero = PackHero::new(); | ||||
|  | ||||
|     // Test package upgrade (this is a real system operation) | ||||
|     let result = hero.upgrade(); | ||||
|     // Validate that we get a proper result type | ||||
|     match result { | ||||
|         Ok(_) => { | ||||
|             // Success means packages were upgraded | ||||
|             println!("Package upgrade completed successfully"); | ||||
|         } | ||||
|         Err(err) => { | ||||
|             // Error is acceptable (no permissions, no packages to upgrade, etc.) | ||||
|             let error_msg = err.to_string(); | ||||
|             assert!(!error_msg.is_empty(), "Error message should not be empty"); | ||||
|             println!("Package upgrade failed as expected: {}", error_msg); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_package_upgrade_interface() { | ||||
|     // Test that the upgrade interface works without actually upgrading | ||||
|     let hero = PackHero::new(); | ||||
|  | ||||
|     // Verify that PackHero has the upgrade method and it returns the right type | ||||
|     // This tests the interface without performing the actual upgrade | ||||
|     let _upgrade_fn = PackHero::upgrade; | ||||
|  | ||||
|     // Test that we can call upgrade (it will likely fail due to permissions/network) | ||||
|     // but we're testing that the interface works correctly | ||||
|     let result = hero.upgrade(); | ||||
|  | ||||
|     // The result should be a proper Result type | ||||
|     match result { | ||||
|         Ok(_) => { | ||||
|             // Upgrade succeeded (unlikely in test environment) | ||||
|         } | ||||
|         Err(err) => { | ||||
|             // Expected in most test environments | ||||
|             // Verify error is meaningful | ||||
|             let error_msg = err.to_string(); | ||||
|             assert!(!error_msg.is_empty(), "Error should have a message"); | ||||
|             assert!(error_msg.len() > 5, "Error message should be descriptive"); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Platform-specific tests | ||||
| #[cfg(target_os = "linux")] | ||||
| #[test] | ||||
| fn test_linux_platform_detection() { | ||||
|     let hero = PackHero::new(); | ||||
|     let platform = hero.platform(); | ||||
|  | ||||
|     // On Linux, should detect Ubuntu or Unknown (if not Ubuntu-based) | ||||
|     match platform { | ||||
|         Platform::Ubuntu | Platform::Unknown => { | ||||
|             // Expected on Linux | ||||
|         } | ||||
|         Platform::MacOS => { | ||||
|             panic!("Should not detect macOS on Linux system"); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[cfg(target_os = "macos")] | ||||
| #[test] | ||||
| fn test_macos_platform_detection() { | ||||
|     let hero = PackHero::new(); | ||||
|     let platform = hero.platform(); | ||||
|  | ||||
|     // On macOS, should detect MacOS | ||||
|     match platform { | ||||
|         Platform::MacOS => { | ||||
|             // Expected on macOS | ||||
|         } | ||||
|         Platform::Ubuntu | Platform::Unknown => { | ||||
|             panic!("Should detect macOS on macOS system, got {:?}", platform); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Integration tests that require actual package managers | ||||
| // These are marked with ignore so they don't run by default | ||||
|  | ||||
| #[test] | ||||
| #[ignore] | ||||
| fn test_real_package_check() { | ||||
|     let hero = PackHero::new(); | ||||
|  | ||||
|     // Test with a package that's commonly installed | ||||
|     #[cfg(target_os = "linux")] | ||||
|     let test_package = "bash"; | ||||
|  | ||||
|     #[cfg(target_os = "macos")] | ||||
|     let test_package = "bash"; | ||||
|  | ||||
|     #[cfg(not(any(target_os = "linux", target_os = "macos")))] | ||||
|     let test_package = "unknown"; | ||||
|  | ||||
|     let result = hero.is_installed(test_package); | ||||
|     if let Ok(is_installed) = result { | ||||
|         println!("Package '{}' is installed: {}", test_package, is_installed); | ||||
|     } else { | ||||
|         println!( | ||||
|             "Failed to check if '{}' is installed: {:?}", | ||||
|             test_package, result | ||||
|         ); | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| #[ignore] | ||||
| fn test_real_package_search() { | ||||
|     let hero = PackHero::new(); | ||||
|  | ||||
|     // Search for a common package | ||||
|     let result = hero.search("git"); | ||||
|     if let Ok(packages) = result { | ||||
|         println!("Found {} packages matching 'git'", packages.len()); | ||||
|         if !packages.is_empty() { | ||||
|             println!( | ||||
|                 "First few matches: {:?}", | ||||
|                 &packages[..std::cmp::min(5, packages.len())] | ||||
|             ); | ||||
|         } | ||||
|     } else { | ||||
|         println!("Package search failed: {:?}", result); | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| #[ignore] | ||||
| fn test_real_package_list() { | ||||
|     let hero = PackHero::new(); | ||||
|  | ||||
|     // List installed packages | ||||
|     let result = hero.list_installed(); | ||||
|     if let Ok(packages) = result { | ||||
|         println!("Total installed packages: {}", packages.len()); | ||||
|         if !packages.is_empty() { | ||||
|             println!( | ||||
|                 "First few packages: {:?}", | ||||
|                 &packages[..std::cmp::min(10, packages.len())] | ||||
|             ); | ||||
|         } | ||||
|     } else { | ||||
|         println!("Package listing failed: {:?}", result); | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_platform_enum_properties() { | ||||
|     // Test that Platform enum can be compared | ||||
|     assert_eq!(Platform::Ubuntu, Platform::Ubuntu); | ||||
|     assert_eq!(Platform::MacOS, Platform::MacOS); | ||||
|     assert_eq!(Platform::Unknown, Platform::Unknown); | ||||
|  | ||||
|     assert_ne!(Platform::Ubuntu, Platform::MacOS); | ||||
|     assert_ne!(Platform::Ubuntu, Platform::Unknown); | ||||
|     assert_ne!(Platform::MacOS, Platform::Unknown); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_pack_hero_multiple_instances() { | ||||
|     // Test that multiple PackHero instances work correctly | ||||
|     let hero1 = PackHero::new(); | ||||
|     let hero2 = PackHero::new(); | ||||
|  | ||||
|     // Both should detect the same platform | ||||
|     assert_eq!( | ||||
|         format!("{:?}", hero1.platform()), | ||||
|         format!("{:?}", hero2.platform()) | ||||
|     ); | ||||
|  | ||||
|     // Both should handle debug mode independently | ||||
|     let mut hero1_mut = hero1; | ||||
|     let mut hero2_mut = hero2; | ||||
|  | ||||
|     hero1_mut.set_debug(true); | ||||
|     hero2_mut.set_debug(false); | ||||
|  | ||||
|     // No assertions here since debug mode doesn't have observable effects in tests | ||||
|     // But this ensures the API works correctly | ||||
| } | ||||
							
								
								
									
										199
									
								
								os/tests/platform_tests.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										199
									
								
								os/tests/platform_tests.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,199 @@ | ||||
| use sal_os::platform; | ||||
|  | ||||
| #[test] | ||||
| fn test_platform_detection_consistency() { | ||||
|     // Test that platform detection functions return consistent results | ||||
|     let is_osx = platform::is_osx(); | ||||
|     let is_linux = platform::is_linux(); | ||||
|      | ||||
|     // On any given system, only one of these should be true | ||||
|     // (or both false if running on Windows or other OS) | ||||
|     if is_osx { | ||||
|         assert!(!is_linux, "Cannot be both macOS and Linux"); | ||||
|     } | ||||
|     if is_linux { | ||||
|         assert!(!is_osx, "Cannot be both Linux and macOS"); | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_architecture_detection_consistency() { | ||||
|     // Test that architecture detection functions return consistent results | ||||
|     let is_arm = platform::is_arm(); | ||||
|     let is_x86 = platform::is_x86(); | ||||
|      | ||||
|     // On any given system, only one of these should be true | ||||
|     // (or both false if running on other architectures) | ||||
|     if is_arm { | ||||
|         assert!(!is_x86, "Cannot be both ARM and x86"); | ||||
|     } | ||||
|     if is_x86 { | ||||
|         assert!(!is_arm, "Cannot be both x86 and ARM"); | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_platform_functions_return_bool() { | ||||
|     // Test that all platform detection functions return boolean values | ||||
|     let _: bool = platform::is_osx(); | ||||
|     let _: bool = platform::is_linux(); | ||||
|     let _: bool = platform::is_arm(); | ||||
|     let _: bool = platform::is_x86(); | ||||
| } | ||||
|  | ||||
| #[cfg(target_os = "macos")] | ||||
| #[test] | ||||
| fn test_macos_detection() { | ||||
|     // When compiled for macOS, is_osx should return true | ||||
|     assert!(platform::is_osx()); | ||||
|     assert!(!platform::is_linux()); | ||||
| } | ||||
|  | ||||
| #[cfg(target_os = "linux")] | ||||
| #[test] | ||||
| fn test_linux_detection() { | ||||
|     // When compiled for Linux, is_linux should return true | ||||
|     assert!(platform::is_linux()); | ||||
|     assert!(!platform::is_osx()); | ||||
| } | ||||
|  | ||||
| #[cfg(target_arch = "aarch64")] | ||||
| #[test] | ||||
| fn test_arm_detection() { | ||||
|     // When compiled for ARM64, is_arm should return true | ||||
|     assert!(platform::is_arm()); | ||||
|     assert!(!platform::is_x86()); | ||||
| } | ||||
|  | ||||
| #[cfg(target_arch = "x86_64")] | ||||
| #[test] | ||||
| fn test_x86_detection() { | ||||
|     // When compiled for x86_64, is_x86 should return true | ||||
|     assert!(platform::is_x86()); | ||||
|     assert!(!platform::is_arm()); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_check_linux_x86() { | ||||
|     let result = platform::check_linux_x86(); | ||||
|      | ||||
|     // The result should depend on the current platform | ||||
|     #[cfg(all(target_os = "linux", target_arch = "x86_64"))] | ||||
|     { | ||||
|         assert!(result.is_ok(), "Should succeed on Linux x86_64"); | ||||
|     } | ||||
|      | ||||
|     #[cfg(not(all(target_os = "linux", target_arch = "x86_64")))] | ||||
|     { | ||||
|         assert!(result.is_err(), "Should fail on non-Linux x86_64 platforms"); | ||||
|          | ||||
|         // Check that the error message is meaningful | ||||
|         let error = result.unwrap_err(); | ||||
|         let error_string = error.to_string(); | ||||
|         assert!(error_string.contains("Linux x86_64"),  | ||||
|                 "Error message should mention Linux x86_64: {}", error_string); | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_check_macos_arm() { | ||||
|     let result = platform::check_macos_arm(); | ||||
|      | ||||
|     // The result should depend on the current platform | ||||
|     #[cfg(all(target_os = "macos", target_arch = "aarch64"))] | ||||
|     { | ||||
|         assert!(result.is_ok(), "Should succeed on macOS ARM"); | ||||
|     } | ||||
|      | ||||
|     #[cfg(not(all(target_os = "macos", target_arch = "aarch64")))] | ||||
|     { | ||||
|         assert!(result.is_err(), "Should fail on non-macOS ARM platforms"); | ||||
|          | ||||
|         // Check that the error message is meaningful | ||||
|         let error = result.unwrap_err(); | ||||
|         let error_string = error.to_string(); | ||||
|         assert!(error_string.contains("macOS ARM"),  | ||||
|                 "Error message should mention macOS ARM: {}", error_string); | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_platform_error_creation() { | ||||
|     use sal_os::platform::PlatformError; | ||||
|      | ||||
|     // Test that we can create platform errors | ||||
|     let error = PlatformError::new("Test Error", "This is a test error message"); | ||||
|     let error_string = error.to_string(); | ||||
|      | ||||
|     assert!(error_string.contains("Test Error")); | ||||
|     assert!(error_string.contains("This is a test error message")); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_platform_error_display() { | ||||
|     use sal_os::platform::PlatformError; | ||||
|      | ||||
|     // Test error display formatting | ||||
|     let error = PlatformError::Generic("Category".to_string(), "Message".to_string()); | ||||
|     let error_string = format!("{}", error); | ||||
|      | ||||
|     assert!(error_string.contains("Category")); | ||||
|     assert!(error_string.contains("Message")); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_platform_error_debug() { | ||||
|     use sal_os::platform::PlatformError; | ||||
|      | ||||
|     // Test error debug formatting | ||||
|     let error = PlatformError::Generic("Category".to_string(), "Message".to_string()); | ||||
|     let debug_string = format!("{:?}", error); | ||||
|      | ||||
|     assert!(debug_string.contains("Generic")); | ||||
|     assert!(debug_string.contains("Category")); | ||||
|     assert!(debug_string.contains("Message")); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_platform_functions_are_deterministic() { | ||||
|     // Platform detection should be deterministic - same result every time | ||||
|     let osx1 = platform::is_osx(); | ||||
|     let osx2 = platform::is_osx(); | ||||
|     assert_eq!(osx1, osx2); | ||||
|      | ||||
|     let linux1 = platform::is_linux(); | ||||
|     let linux2 = platform::is_linux(); | ||||
|     assert_eq!(linux1, linux2); | ||||
|      | ||||
|     let arm1 = platform::is_arm(); | ||||
|     let arm2 = platform::is_arm(); | ||||
|     assert_eq!(arm1, arm2); | ||||
|      | ||||
|     let x86_1 = platform::is_x86(); | ||||
|     let x86_2 = platform::is_x86(); | ||||
|     assert_eq!(x86_1, x86_2); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_platform_check_functions_consistency() { | ||||
|     // The check functions should be consistent with the individual detection functions | ||||
|     let is_linux_x86 = platform::is_linux() && platform::is_x86(); | ||||
|     let check_linux_x86_result = platform::check_linux_x86().is_ok(); | ||||
|     assert_eq!(is_linux_x86, check_linux_x86_result); | ||||
|      | ||||
|     let is_macos_arm = platform::is_osx() && platform::is_arm(); | ||||
|     let check_macos_arm_result = platform::check_macos_arm().is_ok(); | ||||
|     assert_eq!(is_macos_arm, check_macos_arm_result); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_current_platform_info() { | ||||
|     // Print current platform info for debugging (this will show in test output with --nocapture) | ||||
|     println!("Current platform detection:"); | ||||
|     println!("  is_osx(): {}", platform::is_osx()); | ||||
|     println!("  is_linux(): {}", platform::is_linux()); | ||||
|     println!("  is_arm(): {}", platform::is_arm()); | ||||
|     println!("  is_x86(): {}", platform::is_x86()); | ||||
|     println!("  check_linux_x86(): {:?}", platform::check_linux_x86()); | ||||
|     println!("  check_macos_arm(): {:?}", platform::check_macos_arm()); | ||||
| } | ||||
							
								
								
									
										364
									
								
								os/tests/rhai_integration_tests.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										364
									
								
								os/tests/rhai_integration_tests.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,364 @@ | ||||
| use rhai::Engine; | ||||
| use sal_os::rhai::register_os_module; | ||||
| use tempfile::TempDir; | ||||
|  | ||||
| fn create_test_engine() -> Engine { | ||||
|     let mut engine = Engine::new(); | ||||
|     register_os_module(&mut engine).expect("Failed to register OS module"); | ||||
|     engine | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_rhai_module_registration() { | ||||
|     // Test that the OS module can be registered without errors | ||||
|     let _engine = create_test_engine(); | ||||
|  | ||||
|     // If we get here without panicking, the module was registered successfully | ||||
|     // We can't easily test function registration without calling the functions | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_rhai_file_operations() { | ||||
|     let engine = create_test_engine(); | ||||
|     let temp_dir = TempDir::new().unwrap(); | ||||
|     let temp_path = temp_dir.path().to_str().unwrap(); | ||||
|  | ||||
|     // Test file operations through Rhai | ||||
|     let script = format!( | ||||
|         r#" | ||||
|         let test_dir = "{}/test_rhai"; | ||||
|         let test_file = test_dir + "/test.txt"; | ||||
|         let content = "Hello from Rhai!"; | ||||
|          | ||||
|         // Create directory | ||||
|         mkdir(test_dir); | ||||
|          | ||||
|         // Check if directory exists | ||||
|         let dir_exists = exist(test_dir); | ||||
|          | ||||
|         // Write file | ||||
|         file_write(test_file, content); | ||||
|          | ||||
|         // Check if file exists | ||||
|         let file_exists = exist(test_file); | ||||
|          | ||||
|         // Read file | ||||
|         let read_content = file_read(test_file); | ||||
|          | ||||
|         // Return results | ||||
|         #{{"dir_exists": dir_exists, "file_exists": file_exists, "content_match": read_content == content}} | ||||
|     "#, | ||||
|         temp_path | ||||
|     ); | ||||
|  | ||||
|     let result: rhai::Map = engine.eval(&script).expect("Script execution failed"); | ||||
|  | ||||
|     assert_eq!(result["dir_exists"].as_bool().unwrap(), true); | ||||
|     assert_eq!(result["file_exists"].as_bool().unwrap(), true); | ||||
|     assert_eq!(result["content_match"].as_bool().unwrap(), true); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_rhai_file_size() { | ||||
|     let engine = create_test_engine(); | ||||
|     let temp_dir = TempDir::new().unwrap(); | ||||
|     let temp_path = temp_dir.path().to_str().unwrap(); | ||||
|  | ||||
|     let script = format!( | ||||
|         r#" | ||||
|         let test_file = "{}/size_test.txt"; | ||||
|         let content = "12345"; // 5 bytes | ||||
|          | ||||
|         file_write(test_file, content); | ||||
|         let size = file_size(test_file); | ||||
|          | ||||
|         size | ||||
|     "#, | ||||
|         temp_path | ||||
|     ); | ||||
|  | ||||
|     let result: i64 = engine.eval(&script).expect("Script execution failed"); | ||||
|     assert_eq!(result, 5); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_rhai_file_append() { | ||||
|     let engine = create_test_engine(); | ||||
|     let temp_dir = TempDir::new().unwrap(); | ||||
|     let temp_path = temp_dir.path().to_str().unwrap(); | ||||
|  | ||||
|     let script = format!( | ||||
|         r#" | ||||
|         let test_file = "{}/append_test.txt"; | ||||
|          | ||||
|         file_write(test_file, "Line 1\n"); | ||||
|         file_write_append(test_file, "Line 2\n"); | ||||
|          | ||||
|         let content = file_read(test_file); | ||||
|         content | ||||
|     "#, | ||||
|         temp_path | ||||
|     ); | ||||
|  | ||||
|     let result: String = engine.eval(&script).expect("Script execution failed"); | ||||
|     assert_eq!(result, "Line 1\nLine 2\n"); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_rhai_copy_and_move() { | ||||
|     let engine = create_test_engine(); | ||||
|     let temp_dir = TempDir::new().unwrap(); | ||||
|     let temp_path = temp_dir.path().to_str().unwrap(); | ||||
|  | ||||
|     let script = format!( | ||||
|         r#" | ||||
|         let source = "{}/source.txt"; | ||||
|         let copy_dest = "{}/copy.txt"; | ||||
|         let move_dest = "{}/moved.txt"; | ||||
|         let content = "Test content"; | ||||
|  | ||||
|         // Create source file | ||||
|         file_write(source, content); | ||||
|  | ||||
|         // Copy file | ||||
|         copy(source, copy_dest); | ||||
|  | ||||
|         // Move the copy | ||||
|         mv(copy_dest, move_dest); | ||||
|  | ||||
|         // Check results | ||||
|         let source_exists = exist(source); | ||||
|         let copy_exists = exist(copy_dest); | ||||
|         let move_exists = exist(move_dest); | ||||
|         let move_content = file_read(move_dest); | ||||
|  | ||||
|         #{{"source_exists": source_exists, "copy_exists": copy_exists, "move_exists": move_exists, "content_match": move_content == content}} | ||||
|     "#, | ||||
|         temp_path, temp_path, temp_path | ||||
|     ); | ||||
|  | ||||
|     let result: rhai::Map = engine.eval(&script).expect("Script execution failed"); | ||||
|  | ||||
|     assert_eq!(result["source_exists"].as_bool().unwrap(), true); | ||||
|     assert_eq!(result["copy_exists"].as_bool().unwrap(), false); // Should be moved | ||||
|     assert_eq!(result["move_exists"].as_bool().unwrap(), true); | ||||
|     assert_eq!(result["content_match"].as_bool().unwrap(), true); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_rhai_delete() { | ||||
|     let engine = create_test_engine(); | ||||
|     let temp_dir = TempDir::new().unwrap(); | ||||
|     let temp_path = temp_dir.path().to_str().unwrap(); | ||||
|  | ||||
|     let script = format!( | ||||
|         r#" | ||||
|         let test_file = "{}/delete_test.txt"; | ||||
|          | ||||
|         // Create file | ||||
|         file_write(test_file, "content"); | ||||
|         let exists_before = exist(test_file); | ||||
|          | ||||
|         // Delete file | ||||
|         delete(test_file); | ||||
|         let exists_after = exist(test_file); | ||||
|          | ||||
|         #{{"before": exists_before, "after": exists_after}} | ||||
|     "#, | ||||
|         temp_path | ||||
|     ); | ||||
|  | ||||
|     let result: rhai::Map = engine.eval(&script).expect("Script execution failed"); | ||||
|  | ||||
|     assert_eq!(result["before"].as_bool().unwrap(), true); | ||||
|     assert_eq!(result["after"].as_bool().unwrap(), false); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_rhai_find_files() { | ||||
|     let engine = create_test_engine(); | ||||
|     let temp_dir = TempDir::new().unwrap(); | ||||
|     let temp_path = temp_dir.path().to_str().unwrap(); | ||||
|  | ||||
|     let script = format!( | ||||
|         r#" | ||||
|         let test_dir = "{}/find_test"; | ||||
|         mkdir(test_dir); | ||||
|          | ||||
|         // Create test files | ||||
|         file_write(test_dir + "/file1.txt", "content1"); | ||||
|         file_write(test_dir + "/file2.txt", "content2"); | ||||
|         file_write(test_dir + "/other.log", "log content"); | ||||
|          | ||||
|         // Find .txt files | ||||
|         let txt_files = find_files(test_dir, "*.txt"); | ||||
|         let all_files = find_files(test_dir, "*"); | ||||
|          | ||||
|         #{{"txt_count": txt_files.len(), "all_count": all_files.len()}} | ||||
|     "#, | ||||
|         temp_path | ||||
|     ); | ||||
|  | ||||
|     let result: rhai::Map = engine.eval(&script).expect("Script execution failed"); | ||||
|  | ||||
|     assert_eq!(result["txt_count"].as_int().unwrap(), 2); | ||||
|     assert!(result["all_count"].as_int().unwrap() >= 3); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_rhai_which_command() { | ||||
|     let engine = create_test_engine(); | ||||
|  | ||||
|     let script = r#" | ||||
|         let ls_path = which("ls"); | ||||
|         let nonexistent = which("nonexistentcommand12345"); | ||||
|          | ||||
|         #{"ls_found": ls_path.len() > 0, "nonexistent_found": nonexistent.len() > 0} | ||||
|     "#; | ||||
|  | ||||
|     let result: rhai::Map = engine.eval(script).expect("Script execution failed"); | ||||
|  | ||||
|     assert_eq!(result["ls_found"].as_bool().unwrap(), true); | ||||
|     assert_eq!(result["nonexistent_found"].as_bool().unwrap(), false); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_rhai_error_handling() { | ||||
|     let engine = create_test_engine(); | ||||
|  | ||||
|     // Test that errors are properly propagated to Rhai | ||||
|     // Instead of try-catch, just test that the function call fails | ||||
|     let script = r#"file_read("/nonexistent/path/file.txt")"#; | ||||
|  | ||||
|     let result = engine.eval::<String>(script); | ||||
|     assert!( | ||||
|         result.is_err(), | ||||
|         "Expected error when reading non-existent file" | ||||
|     ); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_rhai_package_functions() { | ||||
|     let engine = create_test_engine(); | ||||
|  | ||||
|     // Test that package functions are registered by calling them | ||||
|  | ||||
|     let script = r#" | ||||
|         let platform = package_platform(); | ||||
|         let debug_result = package_set_debug(true); | ||||
|          | ||||
|         #{"platform": platform, "debug": debug_result} | ||||
|     "#; | ||||
|  | ||||
|     let result: rhai::Map = engine.eval(script).expect("Script execution failed"); | ||||
|  | ||||
|     // Platform should be a non-empty string | ||||
|     let platform: String = result["platform"].clone().try_cast().unwrap(); | ||||
|     assert!(!platform.is_empty()); | ||||
|  | ||||
|     // Debug setting should return true | ||||
|     assert_eq!(result["debug"].as_bool().unwrap(), true); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_rhai_download_functions() { | ||||
|     let engine = create_test_engine(); | ||||
|  | ||||
|     // Test that download functions are registered by calling them | ||||
|  | ||||
|     let temp_dir = TempDir::new().unwrap(); | ||||
|     let temp_path = temp_dir.path().to_str().unwrap(); | ||||
|  | ||||
|     let script = format!( | ||||
|         r#" | ||||
|         let test_file = "{}/test_script.sh"; | ||||
|          | ||||
|         // Create a test script | ||||
|         file_write(test_file, "echo 'test'"); | ||||
|          | ||||
|         // Make it executable | ||||
|         try {{ | ||||
|             let result = chmod_exec(test_file); | ||||
|             result.len() >= 0 // chmod_exec returns a string, so check if it's valid | ||||
|         }} catch {{ | ||||
|             false | ||||
|         }} | ||||
|     "#, | ||||
|         temp_path | ||||
|     ); | ||||
|  | ||||
|     let result: bool = engine.eval(&script).expect("Script execution failed"); | ||||
|     assert!(result); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_rhai_array_returns() { | ||||
|     let engine = create_test_engine(); | ||||
|     let temp_dir = TempDir::new().unwrap(); | ||||
|     let temp_path = temp_dir.path().to_str().unwrap(); | ||||
|  | ||||
|     let script = format!( | ||||
|         r#" | ||||
|         let test_dir = "{}/array_test"; | ||||
|         mkdir(test_dir); | ||||
|          | ||||
|         // Create some files | ||||
|         file_write(test_dir + "/file1.txt", "content"); | ||||
|         file_write(test_dir + "/file2.txt", "content"); | ||||
|          | ||||
|         // Test that find_files returns an array | ||||
|         let files = find_files(test_dir, "*.txt"); | ||||
|          | ||||
|         // Test array operations | ||||
|         let count = files.len(); | ||||
|         let first_file = if count > 0 {{ files[0] }} else {{ "" }}; | ||||
|          | ||||
|         #{{"count": count, "has_files": count > 0, "first_file_exists": first_file.len() > 0}} | ||||
|     "#, | ||||
|         temp_path | ||||
|     ); | ||||
|  | ||||
|     let result: rhai::Map = engine.eval(&script).expect("Script execution failed"); | ||||
|  | ||||
|     assert_eq!(result["count"].as_int().unwrap(), 2); | ||||
|     assert_eq!(result["has_files"].as_bool().unwrap(), true); | ||||
|     assert_eq!(result["first_file_exists"].as_bool().unwrap(), true); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_rhai_platform_functions() { | ||||
|     let engine = create_test_engine(); | ||||
|  | ||||
|     let script = r#" | ||||
|         let is_osx = platform_is_osx(); | ||||
|         let is_linux = platform_is_linux(); | ||||
|         let is_arm = platform_is_arm(); | ||||
|         let is_x86 = platform_is_x86(); | ||||
|  | ||||
|         // Test that platform detection is consistent | ||||
|         let platform_consistent = !(is_osx && is_linux); | ||||
|         let arch_consistent = !(is_arm && is_x86); | ||||
|  | ||||
|         #{"osx": is_osx, "linux": is_linux, "arm": is_arm, "x86": is_x86, "platform_consistent": platform_consistent, "arch_consistent": arch_consistent} | ||||
|     "#; | ||||
|  | ||||
|     let result: rhai::Map = engine.eval(script).expect("Script execution failed"); | ||||
|  | ||||
|     // Verify platform detection consistency | ||||
|     assert_eq!(result["platform_consistent"].as_bool().unwrap(), true); | ||||
|     assert_eq!(result["arch_consistent"].as_bool().unwrap(), true); | ||||
|  | ||||
|     // At least one platform should be detected | ||||
|     let osx = result["osx"].as_bool().unwrap(); | ||||
|     let linux = result["linux"].as_bool().unwrap(); | ||||
|  | ||||
|     // At least one architecture should be detected | ||||
|     let arm = result["arm"].as_bool().unwrap(); | ||||
|     let x86 = result["x86"].as_bool().unwrap(); | ||||
|  | ||||
|     // Print current platform for debugging | ||||
|     println!( | ||||
|         "Platform detection: OSX={}, Linux={}, ARM={}, x86={}", | ||||
|         osx, linux, arm, x86 | ||||
|     ); | ||||
| } | ||||
| @@ -40,7 +40,7 @@ pub type Result<T> = std::result::Result<T, Error>; | ||||
| pub mod cmd; | ||||
| pub use sal_mycelium as mycelium; | ||||
| pub mod net; | ||||
| pub mod os; | ||||
| pub use sal_os as os; | ||||
| pub mod postgresclient; | ||||
| pub mod process; | ||||
| pub use sal_redisclient as redisclient; | ||||
|   | ||||
							
								
								
									
										245
									
								
								src/os/README.md
									
									
									
									
									
								
							
							
						
						
									
										245
									
								
								src/os/README.md
									
									
									
									
									
								
							| @@ -1,245 +0,0 @@ | ||||
| # SAL OS Module (`sal::os`) | ||||
|  | ||||
| The `sal::os` module provides a comprehensive suite of operating system interaction utilities. It aims to offer a cross-platform abstraction layer for common OS-level tasks, simplifying system programming in Rust. | ||||
|  | ||||
| This module is composed of three main sub-modules: | ||||
| - [`fs`](#fs): File system operations. | ||||
| - [`download`](#download): File downloading and basic installation. | ||||
| - [`package`](#package): System package management. | ||||
|  | ||||
| ## Key Design Points | ||||
|  | ||||
| The `sal::os` module is engineered with several core principles to provide a robust and developer-friendly interface for OS interactions: | ||||
|  | ||||
| - **Cross-Platform Abstraction**: A primary goal is to offer a unified API for common OS tasks, smoothing over differences between operating systems (primarily Linux and macOS). While it strives for abstraction, it leverages platform-specific tools (e.g., `rsync` on Linux, `robocopy` on Windows for `fs::copy` or `fs::rsync`; `apt` on Debian-based systems, `brew` on macOS for `package` management) for optimal performance and behavior when necessary. | ||||
| - **Modular Structure**: Functionality is organized into logical sub-modules: | ||||
|     - `fs`: For comprehensive file and directory manipulation. | ||||
|     - `download`: For retrieving files from URLs, with support for extraction and basic installation. | ||||
|     - `package`: For interacting with system package managers. | ||||
| - **Granular Error Handling**: Each sub-module features custom error enums (`FsError`, `DownloadError`, `PackageError`) to provide specific and actionable feedback, aiding in debugging and robust error management. | ||||
| - **Sensible Defaults and Defensive Operations**: Many functions are designed to be "defensive," e.g., `mkdir` creates parent directories if they don't exist and doesn't fail if the directory already exists. `delete` doesn't error if the target is already gone. | ||||
| - **Facade for Simplicity**: The `package` sub-module uses a `PackHero` facade to provide a simple entry point for common package operations, automatically detecting the underlying OS and package manager. | ||||
| - **Rhai Scriptability**: A significant portion of the `sal::os` module's functionality is exposed to Rhai scripts via `herodo`, enabling powerful automation of OS-level tasks. | ||||
|  | ||||
| ## `fs` - File System Operations | ||||
|  | ||||
| The `fs` sub-module (`sal::os::fs`) offers a robust set of functions for interacting with the file system. | ||||
|  | ||||
| **Key Features:** | ||||
|  | ||||
| *   **Error Handling**: A custom `FsError` enum for detailed error reporting on file system operations. | ||||
| *   **File Operations**: | ||||
|     *   `copy(src, dest)`: Copies files and directories, with support for wildcards and recursive copying. Uses platform-specific commands (`cp -R`, `robocopy /MIR`). | ||||
|     *   `exist(path)`: Checks if a file or directory exists. | ||||
|     *   `find_file(dir, filename_pattern)`: Finds a single file in a directory, supporting wildcards. | ||||
|     *   `find_files(dir, filename_pattern)`: Finds multiple files in a directory, supporting wildcards. | ||||
|     *   `file_size(path)`: Returns the size of a file in bytes. | ||||
|     *   `file_read(path)`: Reads the entire content of a file into a string. | ||||
|     *   `file_write(path, content)`: Writes content to a file, overwriting if it exists, and creating parent directories if needed. | ||||
|     *   `file_write_append(path, content)`: Appends content to a file, creating it and parent directories if needed. | ||||
| *   **Directory Operations**: | ||||
|     *   `find_dir(parent_dir, dirname_pattern)`: Finds a single directory within a parent directory, supporting wildcards. | ||||
|     *   `find_dirs(parent_dir, dirname_pattern)`: Finds multiple directories recursively within a parent directory, supporting wildcards. | ||||
|     *   `delete(path)`: Deletes files or directories. | ||||
|     *   `mkdir(path)`: Creates a directory, including parent directories if necessary. | ||||
|     *   `rsync(src, dest)`: Synchronizes directories using platform-specific commands (`rsync -a --delete`, `robocopy /MIR`). | ||||
|     *   `chdir(path)`: Changes the current working directory. | ||||
| *   **Path Operations**: | ||||
|     *   `mv(src, dest)`: Moves or renames files and directories. Handles cross-device moves by falling back to copy-then-delete. | ||||
| *   **Command Utilities**: | ||||
|     *   `which(command_name)`: Checks if a command exists in the system's PATH and returns its path. | ||||
|  | ||||
| **Usage Example (fs):** | ||||
|  | ||||
| ```rust | ||||
| use sal::os::fs; | ||||
|  | ||||
| fn main() -> Result<(), Box<dyn std::error::Error>> { | ||||
|     if !fs::exist("my_dir") { | ||||
|         fs::mkdir("my_dir")?; | ||||
|         println!("Created directory 'my_dir'"); | ||||
|     } | ||||
|  | ||||
|     fs::file_write("my_dir/example.txt", "Hello from SAL!")?; | ||||
|     let content = fs::file_read("my_dir/example.txt")?; | ||||
|     println!("File content: {}", content); | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ## `download` - File Downloading and Installation | ||||
|  | ||||
| The `download` sub-module (`sal::os::download`) provides utilities for downloading files from URLs and performing basic installation tasks. | ||||
|  | ||||
| **Key Features:** | ||||
|  | ||||
| *   **Error Handling**: A custom `DownloadError` enum for download-specific errors. | ||||
| *   **File Downloading**: | ||||
|     *   `download(url, dest_dir, min_size_kb)`: Downloads a file to a specified directory. | ||||
|         *   Uses `curl` with progress display. | ||||
|         *   Supports minimum file size checks. | ||||
|         *   Automatically extracts common archive formats (`.tar.gz`, `.tgz`, `.tar`, `.zip`) into `dest_dir`. | ||||
|     *   `download_file(url, dest_file_path, min_size_kb)`: Downloads a file to a specific file path without automatic extraction. | ||||
| *   **File Permissions**: | ||||
|     *   `chmod_exec(path)`: Makes a file executable (equivalent to `chmod +x` on Unix-like systems). | ||||
| *   **Download and Install**: | ||||
|     *   `download_install(url, min_size_kb)`: Downloads a file (to `/tmp/`) and attempts to install it if it's a supported package format. | ||||
|         *   Currently supports `.deb` packages on Debian-based systems. | ||||
|         *   For `.deb` files, it uses `sudo dpkg --install` and attempts `sudo apt-get install -f -y` to fix dependencies if needed. | ||||
|         *   Handles archives by extracting them to `/tmp/` first. | ||||
|  | ||||
| **Usage Example (download):** | ||||
|  | ||||
| ```rust | ||||
| use sal::os::download; | ||||
|  | ||||
| fn main() -> Result<(), Box<dyn std::error::Error>> { | ||||
|     let archive_url = "https://example.com/my_archive.tar.gz"; | ||||
|     let output_dir = "/tmp/my_app"; | ||||
|      | ||||
|     // Download and extract an archive | ||||
|     let extracted_path = download::download(archive_url, output_dir, 1024)?; // Min 1MB | ||||
|     println!("Archive extracted to: {}", extracted_path); | ||||
|  | ||||
|     // Download a script and make it executable | ||||
|     let script_url = "https://example.com/my_script.sh"; | ||||
|     let script_path = "/tmp/my_script.sh"; | ||||
|     download::download_file(script_url, script_path, 0)?; | ||||
|     download::chmod_exec(script_path)?; | ||||
|     println!("Script downloaded and made executable at: {}", script_path); | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ## `package` - System Package Management | ||||
|  | ||||
| The `package` sub-module (`sal::os::package`) offers an abstraction layer for interacting with system package managers like APT (for Debian/Ubuntu) and Homebrew (for macOS). | ||||
|  | ||||
| **Key Features:** | ||||
|  | ||||
| *   **Error Handling**: A custom `PackageError` enum. | ||||
| *   **Platform Detection**: Identifies the current OS (Ubuntu, macOS, or Unknown) to use the appropriate package manager. | ||||
| *   **`PackageManager` Trait**: Defines a common interface for package operations: | ||||
|     *   `install(package_name)` | ||||
|     *   `remove(package_name)` | ||||
|     *   `update()` (updates package lists) | ||||
|     *   `upgrade()` (upgrades all installed packages) | ||||
|     *   `list_installed()` | ||||
|     *   `search(query)` | ||||
|     *   `is_installed(package_name)` | ||||
| *   **Implementations**: | ||||
|     *   `AptPackageManager`: For Debian/Ubuntu systems (uses `apt-get`, `dpkg`). | ||||
|     *   `BrewPackageManager`: For macOS systems (uses `brew`). | ||||
| *   **`PackHero` Facade**: A simple entry point to access package management functions in a platform-agnostic way. | ||||
|     *   `PackHero::new().install("nginx")?` | ||||
|  | ||||
| **Usage Example (package):** | ||||
|  | ||||
| ```rust | ||||
| use sal::os::package::PackHero; | ||||
|  | ||||
| fn main() -> Result<(), Box<dyn std::error::Error>> { | ||||
|     let pack_hero = PackHero::new(); | ||||
|  | ||||
|     // Check if a package is installed | ||||
|     if !pack_hero.is_installed("htop")? { | ||||
|         println!("htop is not installed. Attempting to install..."); | ||||
|         pack_hero.install("htop")?; | ||||
|         println!("htop installed successfully."); | ||||
|     } else { | ||||
|         println!("htop is already installed."); | ||||
|     } | ||||
|  | ||||
|     // Update package lists | ||||
|     println!("Updating package lists..."); | ||||
|     pack_hero.update()?; | ||||
|     println!("Package lists updated."); | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ## Rhai Scripting with `herodo` | ||||
|  | ||||
| The `sal::os` module is extensively scriptable via `herodo`, allowing for automation of various operating system tasks directly from Rhai scripts. The `sal::rhai::os` module registers the necessary functions. | ||||
|  | ||||
| ### File System (`fs`) Functions | ||||
|  | ||||
| - `copy(src: String, dest: String) -> String`: Copies files/directories (supports wildcards). | ||||
| - `exist(path: String) -> bool`: Checks if a file or directory exists. | ||||
| - `find_file(dir: String, filename_pattern: String) -> String`: Finds a single file in `dir` matching `filename_pattern`. | ||||
| - `find_files(dir: String, filename_pattern: String) -> Array`: Finds multiple files in `dir` (recursive). | ||||
| - `find_dir(parent_dir: String, dirname_pattern: String) -> String`: Finds a single directory in `parent_dir`. | ||||
| - `find_dirs(parent_dir: String, dirname_pattern: String) -> Array`: Finds multiple directories in `parent_dir` (recursive). | ||||
| - `delete(path: String) -> String`: Deletes a file or directory. | ||||
| - `mkdir(path: String) -> String`: Creates a directory (and parents if needed). | ||||
| - `file_size(path: String) -> Int`: Returns file size in bytes. | ||||
| - `rsync(src: String, dest: String) -> String`: Synchronizes directories. | ||||
| - `chdir(path: String) -> String`: Changes the current working directory. | ||||
| - `file_read(path: String) -> String`: Reads entire file content. | ||||
| - `file_write(path: String, content: String) -> String`: Writes content to a file (overwrites). | ||||
| - `file_write_append(path: String, content: String) -> String`: Appends content to a file. | ||||
| - `mv(src: String, dest: String) -> String`: Moves/renames a file or directory. | ||||
| - `which(command_name: String) -> String`: Checks if a command exists in PATH and returns its path. | ||||
| - `cmd_ensure_exists(commands: String) -> String`: Ensures one or more commands (comma-separated) exist in PATH; throws an error if any are missing. | ||||
|  | ||||
| ### Download Functions | ||||
|  | ||||
| - `download(url: String, dest_dir: String, min_size_kb: Int) -> String`: Downloads from `url` to `dest_dir`, extracts common archives. | ||||
| - `download_file(url: String, dest_file_path: String, min_size_kb: Int) -> String`: Downloads from `url` to `dest_file_path` (no extraction). | ||||
| - `download_install(url: String, min_size_kb: Int) -> String`: Downloads and attempts to install (e.g., `.deb` packages). | ||||
| - `chmod_exec(path: String) -> String`: Makes a file executable (`chmod +x`). | ||||
|  | ||||
| ### Package Management Functions | ||||
|  | ||||
| - `package_install(package_name: String) -> String`: Installs a package. | ||||
| - `package_remove(package_name: String) -> String`: Removes a package. | ||||
| - `package_update() -> String`: Updates package lists. | ||||
| - `package_upgrade() -> String`: Upgrades all installed packages. | ||||
| - `package_list() -> Array`: Lists all installed packages. | ||||
| - `package_search(query: String) -> Array`: Searches for packages. | ||||
| - `package_is_installed(package_name: String) -> bool`: Checks if a package is installed. | ||||
| - `package_set_debug(debug: bool) -> bool`: Enables/disables debug logging for package operations. | ||||
| - `package_platform() -> String`: Returns the detected package platform (e.g., "Ubuntu", "MacOS"). | ||||
|  | ||||
| ### Rhai Example | ||||
|  | ||||
| ```rhai | ||||
| // File system operations | ||||
| let test_dir = "/tmp/sal_os_rhai_test"; | ||||
| if exist(test_dir) { | ||||
|     delete(test_dir); | ||||
| } | ||||
| mkdir(test_dir); | ||||
| print(`Created directory: ${test_dir}`); | ||||
|  | ||||
| file_write(`${test_dir}/message.txt`, "Hello from Rhai OS module!"); | ||||
| let content = file_read(`${test_dir}/message.txt`); | ||||
| print(`File content: ${content}`); | ||||
|  | ||||
| // Download operation (example URL, may not be active) | ||||
| // let script_url = "https://raw.githubusercontent.com/someuser/somescript/main/script.sh"; | ||||
| // let script_path = `${test_dir}/downloaded_script.sh`; | ||||
| // try { | ||||
| //   download_file(script_url, script_path, 0); | ||||
| //   chmod_exec(script_path); | ||||
| //   print(`Downloaded and made executable: ${script_path}`); | ||||
| // } catch (e) { | ||||
| //   print(`Download example failed (this is okay for a test): ${e}`); | ||||
| // } | ||||
|  | ||||
| // Package management (illustrative, requires sudo for install/remove/update) | ||||
| print(`Package platform: ${package_platform()}`); | ||||
| if !package_is_installed("htop") { | ||||
|     print("htop is not installed."); | ||||
|     // package_install("htop"); // Would require sudo | ||||
| } else { | ||||
|     print("htop is already installed."); | ||||
| } | ||||
|  | ||||
| print("OS module Rhai script finished."); | ||||
| ``` | ||||
|  | ||||
| This module provides a powerful and convenient way to handle common OS-level tasks within your Rust applications. | ||||
| @@ -1,8 +0,0 @@ | ||||
| mod fs; | ||||
| mod download; | ||||
| pub mod package; | ||||
|  | ||||
| pub use fs::*; | ||||
| pub use download::*; | ||||
| pub use package::*; | ||||
| pub mod platform; | ||||
| @@ -2,9 +2,9 @@ | ||||
| //! | ||||
| //! This module provides Rhai wrappers for functions that interact with the Rhai engine itself. | ||||
|  | ||||
| use rhai::{Engine, EvalAltResult, NativeCallContext}; | ||||
| use crate::os; | ||||
| use super::error::ToRhaiError; | ||||
| use rhai::{Engine, EvalAltResult, NativeCallContext}; | ||||
| use sal_os as os; | ||||
|  | ||||
| /// Register core module functions with the Rhai engine | ||||
| /// | ||||
|   | ||||
| @@ -7,8 +7,8 @@ mod buildah; | ||||
| mod core; | ||||
| pub mod error; | ||||
| mod nerdctl; | ||||
| mod os; | ||||
| mod platform; | ||||
| // OS module is now provided by sal-os package | ||||
| // Platform module is now provided by sal-os package | ||||
| mod postgresclient; | ||||
| mod process; | ||||
|  | ||||
| @@ -26,8 +26,8 @@ pub use rhai::{Array, Dynamic, Engine, EvalAltResult, Map}; | ||||
| // Re-export error module | ||||
| pub use error::*; | ||||
|  | ||||
| // Re-export specific functions from modules to avoid name conflicts | ||||
| pub use os::{ | ||||
| // Re-export specific functions from sal-os package | ||||
| pub use sal_os::rhai::{ | ||||
|     delete, | ||||
|     // Download functions | ||||
|     download, | ||||
| @@ -106,7 +106,7 @@ pub use sal_text::rhai::register_text_module; | ||||
| pub use vault::register_crypto_module; | ||||
|  | ||||
| // Rename copy functions to avoid conflicts | ||||
| pub use os::copy as os_copy; | ||||
| pub use sal_os::rhai::copy as os_copy; | ||||
|  | ||||
| /// Register all SAL modules with the Rhai engine | ||||
| /// | ||||
| @@ -132,7 +132,7 @@ pub fn register(engine: &mut Engine) -> Result<(), Box<rhai::EvalAltResult>> { | ||||
|     core::register_core_module(engine)?; | ||||
|  | ||||
|     // Register OS module functions | ||||
|     os::register_os_module(engine)?; | ||||
|     sal_os::rhai::register_os_module(engine)?; | ||||
|  | ||||
|     // Register Process module functions | ||||
|     process::register_process_module(engine)?; | ||||
| @@ -167,8 +167,7 @@ pub fn register(engine: &mut Engine) -> Result<(), Box<rhai::EvalAltResult>> { | ||||
|     // Register PostgreSQL client module functions | ||||
|     postgresclient::register_postgresclient_module(engine)?; | ||||
|  | ||||
|     // Register Platform module functions | ||||
|     platform::register(engine); | ||||
|     // Platform functions are now registered by sal-os package | ||||
|  | ||||
|     // Register Screen module functions | ||||
|     screen::register(engine); | ||||
|   | ||||
| @@ -1,40 +0,0 @@ | ||||
| use crate::os::platform; | ||||
| use rhai::{plugin::*, Engine}; | ||||
|  | ||||
| #[export_module] | ||||
| pub mod platform_functions { | ||||
|     #[rhai_fn(name = "platform_is_osx")] | ||||
|     pub fn is_osx() -> bool { | ||||
|         platform::is_osx() | ||||
|     } | ||||
|  | ||||
|     #[rhai_fn(name = "platform_is_linux")] | ||||
|     pub fn is_linux() -> bool { | ||||
|         platform::is_linux() | ||||
|     } | ||||
|  | ||||
|     #[rhai_fn(name = "platform_is_arm")] | ||||
|     pub fn is_arm() -> bool { | ||||
|         platform::is_arm() | ||||
|     } | ||||
|  | ||||
|     #[rhai_fn(name = "platform_is_x86")] | ||||
|     pub fn is_x86() -> bool { | ||||
|         platform::is_x86() | ||||
|     } | ||||
|  | ||||
|     #[rhai_fn(name = "platform_check_linux_x86")] | ||||
|     pub fn check_linux_x86() -> Result<(), crate::rhai::error::SalError> { | ||||
|         platform::check_linux_x86() | ||||
|     } | ||||
|  | ||||
|     #[rhai_fn(name = "platform_check_macos_arm")] | ||||
|     pub fn check_macos_arm() -> Result<(), crate::rhai::error::SalError> { | ||||
|         platform::check_macos_arm() | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub fn register(engine: &mut Engine) { | ||||
|     let platform_module = exported_module!(platform_functions); | ||||
|     engine.register_global_module(platform_module.into()); | ||||
| } | ||||
| @@ -1,9 +1,9 @@ | ||||
| // File: /root/code/git.threefold.info/herocode/sal/src/virt/nerdctl/container.rs | ||||
|  | ||||
| use std::collections::HashMap; | ||||
| use crate::virt::nerdctl::{execute_nerdctl_command, NerdctlError}; | ||||
| use crate::os; | ||||
| use super::container_types::Container; | ||||
| use crate::virt::nerdctl::{execute_nerdctl_command, NerdctlError}; | ||||
| use sal_os as os; | ||||
| use std::collections::HashMap; | ||||
|  | ||||
| impl Container { | ||||
|     /// Create a new container reference with the given name | ||||
| @@ -18,10 +18,12 @@ impl Container { | ||||
|     pub fn new(name: &str) -> Result<Self, NerdctlError> { | ||||
|         // Check if required commands exist | ||||
|         match os::cmd_ensure_exists("nerdctl,runc,buildah") { | ||||
|             Err(e) => return Err(NerdctlError::CommandExecutionFailed( | ||||
|                 std::io::Error::new(std::io::ErrorKind::NotFound, | ||||
|                 format!("Required commands not found: {}", e)) | ||||
|             )), | ||||
|             Err(e) => { | ||||
|                 return Err(NerdctlError::CommandExecutionFailed(std::io::Error::new( | ||||
|                     std::io::ErrorKind::NotFound, | ||||
|                     format!("Required commands not found: {}", e), | ||||
|                 ))) | ||||
|             } | ||||
|             _ => {} | ||||
|         } | ||||
|  | ||||
| @@ -29,7 +31,9 @@ impl Container { | ||||
|         let result = execute_nerdctl_command(&["ps", "-a", "--format", "{{.Names}} {{.ID}}"])?; | ||||
|  | ||||
|         // Look for the container name in the output | ||||
|         let container_id = result.stdout.lines() | ||||
|         let container_id = result | ||||
|             .stdout | ||||
|             .lines() | ||||
|             .filter_map(|line| { | ||||
|                 if line.starts_with(&format!("{} ", name)) { | ||||
|                     Some(line.split_whitespace().nth(1)?.to_string()) | ||||
|   | ||||
| @@ -15,7 +15,7 @@ use tempfile::NamedTempFile; | ||||
| #[test] | ||||
| fn test_template_builder_basic_string_variable() { | ||||
|     // Create a temporary template file | ||||
|     let mut temp_file = NamedTempFile::new().expect("Failed to create temp file"); | ||||
|     let temp_file = NamedTempFile::new().expect("Failed to create temp file"); | ||||
|     let template_content = "Hello {{name}}!"; | ||||
|     fs::write(temp_file.path(), template_content).expect("Failed to write template"); | ||||
|  | ||||
| @@ -30,7 +30,7 @@ fn test_template_builder_basic_string_variable() { | ||||
|  | ||||
| #[test] | ||||
| fn test_template_builder_multiple_variables() { | ||||
|     let mut temp_file = NamedTempFile::new().expect("Failed to create temp file"); | ||||
|     let temp_file = NamedTempFile::new().expect("Failed to create temp file"); | ||||
|     let template_content = "{{greeting}} {{name}}, you have {{count}} messages."; | ||||
|     fs::write(temp_file.path(), template_content).expect("Failed to write template"); | ||||
|  | ||||
| @@ -47,7 +47,7 @@ fn test_template_builder_multiple_variables() { | ||||
|  | ||||
| #[test] | ||||
| fn test_template_builder_different_types() { | ||||
|     let mut temp_file = NamedTempFile::new().expect("Failed to create temp file"); | ||||
|     let temp_file = NamedTempFile::new().expect("Failed to create temp file"); | ||||
|     let template_content = "String: {{text}}, Int: {{number}}, Float: {{decimal}}, Bool: {{flag}}"; | ||||
|     fs::write(temp_file.path(), template_content).expect("Failed to write template"); | ||||
|  | ||||
| @@ -65,8 +65,9 @@ fn test_template_builder_different_types() { | ||||
|  | ||||
| #[test] | ||||
| fn test_template_builder_array_variable() { | ||||
|     let mut temp_file = NamedTempFile::new().expect("Failed to create temp file"); | ||||
|     let template_content = "Items: {% for item in items %}{{item}}{% if not loop.last %}, {% endif %}{% endfor %}"; | ||||
|     let temp_file = NamedTempFile::new().expect("Failed to create temp file"); | ||||
|     let template_content = | ||||
|         "Items: {% for item in items %}{{item}}{% if not loop.last %}, {% endif %}{% endfor %}"; | ||||
|     fs::write(temp_file.path(), template_content).expect("Failed to write template"); | ||||
|  | ||||
|     let items = vec!["apple", "banana", "cherry"]; | ||||
| @@ -81,7 +82,7 @@ fn test_template_builder_array_variable() { | ||||
|  | ||||
| #[test] | ||||
| fn test_template_builder_add_vars_hashmap() { | ||||
|     let mut temp_file = NamedTempFile::new().expect("Failed to create temp file"); | ||||
|     let temp_file = NamedTempFile::new().expect("Failed to create temp file"); | ||||
|     let template_content = "{{title}}: {{description}}"; | ||||
|     fs::write(temp_file.path(), template_content).expect("Failed to write template"); | ||||
|  | ||||
| @@ -101,7 +102,7 @@ fn test_template_builder_add_vars_hashmap() { | ||||
| #[test] | ||||
| fn test_template_builder_render_to_file() { | ||||
|     // Create template file | ||||
|     let mut template_file = NamedTempFile::new().expect("Failed to create template file"); | ||||
|     let template_file = NamedTempFile::new().expect("Failed to create template file"); | ||||
|     let template_content = "Hello {{name}}, today is {{day}}."; | ||||
|     fs::write(template_file.path(), template_content).expect("Failed to write template"); | ||||
|  | ||||
| @@ -121,8 +122,9 @@ fn test_template_builder_render_to_file() { | ||||
|  | ||||
| #[test] | ||||
| fn test_template_builder_conditional() { | ||||
|     let mut temp_file = NamedTempFile::new().expect("Failed to create temp file"); | ||||
|     let template_content = "{% if show_message %}Message: {{message}}{% else %}No message{% endif %}"; | ||||
|     let temp_file = NamedTempFile::new().expect("Failed to create temp file"); | ||||
|     let template_content = | ||||
|         "{% if show_message %}Message: {{message}}{% else %}No message{% endif %}"; | ||||
|     fs::write(temp_file.path(), template_content).expect("Failed to write template"); | ||||
|  | ||||
|     // Test with condition true | ||||
| @@ -148,7 +150,7 @@ fn test_template_builder_conditional() { | ||||
|  | ||||
| #[test] | ||||
| fn test_template_builder_loop_with_index() { | ||||
|     let mut temp_file = NamedTempFile::new().expect("Failed to create temp file"); | ||||
|     let temp_file = NamedTempFile::new().expect("Failed to create temp file"); | ||||
|     let template_content = "{% for item in items %}{{loop.index}}: {{item}}\n{% endfor %}"; | ||||
|     fs::write(temp_file.path(), template_content).expect("Failed to write template"); | ||||
|  | ||||
| @@ -164,7 +166,7 @@ fn test_template_builder_loop_with_index() { | ||||
|  | ||||
| #[test] | ||||
| fn test_template_builder_nested_variables() { | ||||
|     let mut temp_file = NamedTempFile::new().expect("Failed to create temp file"); | ||||
|     let temp_file = NamedTempFile::new().expect("Failed to create temp file"); | ||||
|     let template_content = "User: {{user.name}} ({{user.email}})"; | ||||
|     fs::write(temp_file.path(), template_content).expect("Failed to write template"); | ||||
|  | ||||
| @@ -183,7 +185,7 @@ fn test_template_builder_nested_variables() { | ||||
|  | ||||
| #[test] | ||||
| fn test_template_builder_missing_variable_error() { | ||||
|     let mut temp_file = NamedTempFile::new().expect("Failed to create temp file"); | ||||
|     let temp_file = NamedTempFile::new().expect("Failed to create temp file"); | ||||
|     let template_content = "Hello {{missing_var}}!"; | ||||
|     fs::write(temp_file.path(), template_content).expect("Failed to write template"); | ||||
|  | ||||
| @@ -196,7 +198,7 @@ fn test_template_builder_missing_variable_error() { | ||||
|  | ||||
| #[test] | ||||
| fn test_template_builder_invalid_template_syntax() { | ||||
|     let mut temp_file = NamedTempFile::new().expect("Failed to create temp file"); | ||||
|     let temp_file = NamedTempFile::new().expect("Failed to create temp file"); | ||||
|     let template_content = "Hello {{unclosed_var!"; | ||||
|     fs::write(temp_file.path(), template_content).expect("Failed to write template"); | ||||
|  | ||||
| @@ -215,7 +217,7 @@ fn test_template_builder_nonexistent_file() { | ||||
|  | ||||
| #[test] | ||||
| fn test_template_builder_empty_template() { | ||||
|     let mut temp_file = NamedTempFile::new().expect("Failed to create temp file"); | ||||
|     let temp_file = NamedTempFile::new().expect("Failed to create temp file"); | ||||
|     fs::write(temp_file.path(), "").expect("Failed to write empty template"); | ||||
|  | ||||
|     let result = TemplateBuilder::open(temp_file.path()) | ||||
| @@ -228,7 +230,7 @@ fn test_template_builder_empty_template() { | ||||
|  | ||||
| #[test] | ||||
| fn test_template_builder_template_with_no_variables() { | ||||
|     let mut temp_file = NamedTempFile::new().expect("Failed to create temp file"); | ||||
|     let temp_file = NamedTempFile::new().expect("Failed to create temp file"); | ||||
|     let template_content = "This is a static template with no variables."; | ||||
|     fs::write(temp_file.path(), template_content).expect("Failed to write template"); | ||||
|  | ||||
| @@ -242,7 +244,7 @@ fn test_template_builder_template_with_no_variables() { | ||||
|  | ||||
| #[test] | ||||
| fn test_template_builder_complex_report() { | ||||
|     let mut temp_file = NamedTempFile::new().expect("Failed to create temp file"); | ||||
|     let temp_file = NamedTempFile::new().expect("Failed to create temp file"); | ||||
|     let template_content = r#" | ||||
| # {{report_title}} | ||||
|  | ||||
|   | ||||
| @@ -1,13 +1,13 @@ | ||||
| //! Unit tests for text replacement functionality | ||||
| //! | ||||
| //! These tests validate the TextReplacer and TextReplacerBuilder including: | ||||
| //! These tests validate the TextReplacer including: | ||||
| //! - Literal string replacement | ||||
| //! - Regex pattern replacement | ||||
| //! - Multiple chained replacements | ||||
| //! - File operations (read, write, in-place) | ||||
| //! - Error handling and edge cases | ||||
|  | ||||
| use sal_text::{TextReplacer, TextReplacerBuilder}; | ||||
| use sal_text::TextReplacer; | ||||
| use std::fs; | ||||
| use tempfile::NamedTempFile; | ||||
|  | ||||
| @@ -141,7 +141,7 @@ fn test_text_replacer_no_matches() { | ||||
| #[test] | ||||
| fn test_text_replacer_file_operations() { | ||||
|     // Create a temporary file with test content | ||||
|     let mut temp_file = NamedTempFile::new().expect("Failed to create temp file"); | ||||
|     let temp_file = NamedTempFile::new().expect("Failed to create temp file"); | ||||
|     let test_content = "Hello world, there are 123 items"; | ||||
|     fs::write(temp_file.path(), test_content).expect("Failed to write to temp file"); | ||||
|  | ||||
| @@ -157,18 +157,21 @@ fn test_text_replacer_file_operations() { | ||||
|         .expect("Failed to build replacer"); | ||||
|  | ||||
|     // Test replace_file | ||||
|     let result = replacer.replace_file(temp_file.path()).expect("Failed to replace file content"); | ||||
|     let result = replacer | ||||
|         .replace_file(temp_file.path()) | ||||
|         .expect("Failed to replace file content"); | ||||
|     assert_eq!(result, "Hello universe, there are NUMBER items"); | ||||
|  | ||||
|     // Verify original file is unchanged | ||||
|     let original_content = fs::read_to_string(temp_file.path()).expect("Failed to read original file"); | ||||
|     let original_content = | ||||
|         fs::read_to_string(temp_file.path()).expect("Failed to read original file"); | ||||
|     assert_eq!(original_content, test_content); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_text_replacer_file_in_place() { | ||||
|     // Create a temporary file with test content | ||||
|     let mut temp_file = NamedTempFile::new().expect("Failed to create temp file"); | ||||
|     let temp_file = NamedTempFile::new().expect("Failed to create temp file"); | ||||
|     let test_content = "Hello world, there are 123 items"; | ||||
|     fs::write(temp_file.path(), test_content).expect("Failed to write to temp file"); | ||||
|  | ||||
| @@ -180,7 +183,9 @@ fn test_text_replacer_file_in_place() { | ||||
|         .expect("Failed to build replacer"); | ||||
|  | ||||
|     // Test replace_file_in_place | ||||
|     replacer.replace_file_in_place(temp_file.path()).expect("Failed to replace file in place"); | ||||
|     replacer | ||||
|         .replace_file_in_place(temp_file.path()) | ||||
|         .expect("Failed to replace file in place"); | ||||
|  | ||||
|     // Verify file content was changed | ||||
|     let new_content = fs::read_to_string(temp_file.path()).expect("Failed to read modified file"); | ||||
| @@ -190,7 +195,7 @@ fn test_text_replacer_file_in_place() { | ||||
| #[test] | ||||
| fn test_text_replacer_file_to_file() { | ||||
|     // Create source file | ||||
|     let mut source_file = NamedTempFile::new().expect("Failed to create source file"); | ||||
|     let source_file = NamedTempFile::new().expect("Failed to create source file"); | ||||
|     let test_content = "Hello world, there are 123 items"; | ||||
|     fs::write(source_file.path(), test_content).expect("Failed to write to source file"); | ||||
|  | ||||
| @@ -205,11 +210,13 @@ fn test_text_replacer_file_to_file() { | ||||
|         .expect("Failed to build replacer"); | ||||
|  | ||||
|     // Test replace_file_to | ||||
|     replacer.replace_file_to(source_file.path(), dest_file.path()) | ||||
|     replacer | ||||
|         .replace_file_to(source_file.path(), dest_file.path()) | ||||
|         .expect("Failed to replace file to destination"); | ||||
|  | ||||
|     // Verify source file is unchanged | ||||
|     let source_content = fs::read_to_string(source_file.path()).expect("Failed to read source file"); | ||||
|     let source_content = | ||||
|         fs::read_to_string(source_file.path()).expect("Failed to read source file"); | ||||
|     assert_eq!(source_content, test_content); | ||||
|  | ||||
|     // Verify destination file has replaced content | ||||
| @@ -263,7 +270,8 @@ fn test_text_replacer_multiline_text() { | ||||
|         .build() | ||||
|         .expect("Failed to build replacer"); | ||||
|  | ||||
|     let input = "function test() {\n    // This is a comment\n    return true;\n    // Another comment\n}"; | ||||
|     let input = | ||||
|         "function test() {\n    // This is a comment\n    return true;\n    // Another comment\n}"; | ||||
|     let result = replacer.replace(input); | ||||
|  | ||||
|     // Note: This test depends on how the regex engine handles multiline mode | ||||
|   | ||||
		Reference in New Issue
	
	Block a user