diff --git a/Cargo.toml b/Cargo.toml index e9b2225..d8d5c1e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ categories = ["os", "filesystem", "api-bindings"] readme = "README.md" [workspace] -members = [".", "vault", "git", "redisclient", "mycelium", "text"] +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] diff --git a/os/Cargo.toml b/os/Cargo.toml new file mode 100644 index 0000000..9609497 --- /dev/null +++ b/os/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "sal-os" +version = "0.1.0" +edition = "2021" +authors = ["PlanetFirst "] +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" diff --git a/os/README.md b/os/README.md new file mode 100644 index 0000000..6f5afc6 --- /dev/null +++ b/os/README.md @@ -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> { + // 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> { + // 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. diff --git a/src/os/download.rs b/os/src/download.rs similarity index 100% rename from src/os/download.rs rename to os/src/download.rs diff --git a/src/os/fs.rs b/os/src/fs.rs similarity index 100% rename from src/os/fs.rs rename to os/src/fs.rs diff --git a/os/src/lib.rs b/os/src/lib.rs new file mode 100644 index 0000000..8464e66 --- /dev/null +++ b/os/src/lib.rs @@ -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; diff --git a/src/os/package.rs b/os/src/package.rs similarity index 99% rename from src/os/package.rs rename to os/src/package.rs index 81a8692..5ea3067 100644 --- a/src/os/package.rs +++ b/os/src/package.rs @@ -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 { diff --git a/src/os/platform.rs b/os/src/platform.rs similarity index 52% rename from src/os/platform.rs rename to os/src/platform.rs index bddc576..a8d864b 100644 --- a/src/os/platform.rs +++ b/os/src/platform.rs @@ -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.", )) } -} \ No newline at end of file +} diff --git a/src/rhai/os.rs b/os/src/rhai.rs similarity index 70% rename from src/rhai/os.rs rename to os/src/rhai.rs index 2c5ae7a..3a67fb0 100644 --- a/src/rhai/os.rs +++ b/os/src/rhai.rs @@ -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 { + fn to_rhai_error(self) -> Result>; +} + +impl ToRhaiError for Result { + fn to_rhai_error(self) -> Result> { + 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>` - Ok if registration was successful, Err otherwise pub fn register_os_module(engine: &mut Engine) -> Result<(), Box> { - // Register error types - register_error_types(engine)?; - // Register file system functions engine.register_fn("copy", copy); engine.register_fn("copy_bin", copy_bin); @@ -36,20 +48,20 @@ pub fn register_os_module(engine: &mut Engine) -> Result<(), Box> engine.register_fn("file_read", file_read); engine.register_fn("file_write", file_write); engine.register_fn("file_write_append", file_write_append); - + // Register command check functions engine.register_fn("which", which); engine.register_fn("cmd_ensure_exists", cmd_ensure_exists); - + // Register download functions engine.register_fn("download", download); engine.register_fn("download_file", download_file); engine.register_fn("download_install", download_install); engine.register_fn("chmod_exec", chmod_exec); - + // Register move function engine.register_fn("mv", mv); - + // Register package management functions engine.register_fn("package_install", package_install); engine.register_fn("package_remove", package_remove); @@ -60,7 +72,15 @@ pub fn register_os_module(engine: &mut Engine) -> Result<(), Box> engine.register_fn("package_is_installed", package_is_installed); 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,132 +88,132 @@ pub fn register_os_module(engine: &mut Engine) -> Result<(), Box> // 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> { - 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> { - 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> { - 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> { - let files = os::find_files(dir, filename).to_rhai_error()?; - + let files = fs::find_files(dir, filename).to_rhai_error()?; + // Convert Vec to Rhai Array let mut array = Array::new(); for file in files { array.push(file.into()); } - + 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> { - 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> { - let dirs = os::find_dirs(dir, dirname).to_rhai_error()?; - + let dirs = fs::find_dirs(dir, dirname).to_rhai_error()?; + // Convert Vec to Rhai Array let mut array = Array::new(); for dir in dirs { array.push(dir.into()); } - + 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> { - 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> { - 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> { - 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> { - 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> { - 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> { - 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> { - 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> { - 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> { - 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> { /// /// Download a file from URL to destination using the curl command. pub fn download(url: &str, dest: &str, min_size_kb: i64) -> Result> { - 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> { - os::download_file(url, dest, min_size_kb).to_rhai_error() +pub fn download_file( + url: &str, + dest: &str, + min_size_kb: i64, +) -> Result> { + 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> { - 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> { - 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> { - os::cmd_ensure_exists(commands).to_rhai_error() + fs::cmd_ensure_exists(commands).to_rhai_error() } // @@ -293,13 +317,13 @@ pub fn package_upgrade() -> Result> { pub fn package_list() -> Result> { let hero = PackHero::new(); let packages = hero.list_installed().to_rhai_error()?; - + // Convert Vec to Rhai Array let mut array = Array::new(); for package in packages { array.push(package.into()); } - + Ok(array) } @@ -309,13 +333,13 @@ pub fn package_list() -> Result> { pub fn package_search(query: &str) -> Result> { let hero = PackHero::new(); let packages = hero.search(query).to_rhai_error()?; - + // Convert Vec to Rhai Array let mut array = Array::new(); for package in packages { array.push(package.into()); } - + Ok(array) } @@ -336,12 +360,12 @@ thread_local! { pub fn package_set_debug(debug: bool) -> bool { let mut hero = PackHero::new(); hero.set_debug(debug); - + // Also set the thread-local debug flag PACKAGE_DEBUG.with(|cell| { *cell.borrow_mut() = debug; }); - + debug } @@ -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(), } -} \ No newline at end of file +} + +// +// 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> { + 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> { + crate::platform::check_macos_arm().map_err(|e| { + Box::new(EvalAltResult::ErrorRuntime( + format!("Platform Check Error: {}", e).into(), + Position::NONE, + )) + }) +} diff --git a/os/tests/download_tests.rs b/os/tests/download_tests.rs new file mode 100644 index 0000000..9d6824a --- /dev/null +++ b/os/tests/download_tests.rs @@ -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; +} diff --git a/os/tests/fs_tests.rs b/os/tests/fs_tests.rs new file mode 100644 index 0000000..d5ad709 --- /dev/null +++ b/os/tests/fs_tests.rs @@ -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 +} diff --git a/os/tests/package_tests.rs b/os/tests/package_tests.rs new file mode 100644 index 0000000..5f3a4f6 --- /dev/null +++ b/os/tests/package_tests.rs @@ -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 +} diff --git a/os/tests/platform_tests.rs b/os/tests/platform_tests.rs new file mode 100644 index 0000000..8b19bfd --- /dev/null +++ b/os/tests/platform_tests.rs @@ -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()); +} diff --git a/rhai_tests/os/01_file_operations.rhai b/os/tests/rhai/01_file_operations.rhai similarity index 100% rename from rhai_tests/os/01_file_operations.rhai rename to os/tests/rhai/01_file_operations.rhai diff --git a/rhai_tests/os/02_download_operations.rhai b/os/tests/rhai/02_download_operations.rhai similarity index 100% rename from rhai_tests/os/02_download_operations.rhai rename to os/tests/rhai/02_download_operations.rhai diff --git a/rhai_tests/os/03_package_operations.rhai b/os/tests/rhai/03_package_operations.rhai similarity index 100% rename from rhai_tests/os/03_package_operations.rhai rename to os/tests/rhai/03_package_operations.rhai diff --git a/rhai_tests/os/run_all_tests.rhai b/os/tests/rhai/run_all_tests.rhai similarity index 100% rename from rhai_tests/os/run_all_tests.rhai rename to os/tests/rhai/run_all_tests.rhai diff --git a/os/tests/rhai_integration_tests.rs b/os/tests/rhai_integration_tests.rs new file mode 100644 index 0000000..c4791d0 --- /dev/null +++ b/os/tests/rhai_integration_tests.rs @@ -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::(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 + ); +} diff --git a/src/lib.rs b/src/lib.rs index 700e01b..f8a3837 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -40,7 +40,7 @@ pub type Result = std::result::Result; 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; diff --git a/src/os/README.md b/src/os/README.md deleted file mode 100644 index c5538d2..0000000 --- a/src/os/README.md +++ /dev/null @@ -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> { - 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> { - 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> { - 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. diff --git a/src/os/mod.rs b/src/os/mod.rs deleted file mode 100644 index 597497a..0000000 --- a/src/os/mod.rs +++ /dev/null @@ -1,8 +0,0 @@ -mod fs; -mod download; -pub mod package; - -pub use fs::*; -pub use download::*; -pub use package::*; -pub mod platform; \ No newline at end of file diff --git a/src/rhai/core.rs b/src/rhai/core.rs index 87f5d64..b3254a2 100644 --- a/src/rhai/core.rs +++ b/src/rhai/core.rs @@ -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 /// @@ -37,7 +37,7 @@ pub fn exec(context: NativeCallContext, source: &str) -> Result Result Result<(), Box> { 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> { // 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); diff --git a/src/rhai/platform.rs b/src/rhai/platform.rs deleted file mode 100644 index 5a9d5f7..0000000 --- a/src/rhai/platform.rs +++ /dev/null @@ -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()); -} \ No newline at end of file diff --git a/src/virt/nerdctl/container.rs b/src/virt/nerdctl/container.rs index d41daf1..73a1c47 100644 --- a/src/virt/nerdctl/container.rs +++ b/src/virt/nerdctl/container.rs @@ -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,18 +18,22 @@ impl Container { pub fn new(name: &str) -> Result { // 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), + ))) + } _ => {} } - + // Check if container exists 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()) @@ -38,7 +42,7 @@ impl Container { } }) .next(); - + Ok(Self { name: name.to_string(), container_id, @@ -59,7 +63,7 @@ impl Container { snapshotter: None, }) } - + /// Create a container from an image /// /// # Arguments diff --git a/text/tests/template_tests.rs b/text/tests/template_tests.rs index a762bcf..768eb63 100644 --- a/text/tests/template_tests.rs +++ b/text/tests/template_tests.rs @@ -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}} diff --git a/text/tests/text_replacement_tests.rs b/text/tests/text_replacement_tests.rs index a07d582..4b8dc54 100644 --- a/text/tests/text_replacement_tests.rs +++ b/text/tests/text_replacement_tests.rs @@ -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,9 +270,10 @@ 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 // The actual behavior might need adjustment based on regex flags assert!(result.contains("function test()")); @@ -288,7 +296,7 @@ fn test_text_replacer_unicode_text() { #[test] fn test_text_replacer_large_text() { let large_text = "word ".repeat(10000); - + let replacer = TextReplacer::builder() .pattern("word") .replacement("term")