feat: Add support for new OS package
Some checks are pending
Rhai Tests / Run Rhai Tests (push) Waiting to run
Some checks are pending
Rhai Tests / Run Rhai Tests (push) Waiting to run
- Add a new `sal-os` package containing OS interaction utilities. - Update workspace members to include the new package. - Add README and basic usage examples for the new package.
This commit is contained in:
parent
a35edc2030
commit
c4cdb8126c
@ -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);
|
||||
@ -36,20 +48,20 @@ pub fn register_os_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>>
|
||||
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<EvalAltResult>>
|
||||
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<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();
|
||||
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<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();
|
||||
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<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()
|
||||
}
|
||||
|
||||
//
|
||||
@ -293,13 +317,13 @@ pub fn package_upgrade() -> Result<String, Box<EvalAltResult>> {
|
||||
pub fn package_list() -> Result<Array, Box<EvalAltResult>> {
|
||||
let hero = PackHero::new();
|
||||
let packages = hero.list_installed().to_rhai_error()?;
|
||||
|
||||
|
||||
// Convert Vec<String> 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<Array, Box<EvalAltResult>> {
|
||||
pub fn package_search(query: &str) -> Result<Array, Box<EvalAltResult>> {
|
||||
let hero = PackHero::new();
|
||||
let packages = hero.search(query).to_rhai_error()?;
|
||||
|
||||
|
||||
// Convert Vec<String> 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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// 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
|
||||
///
|
||||
@ -37,7 +37,7 @@ pub fn exec(context: NativeCallContext, source: &str) -> Result<rhai::Dynamic, B
|
||||
let file_name = source.split('/').last().unwrap_or("script.rhai");
|
||||
let dest_path = temp_dir.join(format!("{}-{}", uuid::Uuid::new_v4(), file_name));
|
||||
let dest_str = dest_path.to_str().unwrap();
|
||||
|
||||
|
||||
os::download_file(source, dest_str, 0).to_rhai_error()?;
|
||||
os::file_read(dest_str).to_rhai_error()?
|
||||
} else if os::exist(source) {
|
||||
@ -50,4 +50,4 @@ pub fn exec(context: NativeCallContext, source: &str) -> Result<rhai::Dynamic, B
|
||||
|
||||
// Execute the script content
|
||||
context.engine().eval(&content)
|
||||
}
|
||||
}
|
||||
|
@ -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,18 +18,22 @@ 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),
|
||||
)))
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
|
||||
// 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
|
||||
|
@ -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,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")
|
||||
|
Loading…
Reference in New Issue
Block a user