Compare commits

..

1 Commits

Author SHA1 Message Date
Timur Gordon
96e78f2eef new rhai wrappers wip 2025-04-13 00:33:55 +02:00
147 changed files with 3635 additions and 12570 deletions

View File

@ -1,73 +0,0 @@
name: Rhai Tests
on:
push:
branches: [ '*' ]
paths:
- 'src/rhai_tests/**'
- 'src/rhai/**'
- 'src/git/**'
- 'src/os/**'
- 'run_rhai_tests.sh'
- '.github/workflows/rhai-tests.yml'
pull_request:
branches: [ '*' ]
paths:
- 'src/rhai_tests/**'
- 'src/rhai/**'
- 'src/git/**'
- 'src/os/**'
- 'run_rhai_tests.sh'
- '.github/workflows/rhai-tests.yml'
workflow_dispatch: # Allow manual triggering
jobs:
rhai-tests:
name: Run Rhai Tests
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
- name: Cache Rust dependencies
uses: actions/cache@v3
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-
- name: Build herodo
run: |
cargo build --bin herodo
echo "${{ github.workspace }}/target/debug" >> $GITHUB_PATH
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y git curl
- name: Run Rhai tests
run: |
chmod +x run_rhai_tests.sh
./run_rhai_tests.sh
- name: Check for test failures
run: |
if grep -q "Some tests failed" run_rhai_tests.log; then
echo "::error::Some Rhai tests failed. Check the logs for details."
exit 1
else
echo "All Rhai tests passed!"
fi
if: always()

8
.gitignore vendored
View File

@ -19,11 +19,3 @@ Cargo.lock
# Added by cargo
/target
/rhai_test_template
/rhai_test_download
/rhai_test_fs
run_rhai_tests.log
new_location
log.txt
file.txt
fix_doc*

View File

@ -11,43 +11,32 @@ categories = ["os", "filesystem", "api-bindings"]
readme = "README.md"
[dependencies]
tera = "1.19.0" # Template engine for text rendering
tera = "1.19.0" # Template engine for text rendering
# Cross-platform functionality
libc = "0.2"
cfg-if = "1.0"
thiserror = "1.0" # For error handling
redis = "0.22.0" # Redis client
postgres = "0.19.4" # PostgreSQL client
tokio-postgres = "0.7.8" # Async PostgreSQL client
postgres-types = "0.2.5" # PostgreSQL type conversions
thiserror = "1.0" # For error handling
redis = "0.22.0" # Redis client
lazy_static = "1.4.0" # For lazy initialization of static variables
regex = "1.8.1" # For regex pattern matching
serde = { version = "1.0", features = [
"derive",
] } # For serialization/deserialization
regex = "1.8.1" # For regex pattern matching
serde = { version = "1.0", features = ["derive"] } # For serialization/deserialization
serde_json = "1.0" # For JSON handling
glob = "0.3.1" # For file pattern matching
tempfile = "3.5" # For temporary file operations
log = "0.4" # Logging facade
glob = "0.3.1" # For file pattern matching
tempfile = "3.5" # For temporary file operations
log = "0.4" # Logging facade
rhai = { version = "1.12.0", features = ["sync"] } # Embedded scripting language
rand = "0.8.5" # Random number generation
clap = "2.33" # Command-line argument parsing
r2d2 = "0.8.10"
r2d2_postgres = "0.18.2"
rand = "0.8.5" # Random number generation
clap = "2.33" # Command-line argument parsing
# Optional features for specific OS functionality
[target.'cfg(unix)'.dependencies]
nix = "0.26" # Unix-specific functionality
nix = "0.26" # Unix-specific functionality
[target.'cfg(windows)'.dependencies]
windows = { version = "0.48", features = [
"Win32_Foundation",
"Win32_System_Threading",
"Win32_Storage_FileSystem",
] }
windows = { version = "0.48", features = ["Win32_Foundation", "Win32_System_Threading", "Win32_Storage_FileSystem"] }
[dev-dependencies]
tempfile = "3.5" # For tests that need temporary files/directories
tempfile = "3.5" # For tests that need temporary files/directories
[[bin]]
name = "herodo"

View File

@ -0,0 +1,207 @@
# Buildah Debug Implementation Plan
## Current State
1. The `Builder` struct already has a `debug` field and methods to get and set it (`debug()` and `set_debug()`).
2. There's a thread-local `DEBUG` variable with functions to get and set it.
3. The `execute_buildah_command` function checks the thread-local debug flag and outputs some debug information.
4. There's an unused `execute_buildah_command_with_debug` function that takes a `Builder` reference.
## Requirements
1. Use only the Builder's debug flag, not the thread-local debug flag.
2. When debug is true, output stdout/stderr regardless of whether the command succeeds or fails.
3. If debug is false but there's an error, still output all information.
## Implementation Plan
### 1. Keep the Existing Thread-Local DEBUG Variable
We'll keep the existing thread-local DEBUG variable that's already in the code:
```rust
// Thread-local storage for debug flag
thread_local! {
static DEBUG: std::cell::RefCell<bool> = std::cell::RefCell::new(false);
}
```
### 2. Modify the Builder Methods to Set/Clear the Thread-Local Debug Flag
We'll modify each Builder method to set the thread-local debug flag from the Builder's debug flag before calling `execute_buildah_command` and restore it afterward:
```rust
pub fn run(&self, command: &str) -> Result<CommandResult, BuildahError> {
if let Some(container_id) = &self.container_id {
// Save the current debug flag
let previous_debug = thread_local_debug();
// Set the thread-local debug flag from the Builder's debug flag
set_thread_local_debug(self.debug);
// Execute the command
let result = execute_buildah_command(&["run", container_id, "sh", "-c", command]);
// Restore the previous debug flag
set_thread_local_debug(previous_debug);
result
} else {
Err(BuildahError::Other("No container ID available".to_string()))
}
}
```
### 3. Keep the Existing execute_buildah_command Function
The existing `execute_buildah_command` function already checks the thread-local debug flag, so we don't need to modify it:
```rust
pub fn execute_buildah_command(args: &[&str]) -> Result<CommandResult, BuildahError> {
// Get the debug flag from thread-local storage
let debug = thread_local_debug();
if debug {
println!("Executing buildah command: buildah {}", args.join(" "));
}
// ... rest of the function ...
}
```
### 4. Update the execute_buildah_command Function to Output stdout/stderr
We need to modify the `execute_buildah_command` function to output stdout/stderr when debug is true, regardless of success/failure:
```rust
pub fn execute_buildah_command(args: &[&str]) -> Result<CommandResult, BuildahError> {
// Get the debug flag from thread-local storage
let debug = thread_local_debug();
if debug {
println!("Executing buildah command: buildah {}", args.join(" "));
}
let output = Command::new("buildah")
.args(args)
.output();
match output {
Ok(output) => {
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
let result = CommandResult {
stdout,
stderr,
success: output.status.success(),
code: output.status.code().unwrap_or(-1),
};
// Always output stdout/stderr when debug is true
if debug {
if !result.stdout.is_empty() {
println!("Command stdout: {}", result.stdout);
}
if !result.stderr.is_empty() {
println!("Command stderr: {}", result.stderr);
}
if result.success {
println!("Command succeeded with code {}", result.code);
} else {
println!("Command failed with code {}", result.code);
}
}
if result.success {
Ok(result)
} else {
// If command failed and debug is false, output stderr
if !debug {
println!("Command failed with code {}: {}", result.code, result.stderr.trim());
}
Err(BuildahError::CommandFailed(format!("Command failed with code {}: {}",
result.code, result.stderr.trim())))
}
},
Err(e) => {
// Always output error information
println!("Command execution failed: {}", e);
Err(BuildahError::CommandExecutionFailed(e))
}
}
}
```
### 5. Handle Static Methods
For static methods, we'll just call `execute_buildah_command` directly, which will use the thread-local debug flag:
```rust
pub fn images() -> Result<Vec<Image>, BuildahError> {
let result = execute_buildah_command(&["images", "--json"])?;
// Rest of the method...
}
```
If we want to support debugging in static methods, we could add an optional debug parameter:
```rust
pub fn images(debug: bool) -> Result<Vec<Image>, BuildahError> {
// Save the current debug flag
let previous_debug = thread_local_debug();
// Set the thread-local debug flag
set_thread_local_debug(debug);
// Execute the command
let result = execute_buildah_command(&["images", "--json"]);
// Restore the previous debug flag
set_thread_local_debug(previous_debug);
// Process the result
match result {
Ok(cmd_result) => {
// Parse JSON and return images...
},
Err(e) => Err(e),
}
}
// Backward compatibility method
pub fn images() -> Result<Vec<Image>, BuildahError> {
Self::images(false)
}
```
### 6. Update Rhai Bindings
We still need to update the Rhai bindings to expose the debug functionality:
```rust
// Add a debug getter and setter
engine.register_get("debug", |builder: &mut Builder| builder.debug());
engine.register_set("debug", |builder: &mut Builder, debug: bool| { builder.set_debug(debug); });
```
### 7. Remove Unused Code
We can remove the unused `execute_buildah_command_with_debug` function.
## Implementation Flow
1. Modify the `execute_buildah_command` function to output stdout/stderr when debug is true
2. Update all Builder methods to set/restore the thread-local debug flag
3. Update static methods to optionally accept a debug parameter
4. Update Rhai bindings to expose the debug functionality
5. Remove unused code
## Benefits
1. Minimal changes to the existing codebase
2. No changes to function signatures
3. Backward compatibility with existing code
4. Improved debugging capabilities

View File

@ -1,215 +0,0 @@
# Rhai Git Module Manual
## Core Concepts
The Git module in Rhai allows interaction with Git repositories through two main objects:
- **`GitTree`**: Represents a collection of Git repositories under a specified base directory. Use it to manage, find, and access these repositories.
- **`GitRepo`**: Represents a single Git repository. Use it to perform common Git operations like `pull`, `commit`, and `push`.
## Error Handling
Methods performing Git operations (e.g., `pull`, `GitTree.get` when cloning) return a `Result`. If an operation fails and the error is not handled within the Rhai script, the script execution will halt, and Rhai will report the error. The examples below show direct usage, relying on this default error-halting behavior.
## `GitTree` Object
The `GitTree` object is the entry point for working with Git repositories.
### `git_tree_new(base_path: String) -> GitTree`
Creates a `GitTree` instance.
- **Description**: Initializes a `GitTree` to operate within the `base_path`. This directory is where repositories are located or will be cloned. It's created if it doesn't exist.
- **Parameters**:
- `base_path: String` - Path to the directory for Git repositories.
- **Returns**: `GitTree` - A new `GitTree` object. Halts on error (e.g., invalid path).
- **Rhai Example**:
```rhai
let git_tree = git_tree_new("./my_projects");
print("GitTree created.");
// To access the base path from Rhai, a `base_path()` getter would need to be exposed.
// // print(`GitTree base path: ${git_tree.base_path()}`);
```
### `list() -> Array`
Lists names of all Git repositories in the `GitTree`'s `base_path`.
- **Description**: Scans `base_path` for immediate subdirectories that are Git repositories and returns their names.
- **Returns**: `Array` - An array of strings (repository names). Returns an empty array if no repositories are found. Halts on other errors.
- **Rhai Example**:
```rhai
let git_tree = git_tree_new("./my_projects");
let repo_names = git_tree.list();
print(`Found ${repo_names.len()} repositories: ${repo_names}`);
```
### `find(pattern: String) -> Array`
Finds Git repositories matching `pattern` and returns them as `GitRepo` objects.
- **Description**: Searches `base_path` for Git repository subdirectories whose names match the `pattern` (e.g., `*`, `service-*`).
- **Parameters**:
- `pattern: String` - A pattern to match repository names.
- **Returns**: `Array` - An array of `GitRepo` objects. Returns an empty array if no repositories match. Halts on other errors (e.g. invalid pattern).
- **Rhai Example**:
```rhai
let git_tree = git_tree_new("./my_projects");
let api_repos = git_tree.find("api-*");
print(`Found ${api_repos.len()} API repositories.`);
for repo in api_repos {
print(`- Path: ${repo.path()}, Has Changes: ${repo.has_changes()}`);
}
```
### `get(name_or_url: String) -> GitRepo`
Retrieves a single `GitRepo` object by its exact local name or by a remote URL.
- **Description**:
- **Local Name**: If `name_or_url` is an exact subdirectory name in `base_path` (e.g., `"myrepo"`), opens that repository.
- **Remote URL**: If `name_or_url` is a Git URL (e.g., `"https://github.com/user/repo.git"`), it clones the repository (if not present) into `base_path` or opens it if it already exists.
- **Note**: Does not support wildcards for local names. Use `find()` for pattern matching.
- **Parameters**:
- `name_or_url: String` - The exact local repository name or a full Git URL.
- **Returns**: `GitRepo` - A single `GitRepo` object.
- **Halts on error if**:
- The local `name` is not found or is ambiguous.
- The `url` is invalid, or the clone/access operation fails.
- The target is not a valid Git repository.
- **Rhai Examples**:
*Get specific local repository by name:*
```rhai
let git_tree = git_tree_new("./my_projects");
// Assumes "my_service_a" is a git repo in "./my_projects/my_service_a"
// Script halts if "my_service_a" is not found or not a git repo.
let service_a_repo = git_tree.get("my_service_a");
print(`Opened repo: ${service_a_repo.path()}`);
service_a_repo.pull(); // Example operation
```
*Clone or get repository by URL:*
```rhai
let git_tree = git_tree_new("./cloned_repos_dest");
let url = "https://github.com/rhai-script/rhai.git";
// Clones if not present, otherwise opens. Halts on error.
let rhai_repo = git_tree.get(url);
print(`Rhai repository path: ${rhai_repo.path()}`);
print(`Rhai repo has changes: ${rhai_repo.has_changes()}`);
```
## `GitRepo` Object
Represents a single Git repository. Obtained from `GitTree.get()` or `GitTree.find()`.
### `path() -> String`
Returns the full file system path of the repository.
- **Returns**: `String` - The absolute path to the repository's root directory.
- **Rhai Example**:
```rhai
let git_tree = git_tree_new("./my_projects");
// Assumes "my_app" exists and is a Git repository.
// get() will halt if "my_app" is not found.
let app_repo = git_tree.get("my_app");
print(`App repository is at: ${app_repo.path()}`);
```
### `has_changes() -> bool`
Checks if the repository has any uncommitted local changes.
- **Description**: Checks for uncommitted modifications in the working directory or staged changes.
- **Returns**: `bool` - `true` if uncommitted changes exist, `false` otherwise. Halts on error.
- **Rhai Example** (assuming `app_repo` is a `GitRepo` object):
```rhai
if app_repo.has_changes() {
print(`Repository ${app_repo.path()} has uncommitted changes.`);
} else {
print(`Repository ${app_repo.path()} is clean.`);
}
```
### `pull() -> GitRepo`
Pulls latest changes from the remote.
- **Description**: Fetches changes from the default remote and merges them into the current local branch (`git pull`).
- **Returns**: `GitRepo` - The same `GitRepo` object for chaining. Halts on error (e.g., network issues, merge conflicts).
- **Rhai Example** (assuming `app_repo` is a `GitRepo` object):
```rhai
print(`Pulling latest changes for ${app_repo.path()}...`);
app_repo.pull(); // Halts on error
print("Pull successful.");
```
### `reset() -> GitRepo`
Resets local changes. **Caution: Discards uncommitted work.**
- **Description**: Discards local modifications and staged changes, resetting the working directory to match the last commit (`git reset --hard HEAD` or similar).
- **Returns**: `GitRepo` - The same `GitRepo` object for chaining. Halts on error.
- **Rhai Example** (assuming `app_repo` is a `GitRepo` object):
```rhai
print(`Resetting local changes in ${app_repo.path()}...`);
app_repo.reset(); // Halts on error
print("Reset successful.");
```
### `commit(message: String) -> GitRepo`
Commits staged changes.
- **Description**: Performs `git commit -m "message"`. Assumes changes are staged. Behavior regarding auto-staging of tracked files depends on the underlying Rust implementation.
- **Parameters**:
- `message: String` - The commit message.
- **Returns**: `GitRepo` - The same `GitRepo` object for chaining. Halts on error (e.g., nothing to commit).
- **Rhai Example** (assuming `app_repo` is a `GitRepo` object):
```rhai
// Ensure there are changes to commit.
if app_repo.has_changes() {
print(`Committing changes in ${app_repo.path()}...`);
app_repo.commit("Automated commit via Rhai script"); // Halts on error
print("Commit successful.");
} else {
print("No changes to commit.");
}
```
### `push() -> GitRepo`
Pushes committed changes to the remote.
- **Description**: Performs `git push` to the default remote and branch.
- **Returns**: `GitRepo`
```rhai
print(`Pushing changes for ${app_repo.path()}...`);
app_repo.push(); // Halts on error
print("Push successful.");
```
## Chaining Operations
```rhai
let git_tree = git_tree_new("./my_projects");
// Assumes "my_writable_app" exists and you have write access.
// get() will halt if not found.
let app_repo = git_tree.get("my_writable_app");
print(`Performing chained operations on ${app_repo.path()}`);
// This example demonstrates a common workflow.
// Ensure the repo state is suitable (e.g., changes exist for commit/push).
app_repo.pull()
.commit("Rhai: Chained operations - automated update") // Commits if pull results in changes or local changes existed and were staged.
.push();
print("Chained pull, commit, and push reported successful.");
// Alternative:
// app_repo.pull();
// if app_repo.has_changes() {
// app_repo.commit("Updates").push();
// }
```

View File

@ -1,60 +0,0 @@
# os.download Module
### `download(url, dest, min_size_kb)`
Download a file from URL to destination using the curl command.
- **Description**: Downloads a file from the given `url`. If `dest` is a directory, the filename is derived from the URL. If `dest` is a file path, it is used directly. Requires the `curl` command to be available. Halts script execution on download or file writing errors, or if the downloaded file size is less than `min_size_kb`. Returns the destination path.
- **Returns**: `String` - The path where the file was downloaded.
- **Arguments**:
- `url`: `String` - The URL of the file to download.
- `dest`: `String` - The destination path (directory or file).
- `min_size_kb`: `Integer` - The minimum expected size of the downloaded file in kilobytes.
```rhai
let download_url = "https://example.com/archive.zip";
let download_dest_dir = "/tmp/downloads";
print(`Downloading ${download_url} to ${download_dest_dir}...`);
let downloaded_file_path = os::download(download_url, download_dest_dir, 50); // Halts on error
print(`Downloaded to: ${downloaded_file_path}`);
```
---
### `download_file(url, dest, min_size_kb)`
Download a file from URL to a specific file destination using the curl command.
- **Description**: Downloads a file from the given `url` directly to the specified file `dest`. Requires the `curl` command. Halts script execution on download or file writing errors, or if the downloaded file size is less than `min_size_kb`. Returns the destination path.
- **Returns**: `String` - The path where the file was downloaded.
- **Arguments**:
- `url`: `String` - The URL of the file to download.
- `dest`: `String` - The full path where the file should be saved.
- `min_size_kb`: `Integer` - The minimum expected size of the downloaded file in kilobytes.
```rhai
let data_url = "https://example.com/dataset.tar.gz";
let local_path = "/opt/data/dataset.tar.gz";
print(`Downloading ${data_url} to ${local_path}...`);
os::download_file(data_url, local_path, 1024); // Halts on error
print(`Downloaded dataset to: ${local_path}`);
```
---
### `download_install(url, min_size_kb)`
Download a file and install it if it\'s a supported package format.
- **Description**: Downloads a file from the given `url` to a temporary location and then attempts to install it using the appropriate system package manager if the file format is supported (e.g., `.deb` on Ubuntu, `.pkg` or `.dmg` on MacOS). Requires the `curl` command and the system package manager. Halts script execution on download, installation, or file size errors. Returns a success message.
- **Returns**: `String` - A success message upon successful download and installation attempt.
- **Arguments**:
- `url`: `String` - The URL of the package file to download and install.
- `min_size_kb`: `Integer` - The minimum expected size of the downloaded file in kilobytes.
```rhai
let package_url = "https://example.com/mytool.deb";
print(`Downloading and installing ${package_url}...`);
os::download_install(package_url, 300); // Halts on error
print("Installation attempt finished.");
```

View File

@ -1,338 +0,0 @@
# os.fs Module
The `os` module provides functions for interacting with the operating system, including file system operations, command execution checks, downloads, and package management.
All functions that interact with the file system or external commands will halt the script execution if an error occurs, unless explicitly noted otherwise.
---
### `copy(src, dest)`
Recursively copy a file or directory from source to destination.
- **Description**: Performs a recursive copy operation. Halts script execution on failure.
- **Returns**: `String` - The destination path.
- **Arguments**:
- `src`: `String` - The path to the source file or directory.
- `dest`: `String` - The path to the destination file or directory.
```rhai
print("Copying directory...");
let source_dir = "/tmp/source_data";
let dest_dir = "/backup/source_data";
let copied_path = os::copy(source_dir, dest_dir); // Halts on error
print(`Copied ${source_dir} to ${copied_path}`);
```
---
### `exist(path)`
Check if a file or directory exists.
- **Description**: Checks for the presence of a file or directory at the given path. This function does NOT halt on error if the path is invalid or permissions prevent checking.
- **Returns**: `Boolean` - `true` if the path exists, `false` otherwise.
- **Arguments**:
- `path`: `String` - The path to check.
```rhai
if os::exist("config.json") {
print(`${file_path} exists.`);
} else {
print(`${file_path} does not exist.`);
}
```
---
### `find_file(dir, filename)`
Find a file in a directory (with support for wildcards).
- **Description**: Searches for a file matching `filename` within the specified `dir`. Supports simple wildcards like `*` and `?`. Halts script execution if the directory cannot be read or if no file is found.
- **Returns**: `String` - The path to the first file found that matches the pattern.
- **Arguments**:
- `dir`: `String` - The directory to search within.
- `filename`: `String` - The filename pattern to search for (e.g., `"*.log"`).
```rhai
let log_file = os::find_file("/var/log", "syslog*.log"); // Halts if not found or directory error
print(`Found log file: ${log_file}`);
```
---
### `find_files(dir, filename)`
Find multiple files in a directory (recursive, with support for wildcards).
- **Description**: Recursively searches for all files matching `filename` within the specified `dir` and its subdirectories. Supports simple wildcards. Halts script execution if the directory cannot be read.
- **Returns**: `Array` of `String` - An array containing paths to all matching files.
- **Arguments**:
- `dir`: `String` - The directory to start the recursive search from.
- `filename`: `String` - The filename pattern to search for (e.g., `"*.tmp"`).
```rhai
let temp_files = os::find_files("/tmp", "*.swp"); // Halts on directory error
print("Found temporary files:");
for file in temp_files {
print(`- ${file}`);
}
```
---
### `find_dir(dir, dirname)`
Find a directory in a parent directory (with support for wildcards).
- **Description**: Searches for a directory matching `dirname` within the specified `dir`. Supports simple wildcards. Halts script execution if the directory cannot be read or if no directory is found.
- **Returns**: `String` - The path to the first directory found that matches the pattern.
- **Arguments**:
- `dir`: `String` - The directory to search within.
- `dirname`: `String` - The directory name pattern to search for (e.g., `"backup_*"`).
```rhai
let latest_backup_dir = os::find_dir("/mnt/backups", "backup_20*"); // Halts if not found or directory error
print(`Found backup directory: ${latest_backup_dir}`);
```
---
### `find_dirs(dir, dirname)`
Find multiple directories in a parent directory (recursive, with support for wildcards).
- **Description**: Recursively searches for all directories matching `dirname` within the specified `dir` and its subdirectories. Supports simple wildcards. Halts script execution if the directory cannot be read.
- **Returns**: `Array` of `String` - An array containing paths to all matching directories.
- **Arguments**:
- `dir`: `String` - The directory to start the recursive search from.
- `dirname`: `String` - The directory name pattern to search for (e.g., `"project_*_v?"`).
```rhai
let project_versions = os::find_dirs("/home/user/dev", "project_*_v?"); // Halts on directory error
print("Found project version directories:");
for dir in project_versions {
print(`- ${dir}`);
}
```
---
### `delete(path)`
Delete a file or directory (defensive - doesn't error if file doesn't exist).
- **Description**: Deletes the file or directory at the given path. If the path does not exist, the function does nothing and does not halt. Halts script execution on other errors (e.g., permission denied, directory not empty). Returns the path that was attempted to be deleted.
- **Returns**: `String` - The path that was given as input.
- **Arguments**:
- `path`: `String` - The path to the file or directory to delete.
```rhai
let temp_path = "/tmp/temporary_item";
print(`Attempting to delete: ${temp_path}`);
os::delete(temp_path); // Halts on permissions or non-empty directory error
print("Deletion attempt finished.");
```
---
### `mkdir(path)`
Create a directory and all parent directories (defensive - doesn't error if directory exists).
- **Description**: Creates the directory at the given path, including any necessary parent directories. If the directory already exists, the function does nothing and does not halt. Halts script execution on other errors (e.g., permission denied). Returns the path that was created (or already existed).
- **Returns**: `String` - The path that was created or checked.
- **Arguments**:
- `path`: `String` - The path to the directory to create.
```rhai
let new_dir = "/data/processed/reports";
print(`Ensuring directory exists: ${new_dir}`);
os::mkdir(new_dir); // Halts on permission error
print("Directory check/creation finished.");
```
---
### `file_size(path)`
Get the size of a file in bytes.
- **Description**: Returns the size of the file at the given path. Halts script execution if the file does not exist or cannot be accessed.
- **Returns**: `Integer` - The size of the file in bytes (as i64).
- **Arguments**:
- `path`: `String` - The path to the file.
```rhai
let file_path = "important_document.pdf";
let size = os::file_size(file_path); // Halts if file not found or cannot read
print(`File size: ${size} bytes`);
```
---
### `rsync(src, dest)`
Sync directories using rsync (or platform equivalent).
- **Description**: Synchronizes the contents of the source directory (`src`) to the destination directory (`dest`) using the system's available rsync-like command. Halts script execution on any error during the sync process. Returns a success message string.
- **Returns**: `String` - A success message indicating the operation completed.
- **Arguments**:
- `src`: `String` - The source directory.
- `dest`: `String` - The destination directory.
```rhai
let source = "/local/project_files";
let destination = "/remote/backup/project_files";
print(`Syncing from ${source} to ${destination}...`);
let result_message = os::rsync(source, destination); // Halts on error
print(`Sync successful: ${result_message}`);
```
---
### `chdir(path)`
Change the current working directory.
- **Description**: Changes the current working directory of the script process. Halts script execution if the directory does not exist or cannot be accessed. Returns the new current working directory path.
- **Returns**: `String` - The absolute path of the directory the process changed into.
- **Arguments**:
- `path`: `String` - The path to change the working directory to.
```rhai
print(`Current directory: ${os::chdir(".")}`); // Use "." to get current path
let new_cwd = "/tmp";
os::chdir(new_cwd); // Halts if directory not found or access denied
print(`Changed directory to: ${os::chdir(".")}`);
```
---
### `file_read(path)`
Read the contents of a file.
- **Description**: Reads the entire content of the file at the given path into a string. Halts script execution if the file does not exist or cannot be read.
- **Returns**: `String` - The content of the file.
- **Arguments**:
- `path`: `String` - The path to the file.
```rhai
let config_content = os::file_read("settings.conf"); // Halts if file not found or cannot read
print("Config content:");
print(config_content);
```
---
### `file_write(path, content)`
Write content to a file (creates the file if it doesn\'t exist, overwrites if it does).
- **Description**: Writes the specified `content` to the file at the given `path`. If the file exists, its content is replaced. If it doesn't exist, it is created. Halts script execution on error (e.g., permission denied, invalid path). Returns the path written to.
- **Returns**: `String` - The path of the file written to.
- **Arguments**:
- `path`: `String` - The path to the file.
- `content`: `String` - The content to write to the file.
```rhai
let output_path = "/tmp/hello.txt";
let text_to_write = "Hello from Rhai!";
os::file_write(output_path, text_to_write); // Halts on error
print(`Wrote to ${output_path}`);
```
---
### `file_write_append(path, content)`
Append content to a file (creates the file if it doesn\'t exist).
- **Description**: Appends the specified `content` to the end of the file at the given `path`. If the file does not exist, it is created. Halts script execution on error (e.g., permission denied, invalid path). Returns the path written to.
- **Returns**: `String` - The path of the file written to.
- **Arguments**:
- `path`: `String` - The path to the file.
- `content`: `String` - The content to append to the file.
```rhai
let log_path = "application.log";
let log_entry = "User login failed.\n";
os::file_write_append(log_path, log_entry); // Halts on error
print(`Appended to ${log_path}`);
```
---
### `mv(src, dest)`
Move a file or directory from source to destination.
- **Description**: Moves the file or directory from `src` to `dest`. Halts script execution on error (e.g., permission denied, source not found, destination exists and cannot be overwritten). Returns the destination path.
- **Returns**: `String` - The path of the destination.
- **Arguments**:
- `src`: `String` - The path to the source file or directory.
- `dest`: `String` - The path to the destination.
```rhai
let old_path = "/tmp/report.csv";
let new_path = "/archive/reports/report_final.csv";
os::mv(old_path, new_path); // Halts on error
print(`Moved ${old_path} to ${new_path}`);
```
---
### `which(command)`
Check if a command exists in the system PATH.
- **Description**: Searches the system's PATH environment variable for the executable `command`. This function does NOT halt on error; it returns an empty string if the command is not found.
- **Returns**: `String` - The full path to the command executable if found, otherwise an empty string (`""`).
- **Arguments**:
- `command`: `String` - The name of the command to search for (e.g., `"git"`).
```rhai
let git_path = os::which("git");
if git_path != "" {
print(`Git executable found at: ${git_path}`);
} else {
print("Git executable not found in PATH.");
}
```
---
### `cmd_ensure_exists(commands)`
Ensure that one or more commands exist in the system PATH.
- **Description**: Checks if all command names specified in the `commands` string (space or comma separated) exist in the system's PATH. Halts script execution if any of the commands are not found. Returns a success message if all commands are found.
- **Returns**: `String` - A success message.
- **Arguments**:
- `commands`: `String` - A string containing one or more command names, separated by spaces or commas (e.g., `"curl,tar,unzip"`).
```rhai
print("Ensuring required commands are available...");
os::cmd_ensure_exists("git curl docker"); // Halts if any command is missing
print("All required commands found.");
```
---
### `chmod_exec(path)`
Make a file executable (equivalent to chmod +x).
- **Description**: Sets the executable permission for the file at the given `path` for the owner, group, and others. Halts script execution on error (e.g., file not found, permission denied). Returns the path modified.
- **Returns**: `String` - The path of the file whose permissions were modified.
- **Arguments**:
- `path`: `String` - The path to the file.
```rhai
let script_path = "/usr/local/bin/myscript";
print(`Making ${script_path} executable...`);
os::chmod_exec(script_path); // Halts on error
print("Permissions updated.");
```

View File

@ -1,157 +0,0 @@
# os.package Module
### `package_install(package)`
Install a package using the system package manager.
- **Description**: Installs the specified `package` using the detected system package manager (e.g., `apt` on Ubuntu, `brew` on MacOS). Halts script execution if the package manager command fails. Returns a success message.
- **Returns**: `String` - A message indicating successful installation.
- **Arguments**:
- `package`: `String` - The name of the package to install (e.g., `"nano"`).
```rhai
print("Installing 'nano' package...");
os::package_install("nano"); // Halts on package manager error
print("'nano' installed successfully.");
```
---
### `package_remove(package)`
Remove a package using the system package manager.
- **Description**: Removes the specified `package` using the detected system package manager. Halts script execution if the package manager command fails. Returns a success message.
- **Returns**: `String` - A message indicating successful removal.
- **Arguments**:
- `package`: `String` - The name of the package to remove (e.g., `"htop"`).
```rhai
print("Removing 'htop' package...");
os::package_remove("htop"); // Halts on package manager error
print("'htop' removed successfully.");
```
---
### `package_update()`
Update package lists using the system package manager.
- **Description**: Updates the package lists that the system package manager uses (e.g., `apt update`, `brew update`). Halts script execution if the package manager command fails. Returns a success message.
- **Returns**: `String` - A message indicating successful update.
- **Arguments**: None.
```rhai
print("Updating package lists...");
os::package_update(); // Halts on package manager error
print("Package lists updated.");
```
---
### `package_upgrade()`
Upgrade installed packages using the system package manager.
- **Description**: Upgrades installed packages using the detected system package manager (e.g., `apt upgrade`, `brew upgrade`). Halts script execution if the package manager command fails. Returns a success message.
- **Returns**: `String` - A message indicating successful upgrade.
- **Arguments**: None.
```rhai
print("Upgrading installed packages...");
os::package_upgrade(); // Halts on package manager error
print("Packages upgraded.");
```
---
### `package_list()`
List installed packages using the system package manager.
- **Description**: Lists the names of packages installed on the system using the detected package manager. Halts script execution if the package manager command fails.
- **Returns**: `Array` of `String` - An array containing the names of installed packages.
- **Arguments**: None.
```rhai
print("Listing installed packages...");
let installed_packages = os::package_list(); // Halts on package manager error
for pkg in installed_packages {
print(`- ${pkg}`);
}
```
---
### `package_search(query)`
Search for packages using the system package manager.
- **Description**: Searches for packages matching the given `query` using the detected system package manager. Halts script execution if the package manager command fails.
- **Returns**: `Array` of `String` - An array containing the search results (package names and/or descriptions).
- **Arguments**:
- `query`: `String` - The search term.
```rhai
print("Searching for 'python' packages...");
let python_packages = os::package_search("python"); // Halts on package manager error
for pkg in python_packages {
print(`- ${pkg}`);
}
```
---
### `package_is_installed(package)`
Check if a package is installed using the system package manager.
- **Description**: Checks if the specified `package` is installed using the detected system package manager. Halts script execution if the package manager command itself fails (e.g., command not found), but does NOT halt if the package is simply not found.
- **Returns**: `Boolean` - `true` if the package is installed, `false` otherwise.
- **Arguments**:
- `package`: `String` - The name of the package to check (e.g., `"wget"`).
```rhai
let package_name = "wget";
if os::package_is_installed(package_name) { // Halts on package manager command error
print(`${package_name} is installed.`);
} else {
print(`${package_name} is not installed.`);
}
```
---
### `package_set_debug(debug)`
Set the debug mode for package management operations.
- **Description**: Enables or disables debug output for subsequent package management operations. This function does NOT halt on error and always returns the boolean value it was set to.
- **Returns**: `Boolean` - The boolean value that the debug flag was set to.
- **Arguments**:
- `debug`: `Boolean` - Set to `true` to enable debug output, `false` to disable.
```rhai
print("Enabling package debug output.");
os::package_set_debug(true);
// Subsequent package operations will print debug info
print("Disabling package debug output.");
os::package_set_debug(false);
```
---
### `package_platform()`
Get the current platform name for package management.
- **Description**: Returns the name of the operating system platform as detected by the package manager logic. This function does NOT halt on error; it returns `"Unknown"` if the platform cannot be determined.
- **Returns**: `String` - The platform name, one of `"Ubuntu"`, `"MacOS"`, or `"Unknown"`.
- **Arguments**: None.
```rhai
let platform = os::package_platform(); // Does not halt on error
print(`Detected package platform: ${platform}`);
```

View File

@ -1,223 +0,0 @@
# Process Module
The `process` module provides functions for running external commands and managing system processes using a builder pattern for command execution.
For running commands, you start with the `run()` function which returns a `CommandBuilder` object. You can then chain configuration methods like `silent()`, `ignore_error()`, and `log()` before finally calling the `do()` method to execute the command.
By default, command execution using the builder (`.do()`) will halt the script execution if the command itself fails (returns a non-zero exit code) or if there's an operating system error preventing the command from running. You can change this behavior with `ignore_error()`.
Other process management functions (`which`, `kill`, `process_list`, `process_get`) have specific error handling behaviors described below.
---
### `CommandResult`
An object returned by command execution functions (`.do()`) containing the result of the command.
- **Properties**:
- `stdout`: `String` - The standard output of the command.
- `stderr`: `String` - The standard error of the command.
- `success`: `Boolean` - `true` if the command exited with code 0, `false` otherwise.
- `code`: `Integer` - The exit code of the command.
```rhai
let result = run("echo hi").do();
print(`Success: ${result.success}, Output: ${result.stdout}`);
```
---
### `ProcessInfo`
An object found by process listing/getting functions (`process_list`, `process_get`) containing information about a running process.
- **Properties**:
- `pid`: `Integer` - The process ID.
- `name`: `String` - The name of the process executable.
- `memory`: `Integer` - The memory usage of the process (unit depends on the operating system, typically KB or bytes).
- `cpu`: `Float` - The CPU usage percentage (value and meaning may vary by operating system).
```rhai
let processes = process_list("my_service");
if (processes.len() > 0) {
let first_proc = processes[0];
print(`Process ${first_proc.name} (PID: ${first_proc.pid}) is running.`);
}
```
---
### `run(command)`
Start building a command execution.
- **Description**: Initializes a `CommandBuilder` for the given command string. This is the entry point to configure and run a process.
- **Returns**: `CommandBuilder` - A builder object for configuring the command.
- **Arguments**:
- `command`: `String` - The command string to execute. Can include arguments and be a simple multiline script.
```rhai
let cmd_builder = run("ls -l");
// Now you can chain methods like .silent(), .ignore_error(), .log()
```
---
### `CommandBuilder:silent()`
Configure the command to run silently.
- **Description**: Suppresses real-time standard output and standard error from being printed to the script's console during command execution. The output is still captured in the resulting `CommandResult`.
- **Returns**: `CommandBuilder` - Returns `self` for chaining.
- **Arguments**: None.
```rhai
print("Running silent command...");
run("echo This won\'t show directly").silent().do();
print("Silent command finished.");
```
---
### `CommandBuilder:ignore_error()`
Configure the command to ignore non-zero exit codes.
- **Description**: By default, the `do()` method halts script execution if the command returns a non-zero exit code. Calling `ignore_error()` prevents this. The `CommandResult` will still indicate `success: false` and contain the non-zero `code`, allowing the script to handle the command failure explicitly. OS errors preventing the command from running will still cause a halt.
- **Returns**: `CommandBuilder` - Returns `self` for chaining.
- **Arguments**: None.
```rhai
print("Running command that will fail but not halt...");
let result = run("exit 1").ignore_error().do(); // Will not halt
if (!result.success) {
print(`Command failed as expected with code: ${result.code}`);
}
```
---
### `CommandBuilder:log()`
Configure the command to log the execution details.
- **Description**: Enables logging of the command string before execution.
- **Returns**: `CommandBuilder` - Returns `self` for chaining.
- **Arguments**: None.
```rhai
print("Running command with logging...");
run("ls /tmp").log().do(); // Will print the "ls /tmp" command before running
print("Command finished.");
```
---
### `CommandBuilder:do()`
Execute the configured command.
- **Description**: Runs the command with the options set by the builder methods. Waits for the command to complete and returns the `CommandResult`. This method is the final step in the command execution builder chain. Halts based on the `ignore_error()` setting and OS errors.
- **Returns**: `CommandResult` - An object containing the output and status of the command.
- **Arguments**: None.
```rhai
print("Running command using builder...");
let command_result = run("pwd")
.log() // Log the command
.silent() // Don't print output live
.do(); // Execute and get result (halts on error by default)
print(`Command output: ${command_result.stdout}`);
// Example with multiple options
let fail_result = run("command_that_does_not_exist")
.ignore_error() // Don't halt on non-zero exit (though OS error might still halt)
.silent() // Don't print error live
.do();
if (!fail_result.success) {
print(`Failed command exited with code: ${fail_result.code} and stderr: ${fail_result.stderr}`);
}
```
---
### `which(cmd)`
Check if a command exists in the system PATH.
- **Description**: Searches the system's PATH environment variable for the executable `cmd`. This function does NOT halt if the command is not found; it returns an empty string.
- **Returns**: `String` - The full path to the command executable if found, otherwise an empty string (`""`).
- **Arguments**:
- `cmd`: `String` - The name of the command to search for (e.g., `"node"`).
```rhai
let node_path = which("node"); // Does not halt if node is not found
if (node_path != "") {
print(`Node executable found at: ${node_path}`);
} else {
print("Node executable not found in PATH.");
}
```
---
### `kill(pattern)`
Kill processes matching a pattern.
- **Description**: Terminates running processes whose names match the provided `pattern`. Uses platform-specific commands (like `pkill` or equivalent). Halts script execution on error interacting with the system process list or kill command.
- **Returns**: `String` - A success message indicating the kill attempt finished.
- **Arguments**:
- `pattern`: `String` - A pattern to match against process names (e.g., `"nginx"`).
```rhai
print("Attempting to kill processes matching 'my_service'...");
// Use with caution!
kill("my_service"); // Halts on OS error during kill attempt
print("Kill command sent.");
```
---
### `process_list(pattern)`
List processes matching a pattern (or all if pattern is empty).
- **Description**: Lists information about running processes whose names match the provided `pattern`. If `pattern` is an empty string `""`, lists all processes. Halts script execution on error interacting with the system process list. Returns an empty array if no processes match the pattern.
- **Returns**: `Array` of `ProcessInfo` - An array of objects, each containing `pid` (Integer), `name` (String), `memory` (Integer), and `cpu` (Float).
- **Arguments**:
- `pattern`: `String` - A pattern to match against process names, or `""` for all processes.
```rhai
print("Listing processes matching 'bash'...");
let bash_processes = process_list("bash"); // Halts on OS error
if (bash_processes.len() > 0) {
print("Found bash processes:");
for proc in bash_processes {
print(`- PID: ${proc.pid}, Name: ${proc.name}, CPU: ${proc.cpu}%, Memory: ${proc.memory}`);
}
} else {
print("No bash processes found.");
}
```
---
### `process_get(pattern)`
Get a single process matching the pattern (error if 0 or more than 1 match).
- **Description**: Finds exactly one running process whose name matches the provided `pattern`. Halts script execution if zero or more than one process matches the pattern, or on error interacting with the system process list.
- **Returns**: `ProcessInfo` - An object containing `pid` (Integer), `name` (String), `memory` (Integer), and `cpu` (Float).
- **Arguments**:
- `pattern`: `String` - A pattern to match against process names, expected to match exactly one process.
```rhai
let expected_service_name = "my_critical_service";
print(`Getting process info for '${expected_service_name}'...`);
// This will halt if the service isn't running, or if multiple services have this name
let service_proc_info = process_get(expected_service_name);
print(`Found process: PID ${service_proc_info.pid}, Name: ${service_proc_info.name}`);
```

View File

@ -1,105 +0,0 @@
# Buildah Module Tests
This document describes the test scripts for the Buildah module in the SAL library. These tests verify the functionality of the Buildah module's container and image operations.
## Test Structure
The tests are organized into three main scripts:
1. **Builder Pattern** (`01_builder_pattern.rhai`): Tests for the Builder pattern, including creating containers, running commands, and working with container content.
2. **Image Operations** (`02_image_operations.rhai`): Tests for image-related operations like pulling, tagging, listing, and removing images.
3. **Container Operations** (`03_container_operations.rhai`): Tests for container-related operations like configuration, isolation, and content management.
Additionally, there's a runner script (`run_all_tests.rhai`) that executes all tests and reports results. The runner script contains simplified versions of the individual tests to avoid dependency issues.
## Running the Tests
To run all tests, execute the following command from the project root:
```bash
herodo --path src/rhai_tests/buildah/run_all_tests.rhai
```
To run individual test scripts:
```bash
herodo --path src/rhai_tests/buildah/01_builder_pattern.rhai
```
## Test Details
### Builder Pattern Test
The Builder Pattern test (`01_builder_pattern.rhai`) verifies the following functions:
- `bah_new`: Creating a new Builder with a container from a specified image
- Builder properties: `container_id`, `name`, `image`, `debug_mode`
- `run`: Running commands in the container
- `write_content`: Writing content to files in the container
- `read_content`: Reading content from files in the container
- `set_entrypoint`: Setting the container's entrypoint
- `set_cmd`: Setting the container's command
- `add`: Adding files to the container
- `copy`: Copying files to the container
- `commit`: Committing the container to an image
- `remove`: Removing the container
- `images`: Listing images
- `image_remove`: Removing images
### Image Operations Test
The Image Operations test (`02_image_operations.rhai`) verifies the following functions:
- `image_pull`: Pulling images from registries
- `image_tag`: Tagging images
- `images`: Listing images
- `build`: Building images from Dockerfiles
- `image_remove`: Removing images
The test creates a temporary directory with a Dockerfile for testing the build functionality.
### Container Operations Test
The Container Operations test (`03_container_operations.rhai`) verifies the following functions:
- `reset`: Resetting a Builder by removing its container
- `config`: Configuring container properties
- `run_with_isolation`: Running commands with isolation
- Content operations: Creating and executing scripts in the container
- `commit` with options: Committing a container with additional configuration
## Test Runner
The test runner script (`run_all_tests.rhai`) provides a framework for executing all tests and reporting results. It:
1. Checks if Buildah is available before running tests
2. Skips tests if Buildah is not available
3. Contains simplified versions of each test
4. Runs each test in a try/catch block to handle errors
5. Catches and reports any errors
6. Provides a summary of passed, failed, and skipped tests
## Buildah Requirements
These tests require the Buildah tool to be installed and available in the system's PATH. The tests will check for Buildah's availability and skip the tests if it's not found, rather than failing.
## Adding New Tests
To add a new test:
1. Create a new Rhai script in the `src/rhai_tests/buildah` directory
2. Add a new test section to the `run_all_tests.rhai` script
3. Update this documentation to include information about the new test
## Best Practices for Writing Tests
When writing tests for the Buildah module:
1. Always check if Buildah is available before running tests
2. Use unique names for containers and images to avoid conflicts
3. Clean up any containers, images, or files created during testing
4. Use assertions to verify expected behavior
5. Print clear messages about what's being tested
6. Handle errors gracefully
7. Make tests independent of each other
8. Keep tests focused on specific functionality

View File

@ -1,71 +0,0 @@
# Continuous Integration for Rhai Tests
This document describes the continuous integration (CI) workflow for running Rhai tests in the SAL library.
## GitHub Actions Workflow
The SAL project includes a GitHub Actions workflow that automatically runs all Rhai tests whenever changes are made to relevant files. This ensures that the Rhai integration continues to work correctly as the codebase evolves.
### Workflow File
The workflow is defined in `.github/workflows/rhai-tests.yml`.
### Trigger Events
The workflow runs automatically when:
1. Changes are pushed to the `main` or `master` branch that affect:
- Rhai test scripts (`src/rhai_tests/**`)
- Rhai module code (`src/rhai/**`)
- Git module code (`src/git/**`)
- OS module code (`src/os/**`)
- The test runner script (`run_rhai_tests.sh`)
- The workflow file itself (`.github/workflows/rhai-tests.yml`)
2. A pull request is opened or updated that affects the same files.
3. The workflow is manually triggered using the GitHub Actions interface.
### Workflow Steps
The workflow performs the following steps:
1. **Checkout Code**: Checks out the repository code.
2. **Set up Rust**: Installs the Rust toolchain.
3. **Cache Dependencies**: Caches Rust dependencies to speed up builds.
4. **Build herodo**: Builds the `herodo` binary used to run Rhai scripts.
5. **Install Dependencies**: Installs system dependencies like Git and curl.
6. **Run Rhai Tests**: Runs the `run_rhai_tests.sh` script to execute all Rhai tests.
7. **Check for Failures**: Verifies that all tests passed.
### Test Results
The workflow will fail if any Rhai test fails. This prevents changes that break the Rhai integration from being merged.
## Local Testing
Before pushing changes, you can run the same tests locally using the `run_rhai_tests.sh` script:
```bash
./run_rhai_tests.sh
```
This will produce the same test results as the CI workflow, allowing you to catch and fix issues before pushing your changes.
## Logs
The test runner script creates a log file (`run_rhai_tests.log`) that contains the output of all tests. This log is used by the CI workflow to check for test failures.
## Adding New Tests
When adding new tests, make sure they are included in the appropriate module's test runner script (`run_all_tests.rhai`). The CI workflow will automatically run the new tests.
## Troubleshooting
If the CI workflow fails, check the GitHub Actions logs for details. Common issues include:
1. **Missing Dependencies**: Ensure all required dependencies are installed.
2. **Test Failures**: Fix any failing tests.
3. **Build Errors**: Fix any errors in the Rust code.
If you need to modify the workflow, edit the `.github/workflows/rhai-tests.yml` file.

View File

@ -1,81 +0,0 @@
# Git Module Tests
This document describes the test scripts for the Git module in the SAL library. These tests verify the functionality of the Git module's repository management and Git operations.
## Test Structure
The tests are organized into two main scripts:
1. **Basic Git Operations** (`01_git_basic.rhai`): Tests basic Git functionality like creating a GitTree, listing repositories, finding repositories, and cloning repositories.
2. **Git Repository Operations** (`02_git_operations.rhai`): Tests Git operations like pull, reset, commit, and push.
Additionally, there's a runner script (`run_all_tests.rhai`) that executes all tests and reports results. The runner script contains simplified versions of the individual tests to avoid dependency issues.
## Running the Tests
To run all tests, execute the following command from the project root:
```bash
herodo --path src/rhai_tests/git/run_all_tests.rhai
```
To run individual test scripts:
```bash
herodo --path src/rhai_tests/git/01_git_basic.rhai
```
## Test Details
### Basic Git Operations Test
The basic Git operations test (`01_git_basic.rhai`) verifies the following functions:
- `git_tree_new`: Creating a GitTree
- `list`: Listing repositories in a GitTree
- `find`: Finding repositories matching a pattern
- `get`: Getting or cloning a repository
- `path`: Getting the path of a repository
- `has_changes`: Checking if a repository has changes
The test creates a temporary directory, performs operations on it, and then cleans up after itself.
### Git Repository Operations Test
The Git repository operations test (`02_git_operations.rhai`) verifies the following functions:
- `pull`: Pulling changes from a remote repository
- `reset`: Resetting local changes
- `commit`: Committing changes (method existence only)
- `push`: Pushing changes to a remote repository (method existence only)
Note: The test does not actually commit or push changes to avoid modifying remote repositories. It only verifies that the methods exist and can be called.
## Test Runner
The test runner script (`run_all_tests.rhai`) provides a framework for executing all tests and reporting results. It:
1. Contains simplified versions of each test
2. Runs each test in a try/catch block to handle errors
3. Catches and reports any errors
4. Provides a summary of passed and failed tests
## Adding New Tests
To add a new test:
1. Create a new Rhai script in the `src/rhai_tests/git` directory
2. Add a new test section to the `run_all_tests.rhai` script
3. Update this documentation to include information about the new test
## Best Practices for Writing Tests
When writing tests for the Git module:
1. Always clean up temporary files and directories
2. Use assertions to verify expected behavior
3. Print clear messages about what's being tested
4. Handle errors gracefully
5. Make tests independent of each other
6. Avoid tests that modify remote repositories
7. Keep tests focused on specific functionality

View File

@ -1,85 +0,0 @@
# Rhai Scripting in SAL
This documentation covers the Rhai scripting integration in the SAL (System Abstraction Layer) library.
## Overview
SAL provides integration with the [Rhai scripting language](https://rhai.rs/), allowing you to use SAL's functionality in scripts. This enables automation of system tasks, testing, and more complex operations without having to write Rust code.
## Modules
SAL exposes the following modules to Rhai scripts:
- [OS Module](os_module_tests.md): File system operations, downloads, and package management
- Process Module: Process management and command execution
- Git Module: Git repository operations
- Text Module: Text processing utilities
- Buildah Module: Container image building
- Nerdctl Module: Container runtime operations
- RFS Module: Remote file system operations
- Redis Client Module: Redis database connection and operations
- PostgreSQL Client Module: PostgreSQL database connection and operations
## Running Rhai Scripts
You can run Rhai scripts using the `herodo` binary:
```bash
herodo --path path/to/script.rhai
```
## Testing
SAL includes test scripts for verifying the functionality of its Rhai integration. These tests are located in the `src/rhai_tests` directory and are organized by module.
- [OS Module Tests](os_module_tests.md): Tests for file system, download, and package management operations
- [Git Module Tests](git_module_tests.md): Tests for Git repository management and operations
- [Process Module Tests](process_module_tests.md): Tests for command execution and process management
- [Redis Client Module Tests](redisclient_module_tests.md): Tests for Redis connection and operations
- [PostgreSQL Client Module Tests](postgresclient_module_tests.md): Tests for PostgreSQL connection and operations
- [Text Module Tests](text_module_tests.md): Tests for text manipulation, normalization, replacement, and template rendering
- [Buildah Module Tests](buildah_module_tests.md): Tests for container and image operations
- [Nerdctl Module Tests](nerdctl_module_tests.md): Tests for container and image operations using nerdctl
- [RFS Module Tests](rfs_module_tests.md): Tests for remote filesystem operations and filesystem layers
- [Running Tests](running_tests.md): Instructions for running all Rhai tests
- [CI Workflow](ci_workflow.md): Continuous integration workflow for Rhai tests
## Examples
For examples of how to use SAL's Rhai integration, see the `examples` directory in the project root. These examples demonstrate various features and use cases.
## Writing Your Own Scripts
When writing Rhai scripts that use SAL:
1. Import the necessary modules (they're automatically registered)
2. Use the functions provided by each module
3. Handle errors appropriately
4. Clean up resources when done
Example:
```rhai
// Simple example of using the OS module
let test_dir = "my_test_dir";
mkdir(test_dir);
if exist(test_dir) {
print(`Directory ${test_dir} created successfully`);
// Create a file
let test_file = test_dir + "/test.txt";
file_write(test_file, "Hello, world!");
// Read the file
let content = file_read(test_file);
print(`File content: ${content}`);
// Clean up
delete(test_dir);
}
```
## API Reference
For detailed information about the functions available in each module, refer to the module-specific documentation.

View File

@ -1,116 +0,0 @@
# Nerdctl Module Tests
This document describes the test scripts for the Nerdctl module in the SAL library. These tests verify the functionality of the Nerdctl module's container and image operations.
## Test Structure
The tests are organized into three main scripts:
1. **Container Operations** (`01_container_operations.rhai`): Tests for basic container operations like creating, running, executing commands, and removing containers.
2. **Image Operations** (`02_image_operations.rhai`): Tests for image-related operations like pulling, tagging, listing, building, and removing images.
3. **Container Builder Pattern** (`03_container_builder.rhai`): Tests for the Container Builder pattern, which provides a fluent interface for configuring and running containers.
Additionally, there's a runner script (`run_all_tests.rhai`) that executes all tests and reports results. The runner script contains simplified versions of the individual tests to avoid dependency issues.
## Running the Tests
To run all tests, execute the following command from the project root:
```bash
herodo --path src/rhai_tests/nerdctl/run_all_tests.rhai
```
To run individual test scripts:
```bash
herodo --path src/rhai_tests/nerdctl/01_container_operations.rhai
```
## Test Details
### Container Operations Test
The Container Operations test (`01_container_operations.rhai`) verifies the following functions:
- `nerdctl_container_new`: Creating a new Container
- Container properties: `name`, `container_id`, `image`, `detach`
- `with_image`: Setting the container image
- `with_detach`: Setting detach mode
- `with_env` and `with_envs`: Setting environment variables
- `with_port` and `with_ports`: Setting port mappings
- `with_volume`: Setting volume mounts
- `with_cpu_limit` and `with_memory_limit`: Setting resource limits
- `run`: Running the container
- `exec`: Executing commands in the container
- `logs`: Getting container logs
- `stop`: Stopping the container
- `remove`: Removing the container
### Image Operations Test
The Image Operations test (`02_image_operations.rhai`) verifies the following functions:
- `nerdctl_image_pull`: Pulling images from registries
- `nerdctl_images`: Listing images
- `nerdctl_image_tag`: Tagging images
- `nerdctl_image_build`: Building images from Dockerfiles
- `nerdctl_run_with_name`: Running containers from images
- `nerdctl_stop` and `nerdctl_remove`: Stopping and removing containers
- `nerdctl_image_remove`: Removing images
The test creates a temporary directory with a Dockerfile for testing the build functionality.
### Container Builder Pattern Test
The Container Builder Pattern test (`03_container_builder.rhai`) verifies the following functions:
- `nerdctl_container_from_image`: Creating a container from an image
- `reset`: Resetting container configuration
- `with_detach`: Setting detach mode
- `with_ports`: Setting multiple port mappings
- `with_volumes`: Setting multiple volume mounts
- `with_envs`: Setting multiple environment variables
- `with_network`: Setting network
- `with_cpu_limit` and `with_memory_limit`: Setting resource limits
- `run`: Running the container
- `exec`: Executing commands in the container
- `stop`: Stopping the container
- `remove`: Removing the container
The test also verifies that environment variables and volume mounts work correctly by writing and reading files between the container and the host.
## Test Runner
The test runner script (`run_all_tests.rhai`) provides a framework for executing all tests and reporting results. It:
1. Checks if nerdctl is available before running tests
2. Skips tests if nerdctl is not available
3. Contains simplified versions of each test
4. Runs each test in a try/catch block to handle errors
5. Catches and reports any errors
6. Provides a summary of passed, failed, and skipped tests
## Nerdctl Requirements
These tests require the nerdctl tool to be installed and available in the system's PATH. The tests will check for nerdctl's availability and skip the tests if it's not found, rather than failing.
## Adding New Tests
To add a new test:
1. Create a new Rhai script in the `src/rhai_tests/nerdctl` directory
2. Add a new test section to the `run_all_tests.rhai` script
3. Update this documentation to include information about the new test
## Best Practices for Writing Tests
When writing tests for the Nerdctl module:
1. Always check if nerdctl is available before running tests
2. Use unique names for containers and images to avoid conflicts
3. Clean up any containers, images, or files created during testing
4. Use assertions to verify expected behavior
5. Print clear messages about what's being tested
6. Handle errors gracefully
7. Make tests independent of each other
8. Keep tests focused on specific functionality

View File

@ -1,105 +0,0 @@
# OS Module Tests
This document describes the test scripts for the OS module in the SAL library. These tests verify the functionality of the OS module's file system operations, download capabilities, and package management features.
## Test Structure
The tests are organized into three main scripts:
1. **File Operations** (`01_file_operations.rhai`): Tests file system operations like creating, reading, writing, and manipulating files and directories.
2. **Download Operations** (`02_download_operations.rhai`): Tests downloading files from the internet and related operations.
3. **Package Operations** (`03_package_operations.rhai`): Tests package management functionality.
Additionally, there's a runner script (`run_all_tests.rhai`) that executes all tests and reports results. The runner script contains simplified versions of the individual tests to avoid dependency on the `run_script` function.
## Running the Tests
To run all tests, execute the following command from the project root:
```bash
# Assume that you have the herodo binary/built into your system
herodo --path src/rhai_tests/os/run_all_tests.rhai
```
To run individual test scripts:
```bash
# Assume that you have the herodo binary/built into your system
herodo --path src/rhai_tests/os/01_file_operations.rhai
```
## Test Details
### File Operations Test
The file operations test (`01_file_operations.rhai`) verifies the following functions:
- `mkdir`: Creating directories
- `file_write`: Writing content to files
- `file_read`: Reading content from files
- `file_size`: Getting file size
- `file_write_append`: Appending content to files
- `copy`: Copying files
- `mv`: Moving files
- `find_file`: Finding a single file matching a pattern
- `find_files`: Finding multiple files matching a pattern
- `find_dir`: Finding a single directory matching a pattern
- `find_dirs`: Finding multiple directories matching a pattern
- `chdir`: Changing the current working directory
- `rsync`: Synchronizing directories
- `delete`: Deleting files and directories
- `exist`: Checking if files or directories exist
The test creates a temporary directory structure, performs operations on it, and then cleans up after itself.
### Download Operations Test
The download operations test (`02_download_operations.rhai`) verifies the following functions:
- `which`: Checking if a command exists in the system PATH
- `cmd_ensure_exists`: Ensuring commands exist
- `download_file`: Downloading a file from a URL
- `chmod_exec`: Making a file executable
The test downloads a small file from GitHub, verifies its content, and then cleans up.
### Package Operations Test
The package operations test (`03_package_operations.rhai`) verifies the following functions:
- `package_platform`: Getting the current platform
- `package_set_debug`: Setting debug mode for package operations
- `package_is_installed`: Checking if a package is installed
- `package_search`: Searching for packages
- `package_list`: Listing installed packages
Note: The test does not verify `package_install`, `package_remove`, `package_update`, or `package_upgrade` as these require root privileges and could modify the system state.
## Test Runner
The test runner script (`run_all_tests.rhai`) provides a framework for executing all tests and reporting results. It:
1. Contains simplified versions of each test
2. Runs each test in a try/catch block to handle errors
3. Catches and reports any errors
4. Provides a summary of passed and failed tests
## Adding New Tests
To add a new test:
1. Create a new Rhai script in the `src/rhai_tests/os` directory
2. Add a new test section to the `run_all_tests.rhai` script
3. Update this documentation to include information about the new test
## Best Practices for Writing Tests
When writing tests for the OS module:
1. Always clean up temporary files and directories
2. Use assertions to verify expected behavior
3. Print clear messages about what's being tested
4. Handle errors gracefully
5. Make tests independent of each other
6. Avoid tests that require root privileges when possible
7. Keep tests focused on specific functionality

View File

@ -1,188 +0,0 @@
# PostgreSQL Client Module Tests
The PostgreSQL client module provides functions for connecting to and interacting with PostgreSQL databases. These tests verify the functionality of the module.
## PostgreSQL Client Features
The PostgreSQL client module provides the following features:
1. **Basic PostgreSQL Operations**: Execute queries, fetch results, etc.
2. **Connection Management**: Automatic connection handling and reconnection
3. **Builder Pattern for Configuration**: Flexible configuration with authentication support
4. **PostgreSQL Installer**: Install and configure PostgreSQL using nerdctl
5. **Database Management**: Create databases and execute SQL scripts
## Prerequisites
For basic PostgreSQL operations:
- PostgreSQL server must be running and accessible
- Environment variables should be set for connection details:
- `POSTGRES_HOST`: PostgreSQL server host (default: localhost)
- `POSTGRES_PORT`: PostgreSQL server port (default: 5432)
- `POSTGRES_USER`: PostgreSQL username (default: postgres)
- `POSTGRES_PASSWORD`: PostgreSQL password
- `POSTGRES_DB`: PostgreSQL database name (default: postgres)
For PostgreSQL installer:
- nerdctl must be installed and working
- Docker images must be accessible
- Sufficient permissions to create and manage containers
## Test Files
### 01_postgres_connection.rhai
Tests basic PostgreSQL connection and operations:
- Connecting to PostgreSQL
- Pinging the server
- Creating a table
- Inserting data
- Querying data
- Dropping a table
- Resetting the connection
### 02_postgres_installer.rhai
Tests PostgreSQL installer functionality:
- Installing PostgreSQL using nerdctl
- Creating a database
- Executing SQL scripts
- Checking if PostgreSQL is running
### run_all_tests.rhai
Runs all PostgreSQL client module tests and provides a summary of the results.
## Running the Tests
You can run the tests using the `herodo` command:
```bash
herodo --path src/rhai_tests/postgresclient/run_all_tests.rhai
```
Or run individual tests:
```bash
herodo --path src/rhai_tests/postgresclient/01_postgres_connection.rhai
```
## Available Functions
### Connection Functions
- `pg_connect()`: Connect to PostgreSQL using environment variables
- `pg_ping()`: Ping the PostgreSQL server to check if it's available
- `pg_reset()`: Reset the PostgreSQL client connection
### Query Functions
- `pg_execute(query)`: Execute a query and return the number of affected rows
- `pg_query(query)`: Execute a query and return the results as an array of maps
- `pg_query_one(query)`: Execute a query and return a single row as a map
### Installer Functions
- `pg_install(container_name, version, port, username, password)`: Install PostgreSQL using nerdctl
- `pg_create_database(container_name, db_name)`: Create a new database in PostgreSQL
- `pg_execute_sql(container_name, db_name, sql)`: Execute a SQL script in PostgreSQL
- `pg_is_running(container_name)`: Check if PostgreSQL is running
## Authentication Support
The PostgreSQL client module will support authentication using the builder pattern in a future update.
The backend implementation is ready, but the Rhai bindings are still in development.
When implemented, the builder pattern will support the following configuration options:
- Host: Set the PostgreSQL host
- Port: Set the PostgreSQL port
- User: Set the PostgreSQL username
- Password: Set the PostgreSQL password
- Database: Set the PostgreSQL database name
- Application name: Set the application name
- Connection timeout: Set the connection timeout in seconds
- SSL mode: Set the SSL mode
## Example Usage
### Basic PostgreSQL Operations
```rust
// Connect to PostgreSQL
if (pg_connect()) {
print("Connected to PostgreSQL!");
// Create a table
let create_table_query = "CREATE TABLE IF NOT EXISTS test_table (id SERIAL PRIMARY KEY, name TEXT)";
pg_execute(create_table_query);
// Insert data
let insert_query = "INSERT INTO test_table (name) VALUES ('test')";
pg_execute(insert_query);
// Query data
let select_query = "SELECT * FROM test_table";
let results = pg_query(select_query);
// Process results
for (result in results) {
print(`ID: ${result.id}, Name: ${result.name}`);
}
// Clean up
let drop_query = "DROP TABLE test_table";
pg_execute(drop_query);
}
```
### PostgreSQL Installer
```rust
// Install PostgreSQL
let container_name = "my-postgres";
let postgres_version = "15";
let postgres_port = 5432;
let postgres_user = "myuser";
let postgres_password = "mypassword";
if (pg_install(container_name, postgres_version, postgres_port, postgres_user, postgres_password)) {
print("PostgreSQL installed successfully!");
// Create a database
let db_name = "mydb";
if (pg_create_database(container_name, db_name)) {
print(`Database '${db_name}' created successfully!`);
// Execute a SQL script
let create_table_sql = `
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL
);
`;
let result = pg_execute_sql(container_name, db_name, create_table_sql);
print("Table created successfully!");
// Insert data
let insert_sql = "#
INSERT INTO users (name, email) VALUES
('John Doe', 'john@example.com'),
('Jane Smith', 'jane@example.com');
#";
result = pg_execute_sql(container_name, db_name, insert_sql);
print("Data inserted successfully!");
// Query data
let query_sql = "SELECT * FROM users;";
result = pg_execute_sql(container_name, db_name, query_sql);
print(`Query result: ${result}`);
}
}
```

View File

@ -1,79 +0,0 @@
# Process Module Tests
This document describes the test scripts for the Process module in the SAL library. These tests verify the functionality of the Process module's command execution and process management features.
## Test Structure
The tests are organized into two main scripts:
1. **Command Execution** (`01_command_execution.rhai`): Tests command execution functions like `run()` and `which()`.
2. **Process Management** (`02_process_management.rhai`): Tests process management functions like `process_list()` and `process_get()`.
Additionally, there's a runner script (`run_all_tests.rhai`) that executes all tests and reports results. The runner script contains simplified versions of the individual tests to avoid dependency issues.
## Running the Tests
To run all tests, execute the following command from the project root:
```bash
herodo --path src/rhai_tests/process/run_all_tests.rhai
```
To run individual test scripts:
```bash
herodo --path src/rhai_tests/process/01_command_execution.rhai
```
## Test Details
### Command Execution Test
The command execution test (`01_command_execution.rhai`) verifies the following functions:
- `run()`: Running shell commands
- `run().do()`: Executing commands and capturing output
- `run().silent()`: Running commands without displaying output
- `run().ignore_error()`: Running commands that might fail without throwing errors
- `which()`: Finding the path of an executable
The test runs various commands and verifies their output and exit status.
### Process Management Test
The process management test (`02_process_management.rhai`) verifies the following functions:
- `process_list()`: Listing running processes
- `process_get()`: Getting information about a specific process
- Process properties: Accessing process information like PID, name, CPU usage, and memory usage
The test lists running processes and verifies that their properties are accessible.
## Test Runner
The test runner script (`run_all_tests.rhai`) provides a framework for executing all tests and reporting results. It:
1. Contains simplified versions of each test
2. Runs each test in a try/catch block to handle errors
3. Catches and reports any errors
4. Provides a summary of passed and failed tests
## Adding New Tests
To add a new test:
1. Create a new Rhai script in the `src/rhai_tests/process` directory
2. Add a new test section to the `run_all_tests.rhai` script
3. Update this documentation to include information about the new test
## Best Practices for Writing Tests
When writing tests for the Process module:
1. Use assertions to verify expected behavior
2. Print clear messages about what's being tested
3. Handle errors gracefully
4. Make tests independent of each other
5. Avoid tests that could disrupt the system (e.g., killing important processes)
6. Keep tests focused on specific functionality
7. Clean up any resources created during testing

View File

@ -1,125 +0,0 @@
# Redis Client Module Tests
This document describes the test scripts for the Redis client module in the SAL library. These tests verify the functionality of the Redis client module's connection management and Redis operations.
## Redis Client Features
The Redis client module provides the following features:
1. **Basic Redis Operations**: SET, GET, DEL, etc.
2. **Hash Operations**: HSET, HGET, HGETALL, HDEL
3. **List Operations**: RPUSH, LPUSH, LLEN, LRANGE
4. **Connection Management**: Automatic connection handling and reconnection
5. **Builder Pattern for Configuration**: Flexible configuration with authentication support
## Test Structure
The tests are organized into two main scripts:
1. **Redis Connection** (`01_redis_connection.rhai`): Tests basic Redis connection and simple operations like PING, SET, GET, and DEL.
2. **Redis Operations** (`02_redis_operations.rhai`): Tests more advanced Redis operations like hash operations (HSET, HGET, HGETALL, HDEL) and list operations (RPUSH, LLEN, LRANGE).
Additionally, there's a runner script (`run_all_tests.rhai`) that executes all tests and reports results. The runner script contains simplified versions of the individual tests to avoid dependency issues.
## Running the Tests
To run all tests, execute the following command from the project root:
```bash
herodo --path src/rhai_tests/redisclient/run_all_tests.rhai
```
To run individual test scripts:
```bash
herodo --path src/rhai_tests/redisclient/01_redis_connection.rhai
```
## Test Details
### Redis Connection Test
The Redis connection test (`01_redis_connection.rhai`) verifies the following functions:
- `redis_ping`: Checking if the Redis server is available
- `redis_set`: Setting a key-value pair
- `redis_get`: Getting a value by key
- `redis_del`: Deleting a key
The test creates a temporary key, performs operations on it, and then cleans up after itself.
### Redis Operations Test
The Redis operations test (`02_redis_operations.rhai`) verifies the following functions:
- Hash operations:
- `redis_hset`: Setting a field in a hash
- `redis_hget`: Getting a field from a hash
- `redis_hgetall`: Getting all fields and values from a hash
- `redis_hdel`: Deleting a field from a hash
- List operations:
- `redis_rpush`: Adding elements to a list
- `redis_llen`: Getting the length of a list
- `redis_lrange`: Getting a range of elements from a list
The test creates temporary keys with a unique prefix, performs operations on them, and then cleans up after itself.
## Test Runner
The test runner script (`run_all_tests.rhai`) provides a framework for executing all tests and reporting results. It:
1. Checks if Redis is available before running tests
2. Skips tests if Redis is not available
3. Contains simplified versions of each test
4. Runs each test in a try/catch block to handle errors
5. Catches and reports any errors
6. Provides a summary of passed, failed, and skipped tests
## Redis Server Requirements
These tests require a Redis server to be running and accessible. The tests will attempt to connect to Redis using the following strategy:
1. First, try to connect via Unix socket at `$HOME/hero/var/myredis.sock`
2. If that fails, try to connect via TCP to `127.0.0.1` on the default Redis port (6379)
If no Redis server is available, the tests will be skipped rather than failing.
## Authentication Support
The Redis client module will support authentication using the builder pattern in a future update.
The backend implementation is ready, but the Rhai bindings are still in development.
When implemented, the builder pattern will support the following configuration options:
- Host: Set the Redis host
- Port: Set the Redis port
- Database: Set the Redis database number
- Username: Set the Redis username (Redis 6.0+)
- Password: Set the Redis password
- TLS: Enable/disable TLS
- Unix socket: Enable/disable Unix socket
- Socket path: Set the Unix socket path
- Connection timeout: Set the connection timeout in seconds
## Adding New Tests
To add a new test:
1. Create a new Rhai script in the `src/rhai_tests/redisclient` directory
2. Add a new test section to the `run_all_tests.rhai` script
3. Update this documentation to include information about the new test
## Best Practices for Writing Tests
When writing tests for the Redis client module:
1. Always check if Redis is available before running tests
2. Use a unique prefix for test keys to avoid conflicts
3. Clean up any keys created during testing
4. Use assertions to verify expected behavior
5. Print clear messages about what's being tested
6. Handle errors gracefully
7. Make tests independent of each other
8. Keep tests focused on specific functionality

View File

@ -1,113 +0,0 @@
# RFS Module Tests
This document describes the test scripts for the RFS (Remote File System) module in the SAL library. These tests verify the functionality of the RFS module's mount operations and filesystem layer management.
## Test Structure
The tests are organized into two main scripts:
1. **Mount Operations** (`01_mount_operations.rhai`): Tests for mounting, listing, and unmounting filesystems.
2. **Filesystem Layer Operations** (`02_filesystem_layer_operations.rhai`): Tests for packing, unpacking, listing, and verifying filesystem layers.
Additionally, there's a runner script (`run_all_tests.rhai`) that executes all tests and reports results. The runner script contains simplified versions of the individual tests to avoid dependency issues.
## Running the Tests
To run all tests, execute the following command from the project root:
```bash
herodo --path src/rhai_tests/rfs/run_all_tests.rhai
```
To run individual test scripts:
```bash
herodo --path src/rhai_tests/rfs/01_mount_operations.rhai
```
## Test Details
### Mount Operations Test
The Mount Operations test (`01_mount_operations.rhai`) verifies the following functions:
- `rfs_mount`: Mounting a filesystem
- Tests mounting a local directory with options
- Verifies mount properties (ID, source, target, type)
- `rfs_list_mounts`: Listing mounted filesystems
- Tests listing all mounts
- Verifies that the mounted filesystem is in the list
- `rfs_get_mount_info`: Getting information about a mounted filesystem
- Tests getting information about a specific mount
- Verifies that the mount information is correct
- `rfs_unmount`: Unmounting a specific filesystem
- Tests unmounting a specific mount
- Verifies that the mount is no longer available
- `rfs_unmount_all`: Unmounting all filesystems
- Tests unmounting all mounts
- Verifies that no mounts remain after the operation
The test also verifies that files in the mounted filesystem are accessible and have the correct content.
### Filesystem Layer Operations Test
The Filesystem Layer Operations test (`02_filesystem_layer_operations.rhai`) verifies the following functions:
- `rfs_pack`: Packing a directory into a filesystem layer
- Tests packing a directory with files and subdirectories
- Verifies that the output file is created
- `rfs_list_contents`: Listing the contents of a filesystem layer
- Tests listing the contents of a packed filesystem layer
- Verifies that the list includes all expected files
- `rfs_verify`: Verifying a filesystem layer
- Tests verifying a packed filesystem layer
- Verifies that the layer is valid
- `rfs_unpack`: Unpacking a filesystem layer
- Tests unpacking a filesystem layer to a directory
- Verifies that all files are unpacked correctly with the right content
The test creates a directory structure with files, packs it into a filesystem layer, and then unpacks it to verify the integrity of the process.
## Test Runner
The test runner script (`run_all_tests.rhai`) provides a framework for executing all tests and reporting results. It:
1. Checks if RFS is available before running tests
2. Skips tests if RFS is not available
3. Contains simplified versions of each test
4. Runs each test in a try/catch block to handle errors
5. Catches and reports any errors
6. Provides a summary of passed, failed, and skipped tests
## RFS Requirements
These tests require the RFS tool to be installed and available in the system's PATH. The tests will check for RFS's availability and skip the tests if it's not found, rather than failing.
## Adding New Tests
To add a new test:
1. Create a new Rhai script in the `src/rhai_tests/rfs` directory
2. Add a new test section to the `run_all_tests.rhai` script
3. Update this documentation to include information about the new test
## Best Practices for Writing Tests
When writing tests for the RFS module:
1. Always check if RFS is available before running tests
2. Clean up any mounts before and after testing
3. Use unique names for test directories and files to avoid conflicts
4. Clean up any files or directories created during testing
5. Use assertions to verify expected behavior
6. Print clear messages about what's being tested
7. Handle errors gracefully
8. Make tests independent of each other
9. Keep tests focused on specific functionality

View File

@ -1,76 +0,0 @@
# Running Rhai Tests
This document describes how to run the Rhai tests for the SAL library.
## Test Structure
The Rhai tests are organized by module in the `src/rhai_tests` directory:
- `src/rhai_tests/os/`: Tests for the OS module
- `src/rhai_tests/git/`: Tests for the Git module
Each module directory contains:
- Individual test scripts (e.g., `01_file_operations.rhai`)
- A test runner script (`run_all_tests.rhai`) that runs all tests for that module
## Running Tests
### Running All Tests
To run all Rhai tests across all modules, use the provided shell script:
```bash
./run_rhai_tests.sh
```
This script:
1. Finds all test runner scripts in the `src/rhai_tests` directory
2. Runs each test runner
3. Reports the results for each module
4. Provides a summary of all test results
The script will exit with code 0 if all tests pass, or code 1 if any tests fail.
### Running Tests for a Specific Module
To run tests for a specific module, use the `herodo` command with the module's test runner:
```bash
herodo --path src/rhai_tests/os/run_all_tests.rhai
```
### Running Individual Tests
To run a specific test, use the `herodo` command with the test script:
```bash
herodo --path src/rhai_tests/os/01_file_operations.rhai
```
## Test Output
The test output includes:
- Information about what's being tested
- Success or failure messages for each test
- A summary of test results
Successful tests are indicated with a checkmark (✓), while failed tests show an error message.
## Adding New Tests
When adding new tests:
1. Create a new test script in the appropriate module directory
2. Update the module's test runner script to include the new test
3. Update the module's documentation to describe the new test
The `run_rhai_tests.sh` script will automatically find and run the new tests as long as they're included in a module's test runner script.
## Troubleshooting
If tests fail, check the following:
1. Make sure the `herodo` binary is in your PATH
2. Verify that the test scripts have the correct permissions
3. Check for any dependencies required by the tests (e.g., `git` for Git module tests)
4. Look for specific error messages in the test output

View File

@ -1,129 +0,0 @@
# Text Module Tests
This document describes the test scripts for the Text module in the SAL library. These tests verify the functionality of the Text module's text manipulation, normalization, replacement, and template rendering capabilities.
## Test Structure
The tests are organized into four main scripts:
1. **Text Indentation** (`01_text_indentation.rhai`): Tests for the `dedent` and `prefix` functions.
2. **Filename and Path Normalization** (`02_name_path_fix.rhai`): Tests for the `name_fix` and `path_fix` functions.
3. **Text Replacement** (`03_text_replacer.rhai`): Tests for the `TextReplacer` class and its methods.
4. **Template Rendering** (`04_template_builder.rhai`): Tests for the `TemplateBuilder` class and its methods.
Additionally, there's a runner script (`run_all_tests.rhai`) that executes all tests and reports results. The runner script contains simplified versions of the individual tests to avoid dependency issues.
## Running the Tests
To run all tests, execute the following command from the project root:
```bash
herodo --path src/rhai_tests/text/run_all_tests.rhai
```
To run individual test scripts:
```bash
herodo --path src/rhai_tests/text/01_text_indentation.rhai
```
## Test Details
### Text Indentation Test
The text indentation test (`01_text_indentation.rhai`) verifies the following functions:
- `dedent`: Removes common leading whitespace from multiline strings
- Tests basic indentation removal
- Tests mixed indentation handling
- Tests preservation of empty lines
- Tests handling of text without indentation
- Tests single line indentation removal
- `prefix`: Adds a specified prefix to each line of a multiline string
- Tests basic prefix addition
- Tests empty prefix handling
- Tests prefix addition to empty lines
- Tests prefix addition to single line
- Tests non-space prefix addition
- Combination of `dedent` and `prefix` functions
### Filename and Path Normalization Test
The filename and path normalization test (`02_name_path_fix.rhai`) verifies the following functions:
- `name_fix`: Normalizes filenames
- Tests basic name fixing (spaces to underscores, lowercase conversion)
- Tests special character handling
- Tests multiple special character handling
- Tests non-ASCII character removal
- Tests uppercase conversion
- `path_fix`: Applies `name_fix` to the filename portion of a path
- Tests paths ending with `/` (directories)
- Tests single filename handling
- Tests path with filename handling
- Tests relative path handling
- Tests path with special characters in filename
### Text Replacement Test
The text replacement test (`03_text_replacer.rhai`) verifies the following functions:
- `TextReplacer` with simple replacements
- Tests basic replacement
- Tests multiple replacements
- `TextReplacer` with regex replacements
- Tests basic regex replacement
- Tests case-insensitive regex replacement
- `TextReplacer` with file operations
- Tests `replace_file` (read file, apply replacements, return result)
- Tests `replace_file_to` (read file, apply replacements, write to new file)
- Tests `replace_file_in_place` (read file, apply replacements, write back to same file)
### Template Rendering Test
The template rendering test (`04_template_builder.rhai`) verifies the following functions:
- `TemplateBuilder` with file template
- Tests basic template with string variable
- Tests template with multiple variables of different types
- Tests template with array variable
- Tests template with map variable
- `TemplateBuilder` with file operations
- Tests template from file
- Tests `render_to_file` (render template, write to file)
Note: The `template_builder_open` function expects a file path, not a string template. The test creates template files on disk for testing.
## Test Runner
The test runner script (`run_all_tests.rhai`) provides a framework for executing all tests and reporting results. It:
1. Contains simplified versions of each test
2. Runs each test in a try/catch block to handle errors
3. Catches and reports any errors
4. Provides a summary of passed and failed tests
## Adding New Tests
To add a new test:
1. Create a new Rhai script in the `src/rhai_tests/text` directory
2. Add a new test section to the `run_all_tests.rhai` script
3. Update this documentation to include information about the new test
## Best Practices for Writing Tests
When writing tests for the Text module:
1. Use the `assert_true` and `assert_eq` functions to verify expected behavior
2. Print clear messages about what's being tested
3. Clean up any temporary files or directories created during testing
4. Handle errors gracefully
5. Make tests independent of each other
6. Keep tests focused on specific functionality

View File

@ -1,28 +0,0 @@
// Simplified Git Basic Operations Example
let git_tree = git_tree_new("/tmp/git"); // Using /tmp/git as base path
print("--- Git Basic Operations ---");
// print(`Base path: ${git_tree.base_path()}`); // base_path() getter would need to be exposed from Rust
let all_repos = git_tree.list();
print(`Listed ${all_repos.len()} repos.`);
// Find repos starting with "home" (adjust pattern if /tmp/git might contain other "home*" repos)
let found_repos = git_tree.find("home*");
print(`Found ${found_repos.len()} repos matching "home*".`);
for r in found_repos {
print(` - Found: ${r.path()}`);
}
print("Getting/Cloning 'https://github.com/freeflowuniverse/home'...");
let repo = git_tree.get("https://github.com/freeflowuniverse/home");
print(`Repo path: ${repo.path()}`);
print(`Has changes: ${repo.has_changes()}`);
print("Performing pull & reset...");
repo.pull().reset();
print("Pull and reset complete.");
print(`Has changes after pull/reset: ${repo.has_changes()}`);
print("--- Example Finished ---");

View File

@ -1,145 +0,0 @@
// PostgreSQL Authentication Example
//
// This example demonstrates how to use the PostgreSQL client module with authentication:
// - Create a PostgreSQL configuration with authentication
// - Connect to PostgreSQL using the configuration
// - Perform basic operations
//
// Prerequisites:
// - PostgreSQL server must be running
// - You need to know the username and password for the PostgreSQL server
// Helper function to check if PostgreSQL is available
fn is_postgres_available() {
try {
// Try to execute a simple connection
let connect_result = pg_connect();
return connect_result;
} catch(err) {
print(`PostgreSQL connection error: ${err}`);
return false;
}
}
// Main function
fn main() {
print("=== PostgreSQL Authentication Example ===");
// Check if PostgreSQL is available
let postgres_available = is_postgres_available();
if !postgres_available {
print("PostgreSQL server is not available. Please check your connection settings.");
return;
}
print("✓ PostgreSQL server is available");
// Step 1: Create a PostgreSQL configuration with authentication
print("\n1. Creating PostgreSQL configuration with authentication...");
// Replace these values with your actual PostgreSQL credentials
let pg_host = "localhost";
let pg_port = 5432;
let pg_user = "postgres";
let pg_password = "your_password_here"; // Replace with your actual password
let pg_database = "postgres";
// Create a configuration builder
let config = pg_config_builder();
// Configure the connection
config = config.host(pg_host);
config = config.port(pg_port);
config = config.user(pg_user);
config = config.password(pg_password);
config = config.database(pg_database);
// Build the connection string
let connection_string = config.build_connection_string();
print(`✓ Created PostgreSQL configuration with connection string: ${connection_string}`);
// Step 2: Connect to PostgreSQL using the configuration
print("\n2. Connecting to PostgreSQL with authentication...");
try {
let connect_result = pg_connect_with_config(config);
if (connect_result) {
print("✓ Successfully connected to PostgreSQL with authentication");
} else {
print("✗ Failed to connect to PostgreSQL with authentication");
return;
}
} catch(err) {
print(`✗ Error connecting to PostgreSQL: ${err}`);
return;
}
// Step 3: Perform basic operations
print("\n3. Performing basic operations...");
// Create a test table
let table_name = "auth_example_table";
let create_table_query = `
CREATE TABLE IF NOT EXISTS ${table_name} (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
value INTEGER
)
`;
try {
let create_result = pg_execute(create_table_query);
print(`✓ Successfully created table ${table_name}`);
} catch(err) {
print(`✗ Error creating table: ${err}`);
return;
}
// Insert data
let insert_query = `
INSERT INTO ${table_name} (name, value)
VALUES ('test_name', 42)
`;
try {
let insert_result = pg_execute(insert_query);
print(`✓ Successfully inserted data into table ${table_name}`);
} catch(err) {
print(`✗ Error inserting data: ${err}`);
}
// Query data
let select_query = `
SELECT * FROM ${table_name}
`;
try {
let select_result = pg_query(select_query);
print(`✓ Successfully queried data from table ${table_name}`);
print(` Found ${select_result.len()} rows`);
// Display the results
for row in select_result {
print(` Row: id=${row.id}, name=${row.name}, value=${row.value}`);
}
} catch(err) {
print(`✗ Error querying data: ${err}`);
}
// Clean up
let drop_query = `
DROP TABLE IF EXISTS ${table_name}
`;
try {
let drop_result = pg_execute(drop_query);
print(`✓ Successfully dropped table ${table_name}`);
} catch(err) {
print(`✗ Error dropping table: ${err}`);
}
print("\nExample completed successfully!");
}
// Run the main function
main();

View File

@ -1,132 +0,0 @@
// PostgreSQL Basic Operations Example
//
// This example demonstrates how to use the PostgreSQL client module to:
// - Connect to a PostgreSQL database
// - Create a table
// - Insert data
// - Query data
// - Update data
// - Delete data
// - Drop a table
//
// Prerequisites:
// - PostgreSQL server must be running
// - Environment variables should be set for connection details:
// - POSTGRES_HOST: PostgreSQL server host (default: localhost)
// - POSTGRES_PORT: PostgreSQL server port (default: 5432)
// - POSTGRES_USER: PostgreSQL username (default: postgres)
// - POSTGRES_PASSWORD: PostgreSQL password
// - POSTGRES_DB: PostgreSQL database name (default: postgres)
// Helper function to check if PostgreSQL is available
fn is_postgres_available() {
try {
// Try to execute a simple connection
let connect_result = pg_connect();
return connect_result;
} catch(err) {
print(`PostgreSQL connection error: ${err}`);
return false;
}
}
// Main function
fn main() {
print("=== PostgreSQL Basic Operations Example ===");
// Check if PostgreSQL is available
let postgres_available = is_postgres_available();
if !postgres_available {
print("PostgreSQL server is not available. Please check your connection settings.");
return;
}
print("✓ Connected to PostgreSQL server");
// Define table name
let table_name = "rhai_example_users";
// Step 1: Create a table
print("\n1. Creating table...");
let create_table_query = `
CREATE TABLE IF NOT EXISTS ${table_name} (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
age INTEGER,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`;
let create_result = pg_execute(create_table_query);
print(`✓ Table created (result: ${create_result})`);
// Step 2: Insert data
print("\n2. Inserting data...");
let insert_queries = [
`INSERT INTO ${table_name} (name, email, age) VALUES ('Alice', 'alice@example.com', 30)`,
`INSERT INTO ${table_name} (name, email, age) VALUES ('Bob', 'bob@example.com', 25)`,
`INSERT INTO ${table_name} (name, email, age) VALUES ('Charlie', 'charlie@example.com', 35)`
];
for query in insert_queries {
let insert_result = pg_execute(query);
print(`✓ Inserted row (result: ${insert_result})`);
}
// Step 3: Query all data
print("\n3. Querying all data...");
let select_query = `SELECT * FROM ${table_name}`;
let rows = pg_query(select_query);
print(`Found ${rows.len()} rows:`);
for row in rows {
print(` ID: ${row.id}, Name: ${row.name}, Email: ${row.email}, Age: ${row.age}, Created: ${row.created_at}`);
}
// Step 4: Query specific data
print("\n4. Querying specific data...");
let select_one_query = `SELECT * FROM ${table_name} WHERE name = 'Alice'`;
let alice = pg_query_one(select_one_query);
print(`Found Alice:`);
print(` ID: ${alice.id}, Name: ${alice.name}, Email: ${alice.email}, Age: ${alice.age}`);
// Step 5: Update data
print("\n5. Updating data...");
let update_query = `UPDATE ${table_name} SET age = 31 WHERE name = 'Alice'`;
let update_result = pg_execute(update_query);
print(`✓ Updated Alice's age (result: ${update_result})`);
// Verify update
let verify_query = `SELECT * FROM ${table_name} WHERE name = 'Alice'`;
let updated_alice = pg_query_one(verify_query);
print(` Updated Alice: ID: ${updated_alice.id}, Name: ${updated_alice.name}, Age: ${updated_alice.age}`);
// Step 6: Delete data
print("\n6. Deleting data...");
let delete_query = `DELETE FROM ${table_name} WHERE name = 'Bob'`;
let delete_result = pg_execute(delete_query);
print(`✓ Deleted Bob (result: ${delete_result})`);
// Verify deletion
let count_query = `SELECT COUNT(*) as count FROM ${table_name}`;
let count_result = pg_query_one(count_query);
print(` Remaining rows: ${count_result.count}`);
// Step 7: Drop table
print("\n7. Dropping table...");
let drop_query = `DROP TABLE IF EXISTS ${table_name}`;
let drop_result = pg_execute(drop_query);
print(`✓ Dropped table (result: ${drop_result})`);
// Reset connection
print("\n8. Resetting connection...");
let reset_result = pg_reset();
print(`✓ Reset connection (result: ${reset_result})`);
print("\nExample completed successfully!");
}
// Run the main function
main();

View File

@ -1,28 +0,0 @@
print("Caution: Use the kill() function with extreme care as it can terminate running applications.");
print("Terminating essential system processes can make your system unstable or unusable.");
print("");
print("This example attempts to kill processes matching a specific name.");
print("Replace 'process_name_to_kill' with the actual name of a process you intend to stop.");
print("Make sure you know what the process does before attempting to kill it.");
print("");
let target_process_name = "process_name_to_kill"; // <--- CHANGE THIS TO A REAL PROCESS NAME (e.g., "sleep" if you start a sleep process)
print(`Attempting to kill processes matching pattern: '${target_process_name}'...`);
// To safely test this, you might want to start a simple process first, like 'sleep 60 &'.
// Then replace 'process_name_to_kill' with 'sleep'.
// Uncomment the line below to execute the kill command.
// let result_message = kill(target_process_name); // Halts on OS error during kill attempt
// if result_message != "" {
// print(`Kill command sent. Result: ${result_message}`);
// } else {
// print("Kill command finished, but no message returned (check for errors above).");
// }
print("");
print("kill() example finished (command was commented out for safety).");
print("Uncomment the 'kill(...)' line to make it active.");

View File

@ -1,39 +0,0 @@
print("Getting a single process using process_get()...\n");
// process_get expects *exactly one* process matching the pattern.
// If zero or more than one processes match, it will halt script execution.
// Example: Get information for a specific process name.
// Replace "my_critical_service" with a name that is likely to match
// exactly one running process on your system.
// Common examples might be "Dock" or "Finder" on macOS,
// "explorer.exe" on Windows, or a specific service name on Linux.
let target_process_name = "process_name_to_get"; // <--- CHANGE THIS TO A REAL, UNIQUE PROCESS NAME
print(`Attempting to get info for process matching pattern: '${target_process_name}'...`);
// This line will halt if the process is not found OR if multiple processes match the name.
// It will only proceed if exactly one process is found.
let service_proc_info = process_get(target_process_name); // Halts on 0 or >1 matches, or OS error
print(`Successfully found exactly one process matching '${target_process_name}':`);
// Access properties of the ProcessInfo object
print(`- PID: ${service_proc_info.pid}`);
print(`- Name: ${service_proc_info.name}`);
print(`- CPU: ${service_proc_info.cpu}%`);
print(`- Memory: ${service_proc_info.memory}`);
// To demonstrate the halting behavior, you could uncomment one of these:
// Example that will halt if "nonexistent_process_xyz" is not running:
// print("\nAttempting to get a nonexistent process (will halt if not found)...");
// let nonexistent_proc = process_get("nonexistent_process_xyz"); // This line likely halts
// Example that might halt if "sh" matches multiple processes:
// print("\nAttempting to get 'sh' (might halt if multiple shell processes exist)...");
// let sh_proc = process_get("sh"); // This line might halt depending on your system processes
print("\nprocess_get() example finished (if the script did not halt above).");

View File

@ -1,29 +0,0 @@
print("Listing processes using process_list()...\n");
// Example: List all processes (use empty string as pattern)
// print("Listing all running processes (this might be a long list!)...\n");
// let all_processes = process_list("");
// print(`Found ${all_processes.len()} total processes.`);
// // Optional: print details for a few processes
// for i in 0..min(all_processes.len(), 5) {
// let proc = all_processes[i];
// print(`- PID: ${proc.pid}, Name: ${proc.name}, CPU: ${proc.cpu}%, Memory: ${proc.memory}`);
// }
print("Listing processes matching 'bash'...\n");
// Example: List processes matching a pattern
let pattern_to_list = "bash"; // Or another common process like "SystemSettings" or "Finder" on macOS, "explorer.exe" on Windows, "systemd" on Linux
let matching_processes = process_list(pattern_to_list); // Halts on OS error during list attempt
if (matching_processes.len() > 0) {
print(`Found ${matching_processes.len()} processes matching '${pattern_to_list}':`);
for proc in matching_processes {
// Access properties of the ProcessInfo object
print(`- PID: ${proc.pid}, Name: ${proc.name}, CPU: ${proc.cpu}%, Memory: ${proc.memory}`);
}
} else {
print(`No processes found matching '${pattern_to_list}'.`);
}
print("\nprocess_list() example finished.");

View File

@ -1,36 +0,0 @@
print("Running a command using multiple builder options...");
// Example combining log, silent, and ignore_error
// This command will:
// 1. Be logged before execution (.log())
// 2. Have its output suppressed during execution (.silent())
// 3. Exit with a non-zero code (fail)
// 4. NOT halt the script execution because .ignore_error() is used
let result = run("echo 'This is logged and silent stdout'; echo 'This is logged and silent stderr' >&2; exit 5")
.log() // Log the command string
.silent() // Suppress real-time output
.ignore_error() // Prevent script halt on non-zero exit code
.execute(); // Execute the command
print("Command execution finished.");
// Print the captured result
print(`Success: ${result.success}`); // Should be false
print(`Exit Code: ${result.code}`); // Should be 5
print(`Captured Stdout:\n${result.stdout}`); // Should contain the stdout string
// The script continues execution because ignore_error() was used
print("Script continues after handling the failed command.");
// Another example with a successful command, still silent and logged
print("\nRunning another command (successful)...");
let success_result = run("echo 'Success message'").log().silent().execute();
print(`Command finished.`);
print(`Success: ${success_result.success}`); // Should be true
print(`Exit Code: ${success_result.code}`); // Should be 0
print(`Captured Stdout:\n${success_result.stdout}`);
print("\nrun().execute() all options example finished.");

View File

@ -1,18 +0,0 @@
print("Running a basic command using run().do()...");
// Execute a simple command
let result = run("echo Hello from run_basic!").do();
// Print the command result
print(`Command: echo Hello from run_basic!`);
print(`Success: ${result.success}`);
print(`Exit Code: ${result.code}`);
print(`Stdout:\n${result.stdout}`);
print(`Stderr:\n${result.stderr}`);
// Example of a command that might fail (if 'nonexistent_command' doesn't exist)
// This will halt execution by default because ignore_error() is not used.
// print("Running a command that will fail (and should halt)...");
// let fail_result = run("nonexistent_command").do(); // This line will cause the script to halt if the command doesn't exist
print("Basic run() example finished.");

View File

@ -1,29 +0,0 @@
print("Running a command that will fail, but ignoring the error...");
// Run a command that exits with a non-zero code (will fail)
// Using .ignore_error() prevents the script from halting
let result = run("exit 1").ignore_error().do();
print(`Command finished.`);
print(`Success: ${result.success}`); // This should be false
print(`Exit Code: ${result.code}`); // This should be 1
// We can now handle the failure in the script
if (!result.success) {
print("Command failed, but we handled it because ignore_error() was used.");
// Optionally print stderr if needed
// print(`Stderr:\\n${result.stderr}`);
} else {
print("Command unexpectedly succeeded.");
}
print("\nScript continued execution after the potentially failing command.");
// Example of a command that might fail due to OS error (e.g., command not found)
// This *might* still halt depending on how the underlying Rust function handles it,
// as ignore_error() primarily prevents halting on *command* non-zero exit codes.
// let os_error_result = run("nonexistent_command_123").ignore_error().do();
// print(`OS Error Command Success: ${os_error_result.success}`);
// print(`OS Error Command Exit Code: ${os_error_result.code}`);
print("ignore_error() example finished.");

View File

@ -1,13 +0,0 @@
print("Running a command using run().log().do()...");
// The .log() method will print the command string to the console before execution.
// This is useful for debugging or tracing which commands are being run.
let result = run("echo This command is logged").log().do();
print(`Command finished.`);
print(`Success: ${result.success}`);
print(`Exit Code: ${result.code}`);
print(`Stdout:\n${result.stdout}`);
print(`Stderr:\n${result.stderr}`);
print("run().log() example finished.");

View File

@ -1,22 +0,0 @@
print("Running a command using run().silent().do()...\n");
// This command will print to standard output and standard error
// However, because .silent() is used, the output will not appear in the console directly
let result = run("echo 'This should be silent stdout.'; echo 'This should be silent stderr.' >&2; exit 0").silent().do();
// The output is still captured in the CommandResult
print(`Command finished.`);
print(`Success: ${result.success}`);
print(`Exit Code: ${result.code}`);
print(`Captured Stdout:\\n${result.stdout}`);
print(`Captured Stderr:\\n${result.stderr}`);
// Example of a silent command that fails (but won't halt because we only suppress output)
// let fail_result = run("echo 'This is silent failure stderr.' >&2; exit 1").silent().do();
// print(`Failed command finished (silent):`);
// print(`Success: ${fail_result.success}`);
// print(`Exit Code: ${fail_result.code}`);
// print(`Captured Stdout:\\n${fail_result.stdout}`);
// print(`Captured Stderr:\\n${fail_result.stderr}`);
print("\nrun().silent() example finished.");

View File

@ -1,25 +0,0 @@
print("Checking if a command exists in the system PATH using which()...\n");
// Check for a command that likely exists (e.g., 'node' or 'git')
let command_name_exists = "node";
let command_path_exists = which(command_name_exists);
if (command_path_exists != "") {
print(`'${command_name_exists}' executable found at: ${command_path_exists}`);
} else {
print(`'${command_name_exists}' executable not found in PATH.`);
}
print("\nChecking for a command that likely does NOT exist...");
// Check for a command that likely does not exist
let command_name_nonexistent = "nonexistent_command_abc_123";
let command_path_nonexistent = which(command_name_nonexistent);
if (command_path_nonexistent != "") {
print(`'${command_name_nonexistent}' executable found at: ${command_path_nonexistent}`);
} else {
print(`'${command_name_nonexistent}' executable not found in PATH.`);
}
print("\nwhich() example finished.");

View File

@ -1,131 +0,0 @@
// Redis Authentication Example
//
// This example demonstrates how to use the Redis client module with authentication:
// - Create a Redis configuration with authentication
// - Connect to Redis using the configuration
// - Perform basic operations
//
// Prerequisites:
// - Redis server must be running with authentication enabled
// - You need to know the password for the Redis server
// Helper function to check if Redis is available
fn is_redis_available() {
try {
// Try to execute a simple ping
let ping_result = redis_ping();
return ping_result == "PONG";
} catch(err) {
print(`Redis connection error: ${err}`);
return false;
}
}
// Main function
fn main() {
print("=== Redis Authentication Example ===");
// Check if Redis is available
let redis_available = is_redis_available();
if !redis_available {
print("Redis server is not available. Please check your connection settings.");
return;
}
print("✓ Redis server is available");
// Step 1: Create a Redis configuration with authentication
print("\n1. Creating Redis configuration with authentication...");
// Replace these values with your actual Redis credentials
let redis_host = "localhost";
let redis_port = 6379;
let redis_password = "your_password_here"; // Replace with your actual password
// Create a configuration builder
let config = redis_config_builder();
// Configure the connection
config = config.host(redis_host);
config = config.port(redis_port);
config = config.password(redis_password);
// Build the connection URL
let connection_url = config.build_connection_url();
print(`✓ Created Redis configuration with URL: ${connection_url}`);
// Step 2: Connect to Redis using the configuration
print("\n2. Connecting to Redis with authentication...");
try {
let connect_result = redis_connect_with_config(config);
if (connect_result) {
print("✓ Successfully connected to Redis with authentication");
} else {
print("✗ Failed to connect to Redis with authentication");
return;
}
} catch(err) {
print(`✗ Error connecting to Redis: ${err}`);
return;
}
// Step 3: Perform basic operations
print("\n3. Performing basic operations...");
// Set a key
let set_key = "auth_example_key";
let set_value = "This value was set using authentication";
try {
let set_result = redis_set(set_key, set_value);
if (set_result) {
print(`✓ Successfully set key '${set_key}'`);
} else {
print(`✗ Failed to set key '${set_key}'`);
}
} catch(err) {
print(`✗ Error setting key: ${err}`);
}
// Get the key
try {
let get_result = redis_get(set_key);
if (get_result == set_value) {
print(`✓ Successfully retrieved key '${set_key}': '${get_result}'`);
} else {
print(`✗ Retrieved incorrect value for key '${set_key}': '${get_result}'`);
}
} catch(err) {
print(`✗ Error getting key: ${err}`);
}
// Delete the key
try {
let del_result = redis_del(set_key);
if (del_result) {
print(`✓ Successfully deleted key '${set_key}'`);
} else {
print(`✗ Failed to delete key '${set_key}'`);
}
} catch(err) {
print(`✗ Error deleting key: ${err}`);
}
// Verify the key is gone
try {
let verify_result = redis_get(set_key);
if (verify_result == "") {
print(`✓ Verified key '${set_key}' was deleted`);
} else {
print(`✗ Key '${set_key}' still exists with value: '${verify_result}'`);
}
} catch(err) {
print(`✗ Error verifying deletion: ${err}`);
}
print("\nExample completed successfully!");
}
// Run the main function
main();

474
rfs_implementation_plan.md Normal file
View File

@ -0,0 +1,474 @@
# RFS Wrapper Implementation Plan
## Overview
We'll create a Rust wrapper for the RFS (Remote File System) tool that follows the builder pattern, similar to the existing implementations for buildah and nerdctl in the codebase. This wrapper will provide a fluent API for mounting, unmounting, listing mounts, configuring mount options, and packing directories into filesystem layers.
## Module Structure
```
src/virt/rfs/
├── mod.rs # Module exports and common types
├── cmd.rs # Command execution functions
├── mount.rs # Mount operations
├── pack.rs # Packing operations
├── builder.rs # Builder pattern implementation
├── types.rs # Type definitions
└── error.rs # Error handling
```
## Implementation Details
### 1. Error Handling
```rust
// error.rs
#[derive(Debug)]
pub enum RfsError {
CommandFailed(String),
InvalidArgument(String),
MountFailed(String),
UnmountFailed(String),
ListFailed(String),
PackFailed(String),
Other(String),
}
impl std::fmt::Display for RfsError {
// Implementation
}
impl std::error::Error for RfsError {
// Implementation
}
```
### 2. Command Execution
```rust
// cmd.rs
use crate::process::{run_command, CommandResult};
use super::error::RfsError;
pub fn execute_rfs_command(args: &[&str]) -> Result<CommandResult, RfsError> {
// Implementation similar to buildah and nerdctl
}
```
### 3. Types
```rust
// types.rs
#[derive(Debug, Clone)]
pub struct Mount {
pub id: String,
pub source: String,
pub target: String,
pub fs_type: String,
pub options: Vec<String>,
}
#[derive(Debug, Clone)]
pub enum MountType {
Local,
SSH,
S3,
WebDAV,
// Other mount types
}
#[derive(Debug, Clone)]
pub struct StoreSpec {
pub spec_type: String,
pub options: std::collections::HashMap<String, String>,
}
impl StoreSpec {
pub fn new(spec_type: &str) -> Self {
Self {
spec_type: spec_type.to_string(),
options: std::collections::HashMap::new(),
}
}
pub fn with_option(mut self, key: &str, value: &str) -> Self {
self.options.insert(key.to_string(), value.to_string());
self
}
pub fn to_string(&self) -> String {
let mut result = self.spec_type.clone();
if !self.options.is_empty() {
result.push_str(":");
let options: Vec<String> = self.options
.iter()
.map(|(k, v)| format!("{}={}", k, v))
.collect();
result.push_str(&options.join(","));
}
result
}
}
```
### 4. Builder Pattern
```rust
// builder.rs
use std::collections::HashMap;
use super::{Mount, MountType, RfsError, execute_rfs_command, StoreSpec};
#[derive(Clone)]
pub struct RfsBuilder {
source: String,
target: String,
mount_type: MountType,
options: HashMap<String, String>,
mount_id: Option<String>,
debug: bool,
}
impl RfsBuilder {
pub fn new(source: &str, target: &str, mount_type: MountType) -> Self {
Self {
source: source.to_string(),
target: target.to_string(),
mount_type,
options: HashMap::new(),
mount_id: None,
debug: false,
}
}
pub fn with_option(mut self, key: &str, value: &str) -> Self {
self.options.insert(key.to_string(), value.to_string());
self
}
pub fn with_options(mut self, options: HashMap<&str, &str>) -> Self {
for (key, value) in options {
self.options.insert(key.to_string(), value.to_string());
}
self
}
pub fn with_debug(mut self, debug: bool) -> Self {
self.debug = debug;
self
}
pub fn mount(self) -> Result<Mount, RfsError> {
// Implementation
}
pub fn unmount(&self) -> Result<(), RfsError> {
// Implementation
}
// Other methods
}
// Packing functionality
pub struct PackBuilder {
directory: String,
output: String,
store_specs: Vec<StoreSpec>,
debug: bool,
}
impl PackBuilder {
pub fn new(directory: &str, output: &str) -> Self {
Self {
directory: directory.to_string(),
output: output.to_string(),
store_specs: Vec::new(),
debug: false,
}
}
pub fn with_store_spec(mut self, store_spec: StoreSpec) -> Self {
self.store_specs.push(store_spec);
self
}
pub fn with_store_specs(mut self, store_specs: Vec<StoreSpec>) -> Self {
self.store_specs.extend(store_specs);
self
}
pub fn with_debug(mut self, debug: bool) -> Self {
self.debug = debug;
self
}
pub fn pack(self) -> Result<(), RfsError> {
// Implementation for packing a directory into a filesystem layer
let mut args = vec!["pack"];
// Add output file
args.push("-m");
args.push(&self.output);
// Add store specs
if !self.store_specs.is_empty() {
args.push("-s");
let specs: Vec<String> = self.store_specs
.iter()
.map(|spec| spec.to_string())
.collect();
args.push(&specs.join(","));
}
// Add directory
args.push(&self.directory);
// Convert to string slices for the command
let args_str: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
// Execute the command
let result = execute_rfs_command(&args_str)?;
// Check for errors
if !result.success {
return Err(RfsError::PackFailed(result.stderr));
}
Ok(())
}
}
```
### 5. Mount Operations
```rust
// mount.rs
use super::{RfsBuilder, Mount, RfsError, execute_rfs_command};
pub fn list_mounts() -> Result<Vec<Mount>, RfsError> {
// Implementation
}
pub fn unmount_all() -> Result<(), RfsError> {
// Implementation
}
// Other mount-related functions
```
### 6. Pack Operations
```rust
// pack.rs
use super::{PackBuilder, StoreSpec, RfsError, execute_rfs_command};
pub fn pack_directory(directory: &str, output: &str, store_specs: &[StoreSpec]) -> Result<(), RfsError> {
PackBuilder::new(directory, output)
.with_store_specs(store_specs.to_vec())
.pack()
}
// Other pack-related functions
```
### 7. Module Exports
```rust
// mod.rs
mod cmd;
mod error;
mod mount;
mod pack;
mod builder;
mod types;
pub use error::RfsError;
pub use builder::{RfsBuilder, PackBuilder};
pub use types::{Mount, MountType, StoreSpec};
pub use mount::{list_mounts, unmount_all};
pub use pack::pack_directory;
// Re-export the execute_rfs_command function for use in other modules
pub(crate) use cmd::execute_rfs_command;
```
## Usage Examples
### Mounting Example
```rust
use crate::virt::rfs::{RfsBuilder, MountType};
// Create a new RFS mount with builder pattern
let mount = RfsBuilder::new("user@example.com:/remote/path", "/local/mount/point", MountType::SSH)
.with_option("port", "2222")
.with_option("identity_file", "/path/to/key")
.with_debug(true)
.mount()?;
// List all mounts
let mounts = list_mounts()?;
for mount in mounts {
println!("Mount ID: {}, Source: {}, Target: {}", mount.id, mount.source, mount.target);
}
// Unmount
mount.unmount()?;
```
### Packing Example
```rust
use crate::virt::rfs::{PackBuilder, StoreSpec};
// Create store specifications
let store_spec1 = StoreSpec::new("file")
.with_option("path", "/path/to/store");
let store_spec2 = StoreSpec::new("s3")
.with_option("bucket", "my-bucket")
.with_option("region", "us-east-1");
// Pack a directory with builder pattern
let result = PackBuilder::new("/path/to/directory", "output.fl")
.with_store_spec(store_spec1)
.with_store_spec(store_spec2)
.with_debug(true)
.pack()?;
// Or use the convenience function
pack_directory("/path/to/directory", "output.fl", &[store_spec1, store_spec2])?;
```
## Rhai Integration
We'll also need to create a Rhai module to expose the RFS functionality to Rhai scripts:
```rust
// src/rhai/rfs.rs
use rhai::{Engine, EvalAltResult, RegisterFn};
use crate::virt::rfs::{RfsBuilder, MountType, list_mounts, unmount_all, PackBuilder, StoreSpec};
pub fn register(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
// Register RFS functions
engine.register_fn("rfs_mount", rfs_mount);
engine.register_fn("rfs_unmount", rfs_unmount);
engine.register_fn("rfs_list_mounts", rfs_list_mounts);
engine.register_fn("rfs_unmount_all", rfs_unmount_all);
engine.register_fn("rfs_pack", rfs_pack);
Ok(())
}
// Function implementations
fn rfs_mount(source: &str, target: &str, mount_type: &str, options_map: rhai::Map) -> Result<(), Box<EvalAltResult>> {
// Implementation
}
fn rfs_unmount(target: &str) -> Result<(), Box<EvalAltResult>> {
// Implementation
}
fn rfs_list_mounts() -> Result<rhai::Array, Box<EvalAltResult>> {
// Implementation
}
fn rfs_unmount_all() -> Result<(), Box<EvalAltResult>> {
// Implementation
}
fn rfs_pack(directory: &str, output: &str, store_specs: &str) -> Result<(), Box<EvalAltResult>> {
// Implementation
}
```
## Implementation Flow
Here's a diagram showing the flow of the implementation:
```mermaid
classDiagram
class RfsBuilder {
+String source
+String target
+MountType mount_type
+HashMap options
+Option~String~ mount_id
+bool debug
+new(source, target, mount_type)
+with_option(key, value)
+with_options(options)
+with_debug(debug)
+mount()
+unmount()
}
class PackBuilder {
+String directory
+String output
+Vec~StoreSpec~ store_specs
+bool debug
+new(directory, output)
+with_store_spec(store_spec)
+with_store_specs(store_specs)
+with_debug(debug)
+pack()
}
class Mount {
+String id
+String source
+String target
+String fs_type
+Vec~String~ options
}
class MountType {
<<enumeration>>
Local
SSH
S3
WebDAV
}
class StoreSpec {
+String spec_type
+HashMap options
+new(spec_type)
+with_option(key, value)
+to_string()
}
class RfsError {
<<enumeration>>
CommandFailed
InvalidArgument
MountFailed
UnmountFailed
ListFailed
PackFailed
Other
}
RfsBuilder --> Mount : creates
RfsBuilder --> RfsError : may throw
RfsBuilder --> MountType : uses
PackBuilder --> RfsError : may throw
PackBuilder --> StoreSpec : uses
Mount --> RfsError : may throw
```
## Implementation Steps
1. Create the directory structure for the RFS module
2. Implement the error handling module
3. Implement the command execution module
4. Define the types for mounts, mount operations, and store specifications
5. Implement the builder pattern for RFS operations (mount and pack)
6. Implement the mount operations
7. Implement the pack operations
8. Create the module exports
9. Add Rhai integration
10. Write tests for the implementation
11. Update documentation

View File

@ -1,73 +0,0 @@
#!/bin/bash
# run_rhai_tests.sh
# Script to run all Rhai tests in the rhai_tests directory
# Set colors for output
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Create log file
LOG_FILE="run_rhai_tests.log"
> $LOG_FILE # Clear log file if it exists
# Function to log messages to both console and log file
log() {
echo -e "$1" | tee -a $LOG_FILE
}
# Print header
log "${BLUE}=======================================${NC}"
log "${BLUE} Running All Rhai Tests ${NC}"
log "${BLUE}=======================================${NC}"
# Find all test runner scripts
RUNNERS=$(find src/rhai_tests -name "run_all_tests.rhai")
# Initialize counters
TOTAL_MODULES=0
PASSED_MODULES=0
FAILED_MODULES=0
# Run each test runner
for runner in $RUNNERS; do
# Extract module name from path
module=$(echo $runner | cut -d'/' -f3)
log "\n${YELLOW}Running tests for module: ${module}${NC}"
log "${YELLOW}-------------------------------------${NC}"
# Run the test runner
herodo --path $runner | tee -a $LOG_FILE
TEST_RESULT=${PIPESTATUS[0]}
# Check if the test passed
if [ $TEST_RESULT -eq 0 ]; then
log "${GREEN}✓ Module ${module} tests passed${NC}"
PASSED_MODULES=$((PASSED_MODULES + 1))
else
log "${RED}✗ Module ${module} tests failed${NC}"
FAILED_MODULES=$((FAILED_MODULES + 1))
fi
TOTAL_MODULES=$((TOTAL_MODULES + 1))
done
# Print summary
log "\n${BLUE}=======================================${NC}"
log "${BLUE} Test Summary ${NC}"
log "${BLUE}=======================================${NC}"
log "Total modules tested: ${TOTAL_MODULES}"
log "Passed: ${GREEN}${PASSED_MODULES}${NC}"
log "Failed: ${RED}${FAILED_MODULES}${NC}"
# Set exit code based on test results
if [ $FAILED_MODULES -eq 0 ]; then
log "\n${GREEN}All tests passed!${NC}"
exit 0
else
log "\n${RED}Some tests failed!${NC}"
exit 1
fi

View File

@ -0,0 +1,79 @@
//! Example of using the Rhai integration with SAL
//!
//! This example demonstrates how to use the Rhai scripting language
//! with the System Abstraction Layer (SAL) library.
use sal::rhai::{self, Engine};
use std::fs;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create a new Rhai engine
let mut engine = Engine::new();
// Register SAL functions with the engine
rhai::register(&mut engine)?;
// Create a test file
let test_file = "rhai_test_file.txt";
fs::write(test_file, "Hello, Rhai!")?;
// Create a test directory
let test_dir = "rhai_test_dir";
if !fs::metadata(test_dir).is_ok() {
fs::create_dir(test_dir)?;
}
// Run a Rhai script that uses SAL functions
let script = r#"
// Check if files exist
let file_exists = exist("rhai_test_file.txt");
let dir_exists = exist("rhai_test_dir");
// Get file size
let size = file_size("rhai_test_file.txt");
// Create a new directory
let new_dir = "rhai_new_dir";
let mkdir_result = mkdir(new_dir);
// Copy a file
let copy_result = copy("rhai_test_file.txt", "rhai_test_dir/copied_file.txt");
// Find files
let files = find_files(".", "*.txt");
// Return a map with all the results
#{
file_exists: file_exists,
dir_exists: dir_exists,
file_size: size,
mkdir_result: mkdir_result,
copy_result: copy_result,
files: files
}
"#;
// Evaluate the script and get the results
let result = engine.eval::<rhai::Map>(script)?;
// Print the results
println!("Script results:");
println!(" File exists: {}", result.get("file_exists").unwrap().clone().cast::<bool>());
println!(" Directory exists: {}", result.get("dir_exists").unwrap().clone().cast::<bool>());
println!(" File size: {} bytes", result.get("file_size").unwrap().clone().cast::<i64>());
println!(" Mkdir result: {}", result.get("mkdir_result").unwrap().clone().cast::<String>());
println!(" Copy result: {}", result.get("copy_result").unwrap().clone().cast::<String>());
// Print the found files
let files = result.get("files").unwrap().clone().cast::<rhai::Array>();
println!(" Found files:");
for file in files {
println!(" - {}", file.cast::<String>());
}
// Clean up
fs::remove_file(test_file)?;
fs::remove_dir_all(test_dir)?;
fs::remove_dir_all("rhai_new_dir")?;
Ok(())
}

View File

@ -0,0 +1,66 @@
use std::collections::HashMap;
use std::error::Error;
use std::fs::File;
use std::io::Write;
use tempfile::NamedTempFile;
use sal::text::TemplateBuilder;
fn main() -> Result<(), Box<dyn Error>> {
// Create a temporary template file for our examples
let temp_file = NamedTempFile::new()?;
let template_content = "Hello, {{ name }}! Welcome to {{ place }}.\n\
{% if show_greeting %}Glad to have you here!{% endif %}\n\
Your items:\n\
{% for item in items %} - {{ item }}{% if not loop.last %}\n{% endif %}{% endfor %}\n";
std::fs::write(temp_file.path(), template_content)?;
println!("Created temporary template at: {}", temp_file.path().display());
// Example 1: Simple variable replacement
println!("\n--- Example 1: Simple variable replacement ---");
let mut builder = TemplateBuilder::open(temp_file.path())?;
builder = builder
.add_var("name", "John")
.add_var("place", "Rust")
.add_var("show_greeting", true)
.add_var("items", vec!["apple", "banana", "cherry"]);
let result = builder.render()?;
println!("Rendered template:\n{}", result);
// Example 2: Using a HashMap for variables
println!("\n--- Example 2: Using a HashMap for variables ---");
let mut vars = HashMap::new();
vars.insert("name", "Alice");
vars.insert("place", "Template World");
let mut builder = TemplateBuilder::open(temp_file.path())?;
builder = builder
.add_vars(vars)
.add_var("show_greeting", false)
.add_var("items", vec!["laptop", "phone", "tablet"]);
let result = builder.render()?;
println!("Rendered template with HashMap:\n{}", result);
// Example 3: Rendering to a file
println!("\n--- Example 3: Rendering to a file ---");
let output_file = NamedTempFile::new()?;
let mut builder = TemplateBuilder::open(temp_file.path())?;
builder = builder
.add_var("name", "Bob")
.add_var("place", "File Output")
.add_var("show_greeting", true)
.add_var("items", vec!["document", "spreadsheet", "presentation"]);
builder.render_to_file(output_file.path())?;
println!("Template rendered to file: {}", output_file.path().display());
// Read the output file to verify
let output_content = std::fs::read_to_string(output_file.path())?;
println!("Content of the rendered file:\n{}", output_content);
Ok(())
}

View File

@ -0,0 +1,93 @@
use std::error::Error;
use std::fs::File;
use std::io::Write;
use tempfile::NamedTempFile;
use sal::text::TextReplacer;
fn main() -> Result<(), Box<dyn Error>> {
// Create a temporary file for our examples
let mut temp_file = NamedTempFile::new()?;
writeln!(temp_file, "This is a foo bar example with FOO and foo occurrences.")?;
println!("Created temporary file at: {}", temp_file.path().display());
// Example 1: Simple regex replacement
println!("\n--- Example 1: Simple regex replacement ---");
let replacer = TextReplacer::builder()
.pattern(r"\bfoo\b")
.replacement("replacement")
.regex(true)
.add_replacement()?
.build()?;
let result = replacer.replace_file(temp_file.path())?;
println!("After regex replacement: {}", result);
// Example 2: Multiple replacements in one pass
println!("\n--- Example 2: Multiple replacements in one pass ---");
let replacer = TextReplacer::builder()
.pattern("foo")
.replacement("AAA")
.add_replacement()?
.pattern("bar")
.replacement("BBB")
.add_replacement()?
.build()?;
// Write new content to the temp file
writeln!(temp_file.as_file_mut(), "foo bar foo baz")?;
temp_file.as_file_mut().flush()?;
let result = replacer.replace_file(temp_file.path())?;
println!("After multiple replacements: {}", result);
// Example 3: Case-insensitive replacement
println!("\n--- Example 3: Case-insensitive replacement ---");
let replacer = TextReplacer::builder()
.pattern("foo")
.replacement("case-insensitive")
.regex(true)
.case_insensitive(true)
.add_replacement()?
.build()?;
// Write new content to the temp file
writeln!(temp_file.as_file_mut(), "FOO foo Foo fOo")?;
temp_file.as_file_mut().flush()?;
let result = replacer.replace_file(temp_file.path())?;
println!("After case-insensitive replacement: {}", result);
// Example 4: File operations
println!("\n--- Example 4: File operations ---");
let output_file = NamedTempFile::new()?;
let replacer = TextReplacer::builder()
.pattern("example")
.replacement("EXAMPLE")
.add_replacement()?
.build()?;
// Write new content to the temp file
writeln!(temp_file.as_file_mut(), "This is an example text file.")?;
temp_file.as_file_mut().flush()?;
// Replace and write to a new file
replacer.replace_file_to(temp_file.path(), output_file.path())?;
// Read the output file to verify
let output_content = std::fs::read_to_string(output_file.path())?;
println!("Content written to new file: {}", output_content);
// Example 5: Replace in-place
println!("\n--- Example 5: Replace in-place ---");
// Replace in the same file
replacer.replace_file_in_place(temp_file.path())?;
// Read the file to verify
let updated_content = std::fs::read_to_string(temp_file.path())?;
println!("Content after in-place replacement: {}", updated_content);
Ok(())
}

View File

@ -197,41 +197,40 @@ impl GitTree {
/// * `Ok(Vec<String>)` - A vector of paths to matching repositories
/// * `Err(GitError)` - If no matching repositories are found,
/// or if multiple repositories match a non-wildcard pattern
pub fn find(&self, pattern: &str) -> Result<Vec<GitRepo>, GitError> {
let repo_names = self.list()?; // list() already ensures these are git repo names
if repo_names.is_empty() {
return Ok(Vec::new()); // If no repos listed, find results in an empty list
pub fn find(&self, pattern: &str) -> Result<Vec<String>, GitError> {
// Get all repos
let repos = self.list()?;
if repos.is_empty() {
return Err(GitError::NoRepositoriesFound);
}
let mut matched_repos: Vec<GitRepo> = Vec::new();
if pattern == "*" {
for name in repo_names {
let full_path = format!("{}/{}", self.base_path, name);
matched_repos.push(GitRepo::new(full_path));
}
} else if pattern.ends_with('*') {
let prefix = &pattern[0..pattern.len()-1];
for name in repo_names {
if name.starts_with(prefix) {
let full_path = format!("{}/{}", self.base_path, name);
matched_repos.push(GitRepo::new(full_path));
}
// Check if pattern ends with wildcard
if pattern.ends_with('*') {
let search_pattern = &pattern[0..pattern.len()-1]; // Remove the *
let matching: Vec<String> = repos.iter()
.filter(|repo| repo.contains(search_pattern))
.cloned()
.collect();
if matching.is_empty() {
return Err(GitError::RepositoryNotFound(pattern.to_string()));
}
Ok(matching)
} else {
// Exact match for the name
for name in repo_names {
if name == pattern {
let full_path = format!("{}/{}", self.base_path, name);
matched_repos.push(GitRepo::new(full_path));
// `find` returns all exact matches. If names aren't unique (unlikely from `list`),
// it could return more than one. For an exact name, typically one is expected.
}
// No wildcard, need to find exactly one match
let matching: Vec<String> = repos.iter()
.filter(|repo| repo.contains(pattern))
.cloned()
.collect();
match matching.len() {
0 => Err(GitError::RepositoryNotFound(pattern.to_string())),
1 => Ok(matching),
_ => Err(GitError::MultipleRepositoriesFound(pattern.to_string(), matching.len())),
}
}
Ok(matched_repos)
}
/// Gets one or more GitRepo objects based on a path pattern or URL.
@ -282,9 +281,14 @@ impl GitTree {
Err(GitError::GitCommandFailed(format!("Git clone error: {}", error)))
}
} else {
// It's a path pattern, find matching repositories using the updated self.find()
// which now directly returns Result<Vec<GitRepo>, GitError>.
let repos = self.find(path_or_url)?;
// It's a path pattern, find matching repositories
let repo_paths = self.find(path_or_url)?;
// Convert paths to GitRepo objects
let repos: Vec<GitRepo> = repo_paths.into_iter()
.map(GitRepo::new)
.collect();
Ok(repos)
}
}

View File

@ -0,0 +1,49 @@
//! Rhai engine creation and registration for Git module
//!
//! This module provides functions to create and configure a Rhai engine with Git functionality.
use rhai::{Engine, EvalAltResult};
use crate::git::{GitTree, GitRepo};
use super::wrapper::{
create_git_tree, list_repositories, find_repositories, get_repositories,
get_repo_path, has_changes, pull_repository, reset_repository,
commit_changes, push_changes, parse_git_url_wrapper
};
/// Create a Rhai engine with Git functionality
pub fn create_rhai_engine() -> Engine {
let mut engine = Engine::new();
// Register the Git module
register_git_module(&mut engine).expect("Failed to register Git module");
engine
}
/// Register Git module functions with the Rhai engine
pub fn register_git_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
// Register GitTree constructor
engine.register_fn("new_git_tree", create_git_tree);
// Register GitTree methods
engine.register_fn("list_repositories", list_repositories);
engine.register_fn("find_repositories", find_repositories);
engine.register_fn("get_repositories", get_repositories);
// Register GitRepo methods
engine.register_fn("get_repo_path", get_repo_path);
engine.register_fn("has_changes", has_changes);
engine.register_fn("pull_repository", pull_repository);
engine.register_fn("reset_repository", reset_repository);
engine.register_fn("commit_changes", commit_changes);
engine.register_fn("push_changes", push_changes);
// Register utility functions
engine.register_fn("parse_git_url", parse_git_url_wrapper);
// Register types
engine.register_type_with_name::<GitTree>("GitTree");
engine.register_type_with_name::<GitRepo>("GitRepo");
Ok(())
}

View File

@ -0,0 +1,132 @@
use std::collections::HashMap;
use rhai::{Dynamic, Map, Array};
/// Local wrapper trait for sal::rhai::ToRhai to avoid orphan rule violations
pub trait ToRhai {
/// Convert to a Rhai Dynamic value
fn to_rhai(&self) -> Dynamic;
}
// Implementation of ToRhai for Dynamic
impl ToRhai for Dynamic {
fn to_rhai(&self) -> Dynamic {
self.clone()
}
}
/// Generic trait for wrapping Rust functions to be used with Rhai
pub trait RhaiWrapper {
/// Wrap a function that takes ownership of self
fn wrap_consuming<F, R>(self, f: F) -> Dynamic
where
Self: Sized + Clone,
F: FnOnce(Self) -> R,
R: ToRhai;
/// Wrap a function that takes a mutable reference to self
fn wrap_mut<F, R>(&mut self, f: F) -> Dynamic
where
Self: Sized + Clone,
F: FnOnce(&mut Self) -> R,
R: ToRhai;
/// Wrap a function that takes an immutable reference to self
fn wrap<F, R>(&self, f: F) -> Dynamic
where
Self: Sized + Clone,
F: FnOnce(&Self) -> R,
R: ToRhai;
}
/// Implementation of RhaiWrapper for any type
impl<T> RhaiWrapper for T {
fn wrap_consuming<F, R>(self, f: F) -> Dynamic
where
Self: Sized + Clone,
F: FnOnce(Self) -> R,
R: ToRhai,
{
let result = f(self);
result.to_rhai()
}
fn wrap_mut<F, R>(&mut self, f: F) -> Dynamic
where
Self: Sized + Clone,
F: FnOnce(&mut Self) -> R,
R: ToRhai,
{
let result = f(self);
result.to_rhai()
}
fn wrap<F, R>(&self, f: F) -> Dynamic
where
Self: Sized + Clone,
F: FnOnce(&Self) -> R,
R: ToRhai,
{
let result = f(self);
result.to_rhai()
}
}
/// Convert a Rhai Map to a Rust HashMap
pub fn map_to_hashmap(map: &Map) -> HashMap<String, String> {
let mut result = HashMap::new();
for (key, value) in map.iter() {
let k = key.clone().to_string();
let v = value.clone().to_string();
if !k.is_empty() && !v.is_empty() {
result.insert(k, v);
}
}
result
}
/// Convert a HashMap<String, String> to a Rhai Map
pub fn hashmap_to_map(map: &HashMap<String, String>) -> Map {
let mut result = Map::new();
for (key, value) in map.iter() {
result.insert(key.clone().into(), Dynamic::from(value.clone()));
}
result
}
/// Convert a Rhai Array to a Vec of strings
pub fn array_to_vec_string(array: &Array) -> Vec<String> {
array.iter()
.filter_map(|item| {
let s = item.clone().to_string();
if !s.is_empty() { Some(s) } else { None }
})
.collect()
}
/// Helper function to convert Dynamic to Option<String>
pub fn dynamic_to_string_option(value: &Dynamic) -> Option<String> {
if value.is_string() {
Some(value.clone().to_string())
} else {
None
}
}
/// Helper function to convert Dynamic to Option<u32>
pub fn dynamic_to_u32_option(value: &Dynamic) -> Option<u32> {
if value.is_int() {
Some(value.as_int().unwrap() as u32)
} else {
None
}
}
/// Helper function to convert Dynamic to Option<&str> with lifetime management
pub fn dynamic_to_str_option<'a>(value: &Dynamic, storage: &'a mut String) -> Option<&'a str> {
if value.is_string() {
*storage = value.clone().to_string();
Some(storage.as_str())
} else {
None
}
}

4
src/git/rhai/src/mod.rs Normal file
View File

@ -0,0 +1,4 @@
pub mod engine;
pub mod wrapper;
pub mod generic_wrapper;
pub mod module;

106
src/git/rhai/src/module.rs Normal file
View File

@ -0,0 +1,106 @@
use rhai::{Dynamic, Engine, EvalAltResult, Module, NativeCallContext, FuncRegistration, FnNamespace, Identifier};
use std::sync::Arc;
use crate::git::{GitTree, GitRepo};
use super::wrapper;
/// Creates and returns a Rhai module with Git functionality.
///
/// This module exposes Git-related functions to Rhai scripts.
pub fn create_git_module() -> Module {
let mut module = Module::new();
// Register GitTree functions
FuncRegistration::new("create_git_tree")
.with_namespace(FnNamespace::Global)
.with_params_info(["base_path: &str", "GitTree"])
.with_comments(["/// Create a new GitTree with the specified base path",
"/// - base_path: Base directory path to search for git repositories",
"/// Returns a GitTree instance"])
.set_into_module(&mut module, wrapper::create_git_tree);
FuncRegistration::new("list_repositories")
.with_namespace(FnNamespace::Global)
.with_params_info(["tree: &mut GitTree", "Array"])
.with_comments(["/// List all git repositories under the base path",
"/// - tree: GitTree instance",
"/// Returns an array of GitRepo instances"])
.set_into_module(&mut module, wrapper::list_repositories);
FuncRegistration::new("find_repositories")
.with_namespace(FnNamespace::Global)
.with_params_info(["tree: &mut GitTree", "pattern: &str", "Array"])
.with_comments(["/// Find repositories matching a pattern",
"/// - tree: GitTree instance",
"/// - pattern: Pattern to match repository names",
"/// Returns an array of matching GitRepo instances"])
.set_into_module(&mut module, wrapper::find_repositories);
FuncRegistration::new("get_repositories")
.with_namespace(FnNamespace::Global)
.with_params_info(["tree: &mut GitTree", "path_or_url: &str", "Array"])
.with_comments(["/// Get repositories based on a path pattern or URL",
"/// - tree: GitTree instance",
"/// - path_or_url: Path pattern or URL to match",
"/// Returns an array of matching GitRepo instances"])
.set_into_module(&mut module, wrapper::get_repositories);
// Register GitRepo functions
FuncRegistration::new("get_repo_path")
.with_namespace(FnNamespace::Global)
.with_params_info(["repo: &mut GitRepo", "String"])
.with_comments(["/// Get the path of the repository",
"/// - repo: GitRepo instance",
"/// Returns the path of the repository as a string"])
.set_into_module(&mut module, wrapper::get_repo_path);
FuncRegistration::new("has_changes")
.with_namespace(FnNamespace::Global)
.with_params_info(["repo: &mut GitRepo", "bool"])
.with_comments(["/// Check if the repository has uncommitted changes",
"/// - repo: GitRepo instance",
"/// Returns true if there are uncommitted changes, false otherwise"])
.set_into_module(&mut module, wrapper::has_changes);
FuncRegistration::new("pull_repository")
.with_namespace(FnNamespace::Global)
.with_params_info(["repo: &mut GitRepo", "String"])
.with_comments(["/// Pull the latest changes from the remote repository",
"/// - repo: GitRepo instance",
"/// Returns a success message or throws an error"])
.set_into_module(&mut module, wrapper::pull_repository);
FuncRegistration::new("reset_repository")
.with_namespace(FnNamespace::Global)
.with_params_info(["repo: &mut GitRepo", "String"])
.with_comments(["/// Reset any local changes in the repository",
"/// - repo: GitRepo instance",
"/// Returns a success message or throws an error"])
.set_into_module(&mut module, wrapper::reset_repository);
FuncRegistration::new("commit_changes")
.with_namespace(FnNamespace::Global)
.with_params_info(["repo: &mut GitRepo", "message: &str", "String"])
.with_comments(["/// Commit changes in the repository",
"/// - repo: GitRepo instance",
"/// - message: Commit message",
"/// Returns a success message or throws an error"])
.set_into_module(&mut module, wrapper::commit_changes);
FuncRegistration::new("push_changes")
.with_namespace(FnNamespace::Global)
.with_params_info(["repo: &mut GitRepo", "String"])
.with_comments(["/// Push changes to the remote repository",
"/// - repo: GitRepo instance",
"/// Returns a success message or throws an error"])
.set_into_module(&mut module, wrapper::push_changes);
FuncRegistration::new("parse_git_url")
.with_namespace(FnNamespace::Global)
.with_params_info(["url: &str", "Array"])
.with_comments(["/// Parse a git URL to extract the server, account, and repository name",
"/// - url: Git URL to parse",
"/// Returns an array with [server, account, repo]"])
.set_into_module(&mut module, wrapper::parse_git_url_wrapper);
module
}

172
src/git/rhai/src/wrapper.rs Normal file
View File

@ -0,0 +1,172 @@
//! Rhai wrappers for Git module functions
//!
//! This module provides Rhai wrappers for the functions in the Git module.
use rhai::{EvalAltResult, Array, Dynamic, Position};
use crate::git::{GitTree, GitRepo, GitError, parse_git_url};
use super::generic_wrapper::{RhaiWrapper, ToRhai};
// Helper function to convert GitError to EvalAltResult
fn git_error_to_rhai_error<T>(result: Result<T, GitError>) -> Result<T, Box<EvalAltResult>> {
result.map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime(
format!("Git error: {}", e).into(),
Position::NONE
))
})
}
//
// GitTree Functions
//
/// Create a new GitTree with the specified base path
pub fn create_git_tree(base_path: &str) -> Result<GitTree, Box<EvalAltResult>> {
git_error_to_rhai_error(GitTree::new(base_path))
}
/// List all git repositories under the base path
pub fn list_repositories(tree: &mut GitTree) -> Result<Array, Box<EvalAltResult>> {
git_error_to_rhai_error(tree.list()).map(|repos| {
let array: Array = repos.into_iter()
.map(|repo| Dynamic::from(repo))
.collect();
array
})
}
/// Find repositories matching a pattern
pub fn find_repositories(tree: &mut GitTree, pattern: &str) -> Result<Array, Box<EvalAltResult>> {
git_error_to_rhai_error(tree.find(pattern)).map(|repos| {
let array: Array = repos.into_iter()
.map(|repo| Dynamic::from(repo))
.collect();
array
})
}
/// Get repositories based on a path pattern or URL
pub fn get_repositories(tree: &mut GitTree, path_or_url: &str) -> Result<Array, Box<EvalAltResult>> {
git_error_to_rhai_error(tree.get(path_or_url)).map(|repos| {
let array: Array = repos.into_iter()
.map(Dynamic::from)
.collect();
array
})
}
//
// GitRepo Functions
//
/// Get the path of the repository
pub fn get_repo_path(repo: &mut GitRepo) -> String {
repo.path().to_string()
}
/// Check if the repository has uncommitted changes
pub fn has_changes(repo: &mut GitRepo) -> Result<bool, Box<EvalAltResult>> {
git_error_to_rhai_error(repo.has_changes())
}
/// Pull the latest changes from the remote repository
pub fn pull_repository(repo: &mut GitRepo) -> Result<String, Box<EvalAltResult>> {
match repo.pull() {
Ok(_) => Ok(String::from("Successfully pulled changes")),
Err(err) => Err(Box::new(EvalAltResult::ErrorRuntime(
format!("Error pulling changes: {}", err).into(),
Position::NONE
)))
}
}
/// Reset any local changes in the repository
pub fn reset_repository(repo: &mut GitRepo) -> Result<String, Box<EvalAltResult>> {
match repo.reset() {
Ok(_) => Ok(String::from("Successfully reset repository")),
Err(err) => Err(Box::new(EvalAltResult::ErrorRuntime(
format!("Error resetting repository: {}", err).into(),
Position::NONE
)))
}
}
/// Commit changes in the repository
pub fn commit_changes(repo: &mut GitRepo, message: &str) -> Result<String, Box<EvalAltResult>> {
match repo.commit(message) {
Ok(_) => Ok(String::from("Successfully committed changes")),
Err(err) => Err(Box::new(EvalAltResult::ErrorRuntime(
format!("Error committing changes: {}", err).into(),
Position::NONE
)))
}
}
/// Push changes to the remote repository
pub fn push_changes(repo: &mut GitRepo) -> Result<String, Box<EvalAltResult>> {
match repo.push() {
Ok(_) => Ok(String::from("Successfully pushed changes")),
Err(err) => Err(Box::new(EvalAltResult::ErrorRuntime(
format!("Error pushing changes: {}", err).into(),
Position::NONE
)))
}
}
/// Parse a git URL to extract the server, account, and repository name
pub fn parse_git_url_wrapper(url: &str) -> Array {
let (server, account, repo) = parse_git_url(url);
let mut array = Array::new();
array.push(Dynamic::from(server));
array.push(Dynamic::from(account));
array.push(Dynamic::from(repo));
array
}
// Implementation of ToRhai for GitTree
impl ToRhai for GitTree {
fn to_rhai(&self) -> Dynamic {
Dynamic::from(self.clone())
}
}
// Implementation of ToRhai for GitRepo
impl ToRhai for GitRepo {
fn to_rhai(&self) -> Dynamic {
Dynamic::from(self.clone())
}
}
// Implementation of ToRhai for String
impl ToRhai for String {
fn to_rhai(&self) -> Dynamic {
Dynamic::from(self.clone())
}
}
// Implementation of ToRhai for bool
impl ToRhai for bool {
fn to_rhai(&self) -> Dynamic {
Dynamic::from(*self)
}
}
// Implementation of ToRhai for Vec<String>
impl ToRhai for Vec<String> {
fn to_rhai(&self) -> Dynamic {
let array: Array = self.iter()
.map(|s| Dynamic::from(s.clone()))
.collect();
Dynamic::from(array)
}
}
// Implementation of ToRhai for Option<GitRepo>
impl<T: ToRhai> ToRhai for Option<T> {
fn to_rhai(&self) -> Dynamic {
match self {
Some(value) => value.to_rhai(),
None => Dynamic::UNIT
}
}
}

View File

@ -0,0 +1,212 @@
# Git Interface Redesign Plan
## Current Understanding
The current git interface consists of standalone functions like `git_clone`, `git_list`, `git_update`, etc. We want to replace this with an object-oriented interface using a builder pattern that allows for method chaining.
## New Interface Design
### Core Components
```mermaid
classDiagram
class GitTree {
+String base_path
+new(base_path: &str) Result<GitTree, GitError>
+list() Result<Vec<String>, GitError>
+find(pattern: &str) Result<Vec<String>, GitError>
+get(path_pattern: &str) Result<Vec<GitRepo>, GitError>
}
class GitRepo {
+String path
+pull() Result<GitRepo, GitError>
+reset() Result<GitRepo, GitError>
+push() Result<GitRepo, GitError>
+commit(message: &str) Result<GitRepo, GitError>
+has_changes() Result<bool, GitError>
}
GitTree --> GitRepo : creates
```
### Implementation Details
1. **GitTree Class**:
- Constructor takes a base path parameter that specifies where all git repositories will be located
- Methods for listing and finding repositories
- A `get()` method that returns one or more GitRepo objects based on a path pattern
- The `get()` method can also accept a URL (git or http format) and will clone the repository if it doesn't exist
2. **GitRepo Class**:
- Represents a single git repository
- Methods for common git operations: pull, reset, push, commit
- Each method returns a Result containing either the GitRepo object (for chaining) or an error
- If an operation fails, subsequent operations in the chain are skipped
3. **Error Handling**:
- Each method returns a Result type for immediate error handling
- Errors are propagated up the call chain
- The existing GitError enum will be reused
## Implementation Plan
### 1. Create the GitTree and GitRepo Structs in git.rs
```rust
pub struct GitTree {
base_path: String,
}
pub struct GitRepo {
path: String,
}
```
### 2. Implement the GitTree Methods
```rust
impl GitTree {
pub fn new(base_path: &str) -> Result<Self, GitError> {
// Validate the base path
// Create the directory if it doesn't exist
Ok(GitTree {
base_path: base_path.to_string(),
})
}
pub fn list(&self) -> Result<Vec<String>, GitError> {
// List all git repositories under the base path
}
pub fn find(&self, pattern: &str) -> Result<Vec<String>, GitError> {
// Find repositories matching the pattern
}
pub fn get(&self, path_pattern: &str) -> Result<Vec<GitRepo>, GitError> {
// Find repositories matching the pattern
// Return GitRepo objects for each match
}
}
```
### 3. Implement the GitRepo Methods
```rust
impl GitRepo {
pub fn pull(&self) -> Result<Self, GitError> {
// Pull the latest changes
// Return self for chaining or an error
}
pub fn reset(&self) -> Result<Self, GitError> {
// Reset any local changes
// Return self for chaining or an error
}
pub fn push(&self) -> Result<Self, GitError> {
// Push changes to the remote
// Return self for chaining or an error
}
pub fn commit(&self, message: &str) -> Result<Self, GitError> {
// Commit changes with the given message
// Return self for chaining or an error
}
pub fn has_changes(&self) -> Result<bool, GitError> {
// Check if the repository has uncommitted changes
}
}
```
### 4. Update the Rhai Wrappers in rhai/git.rs
```rust
// Register the GitTree and GitRepo types with Rhai
pub fn register_git_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
// Register the GitTree type
engine.register_type::<GitTree>();
engine.register_fn("new", git_tree_new);
// Register GitTree methods
engine.register_fn("list", git_tree_list);
engine.register_fn("find", git_tree_find);
engine.register_fn("get", git_tree_get);
// Register GitRepo methods
engine.register_type::<GitRepo>();
engine.register_fn("pull", git_repo_pull);
engine.register_fn("reset", git_repo_reset);
engine.register_fn("push", git_repo_push);
engine.register_fn("commit", git_repo_commit);
engine.register_fn("has_changes", git_repo_has_changes);
Ok(())
}
```
### 5. Update Tests and Examples
- Update the test files to use the new interface
- Create new examples demonstrating the builder pattern and method chaining
## Usage Examples
### Example 1: Basic Repository Operations
```rhai
// Create a new GitTree object
let git_tree = new("/home/user/code");
// List all repositories
let repos = git_tree.list();
print(`Found ${repos.len()} repositories`);
// Find repositories matching a pattern
let matching = git_tree.find("my-project*");
print(`Found ${matching.len()} matching repositories`);
// Get a repository and perform operations
let repo = git_tree.get("my-project")[0];
let result = repo.pull().reset().commit("Update files").push();
```
### Example 2: Working with Multiple Repositories
```rhai
// Create a new GitTree object
let git_tree = new("/home/user/code");
// Get all repositories matching a pattern
let repos = git_tree.get("project*");
print(`Found ${repos.len()} matching repositories`);
// Perform operations on all repositories
for repo in repos {
let result = repo.pull();
if result.is_ok() {
print(`Successfully pulled ${repo.path}`);
} else {
print(`Failed to pull ${repo.path}: ${result.error}`);
}
}
```
### Example 3: Cloning a Repository
```rhai
// Create a new GitTree object
let git_tree = new("/home/user/code");
// Clone a repository by URL
let repos = git_tree.get("https://github.com/username/repo.git");
let repo = repos[0];
print(`Repository cloned to: ${repo.path}`);
```
## Migration Strategy
1. Implement the new interface in git.rs and rhai/git.rs
2. Update all tests and examples to use the new interface
3. Remove the old standalone functions

View File

@ -36,15 +36,14 @@ pub enum Error {
pub type Result<T> = std::result::Result<T, Error>;
// Re-export modules
pub mod cmd;
pub mod process;
pub mod git;
pub mod os;
pub mod postgresclient;
pub mod process;
pub mod redisclient;
pub mod rhai;
pub mod text;
pub mod virt;
pub mod rhai;
pub mod cmd;
// Version information
/// Returns the version of the SAL library

View File

@ -1,9 +1,9 @@
use std::error::Error;
use std::fmt;
use std::fs;
use std::io;
use std::path::Path;
use std::process::Command;
use std::path::Path;
use std::fs;
use std::fmt;
use std::error::Error;
use std::io;
// Define a custom error type for download operations
#[derive(Debug)]
@ -26,17 +26,11 @@ pub enum DownloadError {
impl fmt::Display for DownloadError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DownloadError::CreateDirectoryFailed(e) => {
write!(f, "Error creating directories: {}", e)
}
DownloadError::CreateDirectoryFailed(e) => write!(f, "Error creating directories: {}", e),
DownloadError::CurlExecutionFailed(e) => write!(f, "Error executing curl: {}", e),
DownloadError::DownloadFailed(url) => write!(f, "Error downloading url: {}", url),
DownloadError::FileMetadataError(e) => write!(f, "Error getting file metadata: {}", e),
DownloadError::FileTooSmall(size, min) => write!(
f,
"Error: Downloaded file is too small ({}KB < {}KB)",
size, min
),
DownloadError::FileTooSmall(size, min) => write!(f, "Error: Downloaded file is too small ({}KB < {}KB)", size, min),
DownloadError::RemoveFileFailed(e) => write!(f, "Error removing file: {}", e),
DownloadError::ExtractionFailed(e) => write!(f, "Error extracting archive: {}", e),
DownloadError::CommandExecutionFailed(e) => write!(f, "Error executing command: {}", e),
@ -80,18 +74,12 @@ impl Error for DownloadError {
*
* # Examples
*
* ```no_run
* use sal::os::download;
* ```
* // Download a file with no minimum size requirement
* let path = download("https://example.com/file.txt", "/tmp/", 0)?;
*
* fn main() -> Result<(), Box<dyn std::error::Error>> {
* // Download a file with no minimum size requirement
* let path = download("https://example.com/file.txt", "/tmp/", 0)?;
*
* // Download a file with minimum size requirement of 100KB
* let path = download("https://example.com/file.zip", "/tmp/", 100)?;
*
* Ok(())
* }
* // Download a file with minimum size requirement of 100KB
* let path = download("https://example.com/file.zip", "/tmp/", 100)?;
* ```
*
* # Notes
@ -103,41 +91,30 @@ pub fn download(url: &str, dest: &str, min_size_kb: i64) -> Result<String, Downl
// Create parent directories if they don't exist
let dest_path = Path::new(dest);
fs::create_dir_all(dest_path).map_err(DownloadError::CreateDirectoryFailed)?;
// Extract filename from URL
let filename = match url.split('/').last() {
Some(name) => name,
None => {
return Err(DownloadError::InvalidUrl(
"cannot extract filename".to_string(),
))
}
None => return Err(DownloadError::InvalidUrl("cannot extract filename".to_string()))
};
// Create a full path for the downloaded file
let file_path = format!("{}/{}", dest.trim_end_matches('/'), filename);
// Create a temporary path for downloading
let temp_path = format!("{}.download", file_path);
// Use curl to download the file with progress bar
println!("Downloading {} to {}", url, file_path);
let output = Command::new("curl")
.args(&[
"--progress-bar",
"--location",
"--fail",
"--output",
&temp_path,
url,
])
.args(&["--progress-bar", "--location", "--fail", "--output", &temp_path, url])
.status()
.map_err(DownloadError::CurlExecutionFailed)?;
if !output.success() {
return Err(DownloadError::DownloadFailed(url.to_string()));
}
// Show file size after download
match fs::metadata(&temp_path) {
Ok(metadata) => {
@ -145,20 +122,14 @@ pub fn download(url: &str, dest: &str, min_size_kb: i64) -> Result<String, Downl
let size_kb = size_bytes / 1024;
let size_mb = size_kb / 1024;
if size_mb > 1 {
println!(
"Download complete! File size: {:.2} MB",
size_bytes as f64 / (1024.0 * 1024.0)
);
println!("Download complete! File size: {:.2} MB", size_bytes as f64 / (1024.0 * 1024.0));
} else {
println!(
"Download complete! File size: {:.2} KB",
size_bytes as f64 / 1024.0
);
println!("Download complete! File size: {:.2} KB", size_bytes as f64 / 1024.0);
}
}
},
Err(_) => println!("Download complete!"),
}
// Check file size if minimum size is specified
if min_size_kb > 0 {
let metadata = fs::metadata(&temp_path).map_err(DownloadError::FileMetadataError)?;
@ -168,59 +139,57 @@ pub fn download(url: &str, dest: &str, min_size_kb: i64) -> Result<String, Downl
return Err(DownloadError::FileTooSmall(size_kb, min_size_kb));
}
}
// Check if it's a compressed file that needs extraction
let lower_url = url.to_lowercase();
let is_archive = lower_url.ends_with(".tar.gz")
|| lower_url.ends_with(".tgz")
|| lower_url.ends_with(".tar")
|| lower_url.ends_with(".zip");
let is_archive = lower_url.ends_with(".tar.gz") ||
lower_url.ends_with(".tgz") ||
lower_url.ends_with(".tar") ||
lower_url.ends_with(".zip");
if is_archive {
// Extract the file using the appropriate command with progress indication
println!("Extracting {} to {}", temp_path, dest);
let output = if lower_url.ends_with(".zip") {
Command::new("unzip")
.args(&["-o", &temp_path, "-d", dest]) // Removed -q for verbosity
.args(&["-o", &temp_path, "-d", dest]) // Removed -q for verbosity
.status()
} else if lower_url.ends_with(".tar.gz") || lower_url.ends_with(".tgz") {
Command::new("tar")
.args(&["-xzvf", &temp_path, "-C", dest]) // Added v for verbosity
.args(&["-xzvf", &temp_path, "-C", dest]) // Added v for verbosity
.status()
} else {
Command::new("tar")
.args(&["-xvf", &temp_path, "-C", dest]) // Added v for verbosity
.args(&["-xvf", &temp_path, "-C", dest]) // Added v for verbosity
.status()
};
match output {
Ok(status) => {
if !status.success() {
return Err(DownloadError::ExtractionFailed(
"Error extracting archive".to_string(),
));
return Err(DownloadError::ExtractionFailed("Error extracting archive".to_string()));
}
}
},
Err(e) => return Err(DownloadError::CommandExecutionFailed(e)),
}
// Show number of extracted files
match fs::read_dir(dest) {
Ok(entries) => {
let count = entries.count();
println!("Extraction complete! Extracted {} files/directories", count);
}
},
Err(_) => println!("Extraction complete!"),
}
// Remove the temporary file
fs::remove_file(&temp_path).map_err(DownloadError::RemoveFileFailed)?;
Ok(dest.to_string())
} else {
// Just rename the temporary file to the final destination
fs::rename(&temp_path, &file_path).map_err(|e| DownloadError::CreateDirectoryFailed(e))?;
Ok(file_path)
}
}
@ -241,18 +210,12 @@ pub fn download(url: &str, dest: &str, min_size_kb: i64) -> Result<String, Downl
*
* # Examples
*
* ```no_run
* use sal::os::download_file;
* ```
* // Download a file with no minimum size requirement
* let path = download_file("https://example.com/file.txt", "/tmp/file.txt", 0)?;
*
* fn main() -> Result<(), Box<dyn std::error::Error>> {
* // Download a file with no minimum size requirement
* let path = download_file("https://example.com/file.txt", "/tmp/file.txt", 0)?;
*
* // Download a file with minimum size requirement of 100KB
* let path = download_file("https://example.com/file.zip", "/tmp/file.zip", 100)?;
*
* Ok(())
* }
* // Download a file with minimum size requirement of 100KB
* let path = download_file("https://example.com/file.zip", "/tmp/file.zip", 100)?;
* ```
*/
pub fn download_file(url: &str, dest: &str, min_size_kb: i64) -> Result<String, DownloadError> {
@ -261,28 +224,21 @@ pub fn download_file(url: &str, dest: &str, min_size_kb: i64) -> Result<String,
if let Some(parent) = dest_path.parent() {
fs::create_dir_all(parent).map_err(DownloadError::CreateDirectoryFailed)?;
}
// Create a temporary path for downloading
let temp_path = format!("{}.download", dest);
// Use curl to download the file with progress bar
println!("Downloading {} to {}", url, dest);
let output = Command::new("curl")
.args(&[
"--progress-bar",
"--location",
"--fail",
"--output",
&temp_path,
url,
])
.args(&["--progress-bar", "--location", "--fail", "--output", &temp_path, url])
.status()
.map_err(DownloadError::CurlExecutionFailed)?;
if !output.success() {
return Err(DownloadError::DownloadFailed(url.to_string()));
}
// Show file size after download
match fs::metadata(&temp_path) {
Ok(metadata) => {
@ -290,20 +246,14 @@ pub fn download_file(url: &str, dest: &str, min_size_kb: i64) -> Result<String,
let size_kb = size_bytes / 1024;
let size_mb = size_kb / 1024;
if size_mb > 1 {
println!(
"Download complete! File size: {:.2} MB",
size_bytes as f64 / (1024.0 * 1024.0)
);
println!("Download complete! File size: {:.2} MB", size_bytes as f64 / (1024.0 * 1024.0));
} else {
println!(
"Download complete! File size: {:.2} KB",
size_bytes as f64 / 1024.0
);
println!("Download complete! File size: {:.2} KB", size_bytes as f64 / 1024.0);
}
}
},
Err(_) => println!("Download complete!"),
}
// Check file size if minimum size is specified
if min_size_kb > 0 {
let metadata = fs::metadata(&temp_path).map_err(DownloadError::FileMetadataError)?;
@ -313,10 +263,10 @@ pub fn download_file(url: &str, dest: &str, min_size_kb: i64) -> Result<String,
return Err(DownloadError::FileTooSmall(size_kb, min_size_kb));
}
}
// Rename the temporary file to the final destination
fs::rename(&temp_path, dest).map_err(|e| DownloadError::CreateDirectoryFailed(e))?;
Ok(dest.to_string())
}
@ -334,38 +284,27 @@ pub fn download_file(url: &str, dest: &str, min_size_kb: i64) -> Result<String,
*
* # Examples
*
* ```no_run
* use sal::os::chmod_exec;
*
* fn main() -> Result<(), Box<dyn std::error::Error>> {
* // Make a file executable
* chmod_exec("/path/to/file")?;
* Ok(())
* }
* ```
* // Make a file executable
* chmod_exec("/path/to/file")?;
* ```
*/
pub fn chmod_exec(path: &str) -> Result<String, DownloadError> {
let path_obj = Path::new(path);
// Check if the path exists and is a file
if !path_obj.exists() {
return Err(DownloadError::NotAFile(format!(
"Path does not exist: {}",
path
)));
return Err(DownloadError::NotAFile(format!("Path does not exist: {}", path)));
}
if !path_obj.is_file() {
return Err(DownloadError::NotAFile(format!(
"Path is not a file: {}",
path
)));
return Err(DownloadError::NotAFile(format!("Path is not a file: {}", path)));
}
// Get current permissions
let metadata = fs::metadata(path).map_err(DownloadError::FileMetadataError)?;
let mut permissions = metadata.permissions();
// Set executable bit for user, group, and others
#[cfg(unix)]
{
@ -375,55 +314,47 @@ pub fn chmod_exec(path: &str) -> Result<String, DownloadError> {
let new_mode = mode | 0o111;
permissions.set_mode(new_mode);
}
#[cfg(not(unix))]
{
// On non-Unix platforms, we can't set executable bit directly
// Just return success with a warning
return Ok(format!(
"Made {} executable (note: non-Unix platform, may not be fully supported)",
path
));
return Ok(format!("Made {} executable (note: non-Unix platform, may not be fully supported)", path));
}
// Apply the new permissions
fs::set_permissions(path, permissions).map_err(|e| {
fs::set_permissions(path, permissions).map_err(|e|
DownloadError::CommandExecutionFailed(io::Error::new(
io::ErrorKind::Other,
format!("Failed to set executable permissions: {}", e),
format!("Failed to set executable permissions: {}", e)
))
})?;
)?;
Ok(format!("Made {} executable", path))
}
/**
* Download a file and install it if it's a supported package format.
*
*
* # Arguments
*
*
* * `url` - The URL to download from
* * `min_size_kb` - Minimum required file size in KB (0 for no minimum)
*
*
* # Returns
*
*
* * `Ok(String)` - The path where the file was saved or extracted
* * `Err(DownloadError)` - An error if the download or installation failed
*
*
* # Examples
*
* ```no_run
* use sal::os::download_install;
*
* fn main() -> Result<(), Box<dyn std::error::Error>> {
* // Download and install a .deb package
* let result = download_install("https://example.com/package.deb", 100)?;
* Ok(())
* }
*
* ```
*
* // Download and install a .deb package
* let result = download_install("https://example.com/package.deb", 100)?;
* ```
*
* # Notes
*
*
* Currently only supports .deb packages on Debian-based systems.
* For other file types, it behaves the same as the download function.
*/
@ -431,23 +362,19 @@ pub fn download_install(url: &str, min_size_kb: i64) -> Result<String, DownloadE
// Extract filename from URL
let filename = match url.split('/').last() {
Some(name) => name,
None => {
return Err(DownloadError::InvalidUrl(
"cannot extract filename".to_string(),
))
}
None => return Err(DownloadError::InvalidUrl("cannot extract filename".to_string()))
};
// Create a proper destination path
let dest_path = format!("/tmp/{}", filename);
// Check if it's a compressed file that needs extraction
let lower_url = url.to_lowercase();
let is_archive = lower_url.ends_with(".tar.gz")
|| lower_url.ends_with(".tgz")
|| lower_url.ends_with(".tar")
|| lower_url.ends_with(".zip");
let is_archive = lower_url.ends_with(".tar.gz") ||
lower_url.ends_with(".tgz") ||
lower_url.ends_with(".tar") ||
lower_url.ends_with(".zip");
let download_result = if is_archive {
// For archives, use the directory-based download function
download(url, "/tmp", min_size_kb)?
@ -455,13 +382,13 @@ pub fn download_install(url: &str, min_size_kb: i64) -> Result<String, DownloadE
// For regular files, use the file-specific download function
download_file(url, &dest_path, min_size_kb)?
};
// Check if the downloaded result is a file
let path = Path::new(&dest_path);
if !path.is_file() {
return Ok(download_result); // Not a file, might be an extracted directory
}
// Check if it's a .deb package
if dest_path.to_lowercase().ends_with(".deb") {
// Check if we're on a Debian-based platform
@ -469,28 +396,26 @@ pub fn download_install(url: &str, min_size_kb: i64) -> Result<String, DownloadE
.arg("-c")
.arg("command -v dpkg > /dev/null && command -v apt > /dev/null || test -f /etc/debian_version")
.status();
match platform_check {
Ok(status) => {
if !status.success() {
return Err(DownloadError::PlatformNotSupported(
"Cannot install .deb package: not on a Debian-based system".to_string(),
"Cannot install .deb package: not on a Debian-based system".to_string()
));
}
}
Err(_) => {
return Err(DownloadError::PlatformNotSupported(
"Failed to check system compatibility for .deb installation".to_string(),
))
}
},
Err(_) => return Err(DownloadError::PlatformNotSupported(
"Failed to check system compatibility for .deb installation".to_string()
)),
}
// Install the .deb package non-interactively
println!("Installing package: {}", dest_path);
let install_result = Command::new("sudo")
.args(&["dpkg", "--install", &dest_path])
.status();
match install_result {
Ok(status) => {
if !status.success() {
@ -499,24 +424,24 @@ pub fn download_install(url: &str, min_size_kb: i64) -> Result<String, DownloadE
let fix_deps = Command::new("sudo")
.args(&["apt-get", "install", "-f", "-y"])
.status();
if let Ok(fix_status) = fix_deps {
if !fix_status.success() {
return Err(DownloadError::InstallationFailed(
"Failed to resolve package dependencies".to_string(),
"Failed to resolve package dependencies".to_string()
));
}
} else {
return Err(DownloadError::InstallationFailed(
"Failed to resolve package dependencies".to_string(),
"Failed to resolve package dependencies".to_string()
));
}
}
println!("Package installation completed successfully");
}
},
Err(e) => return Err(DownloadError::CommandExecutionFailed(e)),
}
}
Ok(download_result)
}

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
use crate::process::CommandResult;
use std::process::Command;
use crate::process::CommandResult;
/// Error type for package management operations
#[derive(Debug)]
@ -45,7 +45,7 @@ impl Platform {
if std::path::Path::new("/usr/bin/sw_vers").exists() {
return Platform::MacOS;
}
// Check for Ubuntu
if std::path::Path::new("/etc/lsb-release").exists() {
// Read the file to confirm it's Ubuntu
@ -55,12 +55,12 @@ impl Platform {
}
}
}
Platform::Unknown
}
}
// Thread-local storage for debug flag
/// Thread-local storage for debug flag
thread_local! {
static DEBUG: std::cell::RefCell<bool> = std::cell::RefCell::new(false);
}
@ -74,73 +74,70 @@ pub fn set_thread_local_debug(debug: bool) {
/// Get the debug flag for the current thread
pub fn thread_local_debug() -> bool {
DEBUG.with(|cell| *cell.borrow())
DEBUG.with(|cell| {
*cell.borrow()
})
}
/// Execute a package management command and return the result
pub fn execute_package_command(args: &[&str], debug: bool) -> Result<CommandResult, PackageError> {
// Save the current debug flag
let previous_debug = thread_local_debug();
// Set the thread-local debug flag
set_thread_local_debug(debug);
if debug {
println!("Executing command: {}", args.join(" "));
}
let output = Command::new(args[0]).args(&args[1..]).output();
let output = Command::new(args[0])
.args(&args[1..])
.output();
// Restore the previous debug flag
set_thread_local_debug(previous_debug);
match output {
Ok(output) => {
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
let result = CommandResult {
stdout,
stderr,
success: output.status.success(),
code: output.status.code().unwrap_or(-1),
};
// Always output stdout/stderr when debug is true
if debug {
if !result.stdout.is_empty() {
println!("Command stdout: {}", result.stdout);
}
if !result.stderr.is_empty() {
println!("Command stderr: {}", result.stderr);
}
if result.success {
println!("Command succeeded with code {}", result.code);
} else {
println!("Command failed with code {}", result.code);
}
}
if result.success {
Ok(result)
} else {
// If command failed and debug is false, output stderr
if !debug {
println!(
"Command failed with code {}: {}",
result.code,
result.stderr.trim()
);
println!("Command failed with code {}: {}", result.code, result.stderr.trim());
}
Err(PackageError::CommandFailed(format!(
"Command failed with code {}: {}",
result.code,
result.stderr.trim()
)))
Err(PackageError::CommandFailed(format!("Command failed with code {}: {}",
result.code, result.stderr.trim())))
}
}
},
Err(e) => {
// Always output error information
println!("Command execution failed: {}", e);
@ -153,22 +150,22 @@ pub fn execute_package_command(args: &[&str], debug: bool) -> Result<CommandResu
pub trait PackageManager {
/// Install a package
fn install(&self, package: &str) -> Result<CommandResult, PackageError>;
/// Remove a package
fn remove(&self, package: &str) -> Result<CommandResult, PackageError>;
/// Update package lists
fn update(&self) -> Result<CommandResult, PackageError>;
/// Upgrade installed packages
fn upgrade(&self) -> Result<CommandResult, PackageError>;
/// List installed packages
fn list_installed(&self) -> Result<Vec<String>, PackageError>;
/// Search for packages
fn search(&self, query: &str) -> Result<Vec<String>, PackageError>;
/// Check if a package is installed
fn is_installed(&self, package: &str) -> Result<bool, PackageError>;
}
@ -188,31 +185,27 @@ impl AptPackageManager {
impl PackageManager for AptPackageManager {
fn install(&self, package: &str) -> Result<CommandResult, PackageError> {
// Use -y to make it non-interactive and --quiet to reduce output
execute_package_command(
&["apt-get", "install", "-y", "--quiet", package],
self.debug,
)
execute_package_command(&["apt-get", "install", "-y", "--quiet", package], self.debug)
}
fn remove(&self, package: &str) -> Result<CommandResult, PackageError> {
// Use -y to make it non-interactive and --quiet to reduce output
execute_package_command(&["apt-get", "remove", "-y", "--quiet", package], self.debug)
}
fn update(&self) -> Result<CommandResult, PackageError> {
// Use -y to make it non-interactive and --quiet to reduce output
execute_package_command(&["apt-get", "update", "-y", "--quiet"], self.debug)
}
fn upgrade(&self) -> Result<CommandResult, PackageError> {
// Use -y to make it non-interactive and --quiet to reduce output
execute_package_command(&["apt-get", "upgrade", "-y", "--quiet"], self.debug)
}
fn list_installed(&self) -> Result<Vec<String>, PackageError> {
let result = execute_package_command(&["dpkg", "--get-selections"], self.debug)?;
let packages = result
.stdout
let packages = result.stdout
.lines()
.filter_map(|line| {
let parts: Vec<&str> = line.split_whitespace().collect();
@ -225,11 +218,10 @@ impl PackageManager for AptPackageManager {
.collect();
Ok(packages)
}
fn search(&self, query: &str) -> Result<Vec<String>, PackageError> {
let result = execute_package_command(&["apt-cache", "search", query], self.debug)?;
let packages = result
.stdout
let packages = result.stdout
.lines()
.map(|line| {
let parts: Vec<&str> = line.split_whitespace().collect();
@ -243,7 +235,7 @@ impl PackageManager for AptPackageManager {
.collect();
Ok(packages)
}
fn is_installed(&self, package: &str) -> Result<bool, PackageError> {
let result = execute_package_command(&["dpkg", "-s", package], self.debug);
match result {
@ -270,44 +262,42 @@ impl PackageManager for BrewPackageManager {
// Use --quiet to reduce output
execute_package_command(&["brew", "install", "--quiet", package], self.debug)
}
fn remove(&self, package: &str) -> Result<CommandResult, PackageError> {
// Use --quiet to reduce output
execute_package_command(&["brew", "uninstall", "--quiet", package], self.debug)
}
fn update(&self) -> Result<CommandResult, PackageError> {
// Use --quiet to reduce output
execute_package_command(&["brew", "update", "--quiet"], self.debug)
}
fn upgrade(&self) -> Result<CommandResult, PackageError> {
// Use --quiet to reduce output
execute_package_command(&["brew", "upgrade", "--quiet"], self.debug)
}
fn list_installed(&self) -> Result<Vec<String>, PackageError> {
let result = execute_package_command(&["brew", "list", "--formula"], self.debug)?;
let packages = result
.stdout
let packages = result.stdout
.lines()
.map(|line| line.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
Ok(packages)
}
fn search(&self, query: &str) -> Result<Vec<String>, PackageError> {
let result = execute_package_command(&["brew", "search", query], self.debug)?;
let packages = result
.stdout
let packages = result.stdout
.lines()
.map(|line| line.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
Ok(packages)
}
fn is_installed(&self, package: &str) -> Result<bool, PackageError> {
let result = execute_package_command(&["brew", "list", package], self.debug);
match result {
@ -332,70 +322,68 @@ impl PackHero {
debug: false,
}
}
/// Set the debug mode
pub fn set_debug(&mut self, debug: bool) -> &mut Self {
self.debug = debug;
self
}
/// Get the debug mode
pub fn debug(&self) -> bool {
self.debug
}
/// Get the detected platform
pub fn platform(&self) -> Platform {
self.platform
}
/// Get a package manager for the current platform
fn get_package_manager(&self) -> Result<Box<dyn PackageManager>, PackageError> {
match self.platform {
Platform::Ubuntu => Ok(Box::new(AptPackageManager::new(self.debug))),
Platform::MacOS => Ok(Box::new(BrewPackageManager::new(self.debug))),
Platform::Unknown => Err(PackageError::UnsupportedPlatform(
"Unsupported platform".to_string(),
)),
Platform::Unknown => Err(PackageError::UnsupportedPlatform("Unsupported platform".to_string())),
}
}
/// Install a package
pub fn install(&self, package: &str) -> Result<CommandResult, PackageError> {
let pm = self.get_package_manager()?;
pm.install(package)
}
/// Remove a package
pub fn remove(&self, package: &str) -> Result<CommandResult, PackageError> {
let pm = self.get_package_manager()?;
pm.remove(package)
}
/// Update package lists
pub fn update(&self) -> Result<CommandResult, PackageError> {
let pm = self.get_package_manager()?;
pm.update()
}
/// Upgrade installed packages
pub fn upgrade(&self) -> Result<CommandResult, PackageError> {
let pm = self.get_package_manager()?;
pm.upgrade()
}
/// List installed packages
pub fn list_installed(&self) -> Result<Vec<String>, PackageError> {
let pm = self.get_package_manager()?;
pm.list_installed()
}
/// Search for packages
pub fn search(&self, query: &str) -> Result<Vec<String>, PackageError> {
let pm = self.get_package_manager()?;
pm.search(query)
}
/// Check if a package is installed
pub fn is_installed(&self, package: &str) -> Result<bool, PackageError> {
let pm = self.get_package_manager()?;
@ -406,49 +394,47 @@ impl PackHero {
#[cfg(test)]
mod tests {
// Import the std::process::Command directly for some test-specific commands
use super::*;
use std::process::Command as StdCommand;
use super::*;
use std::sync::{Arc, Mutex};
#[test]
fn test_platform_detection() {
// This test will return different results depending on the platform it's run on
let platform = Platform::detect();
println!("Detected platform: {:?}", platform);
// Just ensure it doesn't panic
assert!(true);
}
#[test]
fn test_debug_flag() {
// Test setting and getting the debug flag
set_thread_local_debug(true);
assert_eq!(thread_local_debug(), true);
set_thread_local_debug(false);
assert_eq!(thread_local_debug(), false);
}
#[test]
fn test_package_error_display() {
// Test the Display implementation for PackageError
let err1 = PackageError::CommandFailed("command failed".to_string());
assert_eq!(err1.to_string(), "Command failed: command failed");
let err2 = PackageError::UnsupportedPlatform("test platform".to_string());
assert_eq!(err2.to_string(), "Unsupported platform: test platform");
let err3 = PackageError::Other("other error".to_string());
assert_eq!(err3.to_string(), "Error: other error");
// We can't easily test CommandExecutionFailed because std::io::Error doesn't implement PartialEq
}
// Mock package manager for testing
struct MockPackageManager {
// debug field is kept for consistency with real package managers
#[allow(dead_code)]
debug: bool,
install_called: Arc<Mutex<bool>>,
remove_called: Arc<Mutex<bool>>,
@ -460,7 +446,7 @@ mod tests {
// Control what the mock returns
should_succeed: bool,
}
impl MockPackageManager {
fn new(debug: bool, should_succeed: bool) -> Self {
Self {
@ -476,7 +462,7 @@ mod tests {
}
}
}
impl PackageManager for MockPackageManager {
fn install(&self, package: &str) -> Result<CommandResult, PackageError> {
*self.install_called.lock().unwrap() = true;
@ -488,12 +474,10 @@ mod tests {
code: 0,
})
} else {
Err(PackageError::CommandFailed(
"Mock install failed".to_string(),
))
Err(PackageError::CommandFailed("Mock install failed".to_string()))
}
}
fn remove(&self, package: &str) -> Result<CommandResult, PackageError> {
*self.remove_called.lock().unwrap() = true;
if self.should_succeed {
@ -504,12 +488,10 @@ mod tests {
code: 0,
})
} else {
Err(PackageError::CommandFailed(
"Mock remove failed".to_string(),
))
Err(PackageError::CommandFailed("Mock remove failed".to_string()))
}
}
fn update(&self) -> Result<CommandResult, PackageError> {
*self.update_called.lock().unwrap() = true;
if self.should_succeed {
@ -520,12 +502,10 @@ mod tests {
code: 0,
})
} else {
Err(PackageError::CommandFailed(
"Mock update failed".to_string(),
))
Err(PackageError::CommandFailed("Mock update failed".to_string()))
}
}
fn upgrade(&self) -> Result<CommandResult, PackageError> {
*self.upgrade_called.lock().unwrap() = true;
if self.should_succeed {
@ -536,57 +516,45 @@ mod tests {
code: 0,
})
} else {
Err(PackageError::CommandFailed(
"Mock upgrade failed".to_string(),
))
Err(PackageError::CommandFailed("Mock upgrade failed".to_string()))
}
}
fn list_installed(&self) -> Result<Vec<String>, PackageError> {
*self.list_installed_called.lock().unwrap() = true;
if self.should_succeed {
Ok(vec!["package1".to_string(), "package2".to_string()])
} else {
Err(PackageError::CommandFailed(
"Mock list_installed failed".to_string(),
))
Err(PackageError::CommandFailed("Mock list_installed failed".to_string()))
}
}
fn search(&self, query: &str) -> Result<Vec<String>, PackageError> {
*self.search_called.lock().unwrap() = true;
if self.should_succeed {
Ok(vec![
format!("result1-{}", query),
format!("result2-{}", query),
])
Ok(vec![format!("result1-{}", query), format!("result2-{}", query)])
} else {
Err(PackageError::CommandFailed(
"Mock search failed".to_string(),
))
Err(PackageError::CommandFailed("Mock search failed".to_string()))
}
}
fn is_installed(&self, package: &str) -> Result<bool, PackageError> {
*self.is_installed_called.lock().unwrap() = true;
if self.should_succeed {
Ok(package == "installed-package")
} else {
Err(PackageError::CommandFailed(
"Mock is_installed failed".to_string(),
))
Err(PackageError::CommandFailed("Mock is_installed failed".to_string()))
}
}
}
// Custom PackHero for testing with a mock package manager
struct TestPackHero {
platform: Platform,
#[allow(dead_code)]
debug: bool,
mock_manager: MockPackageManager,
}
impl TestPackHero {
fn new(platform: Platform, debug: bool, should_succeed: bool) -> Self {
Self {
@ -595,152 +563,144 @@ mod tests {
mock_manager: MockPackageManager::new(debug, should_succeed),
}
}
fn get_package_manager(&self) -> Result<&dyn PackageManager, PackageError> {
match self.platform {
Platform::Ubuntu | Platform::MacOS => Ok(&self.mock_manager),
Platform::Unknown => Err(PackageError::UnsupportedPlatform(
"Unsupported platform".to_string(),
)),
Platform::Unknown => Err(PackageError::UnsupportedPlatform("Unsupported platform".to_string())),
}
}
fn install(&self, package: &str) -> Result<CommandResult, PackageError> {
let pm = self.get_package_manager()?;
pm.install(package)
}
fn remove(&self, package: &str) -> Result<CommandResult, PackageError> {
let pm = self.get_package_manager()?;
pm.remove(package)
}
fn update(&self) -> Result<CommandResult, PackageError> {
let pm = self.get_package_manager()?;
pm.update()
}
fn upgrade(&self) -> Result<CommandResult, PackageError> {
let pm = self.get_package_manager()?;
pm.upgrade()
}
fn list_installed(&self) -> Result<Vec<String>, PackageError> {
let pm = self.get_package_manager()?;
pm.list_installed()
}
fn search(&self, query: &str) -> Result<Vec<String>, PackageError> {
let pm = self.get_package_manager()?;
pm.search(query)
}
fn is_installed(&self, package: &str) -> Result<bool, PackageError> {
let pm = self.get_package_manager()?;
pm.is_installed(package)
}
}
#[test]
fn test_packhero_with_mock_success() {
// Test PackHero with a mock package manager that succeeds
let hero = TestPackHero::new(Platform::Ubuntu, false, true);
// Test install
let result = hero.install("test-package");
assert!(result.is_ok());
assert!(*hero.mock_manager.install_called.lock().unwrap());
// Test remove
let result = hero.remove("test-package");
assert!(result.is_ok());
assert!(*hero.mock_manager.remove_called.lock().unwrap());
// Test update
let result = hero.update();
assert!(result.is_ok());
assert!(*hero.mock_manager.update_called.lock().unwrap());
// Test upgrade
let result = hero.upgrade();
assert!(result.is_ok());
assert!(*hero.mock_manager.upgrade_called.lock().unwrap());
// Test list_installed
let result = hero.list_installed();
assert!(result.is_ok());
assert_eq!(
result.unwrap(),
vec!["package1".to_string(), "package2".to_string()]
);
assert_eq!(result.unwrap(), vec!["package1".to_string(), "package2".to_string()]);
assert!(*hero.mock_manager.list_installed_called.lock().unwrap());
// Test search
let result = hero.search("query");
assert!(result.is_ok());
assert_eq!(
result.unwrap(),
vec!["result1-query".to_string(), "result2-query".to_string()]
);
assert_eq!(result.unwrap(), vec!["result1-query".to_string(), "result2-query".to_string()]);
assert!(*hero.mock_manager.search_called.lock().unwrap());
// Test is_installed
let result = hero.is_installed("installed-package");
assert!(result.is_ok());
assert!(result.unwrap());
assert!(*hero.mock_manager.is_installed_called.lock().unwrap());
let result = hero.is_installed("not-installed-package");
assert!(result.is_ok());
assert!(!result.unwrap());
}
#[test]
fn test_packhero_with_mock_failure() {
// Test PackHero with a mock package manager that fails
let hero = TestPackHero::new(Platform::Ubuntu, false, false);
// Test install
let result = hero.install("test-package");
assert!(result.is_err());
assert!(*hero.mock_manager.install_called.lock().unwrap());
// Test remove
let result = hero.remove("test-package");
assert!(result.is_err());
assert!(*hero.mock_manager.remove_called.lock().unwrap());
// Test update
let result = hero.update();
assert!(result.is_err());
assert!(*hero.mock_manager.update_called.lock().unwrap());
// Test upgrade
let result = hero.upgrade();
assert!(result.is_err());
assert!(*hero.mock_manager.upgrade_called.lock().unwrap());
// Test list_installed
let result = hero.list_installed();
assert!(result.is_err());
assert!(*hero.mock_manager.list_installed_called.lock().unwrap());
// Test search
let result = hero.search("query");
assert!(result.is_err());
assert!(*hero.mock_manager.search_called.lock().unwrap());
// Test is_installed
let result = hero.is_installed("installed-package");
assert!(result.is_err());
assert!(*hero.mock_manager.is_installed_called.lock().unwrap());
}
#[test]
fn test_packhero_unsupported_platform() {
// Test PackHero with an unsupported platform
let hero = TestPackHero::new(Platform::Unknown, false, true);
// All operations should fail with UnsupportedPlatform error
let result = hero.install("test-package");
assert!(result.is_err());
@ -748,14 +708,14 @@ mod tests {
Err(PackageError::UnsupportedPlatform(_)) => (),
_ => panic!("Expected UnsupportedPlatform error"),
}
let result = hero.remove("test-package");
assert!(result.is_err());
match result {
Err(PackageError::UnsupportedPlatform(_)) => (),
_ => panic!("Expected UnsupportedPlatform error"),
}
let result = hero.update();
assert!(result.is_err());
match result {
@ -763,7 +723,7 @@ mod tests {
_ => panic!("Expected UnsupportedPlatform error"),
}
}
// Real-world tests that actually install and remove packages on Ubuntu
// These tests will only run on Ubuntu and will be skipped on other platforms
#[test]
@ -771,22 +731,19 @@ mod tests {
// Check if we're on Ubuntu
let platform = Platform::detect();
if platform != Platform::Ubuntu {
println!(
"Skipping real package operations test on non-Ubuntu platform: {:?}",
platform
);
println!("Skipping real package operations test on non-Ubuntu platform: {:?}", platform);
return;
}
println!("Running real package operations test on Ubuntu");
// Create a PackHero instance with debug enabled
let mut hero = PackHero::new();
hero.set_debug(true);
// Test package to install/remove
let test_package = "wget";
// First, check if the package is already installed
let is_installed_before = match hero.is_installed(test_package) {
Ok(result) => result,
@ -795,12 +752,9 @@ mod tests {
return;
}
};
println!(
"Package {} is installed before test: {}",
test_package, is_installed_before
);
println!("Package {} is installed before test: {}", test_package, is_installed_before);
// If the package is already installed, we'll remove it first
if is_installed_before {
println!("Removing existing package {} before test", test_package);
@ -811,7 +765,7 @@ mod tests {
return;
}
}
// Verify it was removed
match hero.is_installed(test_package) {
Ok(is_installed) => {
@ -821,17 +775,14 @@ mod tests {
} else {
println!("Verified package {} was removed", test_package);
}
}
},
Err(e) => {
println!(
"Error checking if package is installed after removal: {}",
e
);
println!("Error checking if package is installed after removal: {}", e);
return;
}
}
}
// Now install the package
println!("Installing package {}", test_package);
match hero.install(test_package) {
@ -841,7 +792,7 @@ mod tests {
return;
}
}
// Verify it was installed
match hero.is_installed(test_package) {
Ok(is_installed) => {
@ -851,50 +802,41 @@ mod tests {
} else {
println!("Verified package {} was installed", test_package);
}
}
},
Err(e) => {
println!(
"Error checking if package is installed after installation: {}",
e
);
println!("Error checking if package is installed after installation: {}", e);
return;
}
}
// Test the search functionality
println!("Searching for packages with 'wget'");
match hero.search("wget") {
Ok(results) => {
println!("Search results: {:?}", results);
assert!(
results.iter().any(|r| r.contains("wget")),
"Search results should contain wget"
);
}
assert!(results.iter().any(|r| r.contains("wget")), "Search results should contain wget");
},
Err(e) => {
println!("Error searching for packages: {}", e);
return;
}
}
// Test listing installed packages
println!("Listing installed packages");
match hero.list_installed() {
Ok(packages) => {
println!("Found {} installed packages", packages.len());
// Check if our test package is in the list
assert!(
packages.iter().any(|p| p == test_package),
"Installed packages list should contain {}",
test_package
);
}
assert!(packages.iter().any(|p| p == test_package),
"Installed packages list should contain {}", test_package);
},
Err(e) => {
println!("Error listing installed packages: {}", e);
return;
}
}
// Now remove the package if it wasn't installed before
if !is_installed_before {
println!("Removing package {} after test", test_package);
@ -905,7 +847,7 @@ mod tests {
return;
}
}
// Verify it was removed
match hero.is_installed(test_package) {
Ok(is_installed) => {
@ -915,17 +857,14 @@ mod tests {
} else {
println!("Verified package {} was removed", test_package);
}
}
},
Err(e) => {
println!(
"Error checking if package is installed after removal: {}",
e
);
println!("Error checking if package is installed after removal: {}", e);
return;
}
}
}
// Test update functionality
println!("Testing package list update");
match hero.update() {
@ -935,10 +874,10 @@ mod tests {
return;
}
}
println!("All real package operations tests passed on Ubuntu");
}
// Test to check if apt-get is available on the system
#[test]
fn test_apt_get_availability() {
@ -947,18 +886,18 @@ mod tests {
.arg("apt-get")
.output()
.expect("Failed to execute which apt-get");
let success = output.status.success();
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
println!("apt-get available: {}", success);
if success {
println!("apt-get path: {}", stdout.trim());
}
// On Ubuntu, this should pass
if Platform::detect() == Platform::Ubuntu {
assert!(success, "apt-get should be available on Ubuntu");
}
}
}
}

View File

@ -0,0 +1,565 @@
# Package Management Module Implementation Plan
## Overview
The package management module will:
1. Provide a factory called `PackHero` that detects the current platform
2. Implement platform-specific package managers for Ubuntu (apt) and macOS (brew)
3. Support operations: install, remove, update, upgrade, list installed packages, search for packages, and check if a package is installed
4. Include debug functionality similar to buildah
5. Ensure all operations are non-interactive and have proper error propagation
6. Be wrapped in Rhai for scripting access
## Architecture
```mermaid
classDiagram
class PackageError {
+CommandFailed(String)
+CommandExecutionFailed(std::io::Error)
+UnsupportedPlatform(String)
+Other(String)
}
class PackHero {
-platform: Platform
-debug: bool
+new() PackHero
+detect_platform() Platform
+set_debug(bool) PackHero
+debug() bool
+install(package: &str) Result
+remove(package: &str) Result
+update() Result
+upgrade() Result
+list_installed() Result
+search(query: &str) Result
+is_installed(package: &str) Result
}
class Platform {
<<enumeration>>
Ubuntu
MacOS
Unknown
}
class PackageManager {
<<interface>>
+install(package: &str) Result
+remove(package: &str) Result
+update() Result
+upgrade() Result
+list_installed() Result
+search(query: &str) Result
+is_installed(package: &str) Result
}
class AptPackageManager {
-debug: bool
+new(debug: bool) AptPackageManager
+install(package: &str) Result
+remove(package: &str) Result
+update() Result
+upgrade() Result
+list_installed() Result
+search(query: &str) Result
+is_installed(package: &str) Result
}
class BrewPackageManager {
-debug: bool
+new(debug: bool) BrewPackageManager
+install(package: &str) Result
+remove(package: &str) Result
+update() Result
+upgrade() Result
+list_installed() Result
+search(query: &str) Result
+is_installed(package: &str) Result
}
PackHero --> Platform : uses
PackHero --> PackageManager : uses
PackageManager <|.. AptPackageManager : implements
PackageManager <|.. BrewPackageManager : implements
```
## Implementation Details
### 1. Package Error Type
Create a custom error type for package management operations:
```rust
pub enum PackageError {
CommandFailed(String),
CommandExecutionFailed(std::io::Error),
UnsupportedPlatform(String),
Other(String),
}
```
### 2. Platform Detection
Implement platform detection to determine which package manager to use:
```rust
pub enum Platform {
Ubuntu,
MacOS,
Unknown,
}
impl Platform {
pub fn detect() -> Self {
// Check for macOS
if std::path::Path::new("/usr/bin/sw_vers").exists() {
return Platform::MacOS;
}
// Check for Ubuntu
if std::path::Path::new("/etc/lsb-release").exists() {
// Read the file to confirm it's Ubuntu
if let Ok(content) = std::fs::read_to_string("/etc/lsb-release") {
if content.contains("Ubuntu") {
return Platform::Ubuntu;
}
}
}
Platform::Unknown
}
}
```
### 3. Package Manager Trait
Define a trait for package managers to implement:
```rust
pub trait PackageManager {
fn install(&self, package: &str) -> Result<CommandResult, PackageError>;
fn remove(&self, package: &str) -> Result<CommandResult, PackageError>;
fn update(&self) -> Result<CommandResult, PackageError>;
fn upgrade(&self) -> Result<CommandResult, PackageError>;
fn list_installed(&self) -> Result<Vec<String>, PackageError>;
fn search(&self, query: &str) -> Result<Vec<String>, PackageError>;
fn is_installed(&self, package: &str) -> Result<bool, PackageError>;
}
```
### 4. Platform-Specific Implementations
#### Ubuntu (apt) Implementation
```rust
pub struct AptPackageManager {
debug: bool,
}
impl AptPackageManager {
pub fn new(debug: bool) -> Self {
Self { debug }
}
}
impl PackageManager for AptPackageManager {
fn install(&self, package: &str) -> Result<CommandResult, PackageError> {
execute_package_command(&["apt-get", "install", "-y", package], self.debug)
}
fn remove(&self, package: &str) -> Result<CommandResult, PackageError> {
execute_package_command(&["apt-get", "remove", "-y", package], self.debug)
}
fn update(&self) -> Result<CommandResult, PackageError> {
execute_package_command(&["apt-get", "update", "-y"], self.debug)
}
fn upgrade(&self) -> Result<CommandResult, PackageError> {
execute_package_command(&["apt-get", "upgrade", "-y"], self.debug)
}
fn list_installed(&self) -> Result<Vec<String>, PackageError> {
let result = execute_package_command(&["dpkg", "--get-selections"], self.debug)?;
let packages = result.stdout
.lines()
.filter_map(|line| {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 2 && parts[1] == "install" {
Some(parts[0].to_string())
} else {
None
}
})
.collect();
Ok(packages)
}
fn search(&self, query: &str) -> Result<Vec<String>, PackageError> {
let result = execute_package_command(&["apt-cache", "search", query], self.debug)?;
let packages = result.stdout
.lines()
.map(|line| {
let parts: Vec<&str> = line.split_whitespace().collect();
if !parts.is_empty() {
parts[0].to_string()
} else {
String::new()
}
})
.filter(|s| !s.is_empty())
.collect();
Ok(packages)
}
fn is_installed(&self, package: &str) -> Result<bool, PackageError> {
let result = execute_package_command(&["dpkg", "-s", package], self.debug);
match result {
Ok(cmd_result) => Ok(cmd_result.success),
Err(_) => Ok(false),
}
}
}
```
#### macOS (brew) Implementation
```rust
pub struct BrewPackageManager {
debug: bool,
}
impl BrewPackageManager {
pub fn new(debug: bool) -> Self {
Self { debug }
}
}
impl PackageManager for BrewPackageManager {
fn install(&self, package: &str) -> Result<CommandResult, PackageError> {
execute_package_command(&["brew", "install", package], self.debug)
}
fn remove(&self, package: &str) -> Result<CommandResult, PackageError> {
execute_package_command(&["brew", "uninstall", package], self.debug)
}
fn update(&self) -> Result<CommandResult, PackageError> {
execute_package_command(&["brew", "update"], self.debug)
}
fn upgrade(&self) -> Result<CommandResult, PackageError> {
execute_package_command(&["brew", "upgrade"], self.debug)
}
fn list_installed(&self) -> Result<Vec<String>, PackageError> {
let result = execute_package_command(&["brew", "list", "--formula"], self.debug)?;
let packages = result.stdout
.lines()
.map(|line| line.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
Ok(packages)
}
fn search(&self, query: &str) -> Result<Vec<String>, PackageError> {
let result = execute_package_command(&["brew", "search", query], self.debug)?;
let packages = result.stdout
.lines()
.map(|line| line.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
Ok(packages)
}
fn is_installed(&self, package: &str) -> Result<bool, PackageError> {
let result = execute_package_command(&["brew", "list", package], self.debug);
match result {
Ok(cmd_result) => Ok(cmd_result.success),
Err(_) => Ok(false),
}
}
}
```
### 5. Command Execution with Debug Support
Implement a function to execute package management commands with debug support:
```rust
// Thread-local storage for debug flag
thread_local! {
static DEBUG: std::cell::RefCell<bool> = std::cell::RefCell::new(false);
}
/// Set the debug flag for the current thread
pub fn set_thread_local_debug(debug: bool) {
DEBUG.with(|cell| {
*cell.borrow_mut() = debug;
});
}
/// Get the debug flag for the current thread
pub fn thread_local_debug() -> bool {
DEBUG.with(|cell| {
*cell.borrow()
})
}
/// Execute a package management command and return the result
pub fn execute_package_command(args: &[&str], debug: bool) -> Result<CommandResult, PackageError> {
// Save the current debug flag
let previous_debug = thread_local_debug();
// Set the thread-local debug flag
set_thread_local_debug(debug);
if debug {
println!("Executing command: {}", args.join(" "));
}
let output = Command::new(args[0])
.args(&args[1..])
.output();
// Restore the previous debug flag
set_thread_local_debug(previous_debug);
match output {
Ok(output) => {
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
let result = CommandResult {
stdout,
stderr,
success: output.status.success(),
code: output.status.code().unwrap_or(-1),
};
// Always output stdout/stderr when debug is true
if debug {
if !result.stdout.is_empty() {
println!("Command stdout: {}", result.stdout);
}
if !result.stderr.is_empty() {
println!("Command stderr: {}", result.stderr);
}
if result.success {
println!("Command succeeded with code {}", result.code);
} else {
println!("Command failed with code {}", result.code);
}
}
if result.success {
Ok(result)
} else {
// If command failed and debug is false, output stderr
if !debug {
println!("Command failed with code {}: {}", result.code, result.stderr.trim());
}
Err(PackageError::CommandFailed(format!("Command failed with code {}: {}",
result.code, result.stderr.trim())))
}
},
Err(e) => {
// Always output error information
println!("Command execution failed: {}", e);
Err(PackageError::CommandExecutionFailed(e))
}
}
}
```
### 6. PackHero Factory
Implement the PackHero factory to provide a unified interface:
```rust
pub struct PackHero {
platform: Platform,
debug: bool,
}
impl PackHero {
pub fn new() -> Self {
let platform = Platform::detect();
Self {
platform,
debug: false,
}
}
pub fn set_debug(&mut self, debug: bool) -> &mut Self {
self.debug = debug;
self
}
pub fn debug(&self) -> bool {
self.debug
}
fn get_package_manager(&self) -> Result<Box<dyn PackageManager>, PackageError> {
match self.platform {
Platform::Ubuntu => Ok(Box::new(AptPackageManager::new(self.debug))),
Platform::MacOS => Ok(Box::new(BrewPackageManager::new(self.debug))),
Platform::Unknown => Err(PackageError::UnsupportedPlatform("Unsupported platform".to_string())),
}
}
pub fn install(&self, package: &str) -> Result<CommandResult, PackageError> {
let pm = self.get_package_manager()?;
pm.install(package)
}
pub fn remove(&self, package: &str) -> Result<CommandResult, PackageError> {
let pm = self.get_package_manager()?;
pm.remove(package)
}
pub fn update(&self) -> Result<CommandResult, PackageError> {
let pm = self.get_package_manager()?;
pm.update()
}
pub fn upgrade(&self) -> Result<CommandResult, PackageError> {
let pm = self.get_package_manager()?;
pm.upgrade()
}
pub fn list_installed(&self) -> Result<Vec<String>, PackageError> {
let pm = self.get_package_manager()?;
pm.list_installed()
}
pub fn search(&self, query: &str) -> Result<Vec<String>, PackageError> {
let pm = self.get_package_manager()?;
pm.search(query)
}
pub fn is_installed(&self, package: &str) -> Result<bool, PackageError> {
let pm = self.get_package_manager()?;
pm.is_installed(package)
}
}
```
### 7. Rhai Integration
Update the Rhai OS module to include the package management functions:
```rust
// In rhai/os.rs
// Register package management functions
engine.register_fn("package_install", package_install);
engine.register_fn("package_remove", package_remove);
engine.register_fn("package_update", package_update);
engine.register_fn("package_upgrade", package_upgrade);
engine.register_fn("package_list", package_list);
engine.register_fn("package_search", package_search);
engine.register_fn("package_is_installed", package_is_installed);
engine.register_fn("package_set_debug", package_set_debug);
// Wrapper for os::package::install
pub fn package_install(package: &str) -> Result<String, Box<EvalAltResult>> {
let hero = os::package::PackHero::new();
hero.install(package)
.map(|_| "Package installed successfully".to_string())
.to_rhai_error()
}
// Wrapper for os::package::remove
pub fn package_remove(package: &str) -> Result<String, Box<EvalAltResult>> {
let hero = os::package::PackHero::new();
hero.remove(package)
.map(|_| "Package removed successfully".to_string())
.to_rhai_error()
}
// Wrapper for os::package::update
pub fn package_update() -> Result<String, Box<EvalAltResult>> {
let hero = os::package::PackHero::new();
hero.update()
.map(|_| "Package lists updated successfully".to_string())
.to_rhai_error()
}
// Wrapper for os::package::upgrade
pub fn package_upgrade() -> Result<String, Box<EvalAltResult>> {
let hero = os::package::PackHero::new();
hero.upgrade()
.map(|_| "Packages upgraded successfully".to_string())
.to_rhai_error()
}
// Wrapper for os::package::list_installed
pub fn package_list() -> Result<Array, Box<EvalAltResult>> {
let hero = os::package::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)
}
// Wrapper for os::package::search
pub fn package_search(query: &str) -> Result<Array, Box<EvalAltResult>> {
let hero = os::package::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)
}
// Wrapper for os::package::is_installed
pub fn package_is_installed(package: &str) -> Result<bool, Box<EvalAltResult>> {
let hero = os::package::PackHero::new();
hero.is_installed(package).to_rhai_error()
}
// Global debug flag for package management
static mut PACKAGE_DEBUG: bool = false;
// Wrapper for setting package debug mode
pub fn package_set_debug(debug: bool) -> bool {
unsafe {
PACKAGE_DEBUG = debug;
PACKAGE_DEBUG
}
}
```
## Implementation Steps
1. Create the package.rs file in the os directory
2. Implement the PackageError enum
3. Implement the Platform enum with detection logic
4. Implement the PackageManager trait
5. Implement the AptPackageManager for Ubuntu
6. Implement the BrewPackageManager for macOS
7. Implement the debug functionality with thread-local storage
8. Implement the PackHero factory
9. Update os/mod.rs to include the package module
10. Update rhai/os.rs to include the package management functions
11. Test the implementation on both Ubuntu and macOS
## Testing Strategy
1. Create unit tests for each platform-specific implementation
2. Create integration tests that verify the PackHero factory correctly detects the platform and uses the appropriate package manager
3. Create Rhai examples that demonstrate the use of the package management functions

View File

@ -1,245 +0,0 @@
# PostgreSQL Client Module
The PostgreSQL client module provides a simple and efficient way to interact with PostgreSQL databases in Rust. It offers connection management, query execution, and a builder pattern for flexible configuration.
## Features
- **Connection Management**: Automatic connection handling and reconnection
- **Query Execution**: Simple API for executing queries and fetching results
- **Builder Pattern**: Flexible configuration with authentication support
- **Environment Variable Support**: Easy configuration through environment variables
- **Thread Safety**: Safe to use in multi-threaded applications
## Usage
### Basic Usage
```rust
use sal::postgresclient::{execute, query, query_one};
// Execute a query
let create_table_query = "CREATE TABLE IF NOT EXISTS users (id SERIAL PRIMARY KEY, name TEXT)";
execute(create_table_query, &[]).expect("Failed to create table");
// Insert data
let insert_query = "INSERT INTO users (name) VALUES ($1) RETURNING id";
let rows = query(insert_query, &[&"John Doe"]).expect("Failed to insert data");
let id: i32 = rows[0].get(0);
// Query data
let select_query = "SELECT id, name FROM users WHERE id = $1";
let row = query_one(select_query, &[&id]).expect("Failed to query data");
let name: String = row.get(1);
println!("User: {} (ID: {})", name, id);
```
### Connection Management
The module manages connections automatically, but you can also reset the connection if needed:
```rust
use sal::postgresclient::reset;
// Reset the PostgreSQL client connection
reset().expect("Failed to reset connection");
```
### Builder Pattern
The module provides a builder pattern for flexible configuration:
```rust
use sal::postgresclient::{PostgresConfigBuilder, with_config};
// Create a configuration builder
let config = PostgresConfigBuilder::new()
.host("db.example.com")
.port(5432)
.user("postgres")
.password("secret")
.database("mydb")
.application_name("my-app")
.connect_timeout(30)
.ssl_mode("require");
// Connect with the configuration
let client = with_config(config).expect("Failed to connect");
```
## Configuration
### Environment Variables
The module uses the following environment variables for configuration:
- `POSTGRES_HOST`: PostgreSQL server host (default: localhost)
- `POSTGRES_PORT`: PostgreSQL server port (default: 5432)
- `POSTGRES_USER`: PostgreSQL username (default: postgres)
- `POSTGRES_PASSWORD`: PostgreSQL password
- `POSTGRES_DB`: PostgreSQL database name (default: postgres)
### Connection String
The connection string is built from the configuration options:
```
host=localhost port=5432 user=postgres dbname=postgres
```
With authentication:
```
host=localhost port=5432 user=postgres password=secret dbname=postgres
```
With additional options:
```
host=localhost port=5432 user=postgres dbname=postgres application_name=my-app connect_timeout=30 sslmode=require
```
## API Reference
### Connection Functions
- `get_postgres_client() -> Result<Arc<PostgresClientWrapper>, PostgresError>`: Get the PostgreSQL client instance
- `reset() -> Result<(), PostgresError>`: Reset the PostgreSQL client connection
### Query Functions
- `execute(query: &str, params: &[&(dyn postgres::types::ToSql + Sync)]) -> Result<u64, PostgresError>`: Execute a query and return the number of affected rows
- `query(query: &str, params: &[&(dyn postgres::types::ToSql + Sync)]) -> Result<Vec<Row>, PostgresError>`: Execute a query and return the results as a vector of rows
- `query_one(query: &str, params: &[&(dyn postgres::types::ToSql + Sync)]) -> Result<Row, PostgresError>`: Execute a query and return a single row
- `query_opt(query: &str, params: &[&(dyn postgres::types::ToSql + Sync)]) -> Result<Option<Row>, PostgresError>`: Execute a query and return an optional row
### Configuration Functions
- `PostgresConfigBuilder::new() -> PostgresConfigBuilder`: Create a new PostgreSQL configuration builder
- `with_config(config: PostgresConfigBuilder) -> Result<Client, PostgresError>`: Create a new PostgreSQL client with custom configuration
## Error Handling
The module uses the `postgres::Error` type for error handling:
```rust
use sal::postgresclient::{query, query_one};
// Handle errors
match query("SELECT * FROM users", &[]) {
Ok(rows) => {
println!("Found {} users", rows.len());
},
Err(e) => {
eprintln!("Error querying users: {}", e);
}
}
// Using query_one with no results
match query_one("SELECT * FROM users WHERE id = $1", &[&999]) {
Ok(_) => {
println!("User found");
},
Err(e) => {
eprintln!("User not found: {}", e);
}
}
```
## Thread Safety
The PostgreSQL client module is designed to be thread-safe. It uses `Arc` and `Mutex` to ensure safe concurrent access to the client instance.
## Examples
### Basic CRUD Operations
```rust
use sal::postgresclient::{execute, query, query_one};
// Create
let create_query = "INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id";
let rows = query(create_query, &[&"Alice", &"alice@example.com"]).expect("Failed to create user");
let id: i32 = rows[0].get(0);
// Read
let read_query = "SELECT id, name, email FROM users WHERE id = $1";
let row = query_one(read_query, &[&id]).expect("Failed to read user");
let name: String = row.get(1);
let email: String = row.get(2);
// Update
let update_query = "UPDATE users SET email = $1 WHERE id = $2";
let affected = execute(update_query, &[&"new.alice@example.com", &id]).expect("Failed to update user");
// Delete
let delete_query = "DELETE FROM users WHERE id = $1";
let affected = execute(delete_query, &[&id]).expect("Failed to delete user");
```
### Transactions
Transactions are not directly supported by the module, but you can use the PostgreSQL client to implement them:
```rust
use sal::postgresclient::{execute, query};
// Start a transaction
execute("BEGIN", &[]).expect("Failed to start transaction");
// Perform operations
let insert_query = "INSERT INTO accounts (user_id, balance) VALUES ($1, $2)";
execute(insert_query, &[&1, &1000.0]).expect("Failed to insert account");
let update_query = "UPDATE users SET has_account = TRUE WHERE id = $1";
execute(update_query, &[&1]).expect("Failed to update user");
// Commit the transaction
execute("COMMIT", &[]).expect("Failed to commit transaction");
// Or rollback in case of an error
// execute("ROLLBACK", &[]).expect("Failed to rollback transaction");
```
## Testing
The module includes comprehensive tests for both unit and integration testing:
```rust
// Unit tests
#[test]
fn test_postgres_config_builder() {
let config = PostgresConfigBuilder::new()
.host("test-host")
.port(5433)
.user("test-user");
let conn_string = config.build_connection_string();
assert!(conn_string.contains("host=test-host"));
assert!(conn_string.contains("port=5433"));
assert!(conn_string.contains("user=test-user"));
}
// Integration tests
#[test]
fn test_basic_postgres_operations() {
// Skip if PostgreSQL is not available
if !is_postgres_available() {
return;
}
// Create a test table
let create_table_query = "CREATE TEMPORARY TABLE test_table (id SERIAL PRIMARY KEY, name TEXT)";
execute(create_table_query, &[]).expect("Failed to create table");
// Insert data
let insert_query = "INSERT INTO test_table (name) VALUES ($1) RETURNING id";
let rows = query(insert_query, &[&"test"]).expect("Failed to insert data");
let id: i32 = rows[0].get(0);
// Query data
let select_query = "SELECT name FROM test_table WHERE id = $1";
let row = query_one(select_query, &[&id]).expect("Failed to query data");
let name: String = row.get(0);
assert_eq!(name, "test");
}
```

View File

@ -1,355 +0,0 @@
// PostgreSQL installer module
//
// This module provides functionality to install and configure PostgreSQL using nerdctl.
use std::collections::HashMap;
use std::env;
use std::fs;
use std::path::Path;
use std::process::Command;
use std::thread;
use std::time::Duration;
use crate::virt::nerdctl::Container;
use std::error::Error;
use std::fmt;
// Custom error type for PostgreSQL installer
#[derive(Debug)]
pub enum PostgresInstallerError {
IoError(std::io::Error),
NerdctlError(String),
PostgresError(String),
}
impl fmt::Display for PostgresInstallerError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
PostgresInstallerError::IoError(e) => write!(f, "I/O error: {}", e),
PostgresInstallerError::NerdctlError(e) => write!(f, "Nerdctl error: {}", e),
PostgresInstallerError::PostgresError(e) => write!(f, "PostgreSQL error: {}", e),
}
}
}
impl Error for PostgresInstallerError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
PostgresInstallerError::IoError(e) => Some(e),
_ => None,
}
}
}
impl From<std::io::Error> for PostgresInstallerError {
fn from(error: std::io::Error) -> Self {
PostgresInstallerError::IoError(error)
}
}
/// PostgreSQL installer configuration
pub struct PostgresInstallerConfig {
/// Container name for PostgreSQL
pub container_name: String,
/// PostgreSQL version to install
pub version: String,
/// Port to expose PostgreSQL on
pub port: u16,
/// Username for PostgreSQL
pub username: String,
/// Password for PostgreSQL
pub password: String,
/// Data directory for PostgreSQL
pub data_dir: Option<String>,
/// Environment variables for PostgreSQL
pub env_vars: HashMap<String, String>,
/// Whether to use persistent storage
pub persistent: bool,
}
impl Default for PostgresInstallerConfig {
fn default() -> Self {
Self {
container_name: "postgres".to_string(),
version: "latest".to_string(),
port: 5432,
username: "postgres".to_string(),
password: "postgres".to_string(),
data_dir: None,
env_vars: HashMap::new(),
persistent: true,
}
}
}
impl PostgresInstallerConfig {
/// Create a new PostgreSQL installer configuration with default values
pub fn new() -> Self {
Self::default()
}
/// Set the container name
pub fn container_name(mut self, name: &str) -> Self {
self.container_name = name.to_string();
self
}
/// Set the PostgreSQL version
pub fn version(mut self, version: &str) -> Self {
self.version = version.to_string();
self
}
/// Set the port to expose PostgreSQL on
pub fn port(mut self, port: u16) -> Self {
self.port = port;
self
}
/// Set the username for PostgreSQL
pub fn username(mut self, username: &str) -> Self {
self.username = username.to_string();
self
}
/// Set the password for PostgreSQL
pub fn password(mut self, password: &str) -> Self {
self.password = password.to_string();
self
}
/// Set the data directory for PostgreSQL
pub fn data_dir(mut self, data_dir: &str) -> Self {
self.data_dir = Some(data_dir.to_string());
self
}
/// Add an environment variable
pub fn env_var(mut self, key: &str, value: &str) -> Self {
self.env_vars.insert(key.to_string(), value.to_string());
self
}
/// Set whether to use persistent storage
pub fn persistent(mut self, persistent: bool) -> Self {
self.persistent = persistent;
self
}
}
/// Install PostgreSQL using nerdctl
///
/// # Arguments
///
/// * `config` - PostgreSQL installer configuration
///
/// # Returns
///
/// * `Result<Container, PostgresInstallerError>` - Container instance or error
pub fn install_postgres(
config: PostgresInstallerConfig,
) -> Result<Container, PostgresInstallerError> {
// Create the data directory if it doesn't exist and persistent storage is enabled
let data_dir = if config.persistent {
let dir = config.data_dir.unwrap_or_else(|| {
let home_dir = env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
format!("{}/.postgres-data", home_dir)
});
if !Path::new(&dir).exists() {
fs::create_dir_all(&dir).map_err(|e| PostgresInstallerError::IoError(e))?;
}
Some(dir)
} else {
None
};
// Build the image name
let image = format!("postgres:{}", config.version);
// Pull the PostgreSQL image to ensure we have the latest version
println!("Pulling PostgreSQL image: {}...", image);
let pull_result = Command::new("nerdctl")
.args(&["pull", &image])
.output()
.map_err(|e| PostgresInstallerError::IoError(e))?;
if !pull_result.status.success() {
return Err(PostgresInstallerError::NerdctlError(format!(
"Failed to pull PostgreSQL image: {}",
String::from_utf8_lossy(&pull_result.stderr)
)));
}
// Create the container
let mut container = Container::new(&config.container_name).map_err(|e| {
PostgresInstallerError::NerdctlError(format!("Failed to create container: {}", e))
})?;
// Set the image
container.image = Some(image);
// Set the port
container = container.with_port(&format!("{}:5432", config.port));
// Set environment variables
container = container.with_env("POSTGRES_USER", &config.username);
container = container.with_env("POSTGRES_PASSWORD", &config.password);
container = container.with_env("POSTGRES_DB", "postgres");
// Add custom environment variables
for (key, value) in &config.env_vars {
container = container.with_env(key, value);
}
// Add volume for persistent storage if enabled
if let Some(dir) = data_dir {
container = container.with_volume(&format!("{}:/var/lib/postgresql/data", dir));
}
// Set restart policy
container = container.with_restart_policy("unless-stopped");
// Set detach mode
container = container.with_detach(true);
// Build and start the container
let container = container.build().map_err(|e| {
PostgresInstallerError::NerdctlError(format!("Failed to build container: {}", e))
})?;
// Wait for PostgreSQL to start
println!("Waiting for PostgreSQL to start...");
thread::sleep(Duration::from_secs(5));
// Set environment variables for PostgreSQL client
env::set_var("POSTGRES_HOST", "localhost");
env::set_var("POSTGRES_PORT", config.port.to_string());
env::set_var("POSTGRES_USER", config.username);
env::set_var("POSTGRES_PASSWORD", config.password);
env::set_var("POSTGRES_DB", "postgres");
Ok(container)
}
/// Create a new database in PostgreSQL
///
/// # Arguments
///
/// * `container` - PostgreSQL container
/// * `db_name` - Database name
///
/// # Returns
///
/// * `Result<(), PostgresInstallerError>` - Ok if successful, Err otherwise
pub fn create_database(container: &Container, db_name: &str) -> Result<(), PostgresInstallerError> {
// Check if container is running
if container.container_id.is_none() {
return Err(PostgresInstallerError::PostgresError(
"Container is not running".to_string(),
));
}
// Execute the command to create the database
let command = format!(
"createdb -U {} {}",
env::var("POSTGRES_USER").unwrap_or_else(|_| "postgres".to_string()),
db_name
);
container.exec(&command).map_err(|e| {
PostgresInstallerError::NerdctlError(format!("Failed to create database: {}", e))
})?;
Ok(())
}
/// Execute a SQL script in PostgreSQL
///
/// # Arguments
///
/// * `container` - PostgreSQL container
/// * `db_name` - Database name
/// * `sql` - SQL script to execute
///
/// # Returns
///
/// * `Result<String, PostgresInstallerError>` - Output of the command or error
pub fn execute_sql(
container: &Container,
db_name: &str,
sql: &str,
) -> Result<String, PostgresInstallerError> {
// Check if container is running
if container.container_id.is_none() {
return Err(PostgresInstallerError::PostgresError(
"Container is not running".to_string(),
));
}
// Create a temporary file with the SQL script
let temp_file = "/tmp/postgres_script.sql";
fs::write(temp_file, sql).map_err(|e| PostgresInstallerError::IoError(e))?;
// Copy the file to the container
let container_id = container.container_id.as_ref().unwrap();
let copy_result = Command::new("nerdctl")
.args(&[
"cp",
temp_file,
&format!("{}:/tmp/script.sql", container_id),
])
.output()
.map_err(|e| PostgresInstallerError::IoError(e))?;
if !copy_result.status.success() {
return Err(PostgresInstallerError::PostgresError(format!(
"Failed to copy SQL script to container: {}",
String::from_utf8_lossy(&copy_result.stderr)
)));
}
// Execute the SQL script
let command = format!(
"psql -U {} -d {} -f /tmp/script.sql",
env::var("POSTGRES_USER").unwrap_or_else(|_| "postgres".to_string()),
db_name
);
let result = container.exec(&command).map_err(|e| {
PostgresInstallerError::NerdctlError(format!("Failed to execute SQL script: {}", e))
})?;
// Clean up
fs::remove_file(temp_file).ok();
Ok(result.stdout)
}
/// Check if PostgreSQL is running
///
/// # Arguments
///
/// * `container` - PostgreSQL container
///
/// # Returns
///
/// * `Result<bool, PostgresInstallerError>` - true if running, false otherwise, or error
pub fn is_postgres_running(container: &Container) -> Result<bool, PostgresInstallerError> {
// Check if container is running
if container.container_id.is_none() {
return Ok(false);
}
// Execute a simple query to check if PostgreSQL is running
let command = format!(
"psql -U {} -c 'SELECT 1'",
env::var("POSTGRES_USER").unwrap_or_else(|_| "postgres".to_string())
);
match container.exec(&command) {
Ok(_) => Ok(true),
Err(_) => Ok(false),
}
}

View File

@ -1,12 +0,0 @@
// PostgreSQL client module
//
// This module provides a PostgreSQL client for interacting with PostgreSQL databases.
mod installer;
mod postgresclient;
#[cfg(test)]
mod tests;
// Re-export the public API
pub use installer::*;
pub use postgresclient::*;

View File

@ -1,825 +0,0 @@
use lazy_static::lazy_static;
use postgres::types::ToSql;
use postgres::{Client, Error as PostgresError, NoTls, Row};
use r2d2::Pool;
use r2d2_postgres::PostgresConnectionManager;
use std::env;
use std::sync::{Arc, Mutex, Once};
use std::time::Duration;
// Helper function to create a PostgreSQL error
fn create_postgres_error(_message: &str) -> PostgresError {
// Since we can't directly create a PostgresError, we'll create one by
// attempting to connect to an invalid connection string and capturing the error
let result = Client::connect("invalid-connection-string", NoTls);
match result {
Ok(_) => unreachable!(), // This should never happen
Err(e) => {
// We have a valid PostgresError now, but we want to customize the message
// Unfortunately, PostgresError doesn't provide a way to modify the message
// So we'll just return the error we got
e
}
}
}
// Global PostgreSQL client instance using lazy_static
lazy_static! {
static ref POSTGRES_CLIENT: Mutex<Option<Arc<PostgresClientWrapper>>> = Mutex::new(None);
static ref POSTGRES_POOL: Mutex<Option<Arc<Pool<PostgresConnectionManager<NoTls>>>>> =
Mutex::new(None);
static ref INIT: Once = Once::new();
}
/// PostgreSQL connection configuration builder
///
/// This struct is used to build a PostgreSQL connection configuration.
/// It follows the builder pattern to allow for flexible configuration.
#[derive(Debug)]
pub struct PostgresConfigBuilder {
pub host: String,
pub port: u16,
pub user: String,
pub password: Option<String>,
pub database: String,
pub application_name: Option<String>,
pub connect_timeout: Option<u64>,
pub ssl_mode: Option<String>,
// Connection pool settings
pub pool_max_size: Option<u32>,
pub pool_min_idle: Option<u32>,
pub pool_idle_timeout: Option<Duration>,
pub pool_connection_timeout: Option<Duration>,
pub pool_max_lifetime: Option<Duration>,
pub use_pool: bool,
}
impl Default for PostgresConfigBuilder {
fn default() -> Self {
Self {
host: "localhost".to_string(),
port: 5432,
user: "postgres".to_string(),
password: None,
database: "postgres".to_string(),
application_name: None,
connect_timeout: None,
ssl_mode: None,
// Default pool settings
pool_max_size: Some(10),
pool_min_idle: Some(1),
pool_idle_timeout: Some(Duration::from_secs(300)),
pool_connection_timeout: Some(Duration::from_secs(30)),
pool_max_lifetime: Some(Duration::from_secs(1800)),
use_pool: false,
}
}
}
impl PostgresConfigBuilder {
/// Create a new PostgreSQL connection configuration builder with default values
pub fn new() -> Self {
Self::default()
}
/// Set the host for the PostgreSQL connection
pub fn host(mut self, host: &str) -> Self {
self.host = host.to_string();
self
}
/// Set the port for the PostgreSQL connection
pub fn port(mut self, port: u16) -> Self {
self.port = port;
self
}
/// Set the user for the PostgreSQL connection
pub fn user(mut self, user: &str) -> Self {
self.user = user.to_string();
self
}
/// Set the password for the PostgreSQL connection
pub fn password(mut self, password: &str) -> Self {
self.password = Some(password.to_string());
self
}
/// Set the database for the PostgreSQL connection
pub fn database(mut self, database: &str) -> Self {
self.database = database.to_string();
self
}
/// Set the application name for the PostgreSQL connection
pub fn application_name(mut self, application_name: &str) -> Self {
self.application_name = Some(application_name.to_string());
self
}
/// Set the connection timeout in seconds
pub fn connect_timeout(mut self, seconds: u64) -> Self {
self.connect_timeout = Some(seconds);
self
}
/// Set the SSL mode for the PostgreSQL connection
pub fn ssl_mode(mut self, ssl_mode: &str) -> Self {
self.ssl_mode = Some(ssl_mode.to_string());
self
}
/// Enable connection pooling
pub fn use_pool(mut self, use_pool: bool) -> Self {
self.use_pool = use_pool;
self
}
/// Set the maximum size of the connection pool
pub fn pool_max_size(mut self, size: u32) -> Self {
self.pool_max_size = Some(size);
self
}
/// Set the minimum number of idle connections in the pool
pub fn pool_min_idle(mut self, size: u32) -> Self {
self.pool_min_idle = Some(size);
self
}
/// Set the idle timeout for connections in the pool
pub fn pool_idle_timeout(mut self, timeout: Duration) -> Self {
self.pool_idle_timeout = Some(timeout);
self
}
/// Set the connection timeout for the pool
pub fn pool_connection_timeout(mut self, timeout: Duration) -> Self {
self.pool_connection_timeout = Some(timeout);
self
}
/// Set the maximum lifetime of connections in the pool
pub fn pool_max_lifetime(mut self, lifetime: Duration) -> Self {
self.pool_max_lifetime = Some(lifetime);
self
}
/// Build the connection string from the configuration
pub fn build_connection_string(&self) -> String {
let mut conn_string = format!(
"host={} port={} user={} dbname={}",
self.host, self.port, self.user, self.database
);
if let Some(password) = &self.password {
conn_string.push_str(&format!(" password={}", password));
}
if let Some(app_name) = &self.application_name {
conn_string.push_str(&format!(" application_name={}", app_name));
}
if let Some(timeout) = self.connect_timeout {
conn_string.push_str(&format!(" connect_timeout={}", timeout));
}
if let Some(ssl_mode) = &self.ssl_mode {
conn_string.push_str(&format!(" sslmode={}", ssl_mode));
}
conn_string
}
/// Build a PostgreSQL client from the configuration
pub fn build(&self) -> Result<Client, PostgresError> {
let conn_string = self.build_connection_string();
Client::connect(&conn_string, NoTls)
}
/// Build a PostgreSQL connection pool from the configuration
pub fn build_pool(&self) -> Result<Pool<PostgresConnectionManager<NoTls>>, r2d2::Error> {
let conn_string = self.build_connection_string();
let manager = PostgresConnectionManager::new(conn_string.parse().unwrap(), NoTls);
let mut pool_builder = r2d2::Pool::builder();
if let Some(max_size) = self.pool_max_size {
pool_builder = pool_builder.max_size(max_size);
}
if let Some(min_idle) = self.pool_min_idle {
pool_builder = pool_builder.min_idle(Some(min_idle));
}
if let Some(idle_timeout) = self.pool_idle_timeout {
pool_builder = pool_builder.idle_timeout(Some(idle_timeout));
}
if let Some(connection_timeout) = self.pool_connection_timeout {
pool_builder = pool_builder.connection_timeout(connection_timeout);
}
if let Some(max_lifetime) = self.pool_max_lifetime {
pool_builder = pool_builder.max_lifetime(Some(max_lifetime));
}
pool_builder.build(manager)
}
}
/// Wrapper for PostgreSQL client to handle connection
pub struct PostgresClientWrapper {
connection_string: String,
client: Mutex<Option<Client>>,
}
/// Transaction functions for PostgreSQL
///
/// These functions provide a way to execute queries within a transaction.
/// The transaction is automatically committed when the function returns successfully,
/// or rolled back if an error occurs.
///
/// Example:
/// ```
/// use sal::postgresclient::{transaction, QueryParams};
///
/// let result = transaction(|client| {
/// // Execute queries within the transaction
/// client.execute("INSERT INTO users (name) VALUES ($1)", &[&"John"])?;
/// client.execute("UPDATE users SET active = true WHERE name = $1", &[&"John"])?;
///
/// // Return a result from the transaction
/// Ok(())
/// });
/// ```
pub fn transaction<F, T>(operations: F) -> Result<T, PostgresError>
where
F: FnOnce(&mut Client) -> Result<T, PostgresError>,
{
let client = get_postgres_client()?;
let client_mutex = client.get_client()?;
let mut client_guard = client_mutex.lock().unwrap();
if let Some(client) = client_guard.as_mut() {
// Begin transaction
client.execute("BEGIN", &[])?;
// Execute operations
match operations(client) {
Ok(result) => {
// Commit transaction
client.execute("COMMIT", &[])?;
Ok(result)
}
Err(e) => {
// Rollback transaction
let _ = client.execute("ROLLBACK", &[]);
Err(e)
}
}
} else {
Err(create_postgres_error("Failed to get PostgreSQL client"))
}
}
/// Transaction functions for PostgreSQL using the connection pool
///
/// These functions provide a way to execute queries within a transaction using the connection pool.
/// The transaction is automatically committed when the function returns successfully,
/// or rolled back if an error occurs.
///
/// Example:
/// ```
/// use sal::postgresclient::{transaction_with_pool, QueryParams};
///
/// let result = transaction_with_pool(|client| {
/// // Execute queries within the transaction
/// client.execute("INSERT INTO users (name) VALUES ($1)", &[&"John"])?;
/// client.execute("UPDATE users SET active = true WHERE name = $1", &[&"John"])?;
///
/// // Return a result from the transaction
/// Ok(())
/// });
/// ```
pub fn transaction_with_pool<F, T>(operations: F) -> Result<T, PostgresError>
where
F: FnOnce(&mut Client) -> Result<T, PostgresError>,
{
let pool = get_postgres_pool()?;
let mut client = pool.get().map_err(|e| {
create_postgres_error(&format!("Failed to get connection from pool: {}", e))
})?;
// Begin transaction
client.execute("BEGIN", &[])?;
// Execute operations
match operations(&mut client) {
Ok(result) => {
// Commit transaction
client.execute("COMMIT", &[])?;
Ok(result)
}
Err(e) => {
// Rollback transaction
let _ = client.execute("ROLLBACK", &[]);
Err(e)
}
}
}
impl PostgresClientWrapper {
/// Create a new PostgreSQL client wrapper
fn new(connection_string: String) -> Self {
PostgresClientWrapper {
connection_string,
client: Mutex::new(None),
}
}
/// Get a reference to the PostgreSQL client, creating it if it doesn't exist
fn get_client(&self) -> Result<&Mutex<Option<Client>>, PostgresError> {
let mut client_guard = self.client.lock().unwrap();
// If we don't have a client or it's not working, create a new one
if client_guard.is_none() {
*client_guard = Some(Client::connect(&self.connection_string, NoTls)?);
}
Ok(&self.client)
}
/// Execute a query on the PostgreSQL connection
pub fn execute(
&self,
query: &str,
params: &[&(dyn postgres::types::ToSql + Sync)],
) -> Result<u64, PostgresError> {
let client_mutex = self.get_client()?;
let mut client_guard = client_mutex.lock().unwrap();
if let Some(client) = client_guard.as_mut() {
client.execute(query, params)
} else {
Err(create_postgres_error("Failed to get PostgreSQL client"))
}
}
/// Execute a query on the PostgreSQL connection and return the rows
pub fn query(
&self,
query: &str,
params: &[&(dyn postgres::types::ToSql + Sync)],
) -> Result<Vec<Row>, PostgresError> {
let client_mutex = self.get_client()?;
let mut client_guard = client_mutex.lock().unwrap();
if let Some(client) = client_guard.as_mut() {
client.query(query, params)
} else {
Err(create_postgres_error("Failed to get PostgreSQL client"))
}
}
/// Execute a query on the PostgreSQL connection and return a single row
pub fn query_one(
&self,
query: &str,
params: &[&(dyn postgres::types::ToSql + Sync)],
) -> Result<Row, PostgresError> {
let client_mutex = self.get_client()?;
let mut client_guard = client_mutex.lock().unwrap();
if let Some(client) = client_guard.as_mut() {
client.query_one(query, params)
} else {
Err(create_postgres_error("Failed to get PostgreSQL client"))
}
}
/// Execute a query on the PostgreSQL connection and return an optional row
pub fn query_opt(
&self,
query: &str,
params: &[&(dyn postgres::types::ToSql + Sync)],
) -> Result<Option<Row>, PostgresError> {
let client_mutex = self.get_client()?;
let mut client_guard = client_mutex.lock().unwrap();
if let Some(client) = client_guard.as_mut() {
client.query_opt(query, params)
} else {
Err(create_postgres_error("Failed to get PostgreSQL client"))
}
}
/// Ping the PostgreSQL server to check if the connection is alive
pub fn ping(&self) -> Result<bool, PostgresError> {
let result = self.query("SELECT 1", &[]);
match result {
Ok(_) => Ok(true),
Err(e) => Err(e),
}
}
}
/// Get the PostgreSQL client instance
pub fn get_postgres_client() -> Result<Arc<PostgresClientWrapper>, PostgresError> {
// Check if we already have a client
{
let guard = POSTGRES_CLIENT.lock().unwrap();
if let Some(ref client) = &*guard {
return Ok(Arc::clone(client));
}
}
// Create a new client
let client = create_postgres_client()?;
// Store the client globally
{
let mut guard = POSTGRES_CLIENT.lock().unwrap();
*guard = Some(Arc::clone(&client));
}
Ok(client)
}
/// Create a new PostgreSQL client
fn create_postgres_client() -> Result<Arc<PostgresClientWrapper>, PostgresError> {
// Try to get connection details from environment variables
let host = env::var("POSTGRES_HOST").unwrap_or_else(|_| String::from("localhost"));
let port = env::var("POSTGRES_PORT")
.ok()
.and_then(|p| p.parse::<u16>().ok())
.unwrap_or(5432);
let user = env::var("POSTGRES_USER").unwrap_or_else(|_| String::from("postgres"));
let password = env::var("POSTGRES_PASSWORD").ok();
let database = env::var("POSTGRES_DB").unwrap_or_else(|_| String::from("postgres"));
// Build the connection string
let mut builder = PostgresConfigBuilder::new()
.host(&host)
.port(port)
.user(&user)
.database(&database);
if let Some(pass) = password {
builder = builder.password(&pass);
}
let connection_string = builder.build_connection_string();
// Create the client wrapper
let wrapper = Arc::new(PostgresClientWrapper::new(connection_string));
// Test the connection
match wrapper.ping() {
Ok(_) => Ok(wrapper),
Err(e) => Err(e),
}
}
/// Reset the PostgreSQL client
pub fn reset() -> Result<(), PostgresError> {
// Clear the existing client
{
let mut client_guard = POSTGRES_CLIENT.lock().unwrap();
*client_guard = None;
}
// Create a new client, only return error if it fails
get_postgres_client()?;
Ok(())
}
/// Execute a query on the PostgreSQL connection
pub fn execute(
query: &str,
params: &[&(dyn postgres::types::ToSql + Sync)],
) -> Result<u64, PostgresError> {
let client = get_postgres_client()?;
client.execute(query, params)
}
/// Execute a query on the PostgreSQL connection and return the rows
pub fn query(
query: &str,
params: &[&(dyn postgres::types::ToSql + Sync)],
) -> Result<Vec<Row>, PostgresError> {
let client = get_postgres_client()?;
client.query(query, params)
}
/// Execute a query on the PostgreSQL connection and return a single row
pub fn query_one(
query: &str,
params: &[&(dyn postgres::types::ToSql + Sync)],
) -> Result<Row, PostgresError> {
let client = get_postgres_client()?;
client.query_one(query, params)
}
/// Execute a query on the PostgreSQL connection and return an optional row
pub fn query_opt(
query: &str,
params: &[&(dyn postgres::types::ToSql + Sync)],
) -> Result<Option<Row>, PostgresError> {
let client = get_postgres_client()?;
client.query_opt(query, params)
}
/// Create a new PostgreSQL client with custom configuration
pub fn with_config(config: PostgresConfigBuilder) -> Result<Client, PostgresError> {
config.build()
}
/// Create a new PostgreSQL connection pool with custom configuration
pub fn with_pool_config(
config: PostgresConfigBuilder,
) -> Result<Pool<PostgresConnectionManager<NoTls>>, r2d2::Error> {
config.build_pool()
}
/// Get the PostgreSQL connection pool instance
pub fn get_postgres_pool() -> Result<Arc<Pool<PostgresConnectionManager<NoTls>>>, PostgresError> {
// Check if we already have a pool
{
let guard = POSTGRES_POOL.lock().unwrap();
if let Some(ref pool) = &*guard {
return Ok(Arc::clone(pool));
}
}
// Create a new pool
let pool = create_postgres_pool()?;
// Store the pool globally
{
let mut guard = POSTGRES_POOL.lock().unwrap();
*guard = Some(Arc::clone(&pool));
}
Ok(pool)
}
/// Create a new PostgreSQL connection pool
fn create_postgres_pool() -> Result<Arc<Pool<PostgresConnectionManager<NoTls>>>, PostgresError> {
// Try to get connection details from environment variables
let host = env::var("POSTGRES_HOST").unwrap_or_else(|_| String::from("localhost"));
let port = env::var("POSTGRES_PORT")
.ok()
.and_then(|p| p.parse::<u16>().ok())
.unwrap_or(5432);
let user = env::var("POSTGRES_USER").unwrap_or_else(|_| String::from("postgres"));
let password = env::var("POSTGRES_PASSWORD").ok();
let database = env::var("POSTGRES_DB").unwrap_or_else(|_| String::from("postgres"));
// Build the configuration
let mut builder = PostgresConfigBuilder::new()
.host(&host)
.port(port)
.user(&user)
.database(&database)
.use_pool(true);
if let Some(pass) = password {
builder = builder.password(&pass);
}
// Create the pool
match builder.build_pool() {
Ok(pool) => {
// Test the connection
match pool.get() {
Ok(_) => Ok(Arc::new(pool)),
Err(e) => Err(create_postgres_error(&format!(
"Failed to connect to PostgreSQL: {}",
e
))),
}
}
Err(e) => Err(create_postgres_error(&format!(
"Failed to create PostgreSQL connection pool: {}",
e
))),
}
}
/// Reset the PostgreSQL connection pool
pub fn reset_pool() -> Result<(), PostgresError> {
// Clear the existing pool
{
let mut pool_guard = POSTGRES_POOL.lock().unwrap();
*pool_guard = None;
}
// Create a new pool, only return error if it fails
get_postgres_pool()?;
Ok(())
}
/// Execute a query using the connection pool
pub fn execute_with_pool(
query: &str,
params: &[&(dyn postgres::types::ToSql + Sync)],
) -> Result<u64, PostgresError> {
let pool = get_postgres_pool()?;
let mut client = pool.get().map_err(|e| {
create_postgres_error(&format!("Failed to get connection from pool: {}", e))
})?;
client.execute(query, params)
}
/// Execute a query using the connection pool and return the rows
pub fn query_with_pool(
query: &str,
params: &[&(dyn postgres::types::ToSql + Sync)],
) -> Result<Vec<Row>, PostgresError> {
let pool = get_postgres_pool()?;
let mut client = pool.get().map_err(|e| {
create_postgres_error(&format!("Failed to get connection from pool: {}", e))
})?;
client.query(query, params)
}
/// Execute a query using the connection pool and return a single row
pub fn query_one_with_pool(
query: &str,
params: &[&(dyn postgres::types::ToSql + Sync)],
) -> Result<Row, PostgresError> {
let pool = get_postgres_pool()?;
let mut client = pool.get().map_err(|e| {
create_postgres_error(&format!("Failed to get connection from pool: {}", e))
})?;
client.query_one(query, params)
}
/// Execute a query using the connection pool and return an optional row
pub fn query_opt_with_pool(
query: &str,
params: &[&(dyn postgres::types::ToSql + Sync)],
) -> Result<Option<Row>, PostgresError> {
let pool = get_postgres_pool()?;
let mut client = pool.get().map_err(|e| {
create_postgres_error(&format!("Failed to get connection from pool: {}", e))
})?;
client.query_opt(query, params)
}
/// Parameter builder for PostgreSQL queries
///
/// This struct helps build parameterized queries for PostgreSQL.
/// It provides a type-safe way to build query parameters.
#[derive(Default)]
pub struct QueryParams {
params: Vec<Box<dyn ToSql + Sync>>,
}
impl QueryParams {
/// Create a new empty parameter builder
pub fn new() -> Self {
Self { params: Vec::new() }
}
/// Add a parameter to the builder
pub fn add<T: 'static + ToSql + Sync>(&mut self, value: T) -> &mut Self {
self.params.push(Box::new(value));
self
}
/// Add a string parameter to the builder
pub fn add_str(&mut self, value: &str) -> &mut Self {
self.add(value.to_string())
}
/// Add an integer parameter to the builder
pub fn add_int(&mut self, value: i32) -> &mut Self {
self.add(value)
}
/// Add a float parameter to the builder
pub fn add_float(&mut self, value: f64) -> &mut Self {
self.add(value)
}
/// Add a boolean parameter to the builder
pub fn add_bool(&mut self, value: bool) -> &mut Self {
self.add(value)
}
/// Add an optional parameter to the builder
pub fn add_opt<T: 'static + ToSql + Sync>(&mut self, value: Option<T>) -> &mut Self {
if let Some(v) = value {
self.add(v);
} else {
// Add NULL value
self.params.push(Box::new(None::<String>));
}
self
}
/// Get the parameters as a slice of references
pub fn as_slice(&self) -> Vec<&(dyn ToSql + Sync)> {
self.params
.iter()
.map(|p| p.as_ref() as &(dyn ToSql + Sync))
.collect()
}
}
/// Execute a query with the parameter builder
pub fn execute_with_params(query_str: &str, params: &QueryParams) -> Result<u64, PostgresError> {
let client = get_postgres_client()?;
client.execute(query_str, &params.as_slice())
}
/// Execute a query with the parameter builder and return the rows
pub fn query_with_params(query_str: &str, params: &QueryParams) -> Result<Vec<Row>, PostgresError> {
let client = get_postgres_client()?;
client.query(query_str, &params.as_slice())
}
/// Execute a query with the parameter builder and return a single row
pub fn query_one_with_params(query_str: &str, params: &QueryParams) -> Result<Row, PostgresError> {
let client = get_postgres_client()?;
client.query_one(query_str, &params.as_slice())
}
/// Execute a query with the parameter builder and return an optional row
pub fn query_opt_with_params(
query_str: &str,
params: &QueryParams,
) -> Result<Option<Row>, PostgresError> {
let client = get_postgres_client()?;
client.query_opt(query_str, &params.as_slice())
}
/// Execute a query with the parameter builder using the connection pool
pub fn execute_with_pool_params(
query_str: &str,
params: &QueryParams,
) -> Result<u64, PostgresError> {
execute_with_pool(query_str, &params.as_slice())
}
/// Execute a query with the parameter builder using the connection pool and return the rows
pub fn query_with_pool_params(
query_str: &str,
params: &QueryParams,
) -> Result<Vec<Row>, PostgresError> {
query_with_pool(query_str, &params.as_slice())
}
/// Execute a query with the parameter builder using the connection pool and return a single row
pub fn query_one_with_pool_params(
query_str: &str,
params: &QueryParams,
) -> Result<Row, PostgresError> {
query_one_with_pool(query_str, &params.as_slice())
}
/// Execute a query with the parameter builder using the connection pool and return an optional row
pub fn query_opt_with_pool_params(
query_str: &str,
params: &QueryParams,
) -> Result<Option<Row>, PostgresError> {
query_opt_with_pool(query_str, &params.as_slice())
}
/// Send a notification on a channel
///
/// This function sends a notification on the specified channel with the specified payload.
///
/// Example:
/// ```no_run
/// use sal::postgresclient::notify;
///
/// notify("my_channel", "Hello, world!").expect("Failed to send notification");
/// ```
pub fn notify(channel: &str, payload: &str) -> Result<(), PostgresError> {
let client = get_postgres_client()?;
client.execute(&format!("NOTIFY {}, '{}'", channel, payload), &[])?;
Ok(())
}
/// Send a notification on a channel using the connection pool
///
/// This function sends a notification on the specified channel with the specified payload using the connection pool.
///
/// Example:
/// ```no_run
/// use sal::postgresclient::notify_with_pool;
///
/// notify_with_pool("my_channel", "Hello, world!").expect("Failed to send notification");
/// ```
pub fn notify_with_pool(channel: &str, payload: &str) -> Result<(), PostgresError> {
let pool = get_postgres_pool()?;
let mut client = pool.get().map_err(|e| {
create_postgres_error(&format!("Failed to get connection from pool: {}", e))
})?;
client.execute(&format!("NOTIFY {}, '{}'", channel, payload), &[])?;
Ok(())
}

View File

@ -1,843 +0,0 @@
use super::*;
use std::collections::HashMap;
use std::env;
#[cfg(test)]
mod postgres_client_tests {
use super::*;
#[test]
fn test_env_vars() {
// Save original environment variables to restore later
let original_host = env::var("POSTGRES_HOST").ok();
let original_port = env::var("POSTGRES_PORT").ok();
let original_user = env::var("POSTGRES_USER").ok();
let original_password = env::var("POSTGRES_PASSWORD").ok();
let original_db = env::var("POSTGRES_DB").ok();
// Set test environment variables
env::set_var("POSTGRES_HOST", "test-host");
env::set_var("POSTGRES_PORT", "5433");
env::set_var("POSTGRES_USER", "test-user");
env::set_var("POSTGRES_PASSWORD", "test-password");
env::set_var("POSTGRES_DB", "test-db");
// Test with invalid port
env::set_var("POSTGRES_PORT", "invalid");
// Test with unset values
env::remove_var("POSTGRES_HOST");
env::remove_var("POSTGRES_PORT");
env::remove_var("POSTGRES_USER");
env::remove_var("POSTGRES_PASSWORD");
env::remove_var("POSTGRES_DB");
// Restore original environment variables
if let Some(host) = original_host {
env::set_var("POSTGRES_HOST", host);
}
if let Some(port) = original_port {
env::set_var("POSTGRES_PORT", port);
}
if let Some(user) = original_user {
env::set_var("POSTGRES_USER", user);
}
if let Some(password) = original_password {
env::set_var("POSTGRES_PASSWORD", password);
}
if let Some(db) = original_db {
env::set_var("POSTGRES_DB", db);
}
}
#[test]
fn test_postgres_config_builder() {
// Test the PostgreSQL configuration builder
// Test default values
let config = PostgresConfigBuilder::new();
assert_eq!(config.host, "localhost");
assert_eq!(config.port, 5432);
assert_eq!(config.user, "postgres");
assert_eq!(config.password, None);
assert_eq!(config.database, "postgres");
assert_eq!(config.application_name, None);
assert_eq!(config.connect_timeout, None);
assert_eq!(config.ssl_mode, None);
// Test setting values
let config = PostgresConfigBuilder::new()
.host("pg.example.com")
.port(5433)
.user("test-user")
.password("test-password")
.database("test-db")
.application_name("test-app")
.connect_timeout(30)
.ssl_mode("require");
assert_eq!(config.host, "pg.example.com");
assert_eq!(config.port, 5433);
assert_eq!(config.user, "test-user");
assert_eq!(config.password, Some("test-password".to_string()));
assert_eq!(config.database, "test-db");
assert_eq!(config.application_name, Some("test-app".to_string()));
assert_eq!(config.connect_timeout, Some(30));
assert_eq!(config.ssl_mode, Some("require".to_string()));
}
#[test]
fn test_connection_string_building() {
// Test building connection strings
// Test default connection string
let config = PostgresConfigBuilder::new();
let conn_string = config.build_connection_string();
assert!(conn_string.contains("host=localhost"));
assert!(conn_string.contains("port=5432"));
assert!(conn_string.contains("user=postgres"));
assert!(conn_string.contains("dbname=postgres"));
assert!(!conn_string.contains("password="));
// Test with all options
let config = PostgresConfigBuilder::new()
.host("pg.example.com")
.port(5433)
.user("test-user")
.password("test-password")
.database("test-db")
.application_name("test-app")
.connect_timeout(30)
.ssl_mode("require");
let conn_string = config.build_connection_string();
assert!(conn_string.contains("host=pg.example.com"));
assert!(conn_string.contains("port=5433"));
assert!(conn_string.contains("user=test-user"));
assert!(conn_string.contains("password=test-password"));
assert!(conn_string.contains("dbname=test-db"));
assert!(conn_string.contains("application_name=test-app"));
assert!(conn_string.contains("connect_timeout=30"));
assert!(conn_string.contains("sslmode=require"));
}
#[test]
fn test_reset_mock() {
// This is a simplified test that doesn't require an actual PostgreSQL server
// Just verify that the reset function doesn't panic
if let Err(_) = reset() {
// If PostgreSQL is not available, this is expected to fail
// So we don't assert anything here
}
}
}
// Integration tests that require a real PostgreSQL server
// These tests will be skipped if PostgreSQL is not available
#[cfg(test)]
mod postgres_installer_tests {
use super::*;
use crate::virt::nerdctl::Container;
#[test]
fn test_postgres_installer_config() {
// Test default configuration
let config = PostgresInstallerConfig::default();
assert_eq!(config.container_name, "postgres");
assert_eq!(config.version, "latest");
assert_eq!(config.port, 5432);
assert_eq!(config.username, "postgres");
assert_eq!(config.password, "postgres");
assert_eq!(config.data_dir, None);
assert_eq!(config.env_vars.len(), 0);
assert_eq!(config.persistent, true);
// Test builder pattern
let config = PostgresInstallerConfig::new()
.container_name("my-postgres")
.version("15")
.port(5433)
.username("testuser")
.password("testpass")
.data_dir("/tmp/pgdata")
.env_var("POSTGRES_INITDB_ARGS", "--encoding=UTF8")
.persistent(false);
assert_eq!(config.container_name, "my-postgres");
assert_eq!(config.version, "15");
assert_eq!(config.port, 5433);
assert_eq!(config.username, "testuser");
assert_eq!(config.password, "testpass");
assert_eq!(config.data_dir, Some("/tmp/pgdata".to_string()));
assert_eq!(config.env_vars.len(), 1);
assert_eq!(
config.env_vars.get("POSTGRES_INITDB_ARGS").unwrap(),
"--encoding=UTF8"
);
assert_eq!(config.persistent, false);
}
#[test]
fn test_postgres_installer_error() {
// Test IoError
let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "File not found");
let installer_error = PostgresInstallerError::IoError(io_error);
assert!(format!("{}", installer_error).contains("I/O error"));
// Test NerdctlError
let nerdctl_error = PostgresInstallerError::NerdctlError("Container not found".to_string());
assert!(format!("{}", nerdctl_error).contains("Nerdctl error"));
// Test PostgresError
let postgres_error =
PostgresInstallerError::PostgresError("Database not found".to_string());
assert!(format!("{}", postgres_error).contains("PostgreSQL error"));
}
#[test]
fn test_install_postgres_with_defaults() {
// This is a unit test that doesn't actually install PostgreSQL
// It just tests the configuration and error handling
// Test with default configuration
let config = PostgresInstallerConfig::default();
// We expect this to fail because nerdctl is not available
let result = install_postgres(config);
assert!(result.is_err());
// Check that the error is a NerdctlError or IoError
match result {
Err(PostgresInstallerError::NerdctlError(_)) => {
// This is fine, we expected a NerdctlError
}
Err(PostgresInstallerError::IoError(_)) => {
// This is also fine, we expected an error
}
_ => panic!("Expected NerdctlError or IoError"),
}
}
#[test]
fn test_install_postgres_with_custom_config() {
// Test with custom configuration
let config = PostgresInstallerConfig::new()
.container_name("test-postgres")
.version("15")
.port(5433)
.username("testuser")
.password("testpass")
.data_dir("/tmp/pgdata")
.env_var("POSTGRES_INITDB_ARGS", "--encoding=UTF8")
.persistent(true);
// We expect this to fail because nerdctl is not available
let result = install_postgres(config);
assert!(result.is_err());
// Check that the error is a NerdctlError or IoError
match result {
Err(PostgresInstallerError::NerdctlError(_)) => {
// This is fine, we expected a NerdctlError
}
Err(PostgresInstallerError::IoError(_)) => {
// This is also fine, we expected an error
}
_ => panic!("Expected NerdctlError or IoError"),
}
}
#[test]
fn test_create_database() {
// Create a mock container
// In a real test, we would use mockall to create a mock container
// But for this test, we'll just test the error handling
// We expect this to fail because the container is not running
let result = create_database(
&Container {
name: "test-postgres".to_string(),
container_id: None,
image: Some("postgres:15".to_string()),
config: HashMap::new(),
ports: Vec::new(),
volumes: Vec::new(),
env_vars: HashMap::new(),
network: None,
network_aliases: Vec::new(),
cpu_limit: None,
memory_limit: None,
memory_swap_limit: None,
cpu_shares: None,
restart_policy: None,
health_check: None,
detach: false,
snapshotter: None,
},
"testdb",
);
assert!(result.is_err());
// Check that the error is a PostgresError
match result {
Err(PostgresInstallerError::PostgresError(msg)) => {
assert!(msg.contains("Container is not running"));
}
_ => panic!("Expected PostgresError"),
}
}
#[test]
fn test_execute_sql() {
// Create a mock container
// In a real test, we would use mockall to create a mock container
// But for this test, we'll just test the error handling
// We expect this to fail because the container is not running
let result = execute_sql(
&Container {
name: "test-postgres".to_string(),
container_id: None,
image: Some("postgres:15".to_string()),
config: HashMap::new(),
ports: Vec::new(),
volumes: Vec::new(),
env_vars: HashMap::new(),
network: None,
network_aliases: Vec::new(),
cpu_limit: None,
memory_limit: None,
memory_swap_limit: None,
cpu_shares: None,
restart_policy: None,
health_check: None,
detach: false,
snapshotter: None,
},
"testdb",
"SELECT 1",
);
assert!(result.is_err());
// Check that the error is a PostgresError
match result {
Err(PostgresInstallerError::PostgresError(msg)) => {
assert!(msg.contains("Container is not running"));
}
_ => panic!("Expected PostgresError"),
}
}
#[test]
fn test_is_postgres_running() {
// Create a mock container
// In a real test, we would use mockall to create a mock container
// But for this test, we'll just test the error handling
// We expect this to return false because the container is not running
let result = is_postgres_running(&Container {
name: "test-postgres".to_string(),
container_id: None,
image: Some("postgres:15".to_string()),
config: HashMap::new(),
ports: Vec::new(),
volumes: Vec::new(),
env_vars: HashMap::new(),
network: None,
network_aliases: Vec::new(),
cpu_limit: None,
memory_limit: None,
memory_swap_limit: None,
cpu_shares: None,
restart_policy: None,
health_check: None,
detach: false,
snapshotter: None,
});
assert!(result.is_ok());
assert_eq!(result.unwrap(), false);
}
}
#[cfg(test)]
mod postgres_integration_tests {
use super::*;
use std::time::Duration;
// Helper function to check if PostgreSQL is available
fn is_postgres_available() -> bool {
match get_postgres_client() {
Ok(_) => true,
Err(_) => false,
}
}
#[test]
fn test_postgres_client_integration() {
if !is_postgres_available() {
println!("Skipping PostgreSQL integration tests - PostgreSQL server not available");
return;
}
println!("Running PostgreSQL integration tests...");
// Test basic operations
test_basic_postgres_operations();
// Test error handling
test_error_handling();
}
#[test]
fn test_connection_pool() {
if !is_postgres_available() {
println!("Skipping PostgreSQL connection pool tests - PostgreSQL server not available");
return;
}
run_connection_pool_test();
}
fn run_connection_pool_test() {
println!("Running PostgreSQL connection pool tests...");
// Test creating a connection pool
let config = PostgresConfigBuilder::new()
.use_pool(true)
.pool_max_size(5)
.pool_min_idle(1)
.pool_connection_timeout(Duration::from_secs(5));
let pool_result = config.build_pool();
assert!(pool_result.is_ok());
let pool = pool_result.unwrap();
// Test getting a connection from the pool
let conn_result = pool.get();
assert!(conn_result.is_ok());
// Test executing a query with the connection
let mut conn = conn_result.unwrap();
let query_result = conn.query("SELECT 1", &[]);
assert!(query_result.is_ok());
// Test the global pool
let global_pool_result = get_postgres_pool();
assert!(global_pool_result.is_ok());
// Test executing queries with the pool
let create_table_query = "
CREATE TEMPORARY TABLE pool_test (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL
)
";
let create_result = execute_with_pool(create_table_query, &[]);
assert!(create_result.is_ok());
// Test with parameters
let insert_result = execute_with_pool(
"INSERT INTO pool_test (name) VALUES ($1) RETURNING id",
&[&"test_pool"],
);
assert!(insert_result.is_ok());
// Test with QueryParams
let mut params = QueryParams::new();
params.add_str("test_pool_params");
let insert_params_result = execute_with_pool_params(
"INSERT INTO pool_test (name) VALUES ($1) RETURNING id",
&params,
);
assert!(insert_params_result.is_ok());
// Test query functions
let query_result = query_with_pool("SELECT * FROM pool_test", &[]);
assert!(query_result.is_ok());
let rows = query_result.unwrap();
assert_eq!(rows.len(), 2);
// Test query_one
let query_one_result =
query_one_with_pool("SELECT * FROM pool_test WHERE name = $1", &[&"test_pool"]);
assert!(query_one_result.is_ok());
// Test query_opt
let query_opt_result =
query_opt_with_pool("SELECT * FROM pool_test WHERE name = $1", &[&"nonexistent"]);
assert!(query_opt_result.is_ok());
assert!(query_opt_result.unwrap().is_none());
// Test resetting the pool
let reset_result = reset_pool();
assert!(reset_result.is_ok());
// Test getting the pool again after reset
let pool_after_reset = get_postgres_pool();
assert!(pool_after_reset.is_ok());
}
fn test_basic_postgres_operations() {
if !is_postgres_available() {
return;
}
// Create a test table
let create_table_query = "
CREATE TEMPORARY TABLE test_table (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
value INTEGER
)
";
let create_result = execute(create_table_query, &[]);
assert!(create_result.is_ok());
// Insert data
let insert_query = "
INSERT INTO test_table (name, value)
VALUES ($1, $2)
RETURNING id
";
let insert_result = query(insert_query, &[&"test_name", &42]);
assert!(insert_result.is_ok());
let rows = insert_result.unwrap();
assert_eq!(rows.len(), 1);
let id: i32 = rows[0].get(0);
assert!(id > 0);
// Query data
let select_query = "
SELECT id, name, value
FROM test_table
WHERE id = $1
";
let select_result = query_one(select_query, &[&id]);
assert!(select_result.is_ok());
let row = select_result.unwrap();
let name: String = row.get(1);
let value: i32 = row.get(2);
assert_eq!(name, "test_name");
assert_eq!(value, 42);
// Update data
let update_query = "
UPDATE test_table
SET value = $1
WHERE id = $2
";
let update_result = execute(update_query, &[&100, &id]);
assert!(update_result.is_ok());
assert_eq!(update_result.unwrap(), 1); // 1 row affected
// Verify update
let verify_query = "
SELECT value
FROM test_table
WHERE id = $1
";
let verify_result = query_one(verify_query, &[&id]);
assert!(verify_result.is_ok());
let row = verify_result.unwrap();
let updated_value: i32 = row.get(0);
assert_eq!(updated_value, 100);
// Delete data
let delete_query = "
DELETE FROM test_table
WHERE id = $1
";
let delete_result = execute(delete_query, &[&id]);
assert!(delete_result.is_ok());
assert_eq!(delete_result.unwrap(), 1); // 1 row affected
}
#[test]
fn test_query_params() {
if !is_postgres_available() {
println!("Skipping PostgreSQL parameter tests - PostgreSQL server not available");
return;
}
run_query_params_test();
}
#[test]
fn test_transactions() {
if !is_postgres_available() {
println!("Skipping PostgreSQL transaction tests - PostgreSQL server not available");
return;
}
println!("Running PostgreSQL transaction tests...");
// Test successful transaction
let result = transaction(|client| {
// Create a temporary table
client.execute(
"CREATE TEMPORARY TABLE transaction_test (id SERIAL PRIMARY KEY, name TEXT NOT NULL)",
&[],
)?;
// Insert data
client.execute(
"INSERT INTO transaction_test (name) VALUES ($1)",
&[&"test_transaction"],
)?;
// Query data
let rows = client.query(
"SELECT * FROM transaction_test WHERE name = $1",
&[&"test_transaction"],
)?;
assert_eq!(rows.len(), 1);
let name: String = rows[0].get(1);
assert_eq!(name, "test_transaction");
// Return success
Ok(true)
});
assert!(result.is_ok());
assert_eq!(result.unwrap(), true);
// Test failed transaction
let result = transaction(|client| {
// Create a temporary table
client.execute(
"CREATE TEMPORARY TABLE transaction_test_fail (id SERIAL PRIMARY KEY, name TEXT NOT NULL)",
&[],
)?;
// Insert data
client.execute(
"INSERT INTO transaction_test_fail (name) VALUES ($1)",
&[&"test_transaction_fail"],
)?;
// Cause an error with invalid SQL
client.execute("THIS IS INVALID SQL", &[])?;
// This should not be reached
Ok(false)
});
assert!(result.is_err());
// Verify that the table was not created (transaction was rolled back)
let verify_result = query("SELECT * FROM transaction_test_fail", &[]);
assert!(verify_result.is_err());
// Test transaction with pool
let result = transaction_with_pool(|client| {
// Create a temporary table
client.execute(
"CREATE TEMPORARY TABLE transaction_pool_test (id SERIAL PRIMARY KEY, name TEXT NOT NULL)",
&[],
)?;
// Insert data
client.execute(
"INSERT INTO transaction_pool_test (name) VALUES ($1)",
&[&"test_transaction_pool"],
)?;
// Query data
let rows = client.query(
"SELECT * FROM transaction_pool_test WHERE name = $1",
&[&"test_transaction_pool"],
)?;
assert_eq!(rows.len(), 1);
let name: String = rows[0].get(1);
assert_eq!(name, "test_transaction_pool");
// Return success
Ok(true)
});
assert!(result.is_ok());
assert_eq!(result.unwrap(), true);
}
fn run_query_params_test() {
println!("Running PostgreSQL parameter tests...");
// Create a test table
let create_table_query = "
CREATE TEMPORARY TABLE param_test (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
value INTEGER,
active BOOLEAN,
score REAL
)
";
let create_result = execute(create_table_query, &[]);
assert!(create_result.is_ok());
// Test QueryParams builder
let mut params = QueryParams::new();
params.add_str("test_name");
params.add_int(42);
params.add_bool(true);
params.add_float(3.14);
// Insert data using QueryParams
let insert_query = "
INSERT INTO param_test (name, value, active, score)
VALUES ($1, $2, $3, $4)
RETURNING id
";
let insert_result = query_with_params(insert_query, &params);
assert!(insert_result.is_ok());
let rows = insert_result.unwrap();
assert_eq!(rows.len(), 1);
let id: i32 = rows[0].get(0);
assert!(id > 0);
// Query data using QueryParams
let mut query_params = QueryParams::new();
query_params.add_int(id);
let select_query = "
SELECT id, name, value, active, score
FROM param_test
WHERE id = $1
";
let select_result = query_one_with_params(select_query, &query_params);
assert!(select_result.is_ok());
let row = select_result.unwrap();
let name: String = row.get(1);
let value: i32 = row.get(2);
let active: bool = row.get(3);
let score: f64 = row.get(4);
assert_eq!(name, "test_name");
assert_eq!(value, 42);
assert_eq!(active, true);
assert_eq!(score, 3.14);
// Test optional parameters
let mut update_params = QueryParams::new();
update_params.add_int(100);
update_params.add_opt::<String>(None);
update_params.add_int(id);
let update_query = "
UPDATE param_test
SET value = $1, name = COALESCE($2, name)
WHERE id = $3
";
let update_result = execute_with_params(update_query, &update_params);
assert!(update_result.is_ok());
assert_eq!(update_result.unwrap(), 1); // 1 row affected
// Verify update
let verify_result = query_one_with_params(select_query, &query_params);
assert!(verify_result.is_ok());
let row = verify_result.unwrap();
let name: String = row.get(1);
let value: i32 = row.get(2);
assert_eq!(name, "test_name"); // Name should be unchanged
assert_eq!(value, 100); // Value should be updated
// Test query_opt_with_params
let mut nonexistent_params = QueryParams::new();
nonexistent_params.add_int(9999); // ID that doesn't exist
let opt_query = "
SELECT id, name
FROM param_test
WHERE id = $1
";
let opt_result = query_opt_with_params(opt_query, &nonexistent_params);
assert!(opt_result.is_ok());
assert!(opt_result.unwrap().is_none());
// Clean up
let delete_query = "
DELETE FROM param_test
WHERE id = $1
";
let delete_result = execute_with_params(delete_query, &query_params);
assert!(delete_result.is_ok());
assert_eq!(delete_result.unwrap(), 1); // 1 row affected
}
fn test_error_handling() {
if !is_postgres_available() {
return;
}
// Test invalid SQL
let invalid_query = "SELECT * FROM nonexistent_table";
let invalid_result = query(invalid_query, &[]);
assert!(invalid_result.is_err());
// Test parameter type mismatch
let mismatch_query = "SELECT $1::integer";
let mismatch_result = query(mismatch_query, &[&"not_an_integer"]);
assert!(mismatch_result.is_err());
// Test query_one with no results
let empty_query = "SELECT * FROM pg_tables WHERE tablename = 'nonexistent_table'";
let empty_result = query_one(empty_query, &[]);
assert!(empty_result.is_err());
// Test query_opt with no results
let opt_query = "SELECT * FROM pg_tables WHERE tablename = 'nonexistent_table'";
let opt_result = query_opt(opt_query, &[]);
assert!(opt_result.is_ok());
assert!(opt_result.unwrap().is_none());
}
#[test]
fn test_notify() {
if !is_postgres_available() {
println!("Skipping PostgreSQL notification tests - PostgreSQL server not available");
return;
}
println!("Running PostgreSQL notification tests...");
// Test sending a notification
let result = notify("test_channel", "test_payload");
assert!(result.is_ok());
// Test sending a notification with the pool
let result = notify_with_pool("test_channel_pool", "test_payload_pool");
assert!(result.is_ok());
}
}

View File

@ -1,10 +1,10 @@
use std::error::Error;
use std::fmt;
use std::io;
use std::process::Command;
use std::fmt;
use std::error::Error;
use std::io;
/// Error type for process management operations
///
///
/// This enum represents various errors that can occur during process management
/// operations such as listing, finding, or killing processes.
#[derive(Debug)]
@ -23,18 +23,11 @@ pub enum ProcessError {
impl fmt::Display for ProcessError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ProcessError::CommandExecutionFailed(e) => {
write!(f, "Failed to execute command: {}", e)
}
ProcessError::CommandExecutionFailed(e) => write!(f, "Failed to execute command: {}", e),
ProcessError::CommandFailed(e) => write!(f, "{}", e),
ProcessError::NoProcessFound(pattern) => {
write!(f, "No processes found matching '{}'", pattern)
}
ProcessError::MultipleProcessesFound(pattern, count) => write!(
f,
"Multiple processes ({}) found matching '{}'",
count, pattern
),
ProcessError::NoProcessFound(pattern) => write!(f, "No processes found matching '{}'", pattern),
ProcessError::MultipleProcessesFound(pattern, count) =>
write!(f, "Multiple processes ({}) found matching '{}'", count, pattern),
}
}
}
@ -60,20 +53,18 @@ pub struct ProcessInfo {
/**
* Check if a command exists in PATH.
*
*
* # Arguments
*
*
* * `cmd` - The command to check
*
*
* # Returns
*
*
* * `Option<String>` - The full path to the command if found, None otherwise
*
*
* # Examples
*
*
* ```
* use sal::process::which;
*
* match which("git") {
* Some(path) => println!("Git is installed at: {}", path),
* None => println!("Git is not installed"),
@ -83,12 +74,14 @@ pub struct ProcessInfo {
pub fn which(cmd: &str) -> Option<String> {
#[cfg(target_os = "windows")]
let which_cmd = "where";
#[cfg(any(target_os = "macos", target_os = "linux"))]
let which_cmd = "which";
let output = Command::new(which_cmd).arg(cmd).output();
let output = Command::new(which_cmd)
.arg(cmd)
.output();
match output {
Ok(out) => {
if out.status.success() {
@ -97,34 +90,29 @@ pub fn which(cmd: &str) -> Option<String> {
} else {
None
}
}
Err(_) => None,
},
Err(_) => None
}
}
/**
* Kill processes matching a pattern.
*
*
* # Arguments
*
*
* * `pattern` - The pattern to match against process names
*
*
* # Returns
*
*
* * `Ok(String)` - A success message indicating processes were killed or none were found
* * `Err(ProcessError)` - An error if the kill operation failed
*
*
* # Examples
*
*
* ```
* // Kill all processes with "server" in their name
* use sal::process::kill;
*
* fn main() -> Result<(), Box<dyn std::error::Error>> {
* let result = kill("server")?;
* println!("{}", result);
* Ok(())
* }
* let result = kill("server")?;
* println!("{}", result);
* ```
*/
pub fn kill(pattern: &str) -> Result<String, ProcessError> {
@ -133,7 +121,7 @@ pub fn kill(pattern: &str) -> Result<String, ProcessError> {
{
// On Windows, use taskkill with wildcard support
let mut args = vec!["/F"]; // Force kill
if pattern.contains('*') {
// If it contains wildcards, use filter
args.extend(&["/FI", &format!("IMAGENAME eq {}", pattern)]);
@ -141,12 +129,12 @@ pub fn kill(pattern: &str) -> Result<String, ProcessError> {
// Otherwise use image name directly
args.extend(&["/IM", pattern]);
}
let output = Command::new("taskkill")
.args(&args)
.output()
.map_err(ProcessError::CommandExecutionFailed)?;
if output.status.success() {
Ok("Successfully killed processes".to_string())
} else {
@ -156,20 +144,14 @@ pub fn kill(pattern: &str) -> Result<String, ProcessError> {
if stdout.contains("No tasks") {
Ok("No matching processes found".to_string())
} else {
Err(ProcessError::CommandFailed(format!(
"Failed to kill processes: {}",
stdout
)))
Err(ProcessError::CommandFailed(format!("Failed to kill processes: {}", stdout)))
}
} else {
Err(ProcessError::CommandFailed(format!(
"Failed to kill processes: {}",
error
)))
Err(ProcessError::CommandFailed(format!("Failed to kill processes: {}", error)))
}
}
}
#[cfg(any(target_os = "macos", target_os = "linux"))]
{
// On Unix-like systems, use pkill which has built-in pattern matching
@ -178,7 +160,7 @@ pub fn kill(pattern: &str) -> Result<String, ProcessError> {
.arg(pattern)
.output()
.map_err(ProcessError::CommandExecutionFailed)?;
// pkill returns 0 if processes were killed, 1 if none matched
if output.status.success() {
Ok("Successfully killed processes".to_string())
@ -186,47 +168,39 @@ pub fn kill(pattern: &str) -> Result<String, ProcessError> {
Ok("No matching processes found".to_string())
} else {
let error = String::from_utf8_lossy(&output.stderr);
Err(ProcessError::CommandFailed(format!(
"Failed to kill processes: {}",
error
)))
Err(ProcessError::CommandFailed(format!("Failed to kill processes: {}", error)))
}
}
}
/**
* List processes matching a pattern (or all if pattern is empty).
*
*
* # Arguments
*
*
* * `pattern` - The pattern to match against process names (empty string for all processes)
*
*
* # Returns
*
*
* * `Ok(Vec<ProcessInfo>)` - A vector of process information for matching processes
* * `Err(ProcessError)` - An error if the list operation failed
*
*
* # Examples
*
*
* ```
* // List all processes
* use sal::process::process_list;
*
* fn main() -> Result<(), Box<dyn std::error::Error>> {
* let processes = process_list("")?;
*
* // List processes with "server" in their name
* let processes = process_list("server")?;
* for proc in processes {
* println!("PID: {}, Name: {}", proc.pid, proc.name);
* }
* Ok(())
* let processes = process_list("")?;
*
* // List processes with "server" in their name
* let processes = process_list("server")?;
* for proc in processes {
* println!("PID: {}, Name: {}", proc.pid, proc.name);
* }
* ```
*/
pub fn process_list(pattern: &str) -> Result<Vec<ProcessInfo>, ProcessError> {
let mut processes = Vec::new();
// Platform specific implementations
#[cfg(target_os = "windows")]
{
@ -235,23 +209,22 @@ pub fn process_list(pattern: &str) -> Result<Vec<ProcessInfo>, ProcessError> {
.args(&["process", "list", "brief"])
.output()
.map_err(ProcessError::CommandExecutionFailed)?;
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
// Parse output (assuming format: Handle Name Priority)
for line in stdout.lines().skip(1) {
// Skip header
for line in stdout.lines().skip(1) { // Skip header
let parts: Vec<&str> = line.trim().split_whitespace().collect();
if parts.len() >= 2 {
let pid = parts[0].parse::<i64>().unwrap_or(0);
let name = parts[1].to_string();
// Filter by pattern if provided
if !pattern.is_empty() && !name.contains(pattern) {
continue;
}
processes.push(ProcessInfo {
pid,
name,
@ -262,13 +235,10 @@ pub fn process_list(pattern: &str) -> Result<Vec<ProcessInfo>, ProcessError> {
}
} else {
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
return Err(ProcessError::CommandFailed(format!(
"Failed to list processes: {}",
stderr
)));
return Err(ProcessError::CommandFailed(format!("Failed to list processes: {}", stderr)));
}
}
#[cfg(any(target_os = "macos", target_os = "linux"))]
{
// Unix implementation using ps
@ -276,23 +246,22 @@ pub fn process_list(pattern: &str) -> Result<Vec<ProcessInfo>, ProcessError> {
.args(&["-eo", "pid,comm"])
.output()
.map_err(ProcessError::CommandExecutionFailed)?;
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
// Parse output (assuming format: PID COMMAND)
for line in stdout.lines().skip(1) {
// Skip header
for line in stdout.lines().skip(1) { // Skip header
let parts: Vec<&str> = line.trim().split_whitespace().collect();
if parts.len() >= 2 {
let pid = parts[0].parse::<i64>().unwrap_or(0);
let name = parts[1].to_string();
// Filter by pattern if provided
if !pattern.is_empty() && !name.contains(pattern) {
continue;
}
processes.push(ProcessInfo {
pid,
name,
@ -303,49 +272,38 @@ pub fn process_list(pattern: &str) -> Result<Vec<ProcessInfo>, ProcessError> {
}
} else {
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
return Err(ProcessError::CommandFailed(format!(
"Failed to list processes: {}",
stderr
)));
return Err(ProcessError::CommandFailed(format!("Failed to list processes: {}", stderr)));
}
}
Ok(processes)
}
/**
* Get a single process matching the pattern (error if 0 or more than 1 match).
*
*
* # Arguments
*
*
* * `pattern` - The pattern to match against process names
*
*
* # Returns
*
*
* * `Ok(ProcessInfo)` - Information about the matching process
* * `Err(ProcessError)` - An error if no process or multiple processes match
*
*
* # Examples
*
* ```no_run
* use sal::process::process_get;
*
* fn main() -> Result<(), Box<dyn std::error::Error>> {
* let process = process_get("unique-server-name")?;
* println!("Found process: {} (PID: {})", process.name, process.pid);
* Ok(())
* }
*
* ```
* let process = process_get("unique-server-name")?;
* println!("Found process: {} (PID: {})", process.name, process.pid);
* ```
*/
pub fn process_get(pattern: &str) -> Result<ProcessInfo, ProcessError> {
let processes = process_list(pattern)?;
match processes.len() {
0 => Err(ProcessError::NoProcessFound(pattern.to_string())),
1 => Ok(processes[0].clone()),
_ => Err(ProcessError::MultipleProcessesFound(
pattern.to_string(),
processes.len(),
)),
_ => Err(ProcessError::MultipleProcessesFound(pattern.to_string(), processes.len())),
}
}

View File

@ -6,13 +6,10 @@ A robust Redis client wrapper for Rust applications that provides connection man
- **Singleton Pattern**: Maintains a global Redis client instance, so we don't re-int all the time.
- **Connection Management**: Automatically handles connection creation and reconnection
- **Flexible Connectivity**:
- **Flexible Connectivity**:
- Tries Unix socket connection first (`$HOME/hero/var/myredis.sock`)
- Falls back to TCP connection (localhost) if socket connection fails
- **Database Selection**: Uses the `REDISDB` environment variable to select the Redis database (defaults to 0)
- **Authentication Support**: Supports username/password authentication
- **Builder Pattern**: Flexible configuration with a builder pattern
- **TLS Support**: Optional TLS encryption for secure connections
- **Error Handling**: Comprehensive error handling with detailed error messages
- **Thread Safety**: Safe to use in multi-threaded applications
@ -55,51 +52,9 @@ let result: redis::RedisResult<()> = client.execute(&mut cmd);
reset()?;
```
### Builder Pattern
The module provides a builder pattern for flexible configuration:
```rust
use crate::redisclient::{RedisConfigBuilder, with_config};
// Create a configuration builder
let config = RedisConfigBuilder::new()
.host("redis.example.com")
.port(6379)
.db(1)
.username("user")
.password("secret")
.use_tls(true)
.connection_timeout(30);
// Connect with the configuration
let client = with_config(config)?;
```
### Unix Socket Connection
You can explicitly configure a Unix socket connection:
```rust
use crate::redisclient::{RedisConfigBuilder, with_config};
// Create a configuration builder for Unix socket
let config = RedisConfigBuilder::new()
.use_unix_socket(true)
.socket_path("/path/to/redis.sock")
.db(1);
// Connect with the configuration
let client = with_config(config)?;
```
## Environment Variables
- `REDISDB`: Specifies the Redis database number to use (default: 0)
- `REDIS_HOST`: Specifies the Redis host (default: 127.0.0.1)
- `REDIS_PORT`: Specifies the Redis port (default: 6379)
- `REDIS_USERNAME`: Specifies the Redis username for authentication
- `REDIS_PASSWORD`: Specifies the Redis password for authentication
- `HOME`: Used to determine the path to the Redis Unix socket
## Connection Strategy
@ -122,25 +77,6 @@ The module includes both unit tests and integration tests:
- Integration tests that require a real Redis server
- Tests automatically skip if Redis is not available
### Unit Tests
- Tests for the builder pattern and configuration
- Tests for connection URL building
- Tests for environment variable handling
### Integration Tests
- Tests for basic Redis operations (SET, GET, EXPIRE)
- Tests for hash operations (HSET, HGET, HGETALL, HDEL)
- Tests for list operations (RPUSH, LLEN, LRANGE, LPOP)
- Tests for error handling (invalid commands, wrong data types)
Run the tests with:
```bash
cargo test --lib redisclient::tests
```
## Thread Safety
The Redis client is wrapped in an `Arc<Mutex<>>` to ensure thread safety when accessing the global instance.

View File

@ -1,149 +1,9 @@
use lazy_static::lazy_static;
use redis::{Client, Cmd, Connection, RedisError, RedisResult};
use redis::{Client, Connection, RedisError, RedisResult, Cmd};
use std::env;
use std::path::Path;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, Once};
/// Redis connection configuration builder
///
/// This struct is used to build a Redis connection configuration.
/// It follows the builder pattern to allow for flexible configuration.
#[derive(Clone)]
pub struct RedisConfigBuilder {
pub host: String,
pub port: u16,
pub db: i64,
pub username: Option<String>,
pub password: Option<String>,
pub use_tls: bool,
pub use_unix_socket: bool,
pub socket_path: Option<String>,
pub connection_timeout: Option<u64>,
}
impl Default for RedisConfigBuilder {
fn default() -> Self {
Self {
host: "127.0.0.1".to_string(),
port: 6379,
db: 0,
username: None,
password: None,
use_tls: false,
use_unix_socket: false,
socket_path: None,
connection_timeout: None,
}
}
}
impl RedisConfigBuilder {
/// Create a new Redis connection configuration builder with default values
pub fn new() -> Self {
Self::default()
}
/// Set the host for the Redis connection
pub fn host(mut self, host: &str) -> Self {
self.host = host.to_string();
self
}
/// Set the port for the Redis connection
pub fn port(mut self, port: u16) -> Self {
self.port = port;
self
}
/// Set the database for the Redis connection
pub fn db(mut self, db: i64) -> Self {
self.db = db;
self
}
/// Set the username for the Redis connection (Redis 6.0+)
pub fn username(mut self, username: &str) -> Self {
self.username = Some(username.to_string());
self
}
/// Set the password for the Redis connection
pub fn password(mut self, password: &str) -> Self {
self.password = Some(password.to_string());
self
}
/// Enable TLS for the Redis connection
pub fn use_tls(mut self, use_tls: bool) -> Self {
self.use_tls = use_tls;
self
}
/// Use Unix socket for the Redis connection
pub fn use_unix_socket(mut self, use_unix_socket: bool) -> Self {
self.use_unix_socket = use_unix_socket;
self
}
/// Set the Unix socket path for the Redis connection
pub fn socket_path(mut self, socket_path: &str) -> Self {
self.socket_path = Some(socket_path.to_string());
self.use_unix_socket = true;
self
}
/// Set the connection timeout in seconds
pub fn connection_timeout(mut self, seconds: u64) -> Self {
self.connection_timeout = Some(seconds);
self
}
/// Build the connection URL from the configuration
pub fn build_connection_url(&self) -> String {
if self.use_unix_socket {
if let Some(ref socket_path) = self.socket_path {
return format!("unix://{}", socket_path);
} else {
// Default socket path
let home_dir = env::var("HOME").unwrap_or_else(|_| String::from("/root"));
return format!("unix://{}/hero/var/myredis.sock", home_dir);
}
}
let mut url = if self.use_tls {
format!("rediss://{}:{}", self.host, self.port)
} else {
format!("redis://{}:{}", self.host, self.port)
};
// Add authentication if provided
if let Some(ref username) = self.username {
if let Some(ref password) = self.password {
url = format!(
"redis://{}:{}@{}:{}",
username, password, self.host, self.port
);
} else {
url = format!("redis://{}@{}:{}", username, self.host, self.port);
}
} else if let Some(ref password) = self.password {
url = format!("redis://:{}@{}:{}", password, self.host, self.port);
}
// Add database
url = format!("{}/{}", url, self.db);
url
}
/// Build a Redis client from the configuration
pub fn build(&self) -> RedisResult<(Client, i64)> {
let url = self.build_connection_url();
let client = Client::open(url)?;
Ok((client, self.db))
}
}
use std::sync::atomic::{AtomicBool, Ordering};
use lazy_static::lazy_static;
// Global Redis client instance using lazy_static
lazy_static! {
@ -173,7 +33,7 @@ impl RedisClientWrapper {
// Execute a command on the Redis connection
pub fn execute<T: redis::FromRedisValue>(&self, cmd: &mut Cmd) -> RedisResult<T> {
let mut conn_guard = self.connection.lock().unwrap();
// If we don't have a connection or it's not working, create a new one
if conn_guard.is_none() || {
if let Some(ref mut conn) = *conn_guard {
@ -195,25 +55,22 @@ impl RedisClientWrapper {
}
let mut conn = self.client.get_connection()?;
// Ping Redis to ensure it works
let ping_result: String = redis::cmd("PING").query(&mut conn)?;
if ping_result != "PONG" {
return Err(RedisError::from((
redis::ErrorKind::ResponseError,
"Failed to ping Redis server",
)));
return Err(RedisError::from((redis::ErrorKind::ResponseError, "Failed to ping Redis server")));
}
// Select the database
redis::cmd("SELECT").arg(self.db).execute(&mut conn);
self.initialized.store(true, Ordering::Relaxed);
// Store the connection
let mut conn_guard = self.connection.lock().unwrap();
*conn_guard = Some(conn);
Ok(())
}
}
@ -227,91 +84,65 @@ pub fn get_redis_client() -> RedisResult<Arc<RedisClientWrapper>> {
return Ok(Arc::clone(client));
}
}
// Create a new client
let client = create_redis_client()?;
// Store the client globally
{
let mut guard = REDIS_CLIENT.lock().unwrap();
*guard = Some(Arc::clone(&client));
}
Ok(client)
}
// Create a new Redis client
fn create_redis_client() -> RedisResult<Arc<RedisClientWrapper>> {
// Get Redis configuration from environment variables
let db = get_redis_db();
let password = env::var("REDIS_PASSWORD").ok();
let username = env::var("REDIS_USERNAME").ok();
let host = env::var("REDIS_HOST").unwrap_or_else(|_| String::from("127.0.0.1"));
let port = env::var("REDIS_PORT")
.ok()
.and_then(|p| p.parse::<u16>().ok())
.unwrap_or(6379);
// Create a builder with environment variables
let mut builder = RedisConfigBuilder::new().host(&host).port(port).db(db);
if let Some(user) = username {
builder = builder.username(&user);
}
if let Some(pass) = password {
builder = builder.password(&pass);
}
// First try: Connect via Unix socket if it exists
// First try: Connect via Unix socket
let home_dir = env::var("HOME").unwrap_or_else(|_| String::from("/root"));
let socket_path = format!("{}/hero/var/myredis.sock", home_dir);
if Path::new(&socket_path).exists() {
// Try to connect via Unix socket
let socket_builder = builder.clone().socket_path(&socket_path);
match socket_builder.build() {
Ok((client, db)) => {
let socket_url = format!("unix://{}", socket_path);
match Client::open(socket_url) {
Ok(client) => {
let db = get_redis_db();
let wrapper = Arc::new(RedisClientWrapper::new(client, db));
// Initialize the client
if let Err(err) = wrapper.initialize() {
eprintln!(
"Socket exists at {} but connection failed: {}",
socket_path, err
);
eprintln!("Socket exists at {} but connection failed: {}", socket_path, err);
} else {
return Ok(wrapper);
}
}
},
Err(err) => {
eprintln!(
"Socket exists at {} but connection failed: {}",
socket_path, err
);
eprintln!("Socket exists at {} but connection failed: {}", socket_path, err);
}
}
}
// Second try: Connect via TCP
match builder.clone().build() {
Ok((client, db)) => {
// Second try: Connect via TCP to localhost
let tcp_url = "redis://127.0.0.1/";
match Client::open(tcp_url) {
Ok(client) => {
let db = get_redis_db();
let wrapper = Arc::new(RedisClientWrapper::new(client, db));
// Initialize the client
wrapper.initialize()?;
Ok(wrapper)
},
Err(err) => {
Err(RedisError::from((
redis::ErrorKind::IoError,
"Failed to connect to Redis",
format!("Could not connect via socket at {} or via TCP to localhost: {}", socket_path, err)
)))
}
Err(err) => Err(RedisError::from((
redis::ErrorKind::IoError,
"Failed to connect to Redis",
format!(
"Could not connect via socket at {} or via TCP to {}:{}: {}",
socket_path, host, port, err
),
))),
}
}
@ -330,7 +161,7 @@ pub fn reset() -> RedisResult<()> {
let mut client_guard = REDIS_CLIENT.lock().unwrap();
*client_guard = None;
}
// Create a new client, only return error if it fails
// We don't need to return the client itself
get_redis_client()?;
@ -344,18 +175,4 @@ where
{
let client = get_redis_client()?;
client.execute(cmd)
}
/// Create a new Redis client with custom configuration
///
/// # Arguments
///
/// * `config` - The Redis connection configuration builder
///
/// # Returns
///
/// * `RedisResult<Client>` - The Redis client if successful, error otherwise
pub fn with_config(config: RedisConfigBuilder) -> RedisResult<Client> {
let (client, _) = config.build()?;
Ok(client)
}
}

View File

@ -1,25 +1,25 @@
use super::*;
use redis::RedisResult;
use std::env;
use redis::RedisResult;
#[cfg(test)]
mod redis_client_tests {
use super::*;
#[test]
fn test_env_vars() {
// Save original REDISDB value to restore later
let original_redisdb = env::var("REDISDB").ok();
// Set test environment variables
env::set_var("REDISDB", "5");
// Test with invalid value
env::set_var("REDISDB", "invalid");
// Test with unset value
env::remove_var("REDISDB");
// Restore original REDISDB value
if let Some(redisdb) = original_redisdb {
env::set_var("REDISDB", redisdb);
@ -27,21 +27,21 @@ mod redis_client_tests {
env::remove_var("REDISDB");
}
}
#[test]
fn test_redis_client_creation_mock() {
// This is a simplified test that doesn't require an actual Redis server
// It just verifies that the function handles environment variables correctly
// Save original HOME value to restore later
let original_home = env::var("HOME").ok();
// Set HOME to a test value
env::set_var("HOME", "/tmp");
// The actual client creation would be tested in integration tests
// with a real Redis server or a mock
// Restore original HOME value
if let Some(home) = original_home {
env::set_var("HOME", home);
@ -49,12 +49,12 @@ mod redis_client_tests {
env::remove_var("HOME");
}
}
#[test]
fn test_reset_mock() {
// This is a simplified test that doesn't require an actual Redis server
// In a real test, we would need to mock the Redis client
// Just verify that the reset function doesn't panic
// This is a minimal test - in a real scenario, we would use mocking
// to verify that the client is properly reset
@ -63,77 +63,6 @@ mod redis_client_tests {
// So we don't assert anything here
}
}
#[test]
fn test_redis_config_builder() {
// Test the Redis configuration builder
// Test default values
let config = RedisConfigBuilder::new();
assert_eq!(config.host, "127.0.0.1");
assert_eq!(config.port, 6379);
assert_eq!(config.db, 0);
assert_eq!(config.username, None);
assert_eq!(config.password, None);
assert_eq!(config.use_tls, false);
assert_eq!(config.use_unix_socket, false);
assert_eq!(config.socket_path, None);
assert_eq!(config.connection_timeout, None);
// Test setting values
let config = RedisConfigBuilder::new()
.host("redis.example.com")
.port(6380)
.db(1)
.username("user")
.password("pass")
.use_tls(true)
.connection_timeout(30);
assert_eq!(config.host, "redis.example.com");
assert_eq!(config.port, 6380);
assert_eq!(config.db, 1);
assert_eq!(config.username, Some("user".to_string()));
assert_eq!(config.password, Some("pass".to_string()));
assert_eq!(config.use_tls, true);
assert_eq!(config.connection_timeout, Some(30));
// Test socket path setting
let config = RedisConfigBuilder::new().socket_path("/tmp/redis.sock");
assert_eq!(config.use_unix_socket, true);
assert_eq!(config.socket_path, Some("/tmp/redis.sock".to_string()));
}
#[test]
fn test_connection_url_building() {
// Test building connection URLs
// Test default URL
let config = RedisConfigBuilder::new();
let url = config.build_connection_url();
assert_eq!(url, "redis://127.0.0.1:6379/0");
// Test with authentication
let config = RedisConfigBuilder::new().username("user").password("pass");
let url = config.build_connection_url();
assert_eq!(url, "redis://user:pass@127.0.0.1:6379/0");
// Test with password only
let config = RedisConfigBuilder::new().password("pass");
let url = config.build_connection_url();
assert_eq!(url, "redis://:pass@127.0.0.1:6379/0");
// Test with TLS
let config = RedisConfigBuilder::new().use_tls(true);
let url = config.build_connection_url();
assert_eq!(url, "rediss://127.0.0.1:6379/0");
// Test with Unix socket
let config = RedisConfigBuilder::new().socket_path("/tmp/redis.sock");
let url = config.build_connection_url();
assert_eq!(url, "unix:///tmp/redis.sock");
}
}
// Integration tests that require a real Redis server
@ -141,7 +70,7 @@ mod redis_client_tests {
#[cfg(test)]
mod redis_integration_tests {
use super::*;
// Helper function to check if Redis is available
fn is_redis_available() -> bool {
match get_redis_client() {
@ -149,200 +78,49 @@ mod redis_integration_tests {
Err(_) => false,
}
}
#[test]
fn test_redis_client_integration() {
if !is_redis_available() {
println!("Skipping Redis integration tests - Redis server not available");
return;
}
println!("Running Redis integration tests...");
// Test basic operations
test_basic_redis_operations();
// Test more complex operations
test_hash_operations();
test_list_operations();
// Test error handling
test_error_handling();
}
fn test_basic_redis_operations() {
if !is_redis_available() {
return;
}
// Test setting and getting values
let client_result = get_redis_client();
if client_result.is_err() {
// Skip the test if we can't connect to Redis
return;
}
// Create SET command
let mut set_cmd = redis::cmd("SET");
set_cmd.arg("test_key").arg("test_value");
// Execute SET command
let set_result: RedisResult<()> = execute(&mut set_cmd);
assert!(set_result.is_ok());
// Create GET command
let mut get_cmd = redis::cmd("GET");
get_cmd.arg("test_key");
// Execute GET command and check the result
if let Ok(value) = execute::<String>(&mut get_cmd) {
assert_eq!(value, "test_value");
}
// Test expiration
let mut expire_cmd = redis::cmd("EXPIRE");
expire_cmd.arg("test_key").arg(1); // Expire in 1 second
let expire_result: RedisResult<i32> = execute(&mut expire_cmd);
assert!(expire_result.is_ok());
assert_eq!(expire_result.unwrap(), 1);
// Sleep for 2 seconds to let the key expire
std::thread::sleep(std::time::Duration::from_secs(2));
// Check that the key has expired
let mut exists_cmd = redis::cmd("EXISTS");
exists_cmd.arg("test_key");
let exists_result: RedisResult<i32> = execute(&mut exists_cmd);
assert!(exists_result.is_ok());
assert_eq!(exists_result.unwrap(), 0);
// Clean up
let _: RedisResult<()> = execute(&mut redis::cmd("DEL").arg("test_key"));
}
fn test_hash_operations() {
if !is_redis_available() {
return;
}
// Test hash operations
let hash_key = "test_hash";
// Set hash fields
let mut hset_cmd = redis::cmd("HSET");
hset_cmd
.arg(hash_key)
.arg("field1")
.arg("value1")
.arg("field2")
.arg("value2");
let hset_result: RedisResult<i32> = execute(&mut hset_cmd);
assert!(hset_result.is_ok());
assert_eq!(hset_result.unwrap(), 2);
// Get hash field
let mut hget_cmd = redis::cmd("HGET");
hget_cmd.arg(hash_key).arg("field1");
let hget_result: RedisResult<String> = execute(&mut hget_cmd);
assert!(hget_result.is_ok());
assert_eq!(hget_result.unwrap(), "value1");
// Get all hash fields
let mut hgetall_cmd = redis::cmd("HGETALL");
hgetall_cmd.arg(hash_key);
let hgetall_result: RedisResult<Vec<String>> = execute(&mut hgetall_cmd);
assert!(hgetall_result.is_ok());
let hgetall_values = hgetall_result.unwrap();
assert_eq!(hgetall_values.len(), 4); // field1, value1, field2, value2
// Delete hash field
let mut hdel_cmd = redis::cmd("HDEL");
hdel_cmd.arg(hash_key).arg("field1");
let hdel_result: RedisResult<i32> = execute(&mut hdel_cmd);
assert!(hdel_result.is_ok());
assert_eq!(hdel_result.unwrap(), 1);
// Clean up
let _: RedisResult<()> = execute(&mut redis::cmd("DEL").arg(hash_key));
}
fn test_list_operations() {
if !is_redis_available() {
return;
}
// Test list operations
let list_key = "test_list";
// Push items to list
let mut rpush_cmd = redis::cmd("RPUSH");
rpush_cmd
.arg(list_key)
.arg("item1")
.arg("item2")
.arg("item3");
let rpush_result: RedisResult<i32> = execute(&mut rpush_cmd);
assert!(rpush_result.is_ok());
assert_eq!(rpush_result.unwrap(), 3);
// Get list length
let mut llen_cmd = redis::cmd("LLEN");
llen_cmd.arg(list_key);
let llen_result: RedisResult<i32> = execute(&mut llen_cmd);
assert!(llen_result.is_ok());
assert_eq!(llen_result.unwrap(), 3);
// Get list range
let mut lrange_cmd = redis::cmd("LRANGE");
lrange_cmd.arg(list_key).arg(0).arg(-1);
let lrange_result: RedisResult<Vec<String>> = execute(&mut lrange_cmd);
assert!(lrange_result.is_ok());
let lrange_values = lrange_result.unwrap();
assert_eq!(lrange_values.len(), 3);
assert_eq!(lrange_values[0], "item1");
assert_eq!(lrange_values[1], "item2");
assert_eq!(lrange_values[2], "item3");
// Pop item from list
let mut lpop_cmd = redis::cmd("LPOP");
lpop_cmd.arg(list_key);
let lpop_result: RedisResult<String> = execute(&mut lpop_cmd);
assert!(lpop_result.is_ok());
assert_eq!(lpop_result.unwrap(), "item1");
// Clean up
let _: RedisResult<()> = execute(&mut redis::cmd("DEL").arg(list_key));
}
fn test_error_handling() {
if !is_redis_available() {
return;
}
// Test error handling
// Test invalid command
let mut invalid_cmd = redis::cmd("INVALID_COMMAND");
let invalid_result: RedisResult<()> = execute(&mut invalid_cmd);
assert!(invalid_result.is_err());
// Test wrong data type
let key = "test_wrong_type";
// Set a string value
let mut set_cmd = redis::cmd("SET");
set_cmd.arg(key).arg("string_value");
let set_result: RedisResult<()> = execute(&mut set_cmd);
assert!(set_result.is_ok());
// Try to use a hash command on a string
let mut hget_cmd = redis::cmd("HGET");
hget_cmd.arg(key).arg("field");
let hget_result: RedisResult<String> = execute(&mut hget_cmd);
assert!(hget_result.is_err());
// Clean up
let _: RedisResult<()> = execute(&mut redis::cmd("DEL").arg(key));
}
}
}

View File

@ -2,8 +2,8 @@
//!
//! This module provides Rhai wrappers for the functions in the Git module.
use crate::git::{GitError, GitRepo, GitTree};
use rhai::{Array, Dynamic, Engine, EvalAltResult};
use rhai::{Engine, EvalAltResult, Array, Dynamic};
use crate::git::{GitTree, GitRepo, GitError};
/// Register Git module functions with the Rhai engine
///
@ -16,26 +16,21 @@ use rhai::{Array, Dynamic, Engine, EvalAltResult};
/// * `Result<(), Box<EvalAltResult>>` - Ok if registration was successful, Err otherwise
pub fn register_git_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
// Register GitTree constructor
engine.register_type::<GitTree>();
engine.register_fn("git_tree_new", git_tree_new);
engine.register_fn("gittree_new", git_tree_new);
// Register GitTree methods
engine.register_fn("list", git_tree_list);
engine.register_fn("find", git_tree_find);
engine.register_fn("get", git_tree_get);
// Register GitRepo methods
engine.register_type::<GitRepo>();
engine.register_fn("path", git_repo_path);
engine.register_fn("has_changes", git_repo_has_changes);
engine.register_fn("pull", git_repo_pull);
engine.register_fn("reset", git_repo_reset);
engine.register_fn("commit", git_repo_commit);
engine.register_fn("push", git_repo_push);
// Register git_clone function for testing
engine.register_fn("git_clone", git_clone);
Ok(())
}
@ -44,7 +39,7 @@ fn git_error_to_rhai_error<T>(result: Result<T, GitError>) -> Result<T, Box<Eval
result.map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime(
format!("Git error: {}", e).into(),
rhai::Position::NONE,
rhai::Position::NONE
))
})
}
@ -65,61 +60,44 @@ pub fn git_tree_new(base_path: &str) -> Result<GitTree, Box<EvalAltResult>> {
/// Lists all git repositories under the base path.
pub fn git_tree_list(git_tree: &mut GitTree) -> Result<Array, Box<EvalAltResult>> {
let repos = git_error_to_rhai_error(git_tree.list())?;
// Convert Vec<String> to Rhai Array
let mut array = Array::new();
for repo in repos {
array.push(Dynamic::from(repo));
}
Ok(array)
}
/// Wrapper for GitTree::find
///
/// Finds repositories matching a pattern and returns them as an array of GitRepo objects.
/// Assumes the underlying GitTree::find Rust method now returns Result<Vec<GitRepo>, GitError>.
/// Finds repositories matching a pattern or partial path.
pub fn git_tree_find(git_tree: &mut GitTree, pattern: &str) -> Result<Array, Box<EvalAltResult>> {
let repos: Vec<GitRepo> = git_error_to_rhai_error(git_tree.find(pattern))?;
// Convert Vec<GitRepo> to Rhai Array
let repos = git_error_to_rhai_error(git_tree.find(pattern))?;
// Convert Vec<String> to Rhai Array
let mut array = Array::new();
for repo in repos {
array.push(Dynamic::from(repo));
}
Ok(array)
}
/// Wrapper for GitTree::get
///
/// Gets a single GitRepo object based on an exact name or URL.
/// The underlying Rust GitTree::get method returns Result<Vec<GitRepo>, GitError>.
/// This wrapper ensures that for Rhai, 'get' returns a single GitRepo or an error
/// if zero or multiple repositories are found (for local names/patterns),
/// or if a URL operation fails or unexpectedly yields not exactly one result.
pub fn git_tree_get(
git_tree: &mut GitTree,
name_or_url: &str,
) -> Result<GitRepo, Box<EvalAltResult>> {
let mut repos_vec: Vec<GitRepo> = git_error_to_rhai_error(git_tree.get(name_or_url))?;
match repos_vec.len() {
1 => Ok(repos_vec.remove(0)), // Efficient for Vec of size 1, transfers ownership
0 => Err(Box::new(EvalAltResult::ErrorRuntime(
format!("Git error: Repository '{}' not found.", name_or_url).into(),
rhai::Position::NONE,
))),
_ => Err(Box::new(EvalAltResult::ErrorRuntime(
format!(
"Git error: Multiple repositories ({}) found matching '{}'. Use find() for patterns or provide a more specific name for get().",
repos_vec.len(),
name_or_url
)
.into(),
rhai::Position::NONE,
))),
/// Gets one or more GitRepo objects based on a path pattern or URL.
pub fn git_tree_get(git_tree: &mut GitTree, path_or_url: &str) -> Result<Array, Box<EvalAltResult>> {
let repos = git_error_to_rhai_error(git_tree.get(path_or_url))?;
// Convert Vec<GitRepo> to Rhai Array
let mut array = Array::new();
for repo in repos {
array.push(Dynamic::from(repo));
}
Ok(array)
}
//
@ -157,10 +135,7 @@ pub fn git_repo_reset(git_repo: &mut GitRepo) -> Result<GitRepo, Box<EvalAltResu
/// Wrapper for GitRepo::commit
///
/// Commits changes in the repository.
pub fn git_repo_commit(
git_repo: &mut GitRepo,
message: &str,
) -> Result<GitRepo, Box<EvalAltResult>> {
pub fn git_repo_commit(git_repo: &mut GitRepo, message: &str) -> Result<GitRepo, Box<EvalAltResult>> {
git_error_to_rhai_error(git_repo.commit(message))
}
@ -169,15 +144,4 @@ pub fn git_repo_commit(
/// Pushes changes to the remote repository.
pub fn git_repo_push(git_repo: &mut GitRepo) -> Result<GitRepo, Box<EvalAltResult>> {
git_error_to_rhai_error(git_repo.push())
}
/// Dummy implementation of git_clone for testing
///
/// This function is used for testing the git module.
pub fn git_clone(url: &str) -> Result<(), Box<EvalAltResult>> {
// This is a dummy implementation that always fails with a Git error
Err(Box::new(EvalAltResult::ErrorRuntime(
format!("Git error: Failed to clone repository from URL: {}", url).into(),
rhai::Position::NONE,
)))
}
}

View File

@ -3,103 +3,72 @@
//! This module provides integration with the Rhai scripting language,
//! allowing SAL functions to be called from Rhai scripts.
mod buildah;
mod error;
mod git;
mod nerdctl;
mod os;
mod postgresclient;
mod process;
mod redisclient;
mod rfs;
mod buildah;
mod nerdctl;
mod git;
mod text;
mod rfs;
#[cfg(test)]
mod tests;
// Re-export common Rhai types for convenience
pub use rhai::{Array, Dynamic, Engine, EvalAltResult, Map};
pub use rhai::{Array, Dynamic, Map, EvalAltResult, Engine};
// Re-export error module
pub use error::*;
// Re-export specific functions from modules to avoid name conflicts
pub use os::{
delete,
// Download functions
download,
download_install,
// File system functions
exist,
file_size,
find_dir,
find_dirs,
find_file,
find_files,
mkdir,
register_os_module,
rsync,
// File system functions
exist, find_file, find_files, find_dir, find_dirs,
delete, mkdir, file_size, rsync,
// Download functions
download, download_install
};
// Re-export Redis client module registration function
pub use redisclient::register_redisclient_module;
// Re-export PostgreSQL client module registration function
pub use postgresclient::register_postgresclient_module;
pub use process::{
kill,
process_get,
process_list,
register_process_module,
// Run functions
run, run_silent, run_with_options, new_run_options,
// Process management functions
which,
which, kill, process_list, process_get
};
// Re-export buildah functions
pub use buildah::bah_new;
pub use buildah::register_bah_module;
pub use buildah::bah_new;
// Re-export nerdctl functions
pub use nerdctl::register_nerdctl_module;
pub use nerdctl::{
nerdctl_copy,
nerdctl_exec,
nerdctl_image_build,
nerdctl_image_commit,
nerdctl_image_pull,
nerdctl_image_push,
nerdctl_image_remove,
nerdctl_image_tag,
// Image functions
nerdctl_images,
nerdctl_list,
nerdctl_remove,
// Container functions
nerdctl_run,
nerdctl_run_with_name,
nerdctl_run_with_port,
nerdctl_stop,
nerdctl_run, nerdctl_run_with_name, nerdctl_run_with_port,
nerdctl_exec, nerdctl_copy, nerdctl_stop, nerdctl_remove, nerdctl_list,
// Image functions
nerdctl_images, nerdctl_image_remove, nerdctl_image_push, nerdctl_image_tag,
nerdctl_image_pull, nerdctl_image_commit, nerdctl_image_build
};
// Re-export RFS module
pub use rfs::register as register_rfs_module;
// Re-export git module
pub use crate::git::{GitRepo, GitTree};
pub use git::register_git_module;
pub use crate::git::{GitTree, GitRepo};
// Re-export text module
pub use text::register_text_module;
// Re-export text functions directly from text module
pub use crate::text::{
// Dedent functions
dedent,
// Fix functions
name_fix,
path_fix,
prefix,
name_fix, path_fix,
// Dedent functions
dedent, prefix
};
// Re-export TextReplacer functions
@ -116,7 +85,7 @@ pub use os::copy as os_copy;
///
/// # Example
///
/// ```ignore
/// ```
/// use rhai::Engine;
/// use sal::rhai;
///
@ -124,45 +93,32 @@ pub use os::copy as os_copy;
/// rhai::register(&mut engine);
///
/// // Now you can use SAL functions in Rhai scripts
/// // You can evaluate Rhai scripts with SAL functions
/// let result = engine.eval::<i64>("exist('some_file.txt')").unwrap();
/// let result = engine.eval::<bool>("exist('some_file.txt')").unwrap();
/// ```
pub fn register(engine: &mut Engine) -> Result<(), Box<rhai::EvalAltResult>> {
// Register OS module functions
os::register_os_module(engine)?;
// Register Process module functions
process::register_process_module(engine)?;
// Register Buildah module functions
buildah::register_bah_module(engine)?;
// Register Nerdctl module functions
nerdctl::register_nerdctl_module(engine)?;
// Register Git module functions
git::register_git_module(engine)?;
// Register Text module functions
text::register_text_module(engine)?;
// Register RFS module functions
rfs::register(engine)?;
// Register Redis client module functions
redisclient::register_redisclient_module(engine)?;
// Register PostgreSQL client module functions
postgresclient::register_postgresclient_module(engine)?;
// Register utility functions
engine.register_fn("is_def_fn", |_name: &str| -> bool {
// This is a utility function to check if a function is defined in the engine
// For testing purposes, we'll just return true
true
});
// Future modules can be registered here
Ok(())
}
}

View File

@ -1,356 +0,0 @@
//! Rhai wrappers for PostgreSQL client module functions
//!
//! This module provides Rhai wrappers for the functions in the PostgreSQL client module.
use crate::postgresclient;
use postgres::types::ToSql;
use rhai::{Array, Engine, EvalAltResult, Map};
/// Register PostgreSQL client module functions with the Rhai engine
///
/// # Arguments
///
/// * `engine` - The Rhai engine to register the functions with
///
/// # Returns
///
/// * `Result<(), Box<EvalAltResult>>` - Ok if registration was successful, Err otherwise
pub fn register_postgresclient_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
// Register PostgreSQL connection functions
engine.register_fn("pg_connect", pg_connect);
engine.register_fn("pg_ping", pg_ping);
engine.register_fn("pg_reset", pg_reset);
// Register basic query functions
engine.register_fn("pg_execute", pg_execute);
engine.register_fn("pg_query", pg_query);
engine.register_fn("pg_query_one", pg_query_one);
// Register installer functions
engine.register_fn("pg_install", pg_install);
engine.register_fn("pg_create_database", pg_create_database);
engine.register_fn("pg_execute_sql", pg_execute_sql);
engine.register_fn("pg_is_running", pg_is_running);
// Builder pattern functions will be implemented in a future update
Ok(())
}
/// Connect to PostgreSQL using environment variables
///
/// # Returns
///
/// * `Result<bool, Box<EvalAltResult>>` - true if successful, error otherwise
pub fn pg_connect() -> Result<bool, Box<EvalAltResult>> {
match postgresclient::get_postgres_client() {
Ok(_) => Ok(true),
Err(e) => Err(Box::new(EvalAltResult::ErrorRuntime(
format!("PostgreSQL error: {}", e).into(),
rhai::Position::NONE,
))),
}
}
/// Ping the PostgreSQL server
///
/// # Returns
///
/// * `Result<bool, Box<EvalAltResult>>` - true if successful, error otherwise
pub fn pg_ping() -> Result<bool, Box<EvalAltResult>> {
match postgresclient::get_postgres_client() {
Ok(client) => match client.ping() {
Ok(result) => Ok(result),
Err(e) => Err(Box::new(EvalAltResult::ErrorRuntime(
format!("PostgreSQL error: {}", e).into(),
rhai::Position::NONE,
))),
},
Err(e) => Err(Box::new(EvalAltResult::ErrorRuntime(
format!("PostgreSQL error: {}", e).into(),
rhai::Position::NONE,
))),
}
}
/// Reset the PostgreSQL client connection
///
/// # Returns
///
/// * `Result<bool, Box<EvalAltResult>>` - true if successful, error otherwise
pub fn pg_reset() -> Result<bool, Box<EvalAltResult>> {
match postgresclient::reset() {
Ok(_) => Ok(true),
Err(e) => Err(Box::new(EvalAltResult::ErrorRuntime(
format!("PostgreSQL error: {}", e).into(),
rhai::Position::NONE,
))),
}
}
/// Execute a query on the PostgreSQL connection
///
/// # Arguments
///
/// * `query` - The query to execute
///
/// # Returns
///
/// * `Result<i64, Box<EvalAltResult>>` - The number of rows affected if successful, error otherwise
pub fn pg_execute(query: &str) -> Result<i64, Box<EvalAltResult>> {
// We can't directly pass dynamic parameters from Rhai to PostgreSQL
// So we'll only support parameterless queries for now
let params: &[&(dyn ToSql + Sync)] = &[];
match postgresclient::execute(query, params) {
Ok(rows) => Ok(rows as i64),
Err(e) => Err(Box::new(EvalAltResult::ErrorRuntime(
format!("PostgreSQL error: {}", e).into(),
rhai::Position::NONE,
))),
}
}
/// Execute a query on the PostgreSQL connection and return the rows
///
/// # Arguments
///
/// * `query` - The query to execute
///
/// # Returns
///
/// * `Result<Array, Box<EvalAltResult>>` - The rows if successful, error otherwise
pub fn pg_query(query: &str) -> Result<Array, Box<EvalAltResult>> {
// We can't directly pass dynamic parameters from Rhai to PostgreSQL
// So we'll only support parameterless queries for now
let params: &[&(dyn ToSql + Sync)] = &[];
match postgresclient::query(query, params) {
Ok(rows) => {
let mut result = Array::new();
for row in rows {
let mut map = Map::new();
for column in row.columns() {
let name = column.name();
// We'll convert all values to strings for simplicity
let value: Option<String> = row.get(name);
if let Some(val) = value {
map.insert(name.into(), val.into());
} else {
map.insert(name.into(), rhai::Dynamic::UNIT);
}
}
result.push(map.into());
}
Ok(result)
}
Err(e) => Err(Box::new(EvalAltResult::ErrorRuntime(
format!("PostgreSQL error: {}", e).into(),
rhai::Position::NONE,
))),
}
}
/// Execute a query on the PostgreSQL connection and return a single row
///
/// # Arguments
///
/// * `query` - The query to execute
///
/// # Returns
///
/// * `Result<Map, Box<EvalAltResult>>` - The row if successful, error otherwise
pub fn pg_query_one(query: &str) -> Result<Map, Box<EvalAltResult>> {
// We can't directly pass dynamic parameters from Rhai to PostgreSQL
// So we'll only support parameterless queries for now
let params: &[&(dyn ToSql + Sync)] = &[];
match postgresclient::query_one(query, params) {
Ok(row) => {
let mut map = Map::new();
for column in row.columns() {
let name = column.name();
// We'll convert all values to strings for simplicity
let value: Option<String> = row.get(name);
if let Some(val) = value {
map.insert(name.into(), val.into());
} else {
map.insert(name.into(), rhai::Dynamic::UNIT);
}
}
Ok(map)
}
Err(e) => Err(Box::new(EvalAltResult::ErrorRuntime(
format!("PostgreSQL error: {}", e).into(),
rhai::Position::NONE,
))),
}
}
/// Install PostgreSQL using nerdctl
///
/// # Arguments
///
/// * `container_name` - Name for the PostgreSQL container
/// * `version` - PostgreSQL version to install (e.g., "latest", "15", "14")
/// * `port` - Port to expose PostgreSQL on
/// * `username` - Username for PostgreSQL
/// * `password` - Password for PostgreSQL
///
/// # Returns
///
/// * `Result<bool, Box<EvalAltResult>>` - true if successful, error otherwise
pub fn pg_install(
container_name: &str,
version: &str,
port: i64,
username: &str,
password: &str,
) -> Result<bool, Box<EvalAltResult>> {
// Create the installer configuration
let config = postgresclient::PostgresInstallerConfig::new()
.container_name(container_name)
.version(version)
.port(port as u16)
.username(username)
.password(password);
// Install PostgreSQL
match postgresclient::install_postgres(config) {
Ok(_) => Ok(true),
Err(e) => Err(Box::new(EvalAltResult::ErrorRuntime(
format!("PostgreSQL installer error: {}", e).into(),
rhai::Position::NONE,
))),
}
}
/// Create a new database in PostgreSQL
///
/// # Arguments
///
/// * `container_name` - Name of the PostgreSQL container
/// * `db_name` - Database name to create
///
/// # Returns
///
/// * `Result<bool, Box<EvalAltResult>>` - true if successful, error otherwise
pub fn pg_create_database(container_name: &str, db_name: &str) -> Result<bool, Box<EvalAltResult>> {
// Create a container reference
let container = crate::virt::nerdctl::Container {
name: container_name.to_string(),
container_id: Some(container_name.to_string()), // Use name as ID for simplicity
image: None,
config: std::collections::HashMap::new(),
ports: Vec::new(),
volumes: Vec::new(),
env_vars: std::collections::HashMap::new(),
network: None,
network_aliases: Vec::new(),
cpu_limit: None,
memory_limit: None,
memory_swap_limit: None,
cpu_shares: None,
restart_policy: None,
health_check: None,
detach: false,
snapshotter: None,
};
// Create the database
match postgresclient::create_database(&container, db_name) {
Ok(_) => Ok(true),
Err(e) => Err(Box::new(EvalAltResult::ErrorRuntime(
format!("PostgreSQL error: {}", e).into(),
rhai::Position::NONE,
))),
}
}
/// Execute a SQL script in PostgreSQL
///
/// # Arguments
///
/// * `container_name` - Name of the PostgreSQL container
/// * `db_name` - Database name
/// * `sql` - SQL script to execute
///
/// # Returns
///
/// * `Result<String, Box<EvalAltResult>>` - Output of the command if successful, error otherwise
pub fn pg_execute_sql(
container_name: &str,
db_name: &str,
sql: &str,
) -> Result<String, Box<EvalAltResult>> {
// Create a container reference
let container = crate::virt::nerdctl::Container {
name: container_name.to_string(),
container_id: Some(container_name.to_string()), // Use name as ID for simplicity
image: None,
config: std::collections::HashMap::new(),
ports: Vec::new(),
volumes: Vec::new(),
env_vars: std::collections::HashMap::new(),
network: None,
network_aliases: Vec::new(),
cpu_limit: None,
memory_limit: None,
memory_swap_limit: None,
cpu_shares: None,
restart_policy: None,
health_check: None,
detach: false,
snapshotter: None,
};
// Execute the SQL script
match postgresclient::execute_sql(&container, db_name, sql) {
Ok(output) => Ok(output),
Err(e) => Err(Box::new(EvalAltResult::ErrorRuntime(
format!("PostgreSQL error: {}", e).into(),
rhai::Position::NONE,
))),
}
}
/// Check if PostgreSQL is running
///
/// # Arguments
///
/// * `container_name` - Name of the PostgreSQL container
///
/// # Returns
///
/// * `Result<bool, Box<EvalAltResult>>` - true if running, false otherwise, or error
pub fn pg_is_running(container_name: &str) -> Result<bool, Box<EvalAltResult>> {
// Create a container reference
let container = crate::virt::nerdctl::Container {
name: container_name.to_string(),
container_id: Some(container_name.to_string()), // Use name as ID for simplicity
image: None,
config: std::collections::HashMap::new(),
ports: Vec::new(),
volumes: Vec::new(),
env_vars: std::collections::HashMap::new(),
network: None,
network_aliases: Vec::new(),
cpu_limit: None,
memory_limit: None,
memory_swap_limit: None,
cpu_shares: None,
restart_policy: None,
health_check: None,
detach: false,
snapshotter: None,
};
// Check if PostgreSQL is running
match postgresclient::is_postgres_running(&container) {
Ok(running) => Ok(running),
Err(e) => Err(Box::new(EvalAltResult::ErrorRuntime(
format!("PostgreSQL error: {}", e).into(),
rhai::Position::NONE,
))),
}
}

View File

@ -2,9 +2,8 @@
//!
//! This module provides Rhai wrappers for the functions in the Process module.
use crate::process::{self, CommandResult, ProcessError, ProcessInfo, RunError};
use rhai::{Array, Dynamic, Engine, EvalAltResult, Map};
use std::clone::Clone;
use rhai::{Engine, EvalAltResult, Array, Dynamic, Map};
use crate::process::{self, CommandResult, ProcessInfo, RunError, ProcessError};
/// Register Process module functions with the Rhai engine
///
@ -17,41 +16,43 @@ use std::clone::Clone;
/// * `Result<(), Box<EvalAltResult>>` - Ok if registration was successful, Err otherwise
pub fn register_process_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
// Register types
// register_process_types(engine)?; // Removed
// Register CommandResult type and its methods
engine.register_type_with_name::<CommandResult>("CommandResult");
engine.register_get("stdout", |r: &mut CommandResult| r.stdout.clone());
engine.register_get("stderr", |r: &mut CommandResult| r.stderr.clone());
engine.register_get("success", |r: &mut CommandResult| r.success);
engine.register_get("code", |r: &mut CommandResult| r.code);
// Register ProcessInfo type and its methods
engine.register_type_with_name::<ProcessInfo>("ProcessInfo");
engine.register_get("pid", |p: &mut ProcessInfo| p.pid);
engine.register_get("name", |p: &mut ProcessInfo| p.name.clone());
engine.register_get("memory", |p: &mut ProcessInfo| p.memory);
engine.register_get("cpu", |p: &mut ProcessInfo| p.cpu);
// Register CommandBuilder type and its methods
engine.register_type_with_name::<RhaiCommandBuilder>("CommandBuilder");
engine.register_fn("run", RhaiCommandBuilder::new_rhai); // This is the builder entry point
engine.register_fn("silent", RhaiCommandBuilder::silent); // Method on CommandBuilder
engine.register_fn("ignore_error", RhaiCommandBuilder::ignore_error); // Method on CommandBuilder
engine.register_fn("log", RhaiCommandBuilder::log); // Method on CommandBuilder
engine.register_fn("execute", RhaiCommandBuilder::execute_command); // Method on CommandBuilder
// Register other process management functions
register_process_types(engine)?;
// Register run functions
engine.register_fn("run", run);
engine.register_fn("run_silent", run_silent);
engine.register_fn("run_with_options", run_with_options);
engine.register_fn("new_run_options", new_run_options);
// Register process management functions
engine.register_fn("which", which);
engine.register_fn("kill", kill);
engine.register_fn("process_list", process_list);
engine.register_fn("process_get", process_get);
Ok(())
}
// Register legacy functions for backward compatibility
engine.register_fn("run_command", run_command);
engine.register_fn("run_silent", run_silent);
engine.register_fn("run", run_with_options);
/// Register Process module types with the Rhai engine
fn register_process_types(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
// Register CommandResult type and methods
engine.register_type_with_name::<CommandResult>("CommandResult");
// Register getters for CommandResult properties
engine.register_get("stdout", |r: &mut CommandResult| r.stdout.clone());
engine.register_get("stderr", |r: &mut CommandResult| r.stderr.clone());
engine.register_get("success", |r: &mut CommandResult| r.success);
engine.register_get("code", |r: &mut CommandResult| r.code);
// Register ProcessInfo type and methods
engine.register_type_with_name::<ProcessInfo>("ProcessInfo");
// Register getters for ProcessInfo properties
engine.register_get("pid", |p: &mut ProcessInfo| p.pid);
engine.register_get("name", |p: &mut ProcessInfo| p.name.clone());
engine.register_get("memory", |p: &mut ProcessInfo| p.memory);
engine.register_get("cpu", |p: &mut ProcessInfo| p.cpu);
Ok(())
}
@ -60,72 +61,92 @@ fn run_error_to_rhai_error<T>(result: Result<T, RunError>) -> Result<T, Box<Eval
result.map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime(
format!("Run error: {}", e).into(),
rhai::Position::NONE,
rhai::Position::NONE
))
})
}
// Define a Rhai-facing builder struct
#[derive(Clone)]
struct RhaiCommandBuilder {
command: String,
die_on_error: bool,
is_silent: bool,
enable_log: bool,
}
impl RhaiCommandBuilder {
// Constructor function for Rhai (registered as `run`)
pub fn new_rhai(command: &str) -> Self {
Self {
command: command.to_string(),
die_on_error: true, // Default: die on error
is_silent: false,
enable_log: false,
}
}
// Rhai method: .silent()
pub fn silent(mut self) -> Self {
self.is_silent = true;
self
}
// Rhai method: .ignore_error()
pub fn ignore_error(mut self) -> Self {
self.die_on_error = false;
self
}
// Rhai method: .log()
pub fn log(mut self) -> Self {
self.enable_log = true;
self
}
// Rhai method: .execute() - Execute the command
pub fn execute_command(self) -> Result<CommandResult, Box<EvalAltResult>> {
let builder = process::run(&self.command)
.die(self.die_on_error)
.silent(self.is_silent)
.log(self.enable_log);
// Execute the command
run_error_to_rhai_error(builder.execute())
}
}
fn process_error_to_rhai_error<T>(
result: Result<T, ProcessError>,
) -> Result<T, Box<EvalAltResult>> {
fn process_error_to_rhai_error<T>(result: Result<T, ProcessError>) -> Result<T, Box<EvalAltResult>> {
result.map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime(
format!("Process error: {}", e).into(),
rhai::Position::NONE,
rhai::Position::NONE
))
})
}
/// Create a new Map with default run options
pub fn new_run_options() -> Map {
let mut map = Map::new();
map.insert("die".into(), Dynamic::from(true));
map.insert("silent".into(), Dynamic::from(false));
map.insert("async_exec".into(), Dynamic::from(false));
map.insert("log".into(), Dynamic::from(false));
map
}
//
// Run Function Wrappers
//
/// Wrapper for process::run_command
///
/// Run a command or multiline script with arguments.
pub fn run(command: &str) -> Result<CommandResult, Box<EvalAltResult>> {
run_error_to_rhai_error(process::run_command(command))
}
/// Wrapper for process::run_silent
///
/// Run a command or multiline script with arguments silently.
pub fn run_silent(command: &str) -> Result<CommandResult, Box<EvalAltResult>> {
run_error_to_rhai_error(process::run_silent(command))
}
/// Run a command with options specified in a Map
///
/// This provides a builder-style interface for Rhai scripts.
///
/// # Example
///
/// ```rhai
/// let options = new_run_options();
/// options.die = false;
/// options.silent = true;
/// let result = run("echo Hello", options);
/// ```
pub fn run_with_options(command: &str, options: Map) -> Result<CommandResult, Box<EvalAltResult>> {
let mut builder = process::run(command);
// Apply options from the map
if let Some(die) = options.get("die") {
if let Ok(die_val) = die.clone().as_bool() {
builder = builder.die(die_val);
}
}
if let Some(silent) = options.get("silent") {
if let Ok(silent_val) = silent.clone().as_bool() {
builder = builder.silent(silent_val);
}
}
if let Some(async_exec) = options.get("async_exec") {
if let Ok(async_val) = async_exec.clone().as_bool() {
builder = builder.async_exec(async_val);
}
}
if let Some(log) = options.get("log") {
if let Ok(log_val) = log.clone().as_bool() {
builder = builder.log(log_val);
}
}
// Execute the command
run_error_to_rhai_error(builder.execute())
}
//
// Process Management Function Wrappers
//
@ -136,7 +157,7 @@ fn process_error_to_rhai_error<T>(
pub fn which(cmd: &str) -> Dynamic {
match process::which(cmd) {
Some(path) => path.into(),
None => Dynamic::UNIT,
None => Dynamic::UNIT
}
}
@ -152,13 +173,13 @@ pub fn kill(pattern: &str) -> Result<String, Box<EvalAltResult>> {
/// List processes matching a pattern (or all if pattern is empty).
pub fn process_list(pattern: &str) -> Result<Array, Box<EvalAltResult>> {
let processes = process_error_to_rhai_error(process::process_list(pattern))?;
// Convert Vec<ProcessInfo> to Rhai Array
let mut array = Array::new();
for process in processes {
array.push(Dynamic::from(process));
}
Ok(array)
}
@ -167,46 +188,4 @@ pub fn process_list(pattern: &str) -> Result<Array, Box<EvalAltResult>> {
/// Get a single process matching the pattern (error if 0 or more than 1 match).
pub fn process_get(pattern: &str) -> Result<ProcessInfo, Box<EvalAltResult>> {
process_error_to_rhai_error(process::process_get(pattern))
}
/// Legacy wrapper for process::run
///
/// Run a command and return the result.
pub fn run_command(cmd: &str) -> Result<CommandResult, Box<EvalAltResult>> {
run_error_to_rhai_error(process::run(cmd).execute())
}
/// Legacy wrapper for process::run with silent option
///
/// Run a command silently and return the result.
pub fn run_silent(cmd: &str) -> Result<CommandResult, Box<EvalAltResult>> {
run_error_to_rhai_error(process::run(cmd).silent(true).execute())
}
/// Legacy wrapper for process::run with options
///
/// Run a command with options and return the result.
pub fn run_with_options(cmd: &str, options: Map) -> Result<CommandResult, Box<EvalAltResult>> {
let mut builder = process::run(cmd);
// Apply options
if let Some(silent) = options.get("silent") {
if let Ok(silent_bool) = silent.as_bool() {
builder = builder.silent(silent_bool);
}
}
if let Some(die) = options.get("die") {
if let Ok(die_bool) = die.as_bool() {
builder = builder.die(die_bool);
}
}
if let Some(log) = options.get("log") {
if let Ok(log_bool) = log.as_bool() {
builder = builder.log(log_bool);
}
}
run_error_to_rhai_error(builder.execute())
}
}

View File

@ -1,327 +0,0 @@
//! Rhai wrappers for Redis client module functions
//!
//! This module provides Rhai wrappers for the functions in the Redis client module.
use crate::redisclient;
use rhai::{Engine, EvalAltResult, Map};
use std::collections::HashMap;
/// Register Redis client module functions with the Rhai engine
///
/// # Arguments
///
/// * `engine` - The Rhai engine to register the functions with
///
/// # Returns
///
/// * `Result<(), Box<EvalAltResult>>` - Ok if registration was successful, Err otherwise
pub fn register_redisclient_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
// Register basic Redis operations
engine.register_fn("redis_ping", redis_ping);
engine.register_fn("redis_set", redis_set);
engine.register_fn("redis_get", redis_get);
engine.register_fn("redis_del", redis_del);
// Register hash operations
engine.register_fn("redis_hset", redis_hset);
engine.register_fn("redis_hget", redis_hget);
engine.register_fn("redis_hgetall", redis_hgetall);
engine.register_fn("redis_hdel", redis_hdel);
// Register list operations
engine.register_fn("redis_rpush", redis_rpush);
engine.register_fn("redis_lpush", redis_lpush);
engine.register_fn("redis_llen", redis_llen);
engine.register_fn("redis_lrange", redis_lrange);
// Register other operations
engine.register_fn("redis_reset", redis_reset);
// We'll implement the builder pattern in a future update
Ok(())
}
/// Ping the Redis server
///
/// # Returns
///
/// * `Result<String, Box<EvalAltResult>>` - "PONG" if successful, error otherwise
pub fn redis_ping() -> Result<String, Box<EvalAltResult>> {
let mut cmd = redis::cmd("PING");
redisclient::execute(&mut cmd).map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime(
format!("Redis error: {}", e).into(),
rhai::Position::NONE,
))
})
}
/// Set a key-value pair in Redis
///
/// # Arguments
///
/// * `key` - The key to set
/// * `value` - The value to set
///
/// # Returns
///
/// * `Result<bool, Box<EvalAltResult>>` - true if successful, error otherwise
pub fn redis_set(key: &str, value: &str) -> Result<bool, Box<EvalAltResult>> {
let mut cmd = redis::cmd("SET");
cmd.arg(key).arg(value);
let result: redis::RedisResult<String> = redisclient::execute(&mut cmd);
match result {
Ok(s) if s == "OK" => Ok(true),
Ok(_) => Ok(false),
Err(e) => Err(Box::new(EvalAltResult::ErrorRuntime(
format!("Redis error: {}", e).into(),
rhai::Position::NONE,
))),
}
}
/// Get a value from Redis by key
///
/// # Arguments
///
/// * `key` - The key to get
///
/// # Returns
///
/// * `Result<String, Box<EvalAltResult>>` - The value if found, empty string if not found, error otherwise
pub fn redis_get(key: &str) -> Result<String, Box<EvalAltResult>> {
let mut cmd = redis::cmd("GET");
cmd.arg(key);
let result: redis::RedisResult<Option<String>> = redisclient::execute(&mut cmd);
match result {
Ok(Some(value)) => Ok(value),
Ok(None) => Ok(String::new()),
Err(e) => Err(Box::new(EvalAltResult::ErrorRuntime(
format!("Redis error: {}", e).into(),
rhai::Position::NONE,
))),
}
}
/// Delete a key from Redis
///
/// # Arguments
///
/// * `key` - The key to delete
///
/// # Returns
///
/// * `Result<bool, Box<EvalAltResult>>` - true if successful, error otherwise
pub fn redis_del(key: &str) -> Result<bool, Box<EvalAltResult>> {
let mut cmd = redis::cmd("DEL");
cmd.arg(key);
let result: redis::RedisResult<i64> = redisclient::execute(&mut cmd);
match result {
Ok(n) => Ok(n > 0),
Err(e) => Err(Box::new(EvalAltResult::ErrorRuntime(
format!("Redis error: {}", e).into(),
rhai::Position::NONE,
))),
}
}
/// Set a field in a hash
///
/// # Arguments
///
/// * `key` - The hash key
/// * `field` - The field to set
/// * `value` - The value to set
///
/// # Returns
///
/// * `Result<bool, Box<EvalAltResult>>` - true if successful, error otherwise
pub fn redis_hset(key: &str, field: &str, value: &str) -> Result<bool, Box<EvalAltResult>> {
let mut cmd = redis::cmd("HSET");
cmd.arg(key).arg(field).arg(value);
let result: redis::RedisResult<i64> = redisclient::execute(&mut cmd);
match result {
Ok(_) => Ok(true),
Err(e) => Err(Box::new(EvalAltResult::ErrorRuntime(
format!("Redis error: {}", e).into(),
rhai::Position::NONE,
))),
}
}
/// Get a field from a hash
///
/// # Arguments
///
/// * `key` - The hash key
/// * `field` - The field to get
///
/// # Returns
///
/// * `Result<String, Box<EvalAltResult>>` - The value if found, empty string if not found, error otherwise
pub fn redis_hget(key: &str, field: &str) -> Result<String, Box<EvalAltResult>> {
let mut cmd = redis::cmd("HGET");
cmd.arg(key).arg(field);
let result: redis::RedisResult<Option<String>> = redisclient::execute(&mut cmd);
match result {
Ok(Some(value)) => Ok(value),
Ok(None) => Ok(String::new()),
Err(e) => Err(Box::new(EvalAltResult::ErrorRuntime(
format!("Redis error: {}", e).into(),
rhai::Position::NONE,
))),
}
}
/// Get all fields and values from a hash
///
/// # Arguments
///
/// * `key` - The hash key
///
/// # Returns
///
/// * `Result<Map, Box<EvalAltResult>>` - A map of field-value pairs, error otherwise
pub fn redis_hgetall(key: &str) -> Result<Map, Box<EvalAltResult>> {
let mut cmd = redis::cmd("HGETALL");
cmd.arg(key);
let result: redis::RedisResult<HashMap<String, String>> = redisclient::execute(&mut cmd);
match result {
Ok(hash_map) => {
let mut map = Map::new();
for (k, v) in hash_map {
map.insert(k.into(), v.into());
}
Ok(map)
}
Err(e) => Err(Box::new(EvalAltResult::ErrorRuntime(
format!("Redis error: {}", e).into(),
rhai::Position::NONE,
))),
}
}
/// Delete a field from a hash
///
/// # Arguments
///
/// * `key` - The hash key
/// * `field` - The field to delete
///
/// # Returns
///
/// * `Result<bool, Box<EvalAltResult>>` - true if successful, error otherwise
pub fn redis_hdel(key: &str, field: &str) -> Result<bool, Box<EvalAltResult>> {
let mut cmd = redis::cmd("HDEL");
cmd.arg(key).arg(field);
let result: redis::RedisResult<i64> = redisclient::execute(&mut cmd);
match result {
Ok(n) => Ok(n > 0),
Err(e) => Err(Box::new(EvalAltResult::ErrorRuntime(
format!("Redis error: {}", e).into(),
rhai::Position::NONE,
))),
}
}
/// Push an element to the end of a list
///
/// # Arguments
///
/// * `key` - The list key
/// * `value` - The value to push
///
/// # Returns
///
/// * `Result<i64, Box<EvalAltResult>>` - The new length of the list, error otherwise
pub fn redis_rpush(key: &str, value: &str) -> Result<i64, Box<EvalAltResult>> {
let mut cmd = redis::cmd("RPUSH");
cmd.arg(key).arg(value);
redisclient::execute(&mut cmd).map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime(
format!("Redis error: {}", e).into(),
rhai::Position::NONE,
))
})
}
/// Push an element to the beginning of a list
///
/// # Arguments
///
/// * `key` - The list key
/// * `value` - The value to push
///
/// # Returns
///
/// * `Result<i64, Box<EvalAltResult>>` - The new length of the list, error otherwise
pub fn redis_lpush(key: &str, value: &str) -> Result<i64, Box<EvalAltResult>> {
let mut cmd = redis::cmd("LPUSH");
cmd.arg(key).arg(value);
redisclient::execute(&mut cmd).map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime(
format!("Redis error: {}", e).into(),
rhai::Position::NONE,
))
})
}
/// Get the length of a list
///
/// # Arguments
///
/// * `key` - The list key
///
/// # Returns
///
/// * `Result<i64, Box<EvalAltResult>>` - The length of the list, error otherwise
pub fn redis_llen(key: &str) -> Result<i64, Box<EvalAltResult>> {
let mut cmd = redis::cmd("LLEN");
cmd.arg(key);
redisclient::execute(&mut cmd).map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime(
format!("Redis error: {}", e).into(),
rhai::Position::NONE,
))
})
}
/// Get a range of elements from a list
///
/// # Arguments
///
/// * `key` - The list key
/// * `start` - The start index
/// * `stop` - The stop index
///
/// # Returns
///
/// * `Result<Vec<String>, Box<EvalAltResult>>` - The elements in the range, error otherwise
pub fn redis_lrange(key: &str, start: i64, stop: i64) -> Result<Vec<String>, Box<EvalAltResult>> {
let mut cmd = redis::cmd("LRANGE");
cmd.arg(key).arg(start).arg(stop);
redisclient::execute(&mut cmd).map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime(
format!("Redis error: {}", e).into(),
rhai::Position::NONE,
))
})
}
/// Reset the Redis client connection
///
/// # Returns
///
/// * `Result<bool, Box<EvalAltResult>>` - true if successful, error otherwise
pub fn redis_reset() -> Result<bool, Box<EvalAltResult>> {
match redisclient::reset() {
Ok(_) => Ok(true),
Err(e) => Err(Box::new(EvalAltResult::ErrorRuntime(
format!("Redis error: {}", e).into(),
rhai::Position::NONE,
))),
}
}
// Builder pattern functions will be implemented in a future update

View File

@ -4,128 +4,124 @@
#[cfg(test)]
mod tests {
use super::super::register;
use rhai::Engine;
use super::super::register;
use std::fs;
use std::path::Path;
#[test]
fn test_register() {
let mut engine = Engine::new();
assert!(register(&mut engine).is_ok());
}
// OS Module Tests
#[test]
fn test_exist_function() {
let mut engine = Engine::new();
register(&mut engine).unwrap();
// Test with a file that definitely exists
let result = engine.eval::<bool>(r#"exist("Cargo.toml")"#).unwrap();
assert!(result);
// Test with a file that definitely doesn't exist
let result = engine
.eval::<bool>(r#"exist("non_existent_file.xyz")"#)
.unwrap();
let result = engine.eval::<bool>(r#"exist("non_existent_file.xyz")"#).unwrap();
assert!(!result);
}
#[test]
fn test_mkdir_and_delete() {
let mut engine = Engine::new();
register(&mut engine).unwrap();
let test_dir = "test_rhai_dir";
// Clean up from previous test runs if necessary
if Path::new(test_dir).exists() {
fs::remove_dir_all(test_dir).unwrap();
}
// Create directory using Rhai
let script = format!(r#"mkdir("{}")"#, test_dir);
let result = engine.eval::<String>(&script).unwrap();
assert!(result.contains("Successfully created directory"));
assert!(Path::new(test_dir).exists());
// Delete directory using Rhai
let script = format!(r#"delete("{}")"#, test_dir);
let result = engine.eval::<String>(&script).unwrap();
assert!(result.contains("Successfully deleted directory"));
assert!(!Path::new(test_dir).exists());
}
#[test]
fn test_file_size() {
let mut engine = Engine::new();
register(&mut engine).unwrap();
// Create a test file
let test_file = "test_rhai_file.txt";
let test_content = "Hello, Rhai!";
fs::write(test_file, test_content).unwrap();
// Get file size using Rhai
let script = format!(r#"file_size("{}")"#, test_file);
let size = engine.eval::<i64>(&script).unwrap();
assert_eq!(size, test_content.len() as i64);
// Clean up
fs::remove_file(test_file).unwrap();
}
#[test]
fn test_error_handling() {
let mut engine = Engine::new();
register(&mut engine).unwrap();
// Try to get the size of a non-existent file
let result = engine.eval::<i64>(r#"file_size("non_existent_file.xyz")"#);
assert!(result.is_err());
let err = result.unwrap_err();
let err_str = err.to_string();
println!("Error string: {}", err_str);
// The actual error message is "No files found matching..."
assert!(
err_str.contains("No files found matching")
|| err_str.contains("File not found")
|| err_str.contains("File system error")
);
assert!(err_str.contains("No files found matching") ||
err_str.contains("File not found") ||
err_str.contains("File system error"));
}
// Process Module Tests
#[test]
fn test_which_function() {
let mut engine = Engine::new();
register(&mut engine).unwrap();
// Test with a command that definitely exists (like "ls" on Unix or "cmd" on Windows)
#[cfg(target_os = "windows")]
let cmd = "cmd";
#[cfg(any(target_os = "macos", target_os = "linux"))]
let cmd = "ls";
let script = format!(r#"which("{}")"#, cmd);
let result = engine.eval::<String>(&script).unwrap();
assert!(!result.is_empty());
// Test with a command that definitely doesn't exist
let script = r#"which("non_existent_command_xyz123")"#;
let result = engine.eval::<()>(&script).unwrap();
assert_eq!(result, ());
}
#[test]
fn test_run_with_options() {
let mut engine = Engine::new();
register(&mut engine).unwrap();
// Test running a command with custom options
#[cfg(target_os = "windows")]
let script = r#"
@ -136,7 +132,7 @@ mod tests {
let result = run("echo Hello World", options);
result.success && result.stdout.contains("Hello World")
"#;
#[cfg(any(target_os = "macos", target_os = "linux"))]
let script = r#"
let options = new_run_options();
@ -146,7 +142,7 @@ mod tests {
let result = run("echo 'Hello World'", options);
result.success && result.stdout.contains("Hello World")
"#;
let result = engine.eval::<bool>(script).unwrap();
assert!(result);
}
@ -155,101 +151,92 @@ mod tests {
fn test_run_command() {
let mut engine = Engine::new();
register(&mut engine).unwrap();
// Test a simple echo command
#[cfg(target_os = "windows")]
let script = r#"
let result = run_command("echo Hello World");
result.success && result.stdout.contains("Hello World")
"#;
#[cfg(any(target_os = "macos", target_os = "linux"))]
let script = r#"
let result = run_command("echo 'Hello World'");
result.success && result.stdout.contains("Hello World")
"#;
let result = engine.eval::<bool>(script).unwrap();
assert!(result);
}
#[test]
fn test_run_silent() {
let mut engine = Engine::new();
register(&mut engine).unwrap();
// Test a simple echo command with silent execution
#[cfg(target_os = "windows")]
let script = r#"
let result = run_silent("echo Hello World");
result.success && result.stdout.contains("Hello World")
"#;
#[cfg(any(target_os = "macos", target_os = "linux"))]
let script = r#"
let result = run_silent("echo 'Hello World'");
result.success && result.stdout.contains("Hello World")
"#;
let result = engine.eval::<bool>(script).unwrap();
assert!(result);
}
#[test]
fn test_process_list() {
let mut engine = Engine::new();
register(&mut engine).unwrap();
// Test listing processes (should return a non-empty array)
let script = r#"
let processes = process_list("");
processes.len() > 0
"#;
let result = engine.eval::<bool>(script).unwrap();
assert!(result);
}
// Git Module Tests
#[test]
fn test_git_module_registration() {
let mut engine = Engine::new();
register(&mut engine).unwrap();
// Test that git functions are registered by trying to use them
// Test that git functions are registered
let script = r#"
// Try to use git_clone function
let result = true;
try {
// This should fail but not crash
git_clone("test-url");
} catch(err) {
// Expected error
result = err.contains("Git error");
}
result
// Check if git_clone function exists
let fn_exists = is_def_fn("git_clone");
fn_exists
"#;
let result = engine.eval::<bool>(script).unwrap();
assert!(result);
}
#[test]
fn test_git_parse_url() {
let mut engine = Engine::new();
register(&mut engine).unwrap();
// Test parsing a git URL
let script = r#"
// We can't directly test git_clone without actually cloning,
// but we can test that the function exists and doesn't error
// when called with invalid parameters
let result = false;
try {
// This should fail but not crash
git_clone("invalid-url");
@ -257,11 +244,11 @@ mod tests {
// Expected error
result = err.contains("Git error");
}
result
"#;
let result = engine.eval::<bool>(script).unwrap();
assert!(result);
}
}
}

View File

@ -6,7 +6,9 @@ use rhai::{Engine, EvalAltResult, Array, Map, Position};
use std::collections::HashMap;
use crate::text::{
TextReplacer, TextReplacerBuilder,
TemplateBuilder
TemplateBuilder,
dedent, prefix,
name_fix, path_fix
};
/// Register Text module functions with the Rhai engine

View File

@ -1,172 +0,0 @@
// 01_builder_pattern.rhai
// Tests for Buildah Builder pattern
// Custom assert function
fn assert_true(condition, message) {
if !condition {
print(`ASSERTION FAILED: ${message}`);
throw message;
}
}
// Custom assert_eq function
fn assert_eq(actual, expected, message) {
if actual != expected {
print(`ASSERTION FAILED: ${message}`);
print(`Expected: "${expected}"`);
print(`Actual: "${actual}"`);
throw message;
}
}
// Helper function to check if buildah is available
fn is_buildah_available() {
try {
let result = run("which buildah");
return result.success;
} catch(err) {
return false;
}
}
print("=== Testing Buildah Builder Pattern ===");
// Check if buildah is available
let buildah_available = is_buildah_available();
if !buildah_available {
print("Buildah is not available. Skipping Buildah tests.");
// Exit gracefully without error
return;
}
print("✓ Buildah is available");
// Test creating a new Builder
print("Testing bah_new()...");
try {
let builder = bah_new("rhai_test_container", "alpine:latest");
// Test Builder properties
print("Testing Builder properties...");
assert_true(builder.container_id != "", "Container ID should not be empty");
assert_eq(builder.name, "rhai_test_container", "Container name should match");
assert_eq(builder.image, "alpine:latest", "Image name should match");
// Test debug mode
print("Testing debug mode...");
assert_true(!builder.debug_mode, "Debug mode should be off by default");
builder.debug_mode = true;
assert_true(builder.debug_mode, "Debug mode should be on after setting");
// Test running a command
print("Testing run()...");
let result = builder.run("echo 'Hello from container'");
assert_true(result.success, "Command should succeed");
assert_true(result.stdout.contains("Hello from container"), "Command output should contain expected text");
print("✓ run(): Command executed successfully");
// Test writing content to a file in the container
print("Testing write_content()...");
let content = "Hello from a file";
builder.write_content(content, "/test_file.txt");
// Verify the content was written
let read_result = builder.run("cat /test_file.txt");
assert_true(read_result.success, "Command should succeed");
assert_true(read_result.stdout.contains(content), "File content should match what was written");
print("✓ write_content(): Content written successfully");
// Test reading content from a file in the container
print("Testing read_content()...");
let read_content = builder.read_content("/test_file.txt");
assert_true(read_content.contains(content), "Read content should match what was written");
print("✓ read_content(): Content read successfully");
// Test setting entrypoint
print("Testing set_entrypoint()...");
let entrypoint = ["/bin/sh", "-c"];
builder.set_entrypoint(entrypoint);
print("✓ set_entrypoint(): Entrypoint set successfully");
// Test setting cmd
print("Testing set_cmd()...");
let cmd = ["echo", "Hello from CMD"];
builder.set_cmd(cmd);
print("✓ set_cmd(): CMD set successfully");
// Test adding a file
print("Testing add()...");
// Create a test file
file_write("test_add_file.txt", "Test content for add");
builder.add("test_add_file.txt", "/");
// Verify the file was added
let add_result = builder.run("cat /test_add_file.txt");
assert_true(add_result.success, "Command should succeed");
assert_true(add_result.stdout.contains("Test content for add"), "Added file content should match");
print("✓ add(): File added successfully");
// Test copying a file
print("Testing copy()...");
// Create a test file
file_write("test_copy_file.txt", "Test content for copy");
builder.copy("test_copy_file.txt", "/");
// Verify the file was copied
let copy_result = builder.run("cat /test_copy_file.txt");
assert_true(copy_result.success, "Command should succeed");
assert_true(copy_result.stdout.contains("Test content for copy"), "Copied file content should match");
print("✓ copy(): File copied successfully");
// Test committing to an image
print("Testing commit()...");
let image_name = "rhai_test_image:latest";
builder.commit(image_name);
print("✓ commit(): Container committed to image successfully");
// Test removing the container
print("Testing remove()...");
builder.remove();
print("✓ remove(): Container removed successfully");
// Clean up test files
delete("test_add_file.txt");
delete("test_copy_file.txt");
// Test image operations
print("Testing image operations...");
// Test listing images
print("Testing images()...");
let images = builder.images();
assert_true(images.len() > 0, "There should be at least one image");
print("✓ images(): Images listed successfully");
// Test removing the image
print("Testing image_remove()...");
builder.image_remove(image_name);
print("✓ image_remove(): Image removed successfully");
print("All Builder pattern tests completed successfully!");
} catch(err) {
print(`Error: ${err}`);
// Clean up in case of error
try {
// Remove test container if it exists
run("buildah rm rhai_test_container");
} catch(_) {}
try {
// Remove test image if it exists
run("buildah rmi rhai_test_image:latest");
} catch(_) {}
try {
// Remove test files if they exist
delete("test_add_file.txt");
delete("test_copy_file.txt");
} catch(_) {}
throw err;
}

View File

@ -1,150 +0,0 @@
// 02_image_operations.rhai
// Tests for Buildah image operations
// Custom assert function
fn assert_true(condition, message) {
if !condition {
print(`ASSERTION FAILED: ${message}`);
throw message;
}
}
// Custom assert_eq function
fn assert_eq(actual, expected, message) {
if actual != expected {
print(`ASSERTION FAILED: ${message}`);
print(`Expected: "${expected}"`);
print(`Actual: "${actual}"`);
throw message;
}
}
// Helper function to check if buildah is available
fn is_buildah_available() {
try {
let result = run("which buildah");
return result.success;
} catch(err) {
return false;
}
}
// Helper function to check if an image exists
fn image_exists(image_name) {
try {
let result = run(`buildah images -q ${image_name}`);
return result.success && result.stdout.trim() != "";
} catch(err) {
return false;
}
}
print("=== Testing Buildah Image Operations ===");
// Check if buildah is available
let buildah_available = is_buildah_available();
if !buildah_available {
print("Buildah is not available. Skipping Buildah tests.");
// Exit gracefully without error
return;
}
print("✓ Buildah is available");
// Create a temporary directory for testing
let test_dir = "rhai_test_buildah";
mkdir(test_dir);
try {
// Create a builder for testing
let builder = bah_new("rhai_test_container", "alpine:latest");
// Enable debug mode
builder.debug_mode = true;
// Test image_pull
print("Testing image_pull()...");
// Use a small image for testing
let pull_result = builder.image_pull("alpine:3.14", true);
assert_true(pull_result.success, "Image pull should succeed");
print("✓ image_pull(): Image pulled successfully");
// Test image_tag
print("Testing image_tag()...");
let tag_result = builder.image_tag("alpine:3.14", "rhai_test_tag:latest");
assert_true(tag_result.success, "Image tag should succeed");
print("✓ image_tag(): Image tagged successfully");
// Test images (list)
print("Testing images()...");
let images = builder.images();
assert_true(images.len() > 0, "There should be at least one image");
// Find our tagged image
let found_tag = false;
for image in images {
if image.names.contains("rhai_test_tag:latest") {
found_tag = true;
break;
}
}
assert_true(found_tag, "Tagged image should be in the list");
print("✓ images(): Images listed successfully");
// Test build
print("Testing build()...");
// Create a simple Dockerfile
let dockerfile_content = `FROM alpine:latest
RUN echo "Hello from Dockerfile" > /hello.txt
CMD ["cat", "/hello.txt"]
`;
file_write(`${test_dir}/Dockerfile`, dockerfile_content);
// Build the image
let build_result = builder.build("rhai_test_build:latest", test_dir, "Dockerfile", "oci");
assert_true(build_result.success, "Image build should succeed");
print("✓ build(): Image built successfully");
// Verify the built image exists
assert_true(image_exists("rhai_test_build:latest"), "Built image should exist");
// Test image_remove
print("Testing image_remove()...");
// Remove the tagged image
let remove_tag_result = builder.image_remove("rhai_test_tag:latest");
assert_true(remove_tag_result.success, "Image removal should succeed");
print("✓ image_remove(): Tagged image removed successfully");
// Remove the built image
let remove_build_result = builder.image_remove("rhai_test_build:latest");
assert_true(remove_build_result.success, "Image removal should succeed");
print("✓ image_remove(): Built image removed successfully");
// Clean up
builder.remove();
print("✓ Cleanup: Container removed");
print("All image operations tests completed successfully!");
} catch(err) {
print(`Error: ${err}`);
// Clean up in case of error
try {
// Remove test container if it exists
run("buildah rm rhai_test_container");
} catch(_) {}
try {
// Remove test images if they exist
run("buildah rmi rhai_test_tag:latest");
run("buildah rmi rhai_test_build:latest");
} catch(_) {}
throw err;
} finally {
// Clean up test directory
delete(test_dir);
print("✓ Cleanup: Test directory removed");
}

View File

@ -1,127 +0,0 @@
// 03_container_operations.rhai
// Tests for Buildah container operations
// Custom assert function
fn assert_true(condition, message) {
if !condition {
print(`ASSERTION FAILED: ${message}`);
throw message;
}
}
// Custom assert_eq function
fn assert_eq(actual, expected, message) {
if actual != expected {
print(`ASSERTION FAILED: ${message}`);
print(`Expected: "${expected}"`);
print(`Actual: "${actual}"`);
throw message;
}
}
// Helper function to check if buildah is available
fn is_buildah_available() {
try {
let result = run("which buildah");
return result.success;
} catch(err) {
return false;
}
}
print("=== Testing Buildah Container Operations ===");
// Check if buildah is available
let buildah_available = is_buildah_available();
if !buildah_available {
print("Buildah is not available. Skipping Buildah tests.");
// Exit gracefully without error
return;
}
print("✓ Buildah is available");
try {
// Test creating a new Builder
print("Testing bah_new() and reset()...");
let builder = bah_new("rhai_test_container", "alpine:latest");
// Enable debug mode
builder.debug_mode = true;
// Test reset
print("Testing reset()...");
builder.reset();
print("✓ reset(): Container reset successfully");
// Create a new container
builder = bah_new("rhai_test_container", "alpine:latest");
// Test config
print("Testing config()...");
let config_options = #{
"LABEL": "rhai_test=true",
"ENV": "TEST_VAR=test_value"
};
builder.config(config_options);
print("✓ config(): Container configured successfully");
// Test run with isolation
print("Testing run_with_isolation()...");
let isolation_result = builder.run_with_isolation("echo 'Hello with isolation'", "oci");
assert_true(isolation_result.success, "Command with isolation should succeed");
assert_true(isolation_result.stdout.contains("Hello with isolation"), "Command output should contain expected text");
print("✓ run_with_isolation(): Command executed successfully");
// Test content operations
print("Testing content operations...");
// Write content to a file
let script_content = `#!/bin/sh
echo "Hello from script"
`;
builder.write_content(script_content, "/script.sh");
// Make the script executable
builder.run("chmod +x /script.sh");
// Run the script
let script_result = builder.run("/script.sh");
assert_true(script_result.success, "Script should execute successfully");
assert_true(script_result.stdout.contains("Hello from script"), "Script output should contain expected text");
print("✓ Content operations: Script created and executed successfully");
// Test commit with config
print("Testing commit with config...");
let commit_options = #{
"author": "Rhai Test",
"message": "Test commit"
};
builder.commit("rhai_test_commit:latest", commit_options);
print("✓ commit(): Container committed with config successfully");
// Clean up
builder.remove();
print("✓ Cleanup: Container removed");
// Remove the committed image
builder.image_remove("rhai_test_commit:latest");
print("✓ Cleanup: Committed image removed");
print("All container operations tests completed successfully!");
} catch(err) {
print(`Error: ${err}`);
// Clean up in case of error
try {
// Remove test container if it exists
run("buildah rm rhai_test_container");
} catch(_) {}
try {
// Remove test image if it exists
run("buildah rmi rhai_test_commit:latest");
} catch(_) {}
throw err;
}

View File

@ -1,155 +0,0 @@
// run_all_tests.rhai
// Runs all Buildah module tests
print("=== Running Buildah Module Tests ===");
// Custom assert function
fn assert_true(condition, message) {
if !condition {
print(`ASSERTION FAILED: ${message}`);
throw message;
}
}
// Helper function to check if buildah is available
fn is_buildah_available() {
try {
let result = run("which buildah");
return result.success;
} catch(e) {
return false;
}
}
// Run each test directly
let passed = 0;
let failed = 0;
let skipped = 0;
let total = 0;
// Check if buildah is available
let buildah_available = is_buildah_available();
if !buildah_available {
print("Buildah is not available. Skipping all Buildah tests.");
skipped = 3; // Skip all three tests
total = 3;
} else {
// Test 1: Builder Pattern
print("\n--- Running Builder Pattern Tests ---");
try {
// Create a builder
let builder = bah_new("rhai_test_container", "alpine:latest");
// Test basic properties
assert_true(builder.container_id != "", "Container ID should not be empty");
assert_true(builder.name == "rhai_test_container", "Container name should match");
// Run a simple command
let result = builder.run("echo 'Hello from container'");
assert_true(result.success, "Command should succeed");
// Clean up
builder.remove();
print("--- Builder Pattern Tests completed successfully ---");
passed += 1;
} catch(err) {
print(`!!! Error in Builder Pattern Tests: ${err}`);
failed += 1;
// Clean up in case of error
try {
run("buildah rm rhai_test_container");
} catch(e) {
// Ignore errors during cleanup
}
}
total += 1;
// Test 2: Image Operations
print("\n--- Running Image Operations Tests ---");
try {
// Create a temporary directory for testing
let test_dir = "rhai_test_buildah";
mkdir(test_dir);
// Create a builder
let builder = bah_new("rhai_test_container", "alpine:latest");
// List images
let images = builder.images();
assert_true(images.len() > 0, "There should be at least one image");
// Clean up
builder.remove();
delete(test_dir);
print("--- Image Operations Tests completed successfully ---");
passed += 1;
} catch(err) {
print(`!!! Error in Image Operations Tests: ${err}`);
failed += 1;
// Clean up in case of error
try {
run("buildah rm rhai_test_container");
delete("rhai_test_buildah");
} catch(e) {
// Ignore errors during cleanup
}
}
total += 1;
// Test 3: Container Operations
print("\n--- Running Container Operations Tests ---");
try {
// Create a builder
let builder = bah_new("rhai_test_container", "alpine:latest");
// Test reset
builder.reset();
// Create a new container
builder = bah_new("rhai_test_container", "alpine:latest");
// Run a command
let result = builder.run("echo 'Hello from container'");
assert_true(result.success, "Command should succeed");
// Clean up
builder.remove();
print("--- Container Operations Tests completed successfully ---");
passed += 1;
} catch(err) {
print(`!!! Error in Container Operations Tests: ${err}`);
failed += 1;
// Clean up in case of error
try {
run("buildah rm rhai_test_container");
} catch(e) {
// Ignore errors during cleanup
}
}
total += 1;
}
print("\n=== Test Summary ===");
print(`Passed: ${passed}`);
print(`Failed: ${failed}`);
print(`Skipped: ${skipped}`);
print(`Total: ${total}`);
if failed == 0 {
if skipped > 0 {
print("\n⚠ All tests skipped or passed!");
} else {
print("\n✅ All tests passed!");
}
} else {
print("\n❌ Some tests failed!");
}
// Return the number of failed tests (0 means success)
failed;

View File

@ -1,76 +0,0 @@
// 01_git_basic.rhai
// Tests for basic Git operations in the Git module
// Custom assert function
fn assert_true(condition, message) {
if !condition {
print(`ASSERTION FAILED: ${message}`);
throw message;
}
}
// Create a temporary directory for Git operations
let test_dir = "rhai_test_git";
mkdir(test_dir);
print(`Created test directory: ${test_dir}`);
// Test GitTree constructor
print("Testing GitTree constructor...");
let git_tree = git_tree_new(test_dir);
print("✓ GitTree created successfully");
// Test GitTree.list() with empty directory
print("Testing GitTree.list() with empty directory...");
let repos = git_tree.list();
assert_true(repos.len() == 0, "Expected empty list of repositories");
print(`✓ GitTree.list(): Found ${repos.len()} repositories (expected 0)`);
// Test GitTree.find() with empty directory
print("Testing GitTree.find() with empty directory...");
let found_repos = git_tree.find("*");
assert_true(found_repos.len() == 0, "Expected empty list of repositories");
print(`✓ GitTree.find(): Found ${found_repos.len()} repositories (expected 0)`);
// Test GitTree.get() with a URL to clone a repository
// We'll use a small, public repository for testing
print("Testing GitTree.get() with URL...");
let repo_url = "https://github.com/rhaiscript/playground.git";
let repo = git_tree.get(repo_url);
print(`✓ GitTree.get(): Repository cloned successfully to ${repo.path()}`);
// Test GitRepo.path()
print("Testing GitRepo.path()...");
let repo_path = repo.path();
assert_true(repo_path.contains(test_dir), "Repository path should contain test directory");
print(`✓ GitRepo.path(): ${repo_path}`);
// Test GitRepo.has_changes()
print("Testing GitRepo.has_changes()...");
let has_changes = repo.has_changes();
print(`✓ GitRepo.has_changes(): ${has_changes}`);
// Test GitTree.list() after cloning
print("Testing GitTree.list() after cloning...");
let repos_after_clone = git_tree.list();
assert_true(repos_after_clone.len() > 0, "Expected non-empty list of repositories");
print(`✓ GitTree.list(): Found ${repos_after_clone.len()} repositories`);
// Test GitTree.find() after cloning
print("Testing GitTree.find() after cloning...");
let found_repos_after_clone = git_tree.find("*");
assert_true(found_repos_after_clone.len() > 0, "Expected non-empty list of repositories");
print(`✓ GitTree.find(): Found ${found_repos_after_clone.len()} repositories`);
// Test GitTree.get() with a path to an existing repository
print("Testing GitTree.get() with path...");
let repo_name = repos_after_clone[0];
let repo_by_path = git_tree.get(repo_name);
print(`✓ GitTree.get(): Repository opened successfully from ${repo_by_path.path()}`);
// Clean up
print("Cleaning up...");
delete(test_dir);
assert_true(!exist(test_dir), "Directory deletion failed");
print(`✓ Cleanup: Directory ${test_dir} removed`);
print("All basic Git tests completed successfully!");

View File

@ -1,63 +0,0 @@
// 02_git_operations.rhai
// Tests for Git operations like pull, reset, commit, and push
// Custom assert function
fn assert_true(condition, message) {
if !condition {
print(`ASSERTION FAILED: ${message}`);
throw message;
}
}
// Create a temporary directory for Git operations
let test_dir = "rhai_test_git_ops";
mkdir(test_dir);
print(`Created test directory: ${test_dir}`);
// Create a GitTree
print("Creating GitTree...");
let git_tree = git_tree_new(test_dir);
print("✓ GitTree created successfully");
// Clone a repository
print("Cloning repository...");
let repo_url = "https://github.com/rhaiscript/playground.git";
let repo = git_tree.get(repo_url);
print(`✓ Repository cloned successfully to ${repo.path()}`);
// Test GitRepo.pull()
print("Testing GitRepo.pull()...");
try {
let pull_result = repo.pull();
print("✓ GitRepo.pull(): Pull successful");
} catch(err) {
// Pull might fail if there are local changes or network issues
// This is expected in some cases, so we'll just log it
print(`Note: Pull failed with error: ${err}`);
print("✓ GitRepo.pull(): Error handled gracefully");
}
// Test GitRepo.reset()
print("Testing GitRepo.reset()...");
try {
let reset_result = repo.reset();
print("✓ GitRepo.reset(): Reset successful");
} catch(err) {
// Reset might fail in some cases
print(`Note: Reset failed with error: ${err}`);
print("✓ GitRepo.reset(): Error handled gracefully");
}
// Note: We won't test commit and push as they would modify the remote repository
// Instead, we'll just verify that the methods exist and can be called
print("Note: Not testing commit and push to avoid modifying remote repositories");
print("✓ GitRepo.commit() and GitRepo.push() methods exist");
// Clean up
print("Cleaning up...");
delete(test_dir);
assert_true(!exist(test_dir), "Directory deletion failed");
print(`✓ Cleanup: Directory ${test_dir} removed`);
print("All Git operations tests completed successfully!");

View File

@ -1,94 +0,0 @@
// run_all_tests.rhai
// Runs all Git module tests
print("=== Running Git Module Tests ===");
// Custom assert function
fn assert_true(condition, message) {
if !condition {
print(`ASSERTION FAILED: ${message}`);
throw message;
}
}
// Run each test directly
let passed = 0;
let failed = 0;
// Test 1: Basic Git Operations
print("\n--- Running Basic Git Operations Tests ---");
try {
// Create a temporary directory for Git operations
let test_dir = "rhai_test_git";
mkdir(test_dir);
print(`Created test directory: ${test_dir}`);
// Test GitTree constructor
print("Testing GitTree constructor...");
let git_tree = git_tree_new(test_dir);
print("✓ GitTree created successfully");
// Test GitTree.list() with empty directory
print("Testing GitTree.list() with empty directory...");
let repos = git_tree.list();
assert_true(repos.len() == 0, "Expected empty list of repositories");
print(`✓ GitTree.list(): Found ${repos.len()} repositories (expected 0)`);
// Test GitTree.find() with empty directory
print("Testing GitTree.find() with empty directory...");
let found_repos = git_tree.find("*");
assert_true(found_repos.len() == 0, "Expected empty list of repositories");
print(`✓ GitTree.find(): Found ${found_repos.len()} repositories (expected 0)`);
// Clean up
print("Cleaning up...");
delete(test_dir);
assert_true(!exist(test_dir), "Directory deletion failed");
print(`✓ Cleanup: Directory ${test_dir} removed`);
print("--- Basic Git Operations Tests completed successfully ---");
passed += 1;
} catch(err) {
print(`!!! Error in Basic Git Operations Tests: ${err}`);
failed += 1;
}
// Test 2: Git Repository Operations
print("\n--- Running Git Repository Operations Tests ---");
try {
// Create a temporary directory for Git operations
let test_dir = "rhai_test_git_ops";
mkdir(test_dir);
print(`Created test directory: ${test_dir}`);
// Create a GitTree
print("Creating GitTree...");
let git_tree = git_tree_new(test_dir);
print("✓ GitTree created successfully");
// Clean up
print("Cleaning up...");
delete(test_dir);
assert_true(!exist(test_dir), "Directory deletion failed");
print(`✓ Cleanup: Directory ${test_dir} removed`);
print("--- Git Repository Operations Tests completed successfully ---");
passed += 1;
} catch(err) {
print(`!!! Error in Git Repository Operations Tests: ${err}`);
failed += 1;
}
print("\n=== Test Summary ===");
print(`Passed: ${passed}`);
print(`Failed: ${failed}`);
print(`Total: ${passed + failed}`);
if failed == 0 {
print("\n✅ All tests passed!");
} else {
print("\n❌ Some tests failed!");
}
// Return the number of failed tests (0 means success)
failed;

View File

@ -1,176 +0,0 @@
// 01_container_operations.rhai
// Tests for Nerdctl container operations
// Custom assert function
fn assert_true(condition, message) {
if !condition {
print(`ASSERTION FAILED: ${message}`);
throw message;
}
}
// Custom assert_eq function
fn assert_eq(actual, expected, message) {
if actual != expected {
print(`ASSERTION FAILED: ${message}`);
print(`Expected: "${expected}"`);
print(`Actual: "${actual}"`);
throw message;
}
}
// Helper function to check if nerdctl is available
fn is_nerdctl_available() {
try {
let result = run("which nerdctl");
return result.success;
} catch(err) {
return false;
}
}
// Helper function to check if a container exists
fn container_exists(container_name) {
try {
let result = run(`nerdctl ps -a --format "{{.Names}}" | grep -w ${container_name}`);
return result.success;
} catch(err) {
return false;
}
}
// Helper function to clean up a container if it exists
fn cleanup_container(container_name) {
if container_exists(container_name) {
try {
run(`nerdctl stop ${container_name}`);
run(`nerdctl rm ${container_name}`);
print(`Cleaned up container: ${container_name}`);
} catch(err) {
print(`Error cleaning up container ${container_name}: ${err}`);
}
}
}
print("=== Testing Nerdctl Container Operations ===");
// Check if nerdctl is available
let nerdctl_available = is_nerdctl_available();
if !nerdctl_available {
print("nerdctl is not available. Skipping Nerdctl tests.");
// Exit gracefully without error
return;
}
print("✓ nerdctl is available");
// Define test container name
let container_name = "rhai_test_container";
// Clean up any existing test container
cleanup_container(container_name);
try {
// Test creating a new Container
print("Testing nerdctl_container_new()...");
let container = nerdctl_container_new(container_name);
// Test Container properties
print("Testing Container properties...");
assert_eq(container.name, container_name, "Container name should match");
assert_eq(container.container_id, "", "Container ID should be empty initially");
// Test setting container image
print("Testing with_image()...");
container.with_image("alpine:latest");
assert_eq(container.image, "alpine:latest", "Container image should match");
// Test setting detach mode
print("Testing with_detach()...");
container.with_detach(true);
assert_true(container.detach, "Container detach mode should be true");
// Test setting environment variables
print("Testing with_env()...");
container.with_env("TEST_VAR", "test_value");
// Test setting multiple environment variables
print("Testing with_envs()...");
let env_map = #{
"VAR1": "value1",
"VAR2": "value2"
};
container.with_envs(env_map);
// Test setting ports
print("Testing with_port()...");
container.with_port("8080:80");
// Test setting multiple ports
print("Testing with_ports()...");
container.with_ports(["9090:90", "7070:70"]);
// Test setting volumes
print("Testing with_volume()...");
// Create a test directory for volume mounting
let test_dir = "rhai_test_nerdctl_volume";
mkdir(test_dir);
container.with_volume(`${test_dir}:/data`);
// Test setting resource limits
print("Testing with_cpu_limit() and with_memory_limit()...");
container.with_cpu_limit("0.5");
container.with_memory_limit("256m");
// Test running the container
print("Testing run()...");
let run_result = container.run();
assert_true(run_result.success, "Container run should succeed");
assert_true(container.container_id != "", "Container ID should not be empty after run");
print(`✓ run(): Container started with ID: ${container.container_id}`);
// Test executing a command in the container
print("Testing exec()...");
let exec_result = container.exec("echo 'Hello from container'");
assert_true(exec_result.success, "Container exec should succeed");
assert_true(exec_result.stdout.contains("Hello from container"), "Exec output should contain expected text");
print("✓ exec(): Command executed successfully");
// Test getting container logs
print("Testing logs()...");
let logs_result = container.logs();
assert_true(logs_result.success, "Container logs should succeed");
print("✓ logs(): Logs retrieved successfully");
// Test stopping the container
print("Testing stop()...");
let stop_result = container.stop();
assert_true(stop_result.success, "Container stop should succeed");
print("✓ stop(): Container stopped successfully");
// Test removing the container
print("Testing remove()...");
let remove_result = container.remove();
assert_true(remove_result.success, "Container remove should succeed");
print("✓ remove(): Container removed successfully");
// Clean up test directory
delete(test_dir);
print("✓ Cleanup: Test directory removed");
print("All container operations tests completed successfully!");
} catch(err) {
print(`Error: ${err}`);
// Clean up in case of error
cleanup_container(container_name);
// Clean up test directory
try {
delete("rhai_test_nerdctl_volume");
} catch(e) {
// Ignore errors during cleanup
}
throw err;
}

View File

@ -1,168 +0,0 @@
// 02_image_operations.rhai
// Tests for Nerdctl image operations
// Custom assert function
fn assert_true(condition, message) {
if !condition {
print(`ASSERTION FAILED: ${message}`);
throw message;
}
}
// Custom assert_eq function
fn assert_eq(actual, expected, message) {
if actual != expected {
print(`ASSERTION FAILED: ${message}`);
print(`Expected: "${expected}"`);
print(`Actual: "${actual}"`);
throw message;
}
}
// Helper function to check if nerdctl is available
fn is_nerdctl_available() {
try {
let result = run("which nerdctl");
return result.success;
} catch(err) {
return false;
}
}
// Helper function to check if an image exists
fn image_exists(image_name) {
try {
let result = run(`nerdctl images -q ${image_name}`);
return result.success && result.stdout.trim() != "";
} catch(err) {
return false;
}
}
// Helper function to clean up an image if it exists
fn cleanup_image(image_name) {
if image_exists(image_name) {
try {
run(`nerdctl rmi ${image_name}`);
print(`Cleaned up image: ${image_name}`);
} catch(err) {
print(`Error cleaning up image ${image_name}: ${err}`);
}
}
}
print("=== Testing Nerdctl Image Operations ===");
// Check if nerdctl is available
let nerdctl_available = is_nerdctl_available();
if !nerdctl_available {
print("nerdctl is not available. Skipping Nerdctl tests.");
// Exit gracefully without error
return;
}
print("✓ nerdctl is available");
// Create a temporary directory for testing
let test_dir = "rhai_test_nerdctl";
mkdir(test_dir);
try {
// Test pulling an image
print("Testing nerdctl_image_pull()...");
// Use a small image for testing
let pull_result = nerdctl_image_pull("alpine:latest");
assert_true(pull_result.success, "Image pull should succeed");
print("✓ nerdctl_image_pull(): Image pulled successfully");
// Test listing images
print("Testing nerdctl_images()...");
let images_result = nerdctl_images();
assert_true(images_result.success, "Image listing should succeed");
assert_true(images_result.stdout.contains("alpine"), "Image list should contain alpine");
print("✓ nerdctl_images(): Images listed successfully");
// Test tagging an image
print("Testing nerdctl_image_tag()...");
let tag_result = nerdctl_image_tag("alpine:latest", "rhai_test_image:latest");
assert_true(tag_result.success, "Image tag should succeed");
print("✓ nerdctl_image_tag(): Image tagged successfully");
// Test building an image
print("Testing nerdctl_image_build()...");
// Create a simple Dockerfile
let dockerfile_content = `FROM alpine:latest
RUN echo "Hello from Dockerfile" > /hello.txt
CMD ["cat", "/hello.txt"]
`;
file_write(`${test_dir}/Dockerfile`, dockerfile_content);
// Build the image
let build_result = nerdctl_image_build("rhai_test_build:latest", test_dir);
assert_true(build_result.success, "Image build should succeed");
print("✓ nerdctl_image_build(): Image built successfully");
// Test running a container from the built image
print("Testing container from built image...");
let container_name = "rhai_test_container_from_build";
// Clean up any existing container with the same name
try {
run(`nerdctl stop ${container_name}`);
run(`nerdctl rm ${container_name}`);
} catch(e) {
// Ignore errors during cleanup
}
// Run the container
let run_result = nerdctl_run_with_name("rhai_test_build:latest", container_name);
assert_true(run_result.success, "Container run should succeed");
assert_true(run_result.stdout.contains("Hello from Dockerfile"), "Container output should contain expected text");
print("✓ Container from built image ran successfully");
// Clean up the container
let stop_result = nerdctl_stop(container_name);
assert_true(stop_result.success, "Container stop should succeed");
let remove_result = nerdctl_remove(container_name);
assert_true(remove_result.success, "Container remove should succeed");
print("✓ Cleanup: Container removed");
// Test removing images
print("Testing nerdctl_image_remove()...");
// Remove the tagged image
let remove_tag_result = nerdctl_image_remove("rhai_test_image:latest");
assert_true(remove_tag_result.success, "Image removal should succeed");
print("✓ nerdctl_image_remove(): Tagged image removed successfully");
// Remove the built image
let remove_build_result = nerdctl_image_remove("rhai_test_build:latest");
assert_true(remove_build_result.success, "Image removal should succeed");
print("✓ nerdctl_image_remove(): Built image removed successfully");
print("All image operations tests completed successfully!");
} catch(err) {
print(`Error: ${err}`);
// Clean up in case of error
try {
run("nerdctl stop rhai_test_container_from_build");
run("nerdctl rm rhai_test_container_from_build");
} catch(e) {
// Ignore errors during cleanup
}
try {
cleanup_image("rhai_test_image:latest");
cleanup_image("rhai_test_build:latest");
} catch(e) {
// Ignore errors during cleanup
}
throw err;
} finally {
// Clean up test directory
delete(test_dir);
print("✓ Cleanup: Test directory removed");
}

View File

@ -1,166 +0,0 @@
// 03_container_builder.rhai
// Tests for Nerdctl Container Builder pattern
// Custom assert function
fn assert_true(condition, message) {
if !condition {
print(`ASSERTION FAILED: ${message}`);
throw message;
}
}
// Custom assert_eq function
fn assert_eq(actual, expected, message) {
if actual != expected {
print(`ASSERTION FAILED: ${message}`);
print(`Expected: "${expected}"`);
print(`Actual: "${actual}"`);
throw message;
}
}
// Helper function to check if nerdctl is available
fn is_nerdctl_available() {
try {
let result = run("which nerdctl");
return result.success;
} catch(err) {
return false;
}
}
// Helper function to check if a container exists
fn container_exists(container_name) {
try {
let result = run(`nerdctl ps -a --format "{{.Names}}" | grep -w ${container_name}`);
return result.success;
} catch(err) {
return false;
}
}
// Helper function to clean up a container if it exists
fn cleanup_container(container_name) {
if container_exists(container_name) {
try {
run(`nerdctl stop ${container_name}`);
run(`nerdctl rm ${container_name}`);
print(`Cleaned up container: ${container_name}`);
} catch(err) {
print(`Error cleaning up container ${container_name}: ${err}`);
}
}
}
print("=== Testing Nerdctl Container Builder Pattern ===");
// Check if nerdctl is available
let nerdctl_available = is_nerdctl_available();
if !nerdctl_available {
print("nerdctl is not available. Skipping Nerdctl tests.");
// Exit gracefully without error
return;
}
print("✓ nerdctl is available");
// Define test container name
let container_name = "rhai_test_builder";
// Clean up any existing test container
cleanup_container(container_name);
// Create test directories
let work_dir = "rhai_test_nerdctl_work";
let config_dir = "rhai_test_nerdctl_config";
mkdir(work_dir);
mkdir(config_dir);
try {
// Test creating a container from an image with builder pattern
print("Testing nerdctl_container_from_image() with builder pattern...");
// Create a container with a rich set of options using the builder pattern
let container = nerdctl_container_from_image(container_name, "alpine:latest")
.reset() // Reset to default configuration
.with_detach(true)
.with_ports(["8080:80", "9090:90"])
.with_volumes([`${work_dir}:/data`, `${config_dir}:/config`])
.with_envs(#{
"ENV1": "value1",
"ENV2": "value2",
"TEST_MODE": "true"
})
.with_network("bridge")
.with_cpu_limit("0.5")
.with_memory_limit("256m");
// Verify container properties
assert_eq(container.name, container_name, "Container name should match");
assert_eq(container.image, "alpine:latest", "Container image should match");
assert_true(container.detach, "Container detach mode should be true");
// Run the container
print("Testing run() with builder pattern...");
let run_result = container.run();
assert_true(run_result.success, "Container run should succeed");
assert_true(container.container_id != "", "Container ID should not be empty after run");
print(`✓ run(): Container started with ID: ${container.container_id}`);
// Test environment variables
print("Testing environment variables...");
let env_result = container.exec("env");
assert_true(env_result.success, "Container exec should succeed");
assert_true(env_result.stdout.contains("ENV1=value1"), "Environment variable ENV1 should be set");
assert_true(env_result.stdout.contains("ENV2=value2"), "Environment variable ENV2 should be set");
assert_true(env_result.stdout.contains("TEST_MODE=true"), "Environment variable TEST_MODE should be set");
print("✓ Environment variables set correctly");
// Test volume mounts
print("Testing volume mounts...");
// Create a test file in the work directory
file_write(`${work_dir}/test.txt`, "Hello from host");
// Check if the file is accessible in the container
let volume_result = container.exec("cat /data/test.txt");
assert_true(volume_result.success, "Container exec should succeed");
assert_true(volume_result.stdout.contains("Hello from host"), "Volume mount should work correctly");
print("✓ Volume mounts working correctly");
// Test writing from container to volume
print("Testing writing from container to volume...");
let write_result = container.exec("echo 'Hello from container' > /config/container.txt");
assert_true(write_result.success, "Container exec should succeed");
// Check if the file was created on the host
let host_file_content = file_read(`${config_dir}/container.txt`);
assert_true(host_file_content.contains("Hello from container"), "Container should be able to write to volume");
print("✓ Container can write to volume");
// Test stopping the container
print("Testing stop()...");
let stop_result = container.stop();
assert_true(stop_result.success, "Container stop should succeed");
print("✓ stop(): Container stopped successfully");
// Test removing the container
print("Testing remove()...");
let remove_result = container.remove();
assert_true(remove_result.success, "Container remove should succeed");
print("✓ remove(): Container removed successfully");
print("All container builder pattern tests completed successfully!");
} catch(err) {
print(`Error: ${err}`);
// Clean up in case of error
cleanup_container(container_name);
throw err;
} finally {
// Clean up test directories
delete(work_dir);
delete(config_dir);
print("✓ Cleanup: Test directories removed");
}

View File

@ -1,183 +0,0 @@
// run_all_tests.rhai
// Runs all Nerdctl module tests
print("=== Running Nerdctl Module Tests ===");
// Custom assert function
fn assert_true(condition, message) {
if !condition {
print(`ASSERTION FAILED: ${message}`);
throw message;
}
}
// Helper function to check if nerdctl is available
fn is_nerdctl_available() {
try {
let result = run("which nerdctl");
return result.success;
} catch(e) {
return false;
}
}
// Helper function to clean up a container if it exists
fn cleanup_container(container_name) {
try {
run(`nerdctl stop ${container_name}`);
run(`nerdctl rm ${container_name}`);
} catch(e) {
// Ignore errors during cleanup
}
}
// Run each test directly
let passed = 0;
let failed = 0;
let skipped = 0;
let total = 0;
// Check if nerdctl is available
let nerdctl_available = is_nerdctl_available();
if !nerdctl_available {
print("nerdctl is not available. Skipping all Nerdctl tests.");
skipped = 3; // Skip all three tests
total = 3;
} else {
// Test 1: Container Operations
print("\n--- Running Container Operations Tests ---");
try {
// Define test container name
let container_name = "rhai_test_container";
// Clean up any existing test container
cleanup_container(container_name);
// Create a new Container
let container = nerdctl_container_new(container_name);
// Set container image
container.with_image("alpine:latest");
// Set detach mode
container.with_detach(true);
// Run the container
let run_result = container.run();
assert_true(run_result.success, "Container run should succeed");
// Execute a command in the container
let exec_result = container.exec("echo 'Hello from container'");
assert_true(exec_result.success, "Container exec should succeed");
// Clean up
container.stop();
container.remove();
print("--- Container Operations Tests completed successfully ---");
passed += 1;
} catch(err) {
print(`!!! Error in Container Operations Tests: ${err}`);
failed += 1;
// Clean up in case of error
cleanup_container("rhai_test_container");
}
total += 1;
// Test 2: Image Operations
print("\n--- Running Image Operations Tests ---");
try {
// Create a temporary directory for testing
let test_dir = "rhai_test_nerdctl";
mkdir(test_dir);
// Pull a small image for testing
let pull_result = nerdctl_image_pull("alpine:latest");
assert_true(pull_result.success, "Image pull should succeed");
// List images
let images_result = nerdctl_images();
assert_true(images_result.success, "Image listing should succeed");
// Clean up
delete(test_dir);
print("--- Image Operations Tests completed successfully ---");
passed += 1;
} catch(err) {
print(`!!! Error in Image Operations Tests: ${err}`);
failed += 1;
// Clean up in case of error
try {
delete("rhai_test_nerdctl");
} catch(e) {
// Ignore errors during cleanup
}
}
total += 1;
// Test 3: Container Builder Pattern
print("\n--- Running Container Builder Pattern Tests ---");
try {
// Define test container name
let container_name = "rhai_test_builder";
// Clean up any existing test container
cleanup_container(container_name);
// Create test directory
let work_dir = "rhai_test_nerdctl_work";
mkdir(work_dir);
// Create a container with builder pattern
let container = nerdctl_container_from_image(container_name, "alpine:latest")
.reset()
.with_detach(true)
.with_volumes([`${work_dir}:/data`]);
// Run the container
let run_result = container.run();
assert_true(run_result.success, "Container run should succeed");
// Clean up
container.stop();
container.remove();
delete(work_dir);
print("--- Container Builder Pattern Tests completed successfully ---");
passed += 1;
} catch(err) {
print(`!!! Error in Container Builder Pattern Tests: ${err}`);
failed += 1;
// Clean up in case of error
cleanup_container("rhai_test_builder");
try {
delete("rhai_test_nerdctl_work");
} catch(e) {
// Ignore errors during cleanup
}
}
total += 1;
}
print("\n=== Test Summary ===");
print(`Passed: ${passed}`);
print(`Failed: ${failed}`);
print(`Skipped: ${skipped}`);
print(`Total: ${total}`);
if failed == 0 {
if skipped > 0 {
print("\n⚠ All tests skipped or passed!");
} else {
print("\n✅ All tests passed!");
}
} else {
print("\n❌ Some tests failed!");
}
// Return the number of failed tests (0 means success)
failed;

View File

@ -1,111 +0,0 @@
// 01_file_operations.rhai
// Tests for file system operations in the OS module
// Custom assert function
fn assert_true(condition, message) {
if !condition {
print(`ASSERTION FAILED: ${message}`);
throw message;
}
}
// Create a test directory structure
let test_dir = "rhai_test_fs";
let sub_dir = test_dir + "/subdir";
// Test mkdir function
print("Testing mkdir...");
let mkdir_result = mkdir(test_dir);
assert_true(exist(test_dir), "Directory creation failed");
print(`✓ mkdir: ${mkdir_result}`);
// Test nested directory creation
let nested_result = mkdir(sub_dir);
assert_true(exist(sub_dir), "Nested directory creation failed");
print(`✓ mkdir (nested): ${nested_result}`);
// Test file_write function
let test_file = test_dir + "/test.txt";
let file_content = "This is a test file created by Rhai test script.";
let write_result = file_write(test_file, file_content);
assert_true(exist(test_file), "File creation failed");
print(`✓ file_write: ${write_result}`);
// Test file_read function
let read_content = file_read(test_file);
assert_true(read_content == file_content, "File content doesn't match");
print(`✓ file_read: Content matches`);
// Test file_size function
let size = file_size(test_file);
assert_true(size > 0, "File size should be greater than 0");
print(`✓ file_size: ${size} bytes`);
// Test file_write_append function
let append_content = "\nThis is appended content.";
let append_result = file_write_append(test_file, append_content);
let new_content = file_read(test_file);
assert_true(new_content == file_content + append_content, "Appended content doesn't match");
print(`✓ file_write_append: ${append_result}`);
// Test copy function
let copied_file = test_dir + "/copied.txt";
let copy_result = copy(test_file, copied_file);
assert_true(exist(copied_file), "File copy failed");
print(`✓ copy: ${copy_result}`);
// Test mv function
let moved_file = test_dir + "/moved.txt";
let mv_result = mv(copied_file, moved_file);
assert_true(exist(moved_file), "File move failed");
assert_true(!exist(copied_file), "Source file still exists after move");
print(`✓ mv: ${mv_result}`);
// Test find_file function
let found_file = find_file(test_dir, "*.txt");
assert_true(found_file.contains("test.txt") || found_file.contains("moved.txt"), "find_file failed");
print(`✓ find_file: ${found_file}`);
// Test find_files function
let found_files = find_files(test_dir, "*.txt");
assert_true(found_files.len() == 2, "find_files should find 2 files");
print(`✓ find_files: Found ${found_files.len()} files`);
// Test find_dir function
let found_dir = find_dir(test_dir, "sub*");
assert_true(found_dir.contains("subdir"), "find_dir failed");
print(`✓ find_dir: ${found_dir}`);
// Test find_dirs function
let found_dirs = find_dirs(test_dir, "sub*");
assert_true(found_dirs.len() == 1, "find_dirs should find 1 directory");
print(`✓ find_dirs: Found ${found_dirs.len()} directories`);
// Test chdir function
// Save current directory path before changing
let chdir_result = chdir(test_dir);
print(`✓ chdir: ${chdir_result}`);
// Change back to parent directory
chdir("..");
// Test rsync function (if available)
let rsync_dir = test_dir + "/rsync_dest";
mkdir(rsync_dir);
let rsync_result = rsync(test_dir, rsync_dir);
print(`✓ rsync: ${rsync_result}`);
// Test delete function
let delete_file_result = delete(test_file);
assert_true(!exist(test_file), "File deletion failed");
print(`✓ delete (file): ${delete_file_result}`);
// Clean up
delete(moved_file);
delete(sub_dir);
delete(rsync_dir);
delete(test_dir);
assert_true(!exist(test_dir), "Directory deletion failed");
print(`✓ delete (directory): Directory cleaned up`);
print("All file system tests completed successfully!");

View File

@ -1,53 +0,0 @@
// 02_download_operations.rhai
// Tests for download operations in the OS module
// Custom assert function
fn assert_true(condition, message) {
if !condition {
print(`ASSERTION FAILED: ${message}`);
throw message;
}
}
// Create a test directory
let test_dir = "rhai_test_download";
mkdir(test_dir);
print(`Created test directory: ${test_dir}`);
// Test which function to ensure curl is available
let curl_path = which("curl");
if curl_path == "" {
print("Warning: curl not found, download tests may fail");
} else {
print(`✓ which: curl found at ${curl_path}`);
}
// Test cmd_ensure_exists function
let ensure_result = cmd_ensure_exists("curl");
print(`✓ cmd_ensure_exists: ${ensure_result}`);
// Test download function with a small file
let download_url = "https://raw.githubusercontent.com/rust-lang/rust/master/LICENSE-MIT";
let download_dest = test_dir + "/license.txt";
let min_size_kb = 1; // Minimum size in KB
print(`Downloading ${download_url}...`);
let download_result = download_file(download_url, download_dest, min_size_kb);
assert_true(exist(download_dest), "Download failed");
print(`✓ download_file: ${download_result}`);
// Verify the downloaded file
let file_content = file_read(download_dest);
assert_true(file_content.contains("Permission is hereby granted"), "Downloaded file content is incorrect");
print("✓ Downloaded file content verified");
// Test chmod_exec function
let chmod_result = chmod_exec(download_dest);
print(`✓ chmod_exec: ${chmod_result}`);
// Clean up
delete(test_dir);
assert_true(!exist(test_dir), "Directory deletion failed");
print(`✓ Cleanup: Directory ${test_dir} removed`);
print("All download tests completed successfully!");

View File

@ -1,56 +0,0 @@
// 03_package_operations.rhai
// Tests for package management operations in the OS module
// Custom assert function
fn assert_true(condition, message) {
if !condition {
print(`ASSERTION FAILED: ${message}`);
throw message;
}
}
// Test package_platform function
let platform = package_platform();
print(`Current platform: ${platform}`);
// Test package_set_debug function
let debug_enabled = package_set_debug(true);
assert_true(debug_enabled, "Debug mode should be enabled");
print("✓ package_set_debug: Debug mode enabled");
// Disable debug mode for remaining tests
package_set_debug(false);
// Test package_is_installed function with a package that should exist on most systems
let common_packages = ["bash", "curl", "grep"];
let found_package = false;
for pkg in common_packages {
let is_installed = package_is_installed(pkg);
if is_installed {
print(`✓ package_is_installed: ${pkg} is installed`);
found_package = true;
break;
}
}
if !found_package {
print("Warning: None of the common packages were found installed");
}
// Test package_search function with a common term
// Note: This might be slow and produce a lot of output
print("Testing package_search (this might take a moment)...");
let search_results = package_search("lib");
print(`✓ package_search: Found ${search_results.len()} packages containing 'lib'`);
// Test package_list function
// Note: This might be slow and produce a lot of output
print("Testing package_list (this might take a moment)...");
let installed_packages = package_list();
print(`✓ package_list: Found ${installed_packages.len()} installed packages`);
// Note: We're not testing package_install, package_remove, package_update, or package_upgrade
// as they require root privileges and could modify the system state
print("All package management tests completed successfully!");

View File

@ -1,148 +0,0 @@
// run_all_tests.rhai
// Runs all OS module tests
print("=== Running OS Module Tests ===");
// Custom assert function
fn assert_true(condition, message) {
if !condition {
print(`ASSERTION FAILED: ${message}`);
throw message;
}
}
// Run each test directly
let passed = 0;
let failed = 0;
// Test 1: File Operations
print("\n--- Running File Operations Tests ---");
try {
// Create a test directory structure
let test_dir = "rhai_test_fs";
let sub_dir = test_dir + "/subdir";
// Test mkdir function
print("Testing mkdir...");
let mkdir_result = mkdir(test_dir);
assert_true(exist(test_dir), "Directory creation failed");
print(`✓ mkdir: ${mkdir_result}`);
// Test nested directory creation
let nested_result = mkdir(sub_dir);
assert_true(exist(sub_dir), "Nested directory creation failed");
print(`✓ mkdir (nested): ${nested_result}`);
// Test file_write function
let test_file = test_dir + "/test.txt";
let file_content = "This is a test file created by Rhai test script.";
let write_result = file_write(test_file, file_content);
assert_true(exist(test_file), "File creation failed");
print(`✓ file_write: ${write_result}`);
// Test file_read function
let read_content = file_read(test_file);
assert_true(read_content == file_content, "File content doesn't match");
print(`✓ file_read: Content matches`);
// Test file_size function
let size = file_size(test_file);
assert_true(size > 0, "File size should be greater than 0");
print(`✓ file_size: ${size} bytes`);
// Clean up
delete(test_file);
delete(sub_dir);
delete(test_dir);
assert_true(!exist(test_dir), "Directory deletion failed");
print(`✓ delete: Directory cleaned up`);
print("--- File Operations Tests completed successfully ---");
passed += 1;
} catch(err) {
print(`!!! Error in File Operations Tests: ${err}`);
failed += 1;
}
// Test 2: Download Operations
print("\n--- Running Download Operations Tests ---");
try {
// Create a test directory
let test_dir = "rhai_test_download";
mkdir(test_dir);
print(`Created test directory: ${test_dir}`);
// Test which function to ensure curl is available
let curl_path = which("curl");
if curl_path == "" {
print("Warning: curl not found, download tests may fail");
} else {
print(`✓ which: curl found at ${curl_path}`);
}
// Test cmd_ensure_exists function
let ensure_result = cmd_ensure_exists("curl");
print(`✓ cmd_ensure_exists: ${ensure_result}`);
// Test download function with a small file
let download_url = "https://raw.githubusercontent.com/rust-lang/rust/master/LICENSE-MIT";
let download_dest = test_dir + "/license.txt";
let min_size_kb = 1; // Minimum size in KB
print(`Downloading ${download_url}...`);
let download_result = download_file(download_url, download_dest, min_size_kb);
assert_true(exist(download_dest), "Download failed");
print(`✓ download_file: ${download_result}`);
// Verify the downloaded file
let file_content = file_read(download_dest);
assert_true(file_content.contains("Permission is hereby granted"), "Downloaded file content is incorrect");
print("✓ Downloaded file content verified");
// Clean up
delete(test_dir);
assert_true(!exist(test_dir), "Directory deletion failed");
print(`✓ Cleanup: Directory ${test_dir} removed`);
print("--- Download Operations Tests completed successfully ---");
passed += 1;
} catch(err) {
print(`!!! Error in Download Operations Tests: ${err}`);
failed += 1;
}
// Test 3: Package Operations
print("\n--- Running Package Operations Tests ---");
try {
// Test package_platform function
let platform = package_platform();
print(`Current platform: ${platform}`);
// Test package_set_debug function
let debug_enabled = package_set_debug(true);
assert_true(debug_enabled, "Debug mode should be enabled");
print("✓ package_set_debug: Debug mode enabled");
// Disable debug mode for remaining tests
package_set_debug(false);
print("--- Package Operations Tests completed successfully ---");
passed += 1;
} catch(err) {
print(`!!! Error in Package Operations Tests: ${err}`);
failed += 1;
}
print("\n=== Test Summary ===");
print(`Passed: ${passed}`);
print(`Failed: ${failed}`);
print(`Total: ${passed + failed}`);
if failed == 0 {
print("\n✅ All tests passed!");
} else {
print("\n❌ Some tests failed!");
}
// Return the number of failed tests (0 means success)
failed;

View File

@ -1,106 +0,0 @@
// 01_postgres_connection.rhai
// Tests for PostgreSQL client connection and basic operations
// Custom assert function
fn assert_true(condition, message) {
if !condition {
print(`ASSERTION FAILED: ${message}`);
throw message;
}
}
// Helper function to check if PostgreSQL is available
fn is_postgres_available() {
try {
// Try to execute a simple connection
let connect_result = pg_connect();
return connect_result;
} catch(err) {
print(`PostgreSQL connection error: ${err}`);
return false;
}
}
print("=== Testing PostgreSQL Client Connection ===");
// Check if PostgreSQL is available
let postgres_available = is_postgres_available();
if !postgres_available {
print("PostgreSQL server is not available. Skipping PostgreSQL tests.");
// Exit gracefully without error
return;
}
print("✓ PostgreSQL server is available");
// Test pg_ping function
print("Testing pg_ping()...");
let ping_result = pg_ping();
assert_true(ping_result, "PING should return true");
print(`✓ pg_ping(): Returned ${ping_result}`);
// Test pg_execute function
print("Testing pg_execute()...");
let test_table = "rhai_test_table";
// Create a test table
let create_table_query = `
CREATE TABLE IF NOT EXISTS ${test_table} (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
value INTEGER
)
`;
let create_result = pg_execute(create_table_query);
assert_true(create_result >= 0, "CREATE TABLE operation should succeed");
print(`✓ pg_execute(): Successfully created table ${test_table}`);
// Insert a test row
let insert_query = `
INSERT INTO ${test_table} (name, value)
VALUES ('test_name', 42)
`;
let insert_result = pg_execute(insert_query);
assert_true(insert_result > 0, "INSERT operation should succeed");
print(`✓ pg_execute(): Successfully inserted row into ${test_table}`);
// Test pg_query function
print("Testing pg_query()...");
let select_query = `
SELECT * FROM ${test_table}
`;
let select_result = pg_query(select_query);
assert_true(select_result.len() > 0, "SELECT should return at least one row");
print(`✓ pg_query(): Successfully retrieved ${select_result.len()} rows from ${test_table}`);
// Test pg_query_one function
print("Testing pg_query_one()...");
let select_one_query = `
SELECT * FROM ${test_table} LIMIT 1
`;
let select_one_result = pg_query_one(select_one_query);
assert_true(select_one_result["name"] == "test_name", "SELECT ONE should return the correct name");
assert_true(select_one_result["value"] == "42", "SELECT ONE should return the correct value");
print(`✓ pg_query_one(): Successfully retrieved row with name=${select_one_result["name"]} and value=${select_one_result["value"]}`);
// Clean up
print("Cleaning up...");
let drop_table_query = `
DROP TABLE IF EXISTS ${test_table}
`;
let drop_result = pg_execute(drop_table_query);
assert_true(drop_result >= 0, "DROP TABLE operation should succeed");
print(`✓ pg_execute(): Successfully dropped table ${test_table}`);
// Test pg_reset function
print("Testing pg_reset()...");
let reset_result = pg_reset();
assert_true(reset_result, "RESET should return true");
print(`✓ pg_reset(): Successfully reset PostgreSQL client`);
print("All PostgreSQL connection tests completed successfully!");

View File

@ -1,164 +0,0 @@
// PostgreSQL Installer Test
//
// This test script demonstrates how to use the PostgreSQL installer module to:
// - Install PostgreSQL using nerdctl
// - Create a database
// - Execute SQL scripts
// - Check if PostgreSQL is running
//
// Prerequisites:
// - nerdctl must be installed and working
// - Docker images must be accessible
// Define utility functions
fn assert_true(condition, message) {
if !condition {
print(`ASSERTION FAILED: ${message}`);
throw message;
}
}
// Define test variables (will be used inside the test function)
// Function to check if nerdctl is available
fn is_nerdctl_available() {
try {
// For testing purposes, we'll assume nerdctl is not available
// In a real-world scenario, you would check if nerdctl is installed
return false;
} catch {
return false;
}
}
// Function to clean up any existing PostgreSQL container
fn cleanup_postgres() {
try {
// In a real-world scenario, you would use nerdctl to stop and remove the container
// For this test, we'll just print a message
print("Cleaned up existing PostgreSQL container (simulated)");
} catch {
// Ignore errors if container doesn't exist
}
}
// Main test function
fn run_postgres_installer_test() {
print("\n=== PostgreSQL Installer Test ===");
// Define test variables
let container_name = "postgres-test";
let postgres_version = "15";
let postgres_port = 5433; // Use a non-default port to avoid conflicts
let postgres_user = "testuser";
let postgres_password = "testpassword";
let test_db_name = "testdb";
// // Check if nerdctl is available
// if !is_nerdctl_available() {
// print("nerdctl is not available. Skipping PostgreSQL installer test.");
// return 1; // Skip the test
// }
// Clean up any existing PostgreSQL container
cleanup_postgres();
// Test 1: Install PostgreSQL
print("\n1. Installing PostgreSQL...");
try {
let install_result = pg_install(
container_name,
postgres_version,
postgres_port,
postgres_user,
postgres_password
);
assert_true(install_result, "PostgreSQL installation should succeed");
print("✓ PostgreSQL installed successfully");
// Wait a bit for PostgreSQL to fully initialize
print("Waiting for PostgreSQL to initialize...");
// In a real-world scenario, you would wait for PostgreSQL to initialize
// For this test, we'll just print a message
print("Waited for PostgreSQL to initialize (simulated)")
} catch(e) {
print(`✗ Failed to install PostgreSQL: ${e}`);
cleanup_postgres();
return 1; // Test failed
}
// Test 2: Check if PostgreSQL is running
print("\n2. Checking if PostgreSQL is running...");
try {
let running = pg_is_running(container_name);
assert_true(running, "PostgreSQL should be running");
print("✓ PostgreSQL is running");
} catch(e) {
print(`✗ Failed to check if PostgreSQL is running: ${e}`);
cleanup_postgres();
return 1; // Test failed
}
// Test 3: Create a database
print("\n3. Creating a database...");
try {
let create_result = pg_create_database(container_name, test_db_name);
assert_true(create_result, "Database creation should succeed");
print(`✓ Database '${test_db_name}' created successfully`);
} catch(e) {
print(`✗ Failed to create database: ${e}`);
cleanup_postgres();
return 1; // Test failed
}
// Test 4: Execute SQL script
print("\n4. Executing SQL script...");
try {
// Create a table
let create_table_sql = `
CREATE TABLE test_table (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
value INTEGER
);
`;
let result = pg_execute_sql(container_name, test_db_name, create_table_sql);
print("✓ Created table successfully");
// Insert data
let insert_sql = `
INSERT INTO test_table (name, value) VALUES
('test1', 100),
('test2', 200),
('test3', 300);
`;
result = pg_execute_sql(container_name, test_db_name, insert_sql);
print("✓ Inserted data successfully");
// Query data
let query_sql = "SELECT * FROM test_table ORDER BY id;";
result = pg_execute_sql(container_name, test_db_name, query_sql);
print("✓ Queried data successfully");
print(`Query result: ${result}`);
} catch(e) {
print(`✗ Failed to execute SQL script: ${e}`);
cleanup_postgres();
return 1; // Test failed
}
// Clean up
print("\nCleaning up...");
cleanup_postgres();
print("\n=== PostgreSQL Installer Test Completed Successfully ===");
return 0; // Test passed
}
// Run the test
let result = run_postgres_installer_test();
// Return the result
result

View File

@ -1,61 +0,0 @@
// PostgreSQL Installer Test (Mock)
//
// This test script simulates the PostgreSQL installer module tests
// without actually calling the PostgreSQL functions.
// Define utility functions
fn assert_true(condition, message) {
if !condition {
print(`ASSERTION FAILED: ${message}`);
throw message;
}
}
// Main test function
fn run_postgres_installer_test() {
print("\n=== PostgreSQL Installer Test (Mock) ===");
// Define test variables
let container_name = "postgres-test";
let postgres_version = "15";
let postgres_port = 5433; // Use a non-default port to avoid conflicts
let postgres_user = "testuser";
let postgres_password = "testpassword";
let test_db_name = "testdb";
// Clean up any existing PostgreSQL container
print("Cleaned up existing PostgreSQL container (simulated)");
// Test 1: Install PostgreSQL
print("\n1. Installing PostgreSQL...");
print("✓ PostgreSQL installed successfully (simulated)");
print("Waited for PostgreSQL to initialize (simulated)");
// Test 2: Check if PostgreSQL is running
print("\n2. Checking if PostgreSQL is running...");
print("✓ PostgreSQL is running (simulated)");
// Test 3: Create a database
print("\n3. Creating a database...");
print(`✓ Database '${test_db_name}' created successfully (simulated)`);
// Test 4: Execute SQL script
print("\n4. Executing SQL script...");
print("✓ Created table successfully (simulated)");
print("✓ Inserted data successfully (simulated)");
print("✓ Queried data successfully (simulated)");
print("Query result: (simulated results)");
// Clean up
print("\nCleaning up...");
print("Cleaned up existing PostgreSQL container (simulated)");
print("\n=== PostgreSQL Installer Test Completed Successfully ===");
return 0; // Test passed
}
// Run the test
let result = run_postgres_installer_test();
// Return the result
result

View File

@ -1,101 +0,0 @@
// PostgreSQL Installer Test (Simplified)
//
// This test script demonstrates how to use the PostgreSQL installer module to:
// - Install PostgreSQL using nerdctl
// - Create a database
// - Execute SQL scripts
// - Check if PostgreSQL is running
// Define test variables
let container_name = "postgres-test";
let postgres_version = "15";
let postgres_port = 5433; // Use a non-default port to avoid conflicts
let postgres_user = "testuser";
let postgres_password = "testpassword";
let test_db_name = "testdb";
// Main test function
fn test_postgres_installer() {
print("\n=== PostgreSQL Installer Test ===");
// Test 1: Install PostgreSQL
print("\n1. Installing PostgreSQL...");
try {
let install_result = pg_install(
container_name,
postgres_version,
postgres_port,
postgres_user,
postgres_password
);
print(`PostgreSQL installation result: ${install_result}`);
print("✓ PostgreSQL installed successfully");
} catch(e) {
print(`✗ Failed to install PostgreSQL: ${e}`);
return;
}
// Test 2: Check if PostgreSQL is running
print("\n2. Checking if PostgreSQL is running...");
try {
let running = pg_is_running(container_name);
print(`PostgreSQL running status: ${running}`);
print("✓ PostgreSQL is running");
} catch(e) {
print(`✗ Failed to check if PostgreSQL is running: ${e}`);
return;
}
// Test 3: Create a database
print("\n3. Creating a database...");
try {
let create_result = pg_create_database(container_name, test_db_name);
print(`Database creation result: ${create_result}`);
print(`✓ Database '${test_db_name}' created successfully`);
} catch(e) {
print(`✗ Failed to create database: ${e}`);
return;
}
// Test 4: Execute SQL script
print("\n4. Executing SQL script...");
try {
// Create a table
let create_table_sql = `
CREATE TABLE test_table (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
value INTEGER
);
`;
let result = pg_execute_sql(container_name, test_db_name, create_table_sql);
print("✓ Created table successfully");
// Insert data
let insert_sql = `
INSERT INTO test_table (name, value) VALUES
('test1', 100),
('test2', 200),
('test3', 300);
`;
result = pg_execute_sql(container_name, test_db_name, insert_sql);
print("✓ Inserted data successfully");
// Query data
let query_sql = "SELECT * FROM test_table ORDER BY id;";
result = pg_execute_sql(container_name, test_db_name, query_sql);
print("✓ Queried data successfully");
print(`Query result: ${result}`);
} catch(e) {
print(`✗ Failed to execute SQL script: ${e}`);
return;
}
print("\n=== PostgreSQL Installer Test Completed Successfully ===");
}
// Run the test
test_postgres_installer();

View File

@ -1,82 +0,0 @@
// PostgreSQL Installer Example
//
// This example demonstrates how to use the PostgreSQL installer module to:
// - Install PostgreSQL using nerdctl
// - Create a database
// - Execute SQL scripts
// - Check if PostgreSQL is running
//
// Prerequisites:
// - nerdctl must be installed and working
// - Docker images must be accessible
// Define variables
let container_name = "postgres-example";
let postgres_version = "15";
let postgres_port = 5432;
let postgres_user = "exampleuser";
let postgres_password = "examplepassword";
let db_name = "exampledb";
// Install PostgreSQL
print("Installing PostgreSQL...");
try {
let install_result = pg_install(
container_name,
postgres_version,
postgres_port,
postgres_user,
postgres_password
);
print("PostgreSQL installed successfully!");
// Check if PostgreSQL is running
print("\nChecking if PostgreSQL is running...");
let running = pg_is_running(container_name);
if (running) {
print("PostgreSQL is running!");
// Create a database
print("\nCreating a database...");
let create_result = pg_create_database(container_name, db_name);
print(`Database '${db_name}' created successfully!`);
// Create a table
print("\nCreating a table...");
let create_table_sql = `
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL
);
`;
let result = pg_execute_sql(container_name, db_name, create_table_sql);
print("Table created successfully!");
// Insert data
print("\nInserting data...");
let insert_sql = `
INSERT INTO users (name, email) VALUES
('John Doe', 'john@example.com'),
('Jane Smith', 'jane@example.com');
`;
result = pg_execute_sql(container_name, db_name, insert_sql);
print("Data inserted successfully!");
// Query data
print("\nQuerying data...");
let query_sql = "SELECT * FROM users;";
result = pg_execute_sql(container_name, db_name, query_sql);
print(`Query result: ${result}`);
} else {
print("PostgreSQL is not running!");
}
} catch(e) {
print(`Error: ${e}`);
}
print("\nExample completed!");

View File

@ -1,159 +0,0 @@
// run_all_tests.rhai
// Runs all PostgreSQL client module tests
print("=== Running PostgreSQL Client Module Tests ===");
// Custom assert function
fn assert_true(condition, message) {
if !condition {
print(`ASSERTION FAILED: ${message}`);
throw message;
}
}
// Helper function to check if PostgreSQL is available
fn is_postgres_available() {
try {
// Try to execute a simple connection
let connect_result = pg_connect();
return connect_result;
} catch(err) {
print(`PostgreSQL connection error: ${err}`);
return false;
}
}
// Helper function to check if nerdctl is available
fn is_nerdctl_available() {
try {
// For testing purposes, we'll assume nerdctl is not available
// In a real-world scenario, you would check if nerdctl is installed
return false;
} catch {
return false;
}
}
// Run each test directly
let passed = 0;
let failed = 0;
let skipped = 0;
// Check if PostgreSQL is available
let postgres_available = is_postgres_available();
if !postgres_available {
print("PostgreSQL server is not available. Skipping basic PostgreSQL tests.");
skipped += 1; // Skip the test
} else {
// Test 1: PostgreSQL Connection
print("\n--- Running PostgreSQL Connection Tests ---");
try {
// Test pg_ping function
print("Testing pg_ping()...");
let ping_result = pg_ping();
assert_true(ping_result, "PING should return true");
print(`✓ pg_ping(): Returned ${ping_result}`);
// Test pg_execute function
print("Testing pg_execute()...");
let test_table = "rhai_test_table";
// Create a test table
let create_table_query = `
CREATE TABLE IF NOT EXISTS ${test_table} (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
value INTEGER
)
`;
let create_result = pg_execute(create_table_query);
assert_true(create_result >= 0, "CREATE TABLE operation should succeed");
print(`✓ pg_execute(): Successfully created table ${test_table}`);
// Insert a test row
let insert_query = `
INSERT INTO ${test_table} (name, value)
VALUES ('test_name', 42)
`;
let insert_result = pg_execute(insert_query);
assert_true(insert_result > 0, "INSERT operation should succeed");
print(`✓ pg_execute(): Successfully inserted row into ${test_table}`);
// Test pg_query function
print("Testing pg_query()...");
let select_query = `
SELECT * FROM ${test_table}
`;
let select_result = pg_query(select_query);
assert_true(select_result.len() > 0, "SELECT should return at least one row");
print(`✓ pg_query(): Successfully retrieved ${select_result.len()} rows from ${test_table}`);
// Clean up
print("Cleaning up...");
let drop_table_query = `
DROP TABLE IF EXISTS ${test_table}
`;
let drop_result = pg_execute(drop_table_query);
assert_true(drop_result >= 0, "DROP TABLE operation should succeed");
print(`✓ pg_execute(): Successfully dropped table ${test_table}`);
print("--- PostgreSQL Connection Tests completed successfully ---");
passed += 1;
} catch(err) {
print(`!!! Error in PostgreSQL Connection Tests: ${err}`);
failed += 1;
}
}
// Test 2: PostgreSQL Installer
// Check if nerdctl is available
let nerdctl_available = is_nerdctl_available();
if !nerdctl_available {
print("nerdctl is not available. Running mock PostgreSQL installer tests.");
try {
// Run the mock installer test
let installer_test_result = 0; // Simulate success
print("\n--- Running PostgreSQL Installer Tests (Mock) ---");
print("✓ PostgreSQL installed successfully (simulated)");
print("✓ Database created successfully (simulated)");
print("✓ SQL executed successfully (simulated)");
print("--- PostgreSQL Installer Tests completed successfully (simulated) ---");
passed += 1;
} catch(err) {
print(`!!! Error in PostgreSQL Installer Tests: ${err}`);
failed += 1;
}
} else {
print("\n--- Running PostgreSQL Installer Tests ---");
try {
// For testing purposes, we'll assume the installer tests pass
print("--- PostgreSQL Installer Tests completed successfully ---");
passed += 1;
} catch(err) {
print(`!!! Error in PostgreSQL Installer Tests: ${err}`);
failed += 1;
}
}
print("\n=== Test Summary ===");
print(`Passed: ${passed}`);
print(`Failed: ${failed}`);
print(`Skipped: ${skipped}`);
print(`Total: ${passed + failed + skipped}`);
if failed == 0 {
if skipped > 0 {
print("\n⚠ All tests skipped or passed!");
} else {
print("\n✅ All tests passed!");
}
} else {
print("\n❌ Some tests failed!");
}
// Return the number of failed tests (0 means success)
failed;

View File

@ -1,93 +0,0 @@
// Test script to check if the PostgreSQL functions are registered
// Try to call the basic PostgreSQL functions
try {
print("Trying to call pg_connect()...");
let result = pg_connect();
print("pg_connect result: " + result);
} catch(e) {
print("Error calling pg_connect: " + e);
}
// Try to call the pg_ping function
try {
print("\nTrying to call pg_ping()...");
let result = pg_ping();
print("pg_ping result: " + result);
} catch(e) {
print("Error calling pg_ping: " + e);
}
// Try to call the pg_reset function
try {
print("\nTrying to call pg_reset()...");
let result = pg_reset();
print("pg_reset result: " + result);
} catch(e) {
print("Error calling pg_reset: " + e);
}
// Try to call the pg_execute function
try {
print("\nTrying to call pg_execute()...");
let result = pg_execute("SELECT 1");
print("pg_execute result: " + result);
} catch(e) {
print("Error calling pg_execute: " + e);
}
// Try to call the pg_query function
try {
print("\nTrying to call pg_query()...");
let result = pg_query("SELECT 1");
print("pg_query result: " + result);
} catch(e) {
print("Error calling pg_query: " + e);
}
// Try to call the pg_query_one function
try {
print("\nTrying to call pg_query_one()...");
let result = pg_query_one("SELECT 1");
print("pg_query_one result: " + result);
} catch(e) {
print("Error calling pg_query_one: " + e);
}
// Try to call the pg_install function
try {
print("\nTrying to call pg_install()...");
let result = pg_install("postgres-test", "15", 5433, "testuser", "testpassword");
print("pg_install result: " + result);
} catch(e) {
print("Error calling pg_install: " + e);
}
// Try to call the pg_create_database function
try {
print("\nTrying to call pg_create_database()...");
let result = pg_create_database("postgres-test", "testdb");
print("pg_create_database result: " + result);
} catch(e) {
print("Error calling pg_create_database: " + e);
}
// Try to call the pg_execute_sql function
try {
print("\nTrying to call pg_execute_sql()...");
let result = pg_execute_sql("postgres-test", "testdb", "SELECT 1");
print("pg_execute_sql result: " + result);
} catch(e) {
print("Error calling pg_execute_sql: " + e);
}
// Try to call the pg_is_running function
try {
print("\nTrying to call pg_is_running()...");
let result = pg_is_running("postgres-test");
print("pg_is_running result: " + result);
} catch(e) {
print("Error calling pg_is_running: " + e);
}
print("\nTest completed!");

View File

@ -1,24 +0,0 @@
// Simple test script to verify that the Rhai engine is working
print("Hello, world!");
// Try to access the PostgreSQL installer functions
print("\nTrying to access PostgreSQL installer functions...");
// Check if the pg_install function is defined
print("pg_install function is defined: " + is_def_fn("pg_install"));
// Print the available functions
print("\nAvailable functions:");
print("pg_connect: " + is_def_fn("pg_connect"));
print("pg_ping: " + is_def_fn("pg_ping"));
print("pg_reset: " + is_def_fn("pg_reset"));
print("pg_execute: " + is_def_fn("pg_execute"));
print("pg_query: " + is_def_fn("pg_query"));
print("pg_query_one: " + is_def_fn("pg_query_one"));
print("pg_install: " + is_def_fn("pg_install"));
print("pg_create_database: " + is_def_fn("pg_create_database"));
print("pg_execute_sql: " + is_def_fn("pg_execute_sql"));
print("pg_is_running: " + is_def_fn("pg_is_running"));
print("\nTest completed successfully!");

View File

@ -1,22 +0,0 @@
// Simple test script to verify that the Rhai engine is working
print("Hello, world!");
// Try to access the PostgreSQL installer functions
print("\nTrying to access PostgreSQL installer functions...");
// Try to call the pg_install function
try {
let result = pg_install(
"postgres-test",
"15",
5433,
"testuser",
"testpassword"
);
print("pg_install result: " + result);
} catch(e) {
print("Error calling pg_install: " + e);
}
print("\nTest completed!");

View File

@ -1,61 +0,0 @@
// 01_command_execution.rhai
// Tests for command execution in the Process module
// Custom assert function
fn assert_true(condition, message) {
if !condition {
print(`ASSERTION FAILED: ${message}`);
throw message;
}
}
print("=== Testing Basic Command Execution ===");
// Test running a simple command
print("Testing run() with a simple command...");
let result = run("echo Hello, World!").execute();
assert_true(result.success, "Command should succeed");
assert_true(result.stdout.contains("Hello, World!"), "Command output should contain the expected text");
print(`✓ run().execute(): Command executed successfully`);
// Test running a command with arguments
print("Testing run() with command arguments...");
let result_with_args = run("echo Hello from Rhai tests").execute();
assert_true(result_with_args.success, "Command with arguments should succeed");
assert_true(result_with_args.stdout.contains("Hello from Rhai tests"), "Command output should contain the expected text");
print(`✓ run().execute(): Command with arguments executed successfully`);
// Test running a command with environment variables
print("Testing run() with environment variables...");
let env_result = run("echo $HOME").execute();
assert_true(env_result.success, "Command with environment variables should succeed");
assert_true(env_result.stdout.trim() != "", "Environment variable should be expanded");
print(`✓ run().execute(): Command with environment variables executed successfully`);
// Test running a multiline script
print("Testing run() with a multiline script...");
let script_result = run(`
echo "Line 1"
echo "Line 2"
echo "Line 3"
`).execute();
assert_true(script_result.success, "Multiline script should succeed");
assert_true(script_result.stdout.contains("Line 1") && script_result.stdout.contains("Line 2") && script_result.stdout.contains("Line 3"),
"Script output should contain all lines");
print(`✓ run().execute(): Multiline script executed successfully`);
// Test which function
print("Testing which() function...");
let bash_path = which("bash");
assert_true(bash_path != "", "bash should be found in PATH");
print(`✓ which(): Found bash at ${bash_path}`);
// Test a command that doesn't exist
let nonexistent_cmd = which("this_command_does_not_exist_12345");
if nonexistent_cmd == "" {
print(`✓ which(): Correctly reported that nonexistent command was not found`);
} else {
print(`Note: Unexpectedly found command at ${nonexistent_cmd}`);
}
print("All command execution tests completed successfully!");

View File

@ -1,54 +0,0 @@
// 02_process_management.rhai
// Tests for process management functions in the Process module
// Custom assert function
fn assert_true(condition, message) {
if !condition {
print(`ASSERTION FAILED: ${message}`);
throw message;
}
}
print("=== Testing Process Management Functions ===");
// Test process_list function
print("Testing process_list() function...");
let all_processes = process_list("");
assert_true(all_processes.len() > 0, "There should be at least one running process");
print(`✓ process_list(): Found ${all_processes.len()} processes`);
// Test process properties
print("Testing process properties...");
let first_process = all_processes[0];
assert_true(first_process.pid > 0, "Process PID should be a positive number");
assert_true(first_process.name.len() > 0, "Process name should not be empty");
print(`✓ Process properties: PID=${first_process.pid}, Name=${first_process.name}, CPU=${first_process.cpu}%, Memory=${first_process.memory}`);
// Test process_list with a pattern
print("Testing process_list() with a pattern...");
// Use a pattern that's likely to match at least one process on most systems
let pattern = "sh";
let matching_processes = process_list(pattern);
print(`Found ${matching_processes.len()} processes matching '${pattern}'`);
if (matching_processes.len() > 0) {
let matched_process = matching_processes[0];
print(`✓ process_list(pattern): Found process ${matched_process.name} with PID ${matched_process.pid}`);
} else {
print(`Note: No processes found matching '${pattern}'. This is not necessarily an error.`);
}
// Test process_get function
// Note: We'll only test this if we found matching processes above
if (matching_processes.len() == 1) {
print("Testing process_get() function...");
let process = process_get(pattern);
assert_true(process.pid > 0, "Process PID should be a positive number");
assert_true(process.name.contains(pattern), "Process name should contain the pattern");
print(`✓ process_get(): Found process ${process.name} with PID ${process.pid}`);
} else {
print("Skipping process_get() test as it requires exactly one matching process");
}
// Note: We won't test the kill function as it could disrupt the system
print("All process management tests completed successfully!");

View File

@ -1,76 +0,0 @@
// run_all_tests.rhai
// Runs all Process module tests
print("=== Running Process Module Tests ===");
// Custom assert function
fn assert_true(condition, message) {
if !condition {
print(`ASSERTION FAILED: ${message}`);
throw message;
}
}
// Run each test directly
let passed = 0;
let failed = 0;
// Test 1: Command Execution
print("\n--- Running Command Execution Tests ---");
try {
// Test running a simple command
print("Testing run() with a simple command...");
let result = run("echo Hello, World!").execute();
assert_true(result.success, "Command should succeed");
assert_true(result.stdout.contains("Hello, World!"), "Command output should contain the expected text");
print(`✓ run().execute(): Command executed successfully`);
// Test which function
print("Testing which() function...");
let bash_path = which("bash");
assert_true(bash_path != "", "bash should be found in PATH");
print(`✓ which(): Found bash at ${bash_path}`);
print("--- Command Execution Tests completed successfully ---");
passed += 1;
} catch(err) {
print(`!!! Error in Command Execution Tests: ${err}`);
failed += 1;
}
// Test 2: Process Management
print("\n--- Running Process Management Tests ---");
try {
// Test process_list function
print("Testing process_list() function...");
let all_processes = process_list("");
assert_true(all_processes.len() > 0, "There should be at least one running process");
print(`✓ process_list(): Found ${all_processes.len()} processes`);
// Test process properties
print("Testing process properties...");
let first_process = all_processes[0];
assert_true(first_process.pid > 0, "Process PID should be a positive number");
assert_true(first_process.name.len() > 0, "Process name should not be empty");
print(`✓ Process properties: PID=${first_process.pid}, Name=${first_process.name}`);
print("--- Process Management Tests completed successfully ---");
passed += 1;
} catch(err) {
print(`!!! Error in Process Management Tests: ${err}`);
failed += 1;
}
print("\n=== Test Summary ===");
print(`Passed: ${passed}`);
print(`Failed: ${failed}`);
print(`Total: ${passed + failed}`);
if failed == 0 {
print("\n✅ All tests passed!");
} else {
print("\n❌ Some tests failed!");
}
// Return the number of failed tests (0 means success)
failed;

View File

@ -1,68 +0,0 @@
// 01_redis_connection.rhai
// Tests for Redis client connection and basic operations
// Custom assert function
fn assert_true(condition, message) {
if !condition {
print(`ASSERTION FAILED: ${message}`);
throw message;
}
}
// Helper function to check if Redis is available
fn is_redis_available() {
try {
// Try to execute a simple PING command
let ping_result = redis_ping();
return ping_result == "PONG";
} catch(err) {
print(`Redis connection error: ${err}`);
return false;
}
}
print("=== Testing Redis Client Connection ===");
// Check if Redis is available
let redis_available = is_redis_available();
if !redis_available {
print("Redis server is not available. Skipping Redis tests.");
// Exit gracefully without error
return;
}
print("✓ Redis server is available");
// Test redis_ping function
print("Testing redis_ping()...");
let ping_result = redis_ping();
assert_true(ping_result == "PONG", "PING should return PONG");
print(`✓ redis_ping(): Returned ${ping_result}`);
// Test redis_set and redis_get functions
print("Testing redis_set() and redis_get()...");
let test_key = "rhai_test_key";
let test_value = "Hello from Rhai test";
// Set a value
let set_result = redis_set(test_key, test_value);
assert_true(set_result, "SET operation should succeed");
print(`✓ redis_set(): Successfully set key ${test_key}`);
// Get the value back
let get_result = redis_get(test_key);
assert_true(get_result == test_value, "GET should return the value we set");
print(`✓ redis_get(): Successfully retrieved value for key ${test_key}`);
// Test redis_del function
print("Testing redis_del()...");
let del_result = redis_del(test_key);
assert_true(del_result, "DEL operation should succeed");
print(`✓ redis_del(): Successfully deleted key ${test_key}`);
// Verify the key was deleted
let get_after_del = redis_get(test_key);
assert_true(get_after_del == "", "Key should not exist after deletion");
print("✓ Key was successfully deleted");
print("All Redis connection tests completed successfully!");

View File

@ -1,109 +0,0 @@
// 02_redis_operations.rhai
// Tests for advanced Redis operations
// Custom assert function
fn assert_true(condition, message) {
if !condition {
print(`ASSERTION FAILED: ${message}`);
throw message;
}
}
// Helper function to check if Redis is available
fn is_redis_available() {
try {
// Try to execute a simple PING command
let ping_result = redis_ping();
return ping_result == "PONG";
} catch(err) {
print(`Redis connection error: ${err}`);
return false;
}
}
print("=== Testing Advanced Redis Operations ===");
// Check if Redis is available
let redis_available = is_redis_available();
if !redis_available {
print("Redis server is not available. Skipping Redis tests.");
// Exit gracefully without error
return;
}
print("✓ Redis server is available");
// Test prefix for all keys to avoid conflicts
let prefix = "rhai_test_";
// Test redis_hset and redis_hget functions
print("Testing redis_hset() and redis_hget()...");
let hash_key = prefix + "hash";
let field1 = "field1";
let value1 = "value1";
let field2 = "field2";
let value2 = "value2";
// Set hash fields
let hset_result1 = redis_hset(hash_key, field1, value1);
assert_true(hset_result1, "HSET operation should succeed for field1");
let hset_result2 = redis_hset(hash_key, field2, value2);
assert_true(hset_result2, "HSET operation should succeed for field2");
print(`✓ redis_hset(): Successfully set fields in hash ${hash_key}`);
// Get hash fields
let hget_result1 = redis_hget(hash_key, field1);
assert_true(hget_result1 == value1, "HGET should return the value we set for field1");
let hget_result2 = redis_hget(hash_key, field2);
assert_true(hget_result2 == value2, "HGET should return the value we set for field2");
print(`✓ redis_hget(): Successfully retrieved values from hash ${hash_key}`);
// Test redis_hgetall function
print("Testing redis_hgetall()...");
let hgetall_result = redis_hgetall(hash_key);
assert_true(hgetall_result.len() == 2, "HGETALL should return 2 fields");
assert_true(hgetall_result[field1] == value1, "HGETALL should include field1 with correct value");
assert_true(hgetall_result[field2] == value2, "HGETALL should include field2 with correct value");
print(`✓ redis_hgetall(): Successfully retrieved all fields from hash ${hash_key}`);
// Test redis_hdel function
print("Testing redis_hdel()...");
let hdel_result = redis_hdel(hash_key, field1);
assert_true(hdel_result, "HDEL operation should succeed");
print(`✓ redis_hdel(): Successfully deleted field from hash ${hash_key}`);
// Verify the field was deleted
let hget_after_del = redis_hget(hash_key, field1);
assert_true(hget_after_del == "", "Field should not exist after deletion");
print("✓ Field was successfully deleted from hash");
// Test redis_list operations
print("Testing redis list operations...");
let list_key = prefix + "list";
// Push items to list
let rpush_result = redis_rpush(list_key, "item1");
assert_true(rpush_result > 0, "RPUSH operation should succeed");
redis_rpush(list_key, "item2");
redis_rpush(list_key, "item3");
print(`✓ redis_rpush(): Successfully pushed items to list ${list_key}`);
// Get list length
let llen_result = redis_llen(list_key);
assert_true(llen_result == 3, "List should have 3 items");
print(`✓ redis_llen(): List has ${llen_result} items`);
// Get list range
let lrange_result = redis_lrange(list_key, 0, -1);
assert_true(lrange_result.len() == 3, "LRANGE should return 3 items");
assert_true(lrange_result[0] == "item1", "First item should be 'item1'");
assert_true(lrange_result[2] == "item3", "Last item should be 'item3'");
print(`✓ redis_lrange(): Successfully retrieved all items from list ${list_key}`);
// Clean up
print("Cleaning up...");
redis_del(hash_key);
redis_del(list_key);
print("✓ Cleanup: All test keys removed");
print("All Redis operations tests completed successfully!");

View File

@ -1,59 +0,0 @@
// 03_redis_authentication.rhai
// Tests for Redis client authentication (placeholder for future implementation)
// Custom assert function
fn assert_true(condition, message) {
if !condition {
print(`ASSERTION FAILED: ${message}`);
throw message;
}
}
// Helper function to check if Redis is available
fn is_redis_available() {
try {
// Try to execute a simple ping
let ping_result = redis_ping();
return ping_result == "PONG";
} catch(err) {
print(`Redis connection error: ${err}`);
return false;
}
}
print("=== Testing Redis Client Authentication ===");
// Check if Redis is available
let redis_available = is_redis_available();
if !redis_available {
print("Redis server is not available. Skipping Redis authentication tests.");
// Exit gracefully without error
return;
}
print("✓ Redis server is available");
print("Authentication support will be implemented in a future update.");
print("The backend implementation is ready, but the Rhai bindings are still in development.");
// For now, just test basic Redis functionality
print("\nTesting basic Redis functionality...");
// Test a simple operation
let test_key = "auth_test_key";
let test_value = "auth_test_value";
let set_result = redis_set(test_key, test_value);
assert_true(set_result, "Should be able to set a key");
print("✓ Set key");
let get_result = redis_get(test_key);
assert_true(get_result == test_value, "Should be able to get the key");
print("✓ Got key");
// Clean up
let del_result = redis_del(test_key);
assert_true(del_result, "Should be able to delete the key");
print("✓ Deleted test key");
print("All Redis tests completed successfully!");

View File

@ -1,154 +0,0 @@
// run_all_tests.rhai
// Runs all Redis client module tests
print("=== Running Redis Client Module Tests ===");
// Custom assert function
fn assert_true(condition, message) {
if !condition {
print(`ASSERTION FAILED: ${message}`);
throw message;
}
}
// Helper function to check if Redis is available
fn is_redis_available() {
try {
// Try to execute a simple PING command
let ping_result = redis_ping();
return ping_result == "PONG";
} catch(err) {
print(`Redis connection error: ${err}`);
return false;
}
}
// Run each test directly
let passed = 0;
let failed = 0;
let skipped = 0;
// Check if Redis is available
let redis_available = is_redis_available();
if !redis_available {
print("Redis server is not available. Skipping all Redis tests.");
skipped = 3; // Skip all three tests
} else {
// Test 1: Redis Connection
print("\n--- Running Redis Connection Tests ---");
try {
// Test redis_ping function
print("Testing redis_ping()...");
let ping_result = redis_ping();
assert_true(ping_result == "PONG", "PING should return PONG");
print(`✓ redis_ping(): Returned ${ping_result}`);
// Test redis_set and redis_get functions
print("Testing redis_set() and redis_get()...");
let test_key = "rhai_test_key";
let test_value = "Hello from Rhai test";
// Set a value
let set_result = redis_set(test_key, test_value);
assert_true(set_result, "SET operation should succeed");
print(`✓ redis_set(): Successfully set key ${test_key}`);
// Get the value back
let get_result = redis_get(test_key);
assert_true(get_result == test_value, "GET should return the value we set");
print(`✓ redis_get(): Successfully retrieved value for key ${test_key}`);
// Clean up
redis_del(test_key);
print("--- Redis Connection Tests completed successfully ---");
passed += 1;
} catch(err) {
print(`!!! Error in Redis Connection Tests: ${err}`);
failed += 1;
}
// Test 2: Redis Operations
print("\n--- Running Redis Operations Tests ---");
try {
// Test prefix for all keys to avoid conflicts
let prefix = "rhai_test_";
// Test redis_hset and redis_hget functions
print("Testing redis_hset() and redis_hget()...");
let hash_key = prefix + "hash";
let field = "field1";
let value = "value1";
// Set hash field
let hset_result = redis_hset(hash_key, field, value);
assert_true(hset_result, "HSET operation should succeed");
print(`✓ redis_hset(): Successfully set field in hash ${hash_key}`);
// Get hash field
let hget_result = redis_hget(hash_key, field);
assert_true(hget_result == value, "HGET should return the value we set");
print(`✓ redis_hget(): Successfully retrieved value from hash ${hash_key}`);
// Clean up
redis_del(hash_key);
print("--- Redis Operations Tests completed successfully ---");
passed += 1;
} catch(err) {
print(`!!! Error in Redis Operations Tests: ${err}`);
failed += 1;
}
// Test 3: Redis Authentication
print("\n--- Running Redis Authentication Tests ---");
try {
print("Authentication support will be implemented in a future update.");
print("The backend implementation is ready, but the Rhai bindings are still in development.");
// For now, just test basic Redis functionality
print("\nTesting basic Redis functionality...");
// Test a simple operation
let test_key = "auth_test_key";
let test_value = "auth_test_value";
let set_result = redis_set(test_key, test_value);
assert_true(set_result, "Should be able to set a key");
print("✓ Set key");
let get_result = redis_get(test_key);
assert_true(get_result == test_value, "Should be able to get the key");
print("✓ Got key");
// Clean up
let del_result = redis_del(test_key);
assert_true(del_result, "Should be able to delete the key");
print("✓ Deleted test key");
print("--- Redis Authentication Tests completed successfully ---");
passed += 1;
} catch(err) {
print(`!!! Error in Redis Authentication Tests: ${err}`);
failed += 1;
}
}
print("\n=== Test Summary ===");
print(`Passed: ${passed}`);
print(`Failed: ${failed}`);
print(`Skipped: ${skipped}`);
print(`Total: ${passed + failed + skipped}`);
if failed == 0 {
if skipped > 0 {
print("\n⚠ All tests skipped or passed!");
} else {
print("\n✅ All tests passed!");
}
} else {
print("\n❌ Some tests failed!");
}
// Return the number of failed tests (0 means success)
failed;

Some files were not shown because too many files have changed in this diff Show More