Compare commits

..

2 Commits

Author SHA1 Message Date
Maxime Van Hees
f386890a8a working example to showcase zinit usage in Rhai scripts 2025-05-09 11:53:09 +02:00
Maxime Van Hees
61bd58498a implemented zinit-client for integration with Rhai-scripts 2025-05-08 17:03:00 +02:00
90 changed files with 1867 additions and 11170 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 # Added by cargo
/target /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,36 @@ categories = ["os", "filesystem", "api-bindings"]
readme = "README.md" readme = "README.md"
[dependencies] [dependencies]
tera = "1.19.0" # Template engine for text rendering tera = "1.19.0" # Template engine for text rendering
# Cross-platform functionality # Cross-platform functionality
libc = "0.2" libc = "0.2"
cfg-if = "1.0" cfg-if = "1.0"
thiserror = "1.0" # For error handling thiserror = "1.0" # For error handling
redis = "0.22.0" # Redis client 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
lazy_static = "1.4.0" # For lazy initialization of static variables lazy_static = "1.4.0" # For lazy initialization of static variables
regex = "1.8.1" # For regex pattern matching regex = "1.8.1" # For regex pattern matching
serde = { version = "1.0", features = [ serde = { version = "1.0", features = ["derive"] } # For serialization/deserialization
"derive",
] } # For serialization/deserialization
serde_json = "1.0" # For JSON handling serde_json = "1.0" # For JSON handling
glob = "0.3.1" # For file pattern matching glob = "0.3.1" # For file pattern matching
tempfile = "3.5" # For temporary file operations tempfile = "3.5" # For temporary file operations
log = "0.4" # Logging facade log = "0.4" # Logging facade
rhai = { version = "1.12.0", features = ["sync"] } # Embedded scripting language rhai = { version = "1.12.0", features = ["sync"] } # Embedded scripting language
rand = "0.8.5" # Random number generation rand = "0.8.5" # Random number generation
clap = "2.33" # Command-line argument parsing clap = "2.33" # Command-line argument parsing
r2d2 = "0.8.10" zinit-client = { git = "https://github.com/threefoldtech/zinit", branch = "json_rpc", package = "zinit-client" }
r2d2_postgres = "0.18.2" anyhow = "1.0.98"
jsonrpsee = "0.25.1"
tokio = "1.45.0"
# Optional features for specific OS functionality # Optional features for specific OS functionality
[target.'cfg(unix)'.dependencies] [target.'cfg(unix)'.dependencies]
nix = "0.26" # Unix-specific functionality nix = "0.26" # Unix-specific functionality
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
windows = { version = "0.48", features = [ windows = { version = "0.48", features = ["Win32_Foundation", "Win32_System_Threading", "Win32_Storage_FileSystem"] }
"Win32_Foundation",
"Win32_System_Threading",
"Win32_Storage_FileSystem",
] }
[dev-dependencies] [dev-dependencies]
tempfile = "3.5" # For tests that need temporary files/directories tempfile = "3.5" # For tests that need temporary files/directories
[[bin]] [[bin]]
name = "herodo" name = "herodo"

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,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,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();

View File

@ -0,0 +1,87 @@
// Basic example of using the Zinit client in Rhai
// Socket path for Zinit
let socket_path = "/var/run/zinit.sock";
// List all services
print("Listing all services:");
let services = zinit_list(socket_path);
if services.is_empty() {
print("No services found.");
} else {
// Iterate over the keys of the map
for name in services.keys() {
let state = services[name];
print(`${name}: ${state}`);
}
}
// Get status of a specific service
let service_name = "test";
print(`Getting status for ${service_name}:`);
try {
let status = zinit_status(socket_path, service_name);
print(`Service: ${status.name}`);
print(`PID: ${status.pid}`);
print(`State: ${status.state}`);
print(`Target: ${status.target}`);
print("Dependencies:");
for (dep, state) in status.after.keys() {
print(` ${dep}: ${state}`);
}
} catch(err) {
print(`Error getting status: ${err}`);
}
// Create a new service
print("\nCreating a new service:");
let new_service = "rhai-test-service";
let exec_command = "echo 'Hello from Rhai'";
let oneshot = true;
try {
let result = zinit_create_service(socket_path, new_service, exec_command, oneshot);
print(`Service created: ${result}`);
// Monitor the service
print("\nMonitoring the service:");
let monitor_result = zinit_monitor(socket_path, new_service);
print(`Service monitored: ${monitor_result}`);
// Start the service
print("\nStarting the service:");
let start_result = zinit_start(socket_path, new_service);
print(`Service started: ${start_result}`);
// Get logs for a specific service
print("\nGetting logs:");
let logs = zinit_logs(socket_path, new_service);
for log in logs {
print(log);
}
// Or to get all logs (uncomment if needed)
// print("\nGetting all logs:");
// let all_logs = zinit_logs_all(socket_path);
//
// for log in all_logs {
// print(log);
// }
// Clean up
print("\nCleaning up:");
let stop_result = zinit_stop(socket_path, new_service);
print(`Service stopped: ${stop_result}`);
let forget_result = zinit_forget(socket_path, new_service);
print(`Service forgotten: ${forget_result}`);
let delete_result = zinit_delete_service(socket_path, new_service);
print(`Service deleted: ${delete_result}`);
} catch(err) {
print(`Error: ${err}`);
}

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

@ -36,15 +36,15 @@ pub enum Error {
pub type Result<T> = std::result::Result<T, Error>; pub type Result<T> = std::result::Result<T, Error>;
// Re-export modules // Re-export modules
pub mod cmd; pub mod process;
pub mod git; pub mod git;
pub mod os; pub mod os;
pub mod postgresclient;
pub mod process;
pub mod redisclient; pub mod redisclient;
pub mod rhai;
pub mod text; pub mod text;
pub mod virt; pub mod virt;
pub mod rhai;
pub mod cmd;
pub mod zinit_client;
// Version information // Version information
/// Returns the version of the SAL library /// 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::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 // Define a custom error type for download operations
#[derive(Debug)] #[derive(Debug)]
@ -26,17 +26,11 @@ pub enum DownloadError {
impl fmt::Display for DownloadError { impl fmt::Display for DownloadError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {
DownloadError::CreateDirectoryFailed(e) => { DownloadError::CreateDirectoryFailed(e) => write!(f, "Error creating directories: {}", e),
write!(f, "Error creating directories: {}", e)
}
DownloadError::CurlExecutionFailed(e) => write!(f, "Error executing curl: {}", e), DownloadError::CurlExecutionFailed(e) => write!(f, "Error executing curl: {}", e),
DownloadError::DownloadFailed(url) => write!(f, "Error downloading url: {}", url), DownloadError::DownloadFailed(url) => write!(f, "Error downloading url: {}", url),
DownloadError::FileMetadataError(e) => write!(f, "Error getting file metadata: {}", e), DownloadError::FileMetadataError(e) => write!(f, "Error getting file metadata: {}", e),
DownloadError::FileTooSmall(size, min) => write!( DownloadError::FileTooSmall(size, min) => write!(f, "Error: Downloaded file is too small ({}KB < {}KB)", size, min),
f,
"Error: Downloaded file is too small ({}KB < {}KB)",
size, min
),
DownloadError::RemoveFileFailed(e) => write!(f, "Error removing file: {}", e), DownloadError::RemoveFileFailed(e) => write!(f, "Error removing file: {}", e),
DownloadError::ExtractionFailed(e) => write!(f, "Error extracting archive: {}", e), DownloadError::ExtractionFailed(e) => write!(f, "Error extracting archive: {}", e),
DownloadError::CommandExecutionFailed(e) => write!(f, "Error executing command: {}", e), DownloadError::CommandExecutionFailed(e) => write!(f, "Error executing command: {}", e),
@ -80,18 +74,12 @@ impl Error for DownloadError {
* *
* # Examples * # 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 minimum size requirement of 100KB
* // Download a file with no minimum size requirement * let path = download("https://example.com/file.zip", "/tmp/", 100)?;
* 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(())
* }
* ``` * ```
* *
* # Notes * # 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 // Create parent directories if they don't exist
let dest_path = Path::new(dest); let dest_path = Path::new(dest);
fs::create_dir_all(dest_path).map_err(DownloadError::CreateDirectoryFailed)?; fs::create_dir_all(dest_path).map_err(DownloadError::CreateDirectoryFailed)?;
// Extract filename from URL // Extract filename from URL
let filename = match url.split('/').last() { let filename = match url.split('/').last() {
Some(name) => name, Some(name) => name,
None => { None => return Err(DownloadError::InvalidUrl("cannot extract filename".to_string()))
return Err(DownloadError::InvalidUrl(
"cannot extract filename".to_string(),
))
}
}; };
// Create a full path for the downloaded file // Create a full path for the downloaded file
let file_path = format!("{}/{}", dest.trim_end_matches('/'), filename); let file_path = format!("{}/{}", dest.trim_end_matches('/'), filename);
// Create a temporary path for downloading // Create a temporary path for downloading
let temp_path = format!("{}.download", file_path); let temp_path = format!("{}.download", file_path);
// Use curl to download the file with progress bar // Use curl to download the file with progress bar
println!("Downloading {} to {}", url, file_path); println!("Downloading {} to {}", url, file_path);
let output = Command::new("curl") let output = Command::new("curl")
.args(&[ .args(&["--progress-bar", "--location", "--fail", "--output", &temp_path, url])
"--progress-bar",
"--location",
"--fail",
"--output",
&temp_path,
url,
])
.status() .status()
.map_err(DownloadError::CurlExecutionFailed)?; .map_err(DownloadError::CurlExecutionFailed)?;
if !output.success() { if !output.success() {
return Err(DownloadError::DownloadFailed(url.to_string())); return Err(DownloadError::DownloadFailed(url.to_string()));
} }
// Show file size after download // Show file size after download
match fs::metadata(&temp_path) { match fs::metadata(&temp_path) {
Ok(metadata) => { 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_kb = size_bytes / 1024;
let size_mb = size_kb / 1024; let size_mb = size_kb / 1024;
if size_mb > 1 { if size_mb > 1 {
println!( println!("Download complete! File size: {:.2} MB", size_bytes as f64 / (1024.0 * 1024.0));
"Download complete! File size: {:.2} MB",
size_bytes as f64 / (1024.0 * 1024.0)
);
} else { } else {
println!( println!("Download complete! File size: {:.2} KB", size_bytes as f64 / 1024.0);
"Download complete! File size: {:.2} KB",
size_bytes as f64 / 1024.0
);
} }
} },
Err(_) => println!("Download complete!"), Err(_) => println!("Download complete!"),
} }
// Check file size if minimum size is specified // Check file size if minimum size is specified
if min_size_kb > 0 { if min_size_kb > 0 {
let metadata = fs::metadata(&temp_path).map_err(DownloadError::FileMetadataError)?; 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)); return Err(DownloadError::FileTooSmall(size_kb, min_size_kb));
} }
} }
// Check if it's a compressed file that needs extraction // Check if it's a compressed file that needs extraction
let lower_url = url.to_lowercase(); let lower_url = url.to_lowercase();
let is_archive = lower_url.ends_with(".tar.gz") let is_archive = lower_url.ends_with(".tar.gz") ||
|| lower_url.ends_with(".tgz") lower_url.ends_with(".tgz") ||
|| lower_url.ends_with(".tar") lower_url.ends_with(".tar") ||
|| lower_url.ends_with(".zip"); lower_url.ends_with(".zip");
if is_archive { if is_archive {
// Extract the file using the appropriate command with progress indication // Extract the file using the appropriate command with progress indication
println!("Extracting {} to {}", temp_path, dest); println!("Extracting {} to {}", temp_path, dest);
let output = if lower_url.ends_with(".zip") { let output = if lower_url.ends_with(".zip") {
Command::new("unzip") Command::new("unzip")
.args(&["-o", &temp_path, "-d", dest]) // Removed -q for verbosity .args(&["-o", &temp_path, "-d", dest]) // Removed -q for verbosity
.status() .status()
} else if lower_url.ends_with(".tar.gz") || lower_url.ends_with(".tgz") { } else if lower_url.ends_with(".tar.gz") || lower_url.ends_with(".tgz") {
Command::new("tar") Command::new("tar")
.args(&["-xzvf", &temp_path, "-C", dest]) // Added v for verbosity .args(&["-xzvf", &temp_path, "-C", dest]) // Added v for verbosity
.status() .status()
} else { } else {
Command::new("tar") Command::new("tar")
.args(&["-xvf", &temp_path, "-C", dest]) // Added v for verbosity .args(&["-xvf", &temp_path, "-C", dest]) // Added v for verbosity
.status() .status()
}; };
match output { match output {
Ok(status) => { Ok(status) => {
if !status.success() { if !status.success() {
return Err(DownloadError::ExtractionFailed( return Err(DownloadError::ExtractionFailed("Error extracting archive".to_string()));
"Error extracting archive".to_string(),
));
} }
} },
Err(e) => return Err(DownloadError::CommandExecutionFailed(e)), Err(e) => return Err(DownloadError::CommandExecutionFailed(e)),
} }
// Show number of extracted files // Show number of extracted files
match fs::read_dir(dest) { match fs::read_dir(dest) {
Ok(entries) => { Ok(entries) => {
let count = entries.count(); let count = entries.count();
println!("Extraction complete! Extracted {} files/directories", count); println!("Extraction complete! Extracted {} files/directories", count);
} },
Err(_) => println!("Extraction complete!"), Err(_) => println!("Extraction complete!"),
} }
// Remove the temporary file // Remove the temporary file
fs::remove_file(&temp_path).map_err(DownloadError::RemoveFileFailed)?; fs::remove_file(&temp_path).map_err(DownloadError::RemoveFileFailed)?;
Ok(dest.to_string()) Ok(dest.to_string())
} else { } else {
// Just rename the temporary file to the final destination // Just rename the temporary file to the final destination
fs::rename(&temp_path, &file_path).map_err(|e| DownloadError::CreateDirectoryFailed(e))?; fs::rename(&temp_path, &file_path).map_err(|e| DownloadError::CreateDirectoryFailed(e))?;
Ok(file_path) Ok(file_path)
} }
} }
@ -241,18 +210,12 @@ pub fn download(url: &str, dest: &str, min_size_kb: i64) -> Result<String, Downl
* *
* # Examples * # 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 minimum size requirement of 100KB
* // Download a file with no minimum size requirement * let path = download_file("https://example.com/file.zip", "/tmp/file.zip", 100)?;
* 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(())
* }
* ``` * ```
*/ */
pub fn download_file(url: &str, dest: &str, min_size_kb: i64) -> Result<String, DownloadError> { 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() { if let Some(parent) = dest_path.parent() {
fs::create_dir_all(parent).map_err(DownloadError::CreateDirectoryFailed)?; fs::create_dir_all(parent).map_err(DownloadError::CreateDirectoryFailed)?;
} }
// Create a temporary path for downloading // Create a temporary path for downloading
let temp_path = format!("{}.download", dest); let temp_path = format!("{}.download", dest);
// Use curl to download the file with progress bar // Use curl to download the file with progress bar
println!("Downloading {} to {}", url, dest); println!("Downloading {} to {}", url, dest);
let output = Command::new("curl") let output = Command::new("curl")
.args(&[ .args(&["--progress-bar", "--location", "--fail", "--output", &temp_path, url])
"--progress-bar",
"--location",
"--fail",
"--output",
&temp_path,
url,
])
.status() .status()
.map_err(DownloadError::CurlExecutionFailed)?; .map_err(DownloadError::CurlExecutionFailed)?;
if !output.success() { if !output.success() {
return Err(DownloadError::DownloadFailed(url.to_string())); return Err(DownloadError::DownloadFailed(url.to_string()));
} }
// Show file size after download // Show file size after download
match fs::metadata(&temp_path) { match fs::metadata(&temp_path) {
Ok(metadata) => { 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_kb = size_bytes / 1024;
let size_mb = size_kb / 1024; let size_mb = size_kb / 1024;
if size_mb > 1 { if size_mb > 1 {
println!( println!("Download complete! File size: {:.2} MB", size_bytes as f64 / (1024.0 * 1024.0));
"Download complete! File size: {:.2} MB",
size_bytes as f64 / (1024.0 * 1024.0)
);
} else { } else {
println!( println!("Download complete! File size: {:.2} KB", size_bytes as f64 / 1024.0);
"Download complete! File size: {:.2} KB",
size_bytes as f64 / 1024.0
);
} }
} },
Err(_) => println!("Download complete!"), Err(_) => println!("Download complete!"),
} }
// Check file size if minimum size is specified // Check file size if minimum size is specified
if min_size_kb > 0 { if min_size_kb > 0 {
let metadata = fs::metadata(&temp_path).map_err(DownloadError::FileMetadataError)?; 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)); return Err(DownloadError::FileTooSmall(size_kb, min_size_kb));
} }
} }
// Rename the temporary file to the final destination // Rename the temporary file to the final destination
fs::rename(&temp_path, dest).map_err(|e| DownloadError::CreateDirectoryFailed(e))?; fs::rename(&temp_path, dest).map_err(|e| DownloadError::CreateDirectoryFailed(e))?;
Ok(dest.to_string()) Ok(dest.to_string())
} }
@ -334,38 +284,27 @@ pub fn download_file(url: &str, dest: &str, min_size_kb: i64) -> Result<String,
* *
* # Examples * # Examples
* *
* ```no_run * ```
* use sal::os::chmod_exec; * // Make a file executable
* * chmod_exec("/path/to/file")?;
* fn main() -> Result<(), Box<dyn std::error::Error>> {
* // Make a file executable
* chmod_exec("/path/to/file")?;
* Ok(())
* }
* ``` * ```
*/ */
pub fn chmod_exec(path: &str) -> Result<String, DownloadError> { pub fn chmod_exec(path: &str) -> Result<String, DownloadError> {
let path_obj = Path::new(path); let path_obj = Path::new(path);
// Check if the path exists and is a file // Check if the path exists and is a file
if !path_obj.exists() { if !path_obj.exists() {
return Err(DownloadError::NotAFile(format!( return Err(DownloadError::NotAFile(format!("Path does not exist: {}", path)));
"Path does not exist: {}",
path
)));
} }
if !path_obj.is_file() { if !path_obj.is_file() {
return Err(DownloadError::NotAFile(format!( return Err(DownloadError::NotAFile(format!("Path is not a file: {}", path)));
"Path is not a file: {}",
path
)));
} }
// Get current permissions // Get current permissions
let metadata = fs::metadata(path).map_err(DownloadError::FileMetadataError)?; let metadata = fs::metadata(path).map_err(DownloadError::FileMetadataError)?;
let mut permissions = metadata.permissions(); let mut permissions = metadata.permissions();
// Set executable bit for user, group, and others // Set executable bit for user, group, and others
#[cfg(unix)] #[cfg(unix)]
{ {
@ -375,55 +314,47 @@ pub fn chmod_exec(path: &str) -> Result<String, DownloadError> {
let new_mode = mode | 0o111; let new_mode = mode | 0o111;
permissions.set_mode(new_mode); permissions.set_mode(new_mode);
} }
#[cfg(not(unix))] #[cfg(not(unix))]
{ {
// On non-Unix platforms, we can't set executable bit directly // On non-Unix platforms, we can't set executable bit directly
// Just return success with a warning // Just return success with a warning
return Ok(format!( return Ok(format!("Made {} executable (note: non-Unix platform, may not be fully supported)", path));
"Made {} executable (note: non-Unix platform, may not be fully supported)",
path
));
} }
// Apply the new permissions // 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( DownloadError::CommandExecutionFailed(io::Error::new(
io::ErrorKind::Other, io::ErrorKind::Other,
format!("Failed to set executable permissions: {}", e), format!("Failed to set executable permissions: {}", e)
)) ))
})?; )?;
Ok(format!("Made {} executable", path)) Ok(format!("Made {} executable", path))
} }
/** /**
* Download a file and install it if it's a supported package format. * Download a file and install it if it's a supported package format.
* *
* # Arguments * # Arguments
* *
* * `url` - The URL to download from * * `url` - The URL to download from
* * `min_size_kb` - Minimum required file size in KB (0 for no minimum) * * `min_size_kb` - Minimum required file size in KB (0 for no minimum)
* *
* # Returns * # Returns
* *
* * `Ok(String)` - The path where the file was saved or extracted * * `Ok(String)` - The path where the file was saved or extracted
* * `Err(DownloadError)` - An error if the download or installation failed * * `Err(DownloadError)` - An error if the download or installation failed
* *
* # Examples * # 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 * # Notes
* *
* Currently only supports .deb packages on Debian-based systems. * Currently only supports .deb packages on Debian-based systems.
* For other file types, it behaves the same as the download function. * 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 // Extract filename from URL
let filename = match url.split('/').last() { let filename = match url.split('/').last() {
Some(name) => name, Some(name) => name,
None => { None => return Err(DownloadError::InvalidUrl("cannot extract filename".to_string()))
return Err(DownloadError::InvalidUrl(
"cannot extract filename".to_string(),
))
}
}; };
// Create a proper destination path // Create a proper destination path
let dest_path = format!("/tmp/{}", filename); let dest_path = format!("/tmp/{}", filename);
// Check if it's a compressed file that needs extraction // Check if it's a compressed file that needs extraction
let lower_url = url.to_lowercase(); let lower_url = url.to_lowercase();
let is_archive = lower_url.ends_with(".tar.gz") let is_archive = lower_url.ends_with(".tar.gz") ||
|| lower_url.ends_with(".tgz") lower_url.ends_with(".tgz") ||
|| lower_url.ends_with(".tar") lower_url.ends_with(".tar") ||
|| lower_url.ends_with(".zip"); lower_url.ends_with(".zip");
let download_result = if is_archive { let download_result = if is_archive {
// For archives, use the directory-based download function // For archives, use the directory-based download function
download(url, "/tmp", min_size_kb)? 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 // For regular files, use the file-specific download function
download_file(url, &dest_path, min_size_kb)? download_file(url, &dest_path, min_size_kb)?
}; };
// Check if the downloaded result is a file // Check if the downloaded result is a file
let path = Path::new(&dest_path); let path = Path::new(&dest_path);
if !path.is_file() { if !path.is_file() {
return Ok(download_result); // Not a file, might be an extracted directory return Ok(download_result); // Not a file, might be an extracted directory
} }
// Check if it's a .deb package // Check if it's a .deb package
if dest_path.to_lowercase().ends_with(".deb") { if dest_path.to_lowercase().ends_with(".deb") {
// Check if we're on a Debian-based platform // 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("-c")
.arg("command -v dpkg > /dev/null && command -v apt > /dev/null || test -f /etc/debian_version") .arg("command -v dpkg > /dev/null && command -v apt > /dev/null || test -f /etc/debian_version")
.status(); .status();
match platform_check { match platform_check {
Ok(status) => { Ok(status) => {
if !status.success() { if !status.success() {
return Err(DownloadError::PlatformNotSupported( 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(_) => { Err(_) => return Err(DownloadError::PlatformNotSupported(
return Err(DownloadError::PlatformNotSupported( "Failed to check system compatibility for .deb installation".to_string()
"Failed to check system compatibility for .deb installation".to_string(), )),
))
}
} }
// Install the .deb package non-interactively // Install the .deb package non-interactively
println!("Installing package: {}", dest_path); println!("Installing package: {}", dest_path);
let install_result = Command::new("sudo") let install_result = Command::new("sudo")
.args(&["dpkg", "--install", &dest_path]) .args(&["dpkg", "--install", &dest_path])
.status(); .status();
match install_result { match install_result {
Ok(status) => { Ok(status) => {
if !status.success() { 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") let fix_deps = Command::new("sudo")
.args(&["apt-get", "install", "-f", "-y"]) .args(&["apt-get", "install", "-f", "-y"])
.status(); .status();
if let Ok(fix_status) = fix_deps { if let Ok(fix_status) = fix_deps {
if !fix_status.success() { if !fix_status.success() {
return Err(DownloadError::InstallationFailed( return Err(DownloadError::InstallationFailed(
"Failed to resolve package dependencies".to_string(), "Failed to resolve package dependencies".to_string()
)); ));
} }
} else { } else {
return Err(DownloadError::InstallationFailed( return Err(DownloadError::InstallationFailed(
"Failed to resolve package dependencies".to_string(), "Failed to resolve package dependencies".to_string()
)); ));
} }
} }
println!("Package installation completed successfully"); println!("Package installation completed successfully");
} },
Err(e) => return Err(DownloadError::CommandExecutionFailed(e)), Err(e) => return Err(DownloadError::CommandExecutionFailed(e)),
} }
} }
Ok(download_result) 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 std::process::Command;
use crate::process::CommandResult;
/// Error type for package management operations /// Error type for package management operations
#[derive(Debug)] #[derive(Debug)]
@ -45,7 +45,7 @@ impl Platform {
if std::path::Path::new("/usr/bin/sw_vers").exists() { if std::path::Path::new("/usr/bin/sw_vers").exists() {
return Platform::MacOS; return Platform::MacOS;
} }
// Check for Ubuntu // Check for Ubuntu
if std::path::Path::new("/etc/lsb-release").exists() { if std::path::Path::new("/etc/lsb-release").exists() {
// Read the file to confirm it's Ubuntu // Read the file to confirm it's Ubuntu
@ -55,12 +55,12 @@ impl Platform {
} }
} }
} }
Platform::Unknown Platform::Unknown
} }
} }
// Thread-local storage for debug flag /// Thread-local storage for debug flag
thread_local! { thread_local! {
static DEBUG: std::cell::RefCell<bool> = std::cell::RefCell::new(false); 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 /// Get the debug flag for the current thread
pub fn thread_local_debug() -> bool { 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 /// Execute a package management command and return the result
pub fn execute_package_command(args: &[&str], debug: bool) -> Result<CommandResult, PackageError> { pub fn execute_package_command(args: &[&str], debug: bool) -> Result<CommandResult, PackageError> {
// Save the current debug flag // Save the current debug flag
let previous_debug = thread_local_debug(); let previous_debug = thread_local_debug();
// Set the thread-local debug flag // Set the thread-local debug flag
set_thread_local_debug(debug); set_thread_local_debug(debug);
if debug { if debug {
println!("Executing command: {}", args.join(" ")); 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 // Restore the previous debug flag
set_thread_local_debug(previous_debug); set_thread_local_debug(previous_debug);
match output { match output {
Ok(output) => { Ok(output) => {
let stdout = String::from_utf8_lossy(&output.stdout).to_string(); let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string(); let stderr = String::from_utf8_lossy(&output.stderr).to_string();
let result = CommandResult { let result = CommandResult {
stdout, stdout,
stderr, stderr,
success: output.status.success(), success: output.status.success(),
code: output.status.code().unwrap_or(-1), code: output.status.code().unwrap_or(-1),
}; };
// Always output stdout/stderr when debug is true // Always output stdout/stderr when debug is true
if debug { if debug {
if !result.stdout.is_empty() { if !result.stdout.is_empty() {
println!("Command stdout: {}", result.stdout); println!("Command stdout: {}", result.stdout);
} }
if !result.stderr.is_empty() { if !result.stderr.is_empty() {
println!("Command stderr: {}", result.stderr); println!("Command stderr: {}", result.stderr);
} }
if result.success { if result.success {
println!("Command succeeded with code {}", result.code); println!("Command succeeded with code {}", result.code);
} else { } else {
println!("Command failed with code {}", result.code); println!("Command failed with code {}", result.code);
} }
} }
if result.success { if result.success {
Ok(result) Ok(result)
} else { } else {
// If command failed and debug is false, output stderr // If command failed and debug is false, output stderr
if !debug { if !debug {
println!( println!("Command failed with code {}: {}", result.code, result.stderr.trim());
"Command failed with code {}: {}",
result.code,
result.stderr.trim()
);
} }
Err(PackageError::CommandFailed(format!( Err(PackageError::CommandFailed(format!("Command failed with code {}: {}",
"Command failed with code {}: {}", result.code, result.stderr.trim())))
result.code,
result.stderr.trim()
)))
} }
} },
Err(e) => { Err(e) => {
// Always output error information // Always output error information
println!("Command execution failed: {}", e); println!("Command execution failed: {}", e);
@ -153,22 +150,22 @@ pub fn execute_package_command(args: &[&str], debug: bool) -> Result<CommandResu
pub trait PackageManager { pub trait PackageManager {
/// Install a package /// Install a package
fn install(&self, package: &str) -> Result<CommandResult, PackageError>; fn install(&self, package: &str) -> Result<CommandResult, PackageError>;
/// Remove a package /// Remove a package
fn remove(&self, package: &str) -> Result<CommandResult, PackageError>; fn remove(&self, package: &str) -> Result<CommandResult, PackageError>;
/// Update package lists /// Update package lists
fn update(&self) -> Result<CommandResult, PackageError>; fn update(&self) -> Result<CommandResult, PackageError>;
/// Upgrade installed packages /// Upgrade installed packages
fn upgrade(&self) -> Result<CommandResult, PackageError>; fn upgrade(&self) -> Result<CommandResult, PackageError>;
/// List installed packages /// List installed packages
fn list_installed(&self) -> Result<Vec<String>, PackageError>; fn list_installed(&self) -> Result<Vec<String>, PackageError>;
/// Search for packages /// Search for packages
fn search(&self, query: &str) -> Result<Vec<String>, PackageError>; fn search(&self, query: &str) -> Result<Vec<String>, PackageError>;
/// Check if a package is installed /// Check if a package is installed
fn is_installed(&self, package: &str) -> Result<bool, PackageError>; fn is_installed(&self, package: &str) -> Result<bool, PackageError>;
} }
@ -188,31 +185,27 @@ impl AptPackageManager {
impl PackageManager for AptPackageManager { impl PackageManager for AptPackageManager {
fn install(&self, package: &str) -> Result<CommandResult, PackageError> { fn install(&self, package: &str) -> Result<CommandResult, PackageError> {
// Use -y to make it non-interactive and --quiet to reduce output // Use -y to make it non-interactive and --quiet to reduce output
execute_package_command( execute_package_command(&["apt-get", "install", "-y", "--quiet", package], self.debug)
&["apt-get", "install", "-y", "--quiet", package],
self.debug,
)
} }
fn remove(&self, package: &str) -> Result<CommandResult, PackageError> { fn remove(&self, package: &str) -> Result<CommandResult, PackageError> {
// Use -y to make it non-interactive and --quiet to reduce output // Use -y to make it non-interactive and --quiet to reduce output
execute_package_command(&["apt-get", "remove", "-y", "--quiet", package], self.debug) execute_package_command(&["apt-get", "remove", "-y", "--quiet", package], self.debug)
} }
fn update(&self) -> Result<CommandResult, PackageError> { fn update(&self) -> Result<CommandResult, PackageError> {
// Use -y to make it non-interactive and --quiet to reduce output // Use -y to make it non-interactive and --quiet to reduce output
execute_package_command(&["apt-get", "update", "-y", "--quiet"], self.debug) execute_package_command(&["apt-get", "update", "-y", "--quiet"], self.debug)
} }
fn upgrade(&self) -> Result<CommandResult, PackageError> { fn upgrade(&self) -> Result<CommandResult, PackageError> {
// Use -y to make it non-interactive and --quiet to reduce output // Use -y to make it non-interactive and --quiet to reduce output
execute_package_command(&["apt-get", "upgrade", "-y", "--quiet"], self.debug) execute_package_command(&["apt-get", "upgrade", "-y", "--quiet"], self.debug)
} }
fn list_installed(&self) -> Result<Vec<String>, PackageError> { fn list_installed(&self) -> Result<Vec<String>, PackageError> {
let result = execute_package_command(&["dpkg", "--get-selections"], self.debug)?; let result = execute_package_command(&["dpkg", "--get-selections"], self.debug)?;
let packages = result let packages = result.stdout
.stdout
.lines() .lines()
.filter_map(|line| { .filter_map(|line| {
let parts: Vec<&str> = line.split_whitespace().collect(); let parts: Vec<&str> = line.split_whitespace().collect();
@ -225,11 +218,10 @@ impl PackageManager for AptPackageManager {
.collect(); .collect();
Ok(packages) Ok(packages)
} }
fn search(&self, query: &str) -> Result<Vec<String>, PackageError> { fn search(&self, query: &str) -> Result<Vec<String>, PackageError> {
let result = execute_package_command(&["apt-cache", "search", query], self.debug)?; let result = execute_package_command(&["apt-cache", "search", query], self.debug)?;
let packages = result let packages = result.stdout
.stdout
.lines() .lines()
.map(|line| { .map(|line| {
let parts: Vec<&str> = line.split_whitespace().collect(); let parts: Vec<&str> = line.split_whitespace().collect();
@ -243,7 +235,7 @@ impl PackageManager for AptPackageManager {
.collect(); .collect();
Ok(packages) Ok(packages)
} }
fn is_installed(&self, package: &str) -> Result<bool, PackageError> { fn is_installed(&self, package: &str) -> Result<bool, PackageError> {
let result = execute_package_command(&["dpkg", "-s", package], self.debug); let result = execute_package_command(&["dpkg", "-s", package], self.debug);
match result { match result {
@ -270,44 +262,42 @@ impl PackageManager for BrewPackageManager {
// Use --quiet to reduce output // Use --quiet to reduce output
execute_package_command(&["brew", "install", "--quiet", package], self.debug) execute_package_command(&["brew", "install", "--quiet", package], self.debug)
} }
fn remove(&self, package: &str) -> Result<CommandResult, PackageError> { fn remove(&self, package: &str) -> Result<CommandResult, PackageError> {
// Use --quiet to reduce output // Use --quiet to reduce output
execute_package_command(&["brew", "uninstall", "--quiet", package], self.debug) execute_package_command(&["brew", "uninstall", "--quiet", package], self.debug)
} }
fn update(&self) -> Result<CommandResult, PackageError> { fn update(&self) -> Result<CommandResult, PackageError> {
// Use --quiet to reduce output // Use --quiet to reduce output
execute_package_command(&["brew", "update", "--quiet"], self.debug) execute_package_command(&["brew", "update", "--quiet"], self.debug)
} }
fn upgrade(&self) -> Result<CommandResult, PackageError> { fn upgrade(&self) -> Result<CommandResult, PackageError> {
// Use --quiet to reduce output // Use --quiet to reduce output
execute_package_command(&["brew", "upgrade", "--quiet"], self.debug) execute_package_command(&["brew", "upgrade", "--quiet"], self.debug)
} }
fn list_installed(&self) -> Result<Vec<String>, PackageError> { fn list_installed(&self) -> Result<Vec<String>, PackageError> {
let result = execute_package_command(&["brew", "list", "--formula"], self.debug)?; let result = execute_package_command(&["brew", "list", "--formula"], self.debug)?;
let packages = result let packages = result.stdout
.stdout
.lines() .lines()
.map(|line| line.trim().to_string()) .map(|line| line.trim().to_string())
.filter(|s| !s.is_empty()) .filter(|s| !s.is_empty())
.collect(); .collect();
Ok(packages) Ok(packages)
} }
fn search(&self, query: &str) -> Result<Vec<String>, PackageError> { fn search(&self, query: &str) -> Result<Vec<String>, PackageError> {
let result = execute_package_command(&["brew", "search", query], self.debug)?; let result = execute_package_command(&["brew", "search", query], self.debug)?;
let packages = result let packages = result.stdout
.stdout
.lines() .lines()
.map(|line| line.trim().to_string()) .map(|line| line.trim().to_string())
.filter(|s| !s.is_empty()) .filter(|s| !s.is_empty())
.collect(); .collect();
Ok(packages) Ok(packages)
} }
fn is_installed(&self, package: &str) -> Result<bool, PackageError> { fn is_installed(&self, package: &str) -> Result<bool, PackageError> {
let result = execute_package_command(&["brew", "list", package], self.debug); let result = execute_package_command(&["brew", "list", package], self.debug);
match result { match result {
@ -332,70 +322,68 @@ impl PackHero {
debug: false, debug: false,
} }
} }
/// Set the debug mode /// Set the debug mode
pub fn set_debug(&mut self, debug: bool) -> &mut Self { pub fn set_debug(&mut self, debug: bool) -> &mut Self {
self.debug = debug; self.debug = debug;
self self
} }
/// Get the debug mode /// Get the debug mode
pub fn debug(&self) -> bool { pub fn debug(&self) -> bool {
self.debug self.debug
} }
/// Get the detected platform /// Get the detected platform
pub fn platform(&self) -> Platform { pub fn platform(&self) -> Platform {
self.platform self.platform
} }
/// Get a package manager for the current platform /// Get a package manager for the current platform
fn get_package_manager(&self) -> Result<Box<dyn PackageManager>, PackageError> { fn get_package_manager(&self) -> Result<Box<dyn PackageManager>, PackageError> {
match self.platform { match self.platform {
Platform::Ubuntu => Ok(Box::new(AptPackageManager::new(self.debug))), Platform::Ubuntu => Ok(Box::new(AptPackageManager::new(self.debug))),
Platform::MacOS => Ok(Box::new(BrewPackageManager::new(self.debug))), Platform::MacOS => Ok(Box::new(BrewPackageManager::new(self.debug))),
Platform::Unknown => Err(PackageError::UnsupportedPlatform( Platform::Unknown => Err(PackageError::UnsupportedPlatform("Unsupported platform".to_string())),
"Unsupported platform".to_string(),
)),
} }
} }
/// Install a package /// Install a package
pub fn install(&self, package: &str) -> Result<CommandResult, PackageError> { pub fn install(&self, package: &str) -> Result<CommandResult, PackageError> {
let pm = self.get_package_manager()?; let pm = self.get_package_manager()?;
pm.install(package) pm.install(package)
} }
/// Remove a package /// Remove a package
pub fn remove(&self, package: &str) -> Result<CommandResult, PackageError> { pub fn remove(&self, package: &str) -> Result<CommandResult, PackageError> {
let pm = self.get_package_manager()?; let pm = self.get_package_manager()?;
pm.remove(package) pm.remove(package)
} }
/// Update package lists /// Update package lists
pub fn update(&self) -> Result<CommandResult, PackageError> { pub fn update(&self) -> Result<CommandResult, PackageError> {
let pm = self.get_package_manager()?; let pm = self.get_package_manager()?;
pm.update() pm.update()
} }
/// Upgrade installed packages /// Upgrade installed packages
pub fn upgrade(&self) -> Result<CommandResult, PackageError> { pub fn upgrade(&self) -> Result<CommandResult, PackageError> {
let pm = self.get_package_manager()?; let pm = self.get_package_manager()?;
pm.upgrade() pm.upgrade()
} }
/// List installed packages /// List installed packages
pub fn list_installed(&self) -> Result<Vec<String>, PackageError> { pub fn list_installed(&self) -> Result<Vec<String>, PackageError> {
let pm = self.get_package_manager()?; let pm = self.get_package_manager()?;
pm.list_installed() pm.list_installed()
} }
/// Search for packages /// Search for packages
pub fn search(&self, query: &str) -> Result<Vec<String>, PackageError> { pub fn search(&self, query: &str) -> Result<Vec<String>, PackageError> {
let pm = self.get_package_manager()?; let pm = self.get_package_manager()?;
pm.search(query) pm.search(query)
} }
/// Check if a package is installed /// Check if a package is installed
pub fn is_installed(&self, package: &str) -> Result<bool, PackageError> { pub fn is_installed(&self, package: &str) -> Result<bool, PackageError> {
let pm = self.get_package_manager()?; let pm = self.get_package_manager()?;
@ -406,49 +394,47 @@ impl PackHero {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
// Import the std::process::Command directly for some test-specific commands // Import the std::process::Command directly for some test-specific commands
use super::*;
use std::process::Command as StdCommand; use std::process::Command as StdCommand;
use super::*;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
#[test] #[test]
fn test_platform_detection() { fn test_platform_detection() {
// This test will return different results depending on the platform it's run on // This test will return different results depending on the platform it's run on
let platform = Platform::detect(); let platform = Platform::detect();
println!("Detected platform: {:?}", platform); println!("Detected platform: {:?}", platform);
// Just ensure it doesn't panic // Just ensure it doesn't panic
assert!(true); assert!(true);
} }
#[test] #[test]
fn test_debug_flag() { fn test_debug_flag() {
// Test setting and getting the debug flag // Test setting and getting the debug flag
set_thread_local_debug(true); set_thread_local_debug(true);
assert_eq!(thread_local_debug(), true); assert_eq!(thread_local_debug(), true);
set_thread_local_debug(false); set_thread_local_debug(false);
assert_eq!(thread_local_debug(), false); assert_eq!(thread_local_debug(), false);
} }
#[test] #[test]
fn test_package_error_display() { fn test_package_error_display() {
// Test the Display implementation for PackageError // Test the Display implementation for PackageError
let err1 = PackageError::CommandFailed("command failed".to_string()); let err1 = PackageError::CommandFailed("command failed".to_string());
assert_eq!(err1.to_string(), "Command failed: command failed"); assert_eq!(err1.to_string(), "Command failed: command failed");
let err2 = PackageError::UnsupportedPlatform("test platform".to_string()); let err2 = PackageError::UnsupportedPlatform("test platform".to_string());
assert_eq!(err2.to_string(), "Unsupported platform: test platform"); assert_eq!(err2.to_string(), "Unsupported platform: test platform");
let err3 = PackageError::Other("other error".to_string()); let err3 = PackageError::Other("other error".to_string());
assert_eq!(err3.to_string(), "Error: other error"); assert_eq!(err3.to_string(), "Error: other error");
// We can't easily test CommandExecutionFailed because std::io::Error doesn't implement PartialEq // We can't easily test CommandExecutionFailed because std::io::Error doesn't implement PartialEq
} }
// Mock package manager for testing // Mock package manager for testing
struct MockPackageManager { struct MockPackageManager {
// debug field is kept for consistency with real package managers
#[allow(dead_code)]
debug: bool, debug: bool,
install_called: Arc<Mutex<bool>>, install_called: Arc<Mutex<bool>>,
remove_called: Arc<Mutex<bool>>, remove_called: Arc<Mutex<bool>>,
@ -460,7 +446,7 @@ mod tests {
// Control what the mock returns // Control what the mock returns
should_succeed: bool, should_succeed: bool,
} }
impl MockPackageManager { impl MockPackageManager {
fn new(debug: bool, should_succeed: bool) -> Self { fn new(debug: bool, should_succeed: bool) -> Self {
Self { Self {
@ -476,7 +462,7 @@ mod tests {
} }
} }
} }
impl PackageManager for MockPackageManager { impl PackageManager for MockPackageManager {
fn install(&self, package: &str) -> Result<CommandResult, PackageError> { fn install(&self, package: &str) -> Result<CommandResult, PackageError> {
*self.install_called.lock().unwrap() = true; *self.install_called.lock().unwrap() = true;
@ -488,12 +474,10 @@ mod tests {
code: 0, code: 0,
}) })
} else { } else {
Err(PackageError::CommandFailed( Err(PackageError::CommandFailed("Mock install failed".to_string()))
"Mock install failed".to_string(),
))
} }
} }
fn remove(&self, package: &str) -> Result<CommandResult, PackageError> { fn remove(&self, package: &str) -> Result<CommandResult, PackageError> {
*self.remove_called.lock().unwrap() = true; *self.remove_called.lock().unwrap() = true;
if self.should_succeed { if self.should_succeed {
@ -504,12 +488,10 @@ mod tests {
code: 0, code: 0,
}) })
} else { } else {
Err(PackageError::CommandFailed( Err(PackageError::CommandFailed("Mock remove failed".to_string()))
"Mock remove failed".to_string(),
))
} }
} }
fn update(&self) -> Result<CommandResult, PackageError> { fn update(&self) -> Result<CommandResult, PackageError> {
*self.update_called.lock().unwrap() = true; *self.update_called.lock().unwrap() = true;
if self.should_succeed { if self.should_succeed {
@ -520,12 +502,10 @@ mod tests {
code: 0, code: 0,
}) })
} else { } else {
Err(PackageError::CommandFailed( Err(PackageError::CommandFailed("Mock update failed".to_string()))
"Mock update failed".to_string(),
))
} }
} }
fn upgrade(&self) -> Result<CommandResult, PackageError> { fn upgrade(&self) -> Result<CommandResult, PackageError> {
*self.upgrade_called.lock().unwrap() = true; *self.upgrade_called.lock().unwrap() = true;
if self.should_succeed { if self.should_succeed {
@ -536,57 +516,45 @@ mod tests {
code: 0, code: 0,
}) })
} else { } else {
Err(PackageError::CommandFailed( Err(PackageError::CommandFailed("Mock upgrade failed".to_string()))
"Mock upgrade failed".to_string(),
))
} }
} }
fn list_installed(&self) -> Result<Vec<String>, PackageError> { fn list_installed(&self) -> Result<Vec<String>, PackageError> {
*self.list_installed_called.lock().unwrap() = true; *self.list_installed_called.lock().unwrap() = true;
if self.should_succeed { if self.should_succeed {
Ok(vec!["package1".to_string(), "package2".to_string()]) Ok(vec!["package1".to_string(), "package2".to_string()])
} else { } else {
Err(PackageError::CommandFailed( Err(PackageError::CommandFailed("Mock list_installed failed".to_string()))
"Mock list_installed failed".to_string(),
))
} }
} }
fn search(&self, query: &str) -> Result<Vec<String>, PackageError> { fn search(&self, query: &str) -> Result<Vec<String>, PackageError> {
*self.search_called.lock().unwrap() = true; *self.search_called.lock().unwrap() = true;
if self.should_succeed { if self.should_succeed {
Ok(vec![ Ok(vec![format!("result1-{}", query), format!("result2-{}", query)])
format!("result1-{}", query),
format!("result2-{}", query),
])
} else { } else {
Err(PackageError::CommandFailed( Err(PackageError::CommandFailed("Mock search failed".to_string()))
"Mock search failed".to_string(),
))
} }
} }
fn is_installed(&self, package: &str) -> Result<bool, PackageError> { fn is_installed(&self, package: &str) -> Result<bool, PackageError> {
*self.is_installed_called.lock().unwrap() = true; *self.is_installed_called.lock().unwrap() = true;
if self.should_succeed { if self.should_succeed {
Ok(package == "installed-package") Ok(package == "installed-package")
} else { } else {
Err(PackageError::CommandFailed( Err(PackageError::CommandFailed("Mock is_installed failed".to_string()))
"Mock is_installed failed".to_string(),
))
} }
} }
} }
// Custom PackHero for testing with a mock package manager // Custom PackHero for testing with a mock package manager
struct TestPackHero { struct TestPackHero {
platform: Platform, platform: Platform,
#[allow(dead_code)]
debug: bool, debug: bool,
mock_manager: MockPackageManager, mock_manager: MockPackageManager,
} }
impl TestPackHero { impl TestPackHero {
fn new(platform: Platform, debug: bool, should_succeed: bool) -> Self { fn new(platform: Platform, debug: bool, should_succeed: bool) -> Self {
Self { Self {
@ -595,152 +563,144 @@ mod tests {
mock_manager: MockPackageManager::new(debug, should_succeed), mock_manager: MockPackageManager::new(debug, should_succeed),
} }
} }
fn get_package_manager(&self) -> Result<&dyn PackageManager, PackageError> { fn get_package_manager(&self) -> Result<&dyn PackageManager, PackageError> {
match self.platform { match self.platform {
Platform::Ubuntu | Platform::MacOS => Ok(&self.mock_manager), Platform::Ubuntu | Platform::MacOS => Ok(&self.mock_manager),
Platform::Unknown => Err(PackageError::UnsupportedPlatform( Platform::Unknown => Err(PackageError::UnsupportedPlatform("Unsupported platform".to_string())),
"Unsupported platform".to_string(),
)),
} }
} }
fn install(&self, package: &str) -> Result<CommandResult, PackageError> { fn install(&self, package: &str) -> Result<CommandResult, PackageError> {
let pm = self.get_package_manager()?; let pm = self.get_package_manager()?;
pm.install(package) pm.install(package)
} }
fn remove(&self, package: &str) -> Result<CommandResult, PackageError> { fn remove(&self, package: &str) -> Result<CommandResult, PackageError> {
let pm = self.get_package_manager()?; let pm = self.get_package_manager()?;
pm.remove(package) pm.remove(package)
} }
fn update(&self) -> Result<CommandResult, PackageError> { fn update(&self) -> Result<CommandResult, PackageError> {
let pm = self.get_package_manager()?; let pm = self.get_package_manager()?;
pm.update() pm.update()
} }
fn upgrade(&self) -> Result<CommandResult, PackageError> { fn upgrade(&self) -> Result<CommandResult, PackageError> {
let pm = self.get_package_manager()?; let pm = self.get_package_manager()?;
pm.upgrade() pm.upgrade()
} }
fn list_installed(&self) -> Result<Vec<String>, PackageError> { fn list_installed(&self) -> Result<Vec<String>, PackageError> {
let pm = self.get_package_manager()?; let pm = self.get_package_manager()?;
pm.list_installed() pm.list_installed()
} }
fn search(&self, query: &str) -> Result<Vec<String>, PackageError> { fn search(&self, query: &str) -> Result<Vec<String>, PackageError> {
let pm = self.get_package_manager()?; let pm = self.get_package_manager()?;
pm.search(query) pm.search(query)
} }
fn is_installed(&self, package: &str) -> Result<bool, PackageError> { fn is_installed(&self, package: &str) -> Result<bool, PackageError> {
let pm = self.get_package_manager()?; let pm = self.get_package_manager()?;
pm.is_installed(package) pm.is_installed(package)
} }
} }
#[test] #[test]
fn test_packhero_with_mock_success() { fn test_packhero_with_mock_success() {
// Test PackHero with a mock package manager that succeeds // Test PackHero with a mock package manager that succeeds
let hero = TestPackHero::new(Platform::Ubuntu, false, true); let hero = TestPackHero::new(Platform::Ubuntu, false, true);
// Test install // Test install
let result = hero.install("test-package"); let result = hero.install("test-package");
assert!(result.is_ok()); assert!(result.is_ok());
assert!(*hero.mock_manager.install_called.lock().unwrap()); assert!(*hero.mock_manager.install_called.lock().unwrap());
// Test remove // Test remove
let result = hero.remove("test-package"); let result = hero.remove("test-package");
assert!(result.is_ok()); assert!(result.is_ok());
assert!(*hero.mock_manager.remove_called.lock().unwrap()); assert!(*hero.mock_manager.remove_called.lock().unwrap());
// Test update // Test update
let result = hero.update(); let result = hero.update();
assert!(result.is_ok()); assert!(result.is_ok());
assert!(*hero.mock_manager.update_called.lock().unwrap()); assert!(*hero.mock_manager.update_called.lock().unwrap());
// Test upgrade // Test upgrade
let result = hero.upgrade(); let result = hero.upgrade();
assert!(result.is_ok()); assert!(result.is_ok());
assert!(*hero.mock_manager.upgrade_called.lock().unwrap()); assert!(*hero.mock_manager.upgrade_called.lock().unwrap());
// Test list_installed // Test list_installed
let result = hero.list_installed(); let result = hero.list_installed();
assert!(result.is_ok()); assert!(result.is_ok());
assert_eq!( assert_eq!(result.unwrap(), vec!["package1".to_string(), "package2".to_string()]);
result.unwrap(),
vec!["package1".to_string(), "package2".to_string()]
);
assert!(*hero.mock_manager.list_installed_called.lock().unwrap()); assert!(*hero.mock_manager.list_installed_called.lock().unwrap());
// Test search // Test search
let result = hero.search("query"); let result = hero.search("query");
assert!(result.is_ok()); assert!(result.is_ok());
assert_eq!( assert_eq!(result.unwrap(), vec!["result1-query".to_string(), "result2-query".to_string()]);
result.unwrap(),
vec!["result1-query".to_string(), "result2-query".to_string()]
);
assert!(*hero.mock_manager.search_called.lock().unwrap()); assert!(*hero.mock_manager.search_called.lock().unwrap());
// Test is_installed // Test is_installed
let result = hero.is_installed("installed-package"); let result = hero.is_installed("installed-package");
assert!(result.is_ok()); assert!(result.is_ok());
assert!(result.unwrap()); assert!(result.unwrap());
assert!(*hero.mock_manager.is_installed_called.lock().unwrap()); assert!(*hero.mock_manager.is_installed_called.lock().unwrap());
let result = hero.is_installed("not-installed-package"); let result = hero.is_installed("not-installed-package");
assert!(result.is_ok()); assert!(result.is_ok());
assert!(!result.unwrap()); assert!(!result.unwrap());
} }
#[test] #[test]
fn test_packhero_with_mock_failure() { fn test_packhero_with_mock_failure() {
// Test PackHero with a mock package manager that fails // Test PackHero with a mock package manager that fails
let hero = TestPackHero::new(Platform::Ubuntu, false, false); let hero = TestPackHero::new(Platform::Ubuntu, false, false);
// Test install // Test install
let result = hero.install("test-package"); let result = hero.install("test-package");
assert!(result.is_err()); assert!(result.is_err());
assert!(*hero.mock_manager.install_called.lock().unwrap()); assert!(*hero.mock_manager.install_called.lock().unwrap());
// Test remove // Test remove
let result = hero.remove("test-package"); let result = hero.remove("test-package");
assert!(result.is_err()); assert!(result.is_err());
assert!(*hero.mock_manager.remove_called.lock().unwrap()); assert!(*hero.mock_manager.remove_called.lock().unwrap());
// Test update // Test update
let result = hero.update(); let result = hero.update();
assert!(result.is_err()); assert!(result.is_err());
assert!(*hero.mock_manager.update_called.lock().unwrap()); assert!(*hero.mock_manager.update_called.lock().unwrap());
// Test upgrade // Test upgrade
let result = hero.upgrade(); let result = hero.upgrade();
assert!(result.is_err()); assert!(result.is_err());
assert!(*hero.mock_manager.upgrade_called.lock().unwrap()); assert!(*hero.mock_manager.upgrade_called.lock().unwrap());
// Test list_installed // Test list_installed
let result = hero.list_installed(); let result = hero.list_installed();
assert!(result.is_err()); assert!(result.is_err());
assert!(*hero.mock_manager.list_installed_called.lock().unwrap()); assert!(*hero.mock_manager.list_installed_called.lock().unwrap());
// Test search // Test search
let result = hero.search("query"); let result = hero.search("query");
assert!(result.is_err()); assert!(result.is_err());
assert!(*hero.mock_manager.search_called.lock().unwrap()); assert!(*hero.mock_manager.search_called.lock().unwrap());
// Test is_installed // Test is_installed
let result = hero.is_installed("installed-package"); let result = hero.is_installed("installed-package");
assert!(result.is_err()); assert!(result.is_err());
assert!(*hero.mock_manager.is_installed_called.lock().unwrap()); assert!(*hero.mock_manager.is_installed_called.lock().unwrap());
} }
#[test] #[test]
fn test_packhero_unsupported_platform() { fn test_packhero_unsupported_platform() {
// Test PackHero with an unsupported platform // Test PackHero with an unsupported platform
let hero = TestPackHero::new(Platform::Unknown, false, true); let hero = TestPackHero::new(Platform::Unknown, false, true);
// All operations should fail with UnsupportedPlatform error // All operations should fail with UnsupportedPlatform error
let result = hero.install("test-package"); let result = hero.install("test-package");
assert!(result.is_err()); assert!(result.is_err());
@ -748,14 +708,14 @@ mod tests {
Err(PackageError::UnsupportedPlatform(_)) => (), Err(PackageError::UnsupportedPlatform(_)) => (),
_ => panic!("Expected UnsupportedPlatform error"), _ => panic!("Expected UnsupportedPlatform error"),
} }
let result = hero.remove("test-package"); let result = hero.remove("test-package");
assert!(result.is_err()); assert!(result.is_err());
match result { match result {
Err(PackageError::UnsupportedPlatform(_)) => (), Err(PackageError::UnsupportedPlatform(_)) => (),
_ => panic!("Expected UnsupportedPlatform error"), _ => panic!("Expected UnsupportedPlatform error"),
} }
let result = hero.update(); let result = hero.update();
assert!(result.is_err()); assert!(result.is_err());
match result { match result {
@ -763,7 +723,7 @@ mod tests {
_ => panic!("Expected UnsupportedPlatform error"), _ => panic!("Expected UnsupportedPlatform error"),
} }
} }
// Real-world tests that actually install and remove packages on Ubuntu // 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 // These tests will only run on Ubuntu and will be skipped on other platforms
#[test] #[test]
@ -771,22 +731,19 @@ mod tests {
// Check if we're on Ubuntu // Check if we're on Ubuntu
let platform = Platform::detect(); let platform = Platform::detect();
if platform != Platform::Ubuntu { if platform != Platform::Ubuntu {
println!( println!("Skipping real package operations test on non-Ubuntu platform: {:?}", platform);
"Skipping real package operations test on non-Ubuntu platform: {:?}",
platform
);
return; return;
} }
println!("Running real package operations test on Ubuntu"); println!("Running real package operations test on Ubuntu");
// Create a PackHero instance with debug enabled // Create a PackHero instance with debug enabled
let mut hero = PackHero::new(); let mut hero = PackHero::new();
hero.set_debug(true); hero.set_debug(true);
// Test package to install/remove // Test package to install/remove
let test_package = "wget"; let test_package = "wget";
// First, check if the package is already installed // First, check if the package is already installed
let is_installed_before = match hero.is_installed(test_package) { let is_installed_before = match hero.is_installed(test_package) {
Ok(result) => result, Ok(result) => result,
@ -795,12 +752,9 @@ mod tests {
return; return;
} }
}; };
println!( println!("Package {} is installed before test: {}", test_package, is_installed_before);
"Package {} is installed before test: {}",
test_package, is_installed_before
);
// If the package is already installed, we'll remove it first // If the package is already installed, we'll remove it first
if is_installed_before { if is_installed_before {
println!("Removing existing package {} before test", test_package); println!("Removing existing package {} before test", test_package);
@ -811,7 +765,7 @@ mod tests {
return; return;
} }
} }
// Verify it was removed // Verify it was removed
match hero.is_installed(test_package) { match hero.is_installed(test_package) {
Ok(is_installed) => { Ok(is_installed) => {
@ -821,17 +775,14 @@ mod tests {
} else { } else {
println!("Verified package {} was removed", test_package); println!("Verified package {} was removed", test_package);
} }
} },
Err(e) => { Err(e) => {
println!( println!("Error checking if package is installed after removal: {}", e);
"Error checking if package is installed after removal: {}",
e
);
return; return;
} }
} }
} }
// Now install the package // Now install the package
println!("Installing package {}", test_package); println!("Installing package {}", test_package);
match hero.install(test_package) { match hero.install(test_package) {
@ -841,7 +792,7 @@ mod tests {
return; return;
} }
} }
// Verify it was installed // Verify it was installed
match hero.is_installed(test_package) { match hero.is_installed(test_package) {
Ok(is_installed) => { Ok(is_installed) => {
@ -851,50 +802,41 @@ mod tests {
} else { } else {
println!("Verified package {} was installed", test_package); println!("Verified package {} was installed", test_package);
} }
} },
Err(e) => { Err(e) => {
println!( println!("Error checking if package is installed after installation: {}", e);
"Error checking if package is installed after installation: {}",
e
);
return; return;
} }
} }
// Test the search functionality // Test the search functionality
println!("Searching for packages with 'wget'"); println!("Searching for packages with 'wget'");
match hero.search("wget") { match hero.search("wget") {
Ok(results) => { Ok(results) => {
println!("Search results: {:?}", results); println!("Search results: {:?}", results);
assert!( assert!(results.iter().any(|r| r.contains("wget")), "Search results should contain wget");
results.iter().any(|r| r.contains("wget")), },
"Search results should contain wget"
);
}
Err(e) => { Err(e) => {
println!("Error searching for packages: {}", e); println!("Error searching for packages: {}", e);
return; return;
} }
} }
// Test listing installed packages // Test listing installed packages
println!("Listing installed packages"); println!("Listing installed packages");
match hero.list_installed() { match hero.list_installed() {
Ok(packages) => { Ok(packages) => {
println!("Found {} installed packages", packages.len()); println!("Found {} installed packages", packages.len());
// Check if our test package is in the list // Check if our test package is in the list
assert!( assert!(packages.iter().any(|p| p == test_package),
packages.iter().any(|p| p == test_package), "Installed packages list should contain {}", test_package);
"Installed packages list should contain {}", },
test_package
);
}
Err(e) => { Err(e) => {
println!("Error listing installed packages: {}", e); println!("Error listing installed packages: {}", e);
return; return;
} }
} }
// Now remove the package if it wasn't installed before // Now remove the package if it wasn't installed before
if !is_installed_before { if !is_installed_before {
println!("Removing package {} after test", test_package); println!("Removing package {} after test", test_package);
@ -905,7 +847,7 @@ mod tests {
return; return;
} }
} }
// Verify it was removed // Verify it was removed
match hero.is_installed(test_package) { match hero.is_installed(test_package) {
Ok(is_installed) => { Ok(is_installed) => {
@ -915,17 +857,14 @@ mod tests {
} else { } else {
println!("Verified package {} was removed", test_package); println!("Verified package {} was removed", test_package);
} }
} },
Err(e) => { Err(e) => {
println!( println!("Error checking if package is installed after removal: {}", e);
"Error checking if package is installed after removal: {}",
e
);
return; return;
} }
} }
} }
// Test update functionality // Test update functionality
println!("Testing package list update"); println!("Testing package list update");
match hero.update() { match hero.update() {
@ -935,10 +874,10 @@ mod tests {
return; return;
} }
} }
println!("All real package operations tests passed on Ubuntu"); println!("All real package operations tests passed on Ubuntu");
} }
// Test to check if apt-get is available on the system // Test to check if apt-get is available on the system
#[test] #[test]
fn test_apt_get_availability() { fn test_apt_get_availability() {
@ -947,18 +886,18 @@ mod tests {
.arg("apt-get") .arg("apt-get")
.output() .output()
.expect("Failed to execute which apt-get"); .expect("Failed to execute which apt-get");
let success = output.status.success(); let success = output.status.success();
let stdout = String::from_utf8_lossy(&output.stdout).to_string(); let stdout = String::from_utf8_lossy(&output.stdout).to_string();
println!("apt-get available: {}", success); println!("apt-get available: {}", success);
if success { if success {
println!("apt-get path: {}", stdout.trim()); println!("apt-get path: {}", stdout.trim());
} }
// On Ubuntu, this should pass // On Ubuntu, this should pass
if Platform::detect() == Platform::Ubuntu { if Platform::detect() == Platform::Ubuntu {
assert!(success, "apt-get should be available on Ubuntu"); assert!(success, "apt-get should be available on Ubuntu");
} }
} }
} }

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::process::Command;
use std::fmt;
use std::error::Error;
use std::io;
/// Error type for process management operations /// Error type for process management operations
/// ///
/// This enum represents various errors that can occur during process management /// This enum represents various errors that can occur during process management
/// operations such as listing, finding, or killing processes. /// operations such as listing, finding, or killing processes.
#[derive(Debug)] #[derive(Debug)]
@ -23,18 +23,11 @@ pub enum ProcessError {
impl fmt::Display for ProcessError { impl fmt::Display for ProcessError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {
ProcessError::CommandExecutionFailed(e) => { ProcessError::CommandExecutionFailed(e) => write!(f, "Failed to execute command: {}", e),
write!(f, "Failed to execute command: {}", e)
}
ProcessError::CommandFailed(e) => write!(f, "{}", e), ProcessError::CommandFailed(e) => write!(f, "{}", e),
ProcessError::NoProcessFound(pattern) => { ProcessError::NoProcessFound(pattern) => write!(f, "No processes found matching '{}'", pattern),
write!(f, "No processes found matching '{}'", pattern) ProcessError::MultipleProcessesFound(pattern, count) =>
} write!(f, "Multiple processes ({}) found matching '{}'", count, 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. * Check if a command exists in PATH.
* *
* # Arguments * # Arguments
* *
* * `cmd` - The command to check * * `cmd` - The command to check
* *
* # Returns * # Returns
* *
* * `Option<String>` - The full path to the command if found, None otherwise * * `Option<String>` - The full path to the command if found, None otherwise
* *
* # Examples * # Examples
* *
* ``` * ```
* use sal::process::which;
*
* match which("git") { * match which("git") {
* Some(path) => println!("Git is installed at: {}", path), * Some(path) => println!("Git is installed at: {}", path),
* None => println!("Git is not installed"), * None => println!("Git is not installed"),
@ -83,12 +74,14 @@ pub struct ProcessInfo {
pub fn which(cmd: &str) -> Option<String> { pub fn which(cmd: &str) -> Option<String> {
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
let which_cmd = "where"; let which_cmd = "where";
#[cfg(any(target_os = "macos", target_os = "linux"))] #[cfg(any(target_os = "macos", target_os = "linux"))]
let which_cmd = "which"; let which_cmd = "which";
let output = Command::new(which_cmd).arg(cmd).output(); let output = Command::new(which_cmd)
.arg(cmd)
.output();
match output { match output {
Ok(out) => { Ok(out) => {
if out.status.success() { if out.status.success() {
@ -97,34 +90,29 @@ pub fn which(cmd: &str) -> Option<String> {
} else { } else {
None None
} }
} },
Err(_) => None, Err(_) => None
} }
} }
/** /**
* Kill processes matching a pattern. * Kill processes matching a pattern.
* *
* # Arguments * # Arguments
* *
* * `pattern` - The pattern to match against process names * * `pattern` - The pattern to match against process names
* *
* # Returns * # Returns
* *
* * `Ok(String)` - A success message indicating processes were killed or none were found * * `Ok(String)` - A success message indicating processes were killed or none were found
* * `Err(ProcessError)` - An error if the kill operation failed * * `Err(ProcessError)` - An error if the kill operation failed
* *
* # Examples * # Examples
* *
* ``` * ```
* // Kill all processes with "server" in their name * // Kill all processes with "server" in their name
* use sal::process::kill; * let result = kill("server")?;
* * println!("{}", result);
* fn main() -> Result<(), Box<dyn std::error::Error>> {
* let result = kill("server")?;
* println!("{}", result);
* Ok(())
* }
* ``` * ```
*/ */
pub fn kill(pattern: &str) -> Result<String, ProcessError> { 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 // On Windows, use taskkill with wildcard support
let mut args = vec!["/F"]; // Force kill let mut args = vec!["/F"]; // Force kill
if pattern.contains('*') { if pattern.contains('*') {
// If it contains wildcards, use filter // If it contains wildcards, use filter
args.extend(&["/FI", &format!("IMAGENAME eq {}", pattern)]); args.extend(&["/FI", &format!("IMAGENAME eq {}", pattern)]);
@ -141,12 +129,12 @@ pub fn kill(pattern: &str) -> Result<String, ProcessError> {
// Otherwise use image name directly // Otherwise use image name directly
args.extend(&["/IM", pattern]); args.extend(&["/IM", pattern]);
} }
let output = Command::new("taskkill") let output = Command::new("taskkill")
.args(&args) .args(&args)
.output() .output()
.map_err(ProcessError::CommandExecutionFailed)?; .map_err(ProcessError::CommandExecutionFailed)?;
if output.status.success() { if output.status.success() {
Ok("Successfully killed processes".to_string()) Ok("Successfully killed processes".to_string())
} else { } else {
@ -156,20 +144,14 @@ pub fn kill(pattern: &str) -> Result<String, ProcessError> {
if stdout.contains("No tasks") { if stdout.contains("No tasks") {
Ok("No matching processes found".to_string()) Ok("No matching processes found".to_string())
} else { } else {
Err(ProcessError::CommandFailed(format!( Err(ProcessError::CommandFailed(format!("Failed to kill processes: {}", stdout)))
"Failed to kill processes: {}",
stdout
)))
} }
} else { } else {
Err(ProcessError::CommandFailed(format!( Err(ProcessError::CommandFailed(format!("Failed to kill processes: {}", error)))
"Failed to kill processes: {}",
error
)))
} }
} }
} }
#[cfg(any(target_os = "macos", target_os = "linux"))] #[cfg(any(target_os = "macos", target_os = "linux"))]
{ {
// On Unix-like systems, use pkill which has built-in pattern matching // 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) .arg(pattern)
.output() .output()
.map_err(ProcessError::CommandExecutionFailed)?; .map_err(ProcessError::CommandExecutionFailed)?;
// pkill returns 0 if processes were killed, 1 if none matched // pkill returns 0 if processes were killed, 1 if none matched
if output.status.success() { if output.status.success() {
Ok("Successfully killed processes".to_string()) 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()) Ok("No matching processes found".to_string())
} else { } else {
let error = String::from_utf8_lossy(&output.stderr); let error = String::from_utf8_lossy(&output.stderr);
Err(ProcessError::CommandFailed(format!( Err(ProcessError::CommandFailed(format!("Failed to kill processes: {}", error)))
"Failed to kill processes: {}",
error
)))
} }
} }
} }
/** /**
* List processes matching a pattern (or all if pattern is empty). * List processes matching a pattern (or all if pattern is empty).
* *
* # Arguments * # Arguments
* *
* * `pattern` - The pattern to match against process names (empty string for all processes) * * `pattern` - The pattern to match against process names (empty string for all processes)
* *
* # Returns * # Returns
* *
* * `Ok(Vec<ProcessInfo>)` - A vector of process information for matching processes * * `Ok(Vec<ProcessInfo>)` - A vector of process information for matching processes
* * `Err(ProcessError)` - An error if the list operation failed * * `Err(ProcessError)` - An error if the list operation failed
* *
* # Examples * # Examples
* *
* ``` * ```
* // List all processes * // List all processes
* use sal::process::process_list; * let processes = process_list("")?;
* *
* fn main() -> Result<(), Box<dyn std::error::Error>> { * // List processes with "server" in their name
* let processes = process_list("")?; * let processes = process_list("server")?;
* * for proc in processes {
* // List processes with "server" in their name * println!("PID: {}, Name: {}", proc.pid, proc.name);
* let processes = process_list("server")?;
* for proc in processes {
* println!("PID: {}, Name: {}", proc.pid, proc.name);
* }
* Ok(())
* } * }
* ``` * ```
*/ */
pub fn process_list(pattern: &str) -> Result<Vec<ProcessInfo>, ProcessError> { pub fn process_list(pattern: &str) -> Result<Vec<ProcessInfo>, ProcessError> {
let mut processes = Vec::new(); let mut processes = Vec::new();
// Platform specific implementations // Platform specific implementations
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
{ {
@ -235,23 +209,22 @@ pub fn process_list(pattern: &str) -> Result<Vec<ProcessInfo>, ProcessError> {
.args(&["process", "list", "brief"]) .args(&["process", "list", "brief"])
.output() .output()
.map_err(ProcessError::CommandExecutionFailed)?; .map_err(ProcessError::CommandExecutionFailed)?;
if output.status.success() { if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout).to_string(); let stdout = String::from_utf8_lossy(&output.stdout).to_string();
// Parse output (assuming format: Handle Name Priority) // Parse output (assuming format: Handle Name Priority)
for line in stdout.lines().skip(1) { for line in stdout.lines().skip(1) { // Skip header
// Skip header
let parts: Vec<&str> = line.trim().split_whitespace().collect(); let parts: Vec<&str> = line.trim().split_whitespace().collect();
if parts.len() >= 2 { if parts.len() >= 2 {
let pid = parts[0].parse::<i64>().unwrap_or(0); let pid = parts[0].parse::<i64>().unwrap_or(0);
let name = parts[1].to_string(); let name = parts[1].to_string();
// Filter by pattern if provided // Filter by pattern if provided
if !pattern.is_empty() && !name.contains(pattern) { if !pattern.is_empty() && !name.contains(pattern) {
continue; continue;
} }
processes.push(ProcessInfo { processes.push(ProcessInfo {
pid, pid,
name, name,
@ -262,13 +235,10 @@ pub fn process_list(pattern: &str) -> Result<Vec<ProcessInfo>, ProcessError> {
} }
} else { } else {
let stderr = String::from_utf8_lossy(&output.stderr).to_string(); let stderr = String::from_utf8_lossy(&output.stderr).to_string();
return Err(ProcessError::CommandFailed(format!( return Err(ProcessError::CommandFailed(format!("Failed to list processes: {}", stderr)));
"Failed to list processes: {}",
stderr
)));
} }
} }
#[cfg(any(target_os = "macos", target_os = "linux"))] #[cfg(any(target_os = "macos", target_os = "linux"))]
{ {
// Unix implementation using ps // Unix implementation using ps
@ -276,23 +246,22 @@ pub fn process_list(pattern: &str) -> Result<Vec<ProcessInfo>, ProcessError> {
.args(&["-eo", "pid,comm"]) .args(&["-eo", "pid,comm"])
.output() .output()
.map_err(ProcessError::CommandExecutionFailed)?; .map_err(ProcessError::CommandExecutionFailed)?;
if output.status.success() { if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout).to_string(); let stdout = String::from_utf8_lossy(&output.stdout).to_string();
// Parse output (assuming format: PID COMMAND) // Parse output (assuming format: PID COMMAND)
for line in stdout.lines().skip(1) { for line in stdout.lines().skip(1) { // Skip header
// Skip header
let parts: Vec<&str> = line.trim().split_whitespace().collect(); let parts: Vec<&str> = line.trim().split_whitespace().collect();
if parts.len() >= 2 { if parts.len() >= 2 {
let pid = parts[0].parse::<i64>().unwrap_or(0); let pid = parts[0].parse::<i64>().unwrap_or(0);
let name = parts[1].to_string(); let name = parts[1].to_string();
// Filter by pattern if provided // Filter by pattern if provided
if !pattern.is_empty() && !name.contains(pattern) { if !pattern.is_empty() && !name.contains(pattern) {
continue; continue;
} }
processes.push(ProcessInfo { processes.push(ProcessInfo {
pid, pid,
name, name,
@ -303,49 +272,38 @@ pub fn process_list(pattern: &str) -> Result<Vec<ProcessInfo>, ProcessError> {
} }
} else { } else {
let stderr = String::from_utf8_lossy(&output.stderr).to_string(); let stderr = String::from_utf8_lossy(&output.stderr).to_string();
return Err(ProcessError::CommandFailed(format!( return Err(ProcessError::CommandFailed(format!("Failed to list processes: {}", stderr)));
"Failed to list processes: {}",
stderr
)));
} }
} }
Ok(processes) Ok(processes)
} }
/** /**
* Get a single process matching the pattern (error if 0 or more than 1 match). * Get a single process matching the pattern (error if 0 or more than 1 match).
* *
* # Arguments * # Arguments
* *
* * `pattern` - The pattern to match against process names * * `pattern` - The pattern to match against process names
* *
* # Returns * # Returns
* *
* * `Ok(ProcessInfo)` - Information about the matching process * * `Ok(ProcessInfo)` - Information about the matching process
* * `Err(ProcessError)` - An error if no process or multiple processes match * * `Err(ProcessError)` - An error if no process or multiple processes match
* *
* # Examples * # Examples
* *
* ```no_run * ```
* use sal::process::process_get; * let process = process_get("unique-server-name")?;
* * println!("Found process: {} (PID: {})", process.name, process.pid);
* fn main() -> Result<(), Box<dyn std::error::Error>> {
* let process = process_get("unique-server-name")?;
* println!("Found process: {} (PID: {})", process.name, process.pid);
* Ok(())
* }
* ``` * ```
*/ */
pub fn process_get(pattern: &str) -> Result<ProcessInfo, ProcessError> { pub fn process_get(pattern: &str) -> Result<ProcessInfo, ProcessError> {
let processes = process_list(pattern)?; let processes = process_list(pattern)?;
match processes.len() { match processes.len() {
0 => Err(ProcessError::NoProcessFound(pattern.to_string())), 0 => Err(ProcessError::NoProcessFound(pattern.to_string())),
1 => Ok(processes[0].clone()), 1 => Ok(processes[0].clone()),
_ => Err(ProcessError::MultipleProcessesFound( _ => Err(ProcessError::MultipleProcessesFound(pattern.to_string(), processes.len())),
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. - **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 - **Connection Management**: Automatically handles connection creation and reconnection
- **Flexible Connectivity**: - **Flexible Connectivity**:
- Tries Unix socket connection first (`$HOME/hero/var/myredis.sock`) - Tries Unix socket connection first (`$HOME/hero/var/myredis.sock`)
- Falls back to TCP connection (localhost) if socket connection fails - 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) - **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 - **Error Handling**: Comprehensive error handling with detailed error messages
- **Thread Safety**: Safe to use in multi-threaded applications - **Thread Safety**: Safe to use in multi-threaded applications
@ -55,51 +52,9 @@ let result: redis::RedisResult<()> = client.execute(&mut cmd);
reset()?; 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 ## Environment Variables
- `REDISDB`: Specifies the Redis database number to use (default: 0) - `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 - `HOME`: Used to determine the path to the Redis Unix socket
## Connection Strategy ## Connection Strategy
@ -122,25 +77,6 @@ The module includes both unit tests and integration tests:
- Integration tests that require a real Redis server - Integration tests that require a real Redis server
- Tests automatically skip if Redis is not available - 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 ## Thread Safety
The Redis client is wrapped in an `Arc<Mutex<>>` to ensure thread safety when accessing the global instance. 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, Connection, RedisError, RedisResult, Cmd};
use redis::{Client, Cmd, Connection, RedisError, RedisResult};
use std::env; use std::env;
use std::path::Path; use std::path::Path;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, Once}; use std::sync::{Arc, Mutex, Once};
use std::sync::atomic::{AtomicBool, Ordering};
/// Redis connection configuration builder use lazy_static::lazy_static;
///
/// 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))
}
}
// Global Redis client instance using lazy_static // Global Redis client instance using lazy_static
lazy_static! { lazy_static! {
@ -173,7 +33,7 @@ impl RedisClientWrapper {
// Execute a command on the Redis connection // Execute a command on the Redis connection
pub fn execute<T: redis::FromRedisValue>(&self, cmd: &mut Cmd) -> RedisResult<T> { pub fn execute<T: redis::FromRedisValue>(&self, cmd: &mut Cmd) -> RedisResult<T> {
let mut conn_guard = self.connection.lock().unwrap(); 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 we don't have a connection or it's not working, create a new one
if conn_guard.is_none() || { if conn_guard.is_none() || {
if let Some(ref mut conn) = *conn_guard { if let Some(ref mut conn) = *conn_guard {
@ -195,25 +55,22 @@ impl RedisClientWrapper {
} }
let mut conn = self.client.get_connection()?; let mut conn = self.client.get_connection()?;
// Ping Redis to ensure it works // Ping Redis to ensure it works
let ping_result: String = redis::cmd("PING").query(&mut conn)?; let ping_result: String = redis::cmd("PING").query(&mut conn)?;
if ping_result != "PONG" { if ping_result != "PONG" {
return Err(RedisError::from(( return Err(RedisError::from((redis::ErrorKind::ResponseError, "Failed to ping Redis server")));
redis::ErrorKind::ResponseError,
"Failed to ping Redis server",
)));
} }
// Select the database // Select the database
redis::cmd("SELECT").arg(self.db).execute(&mut conn); redis::cmd("SELECT").arg(self.db).execute(&mut conn);
self.initialized.store(true, Ordering::Relaxed); self.initialized.store(true, Ordering::Relaxed);
// Store the connection // Store the connection
let mut conn_guard = self.connection.lock().unwrap(); let mut conn_guard = self.connection.lock().unwrap();
*conn_guard = Some(conn); *conn_guard = Some(conn);
Ok(()) Ok(())
} }
} }
@ -227,91 +84,65 @@ pub fn get_redis_client() -> RedisResult<Arc<RedisClientWrapper>> {
return Ok(Arc::clone(client)); return Ok(Arc::clone(client));
} }
} }
// Create a new client // Create a new client
let client = create_redis_client()?; let client = create_redis_client()?;
// Store the client globally // Store the client globally
{ {
let mut guard = REDIS_CLIENT.lock().unwrap(); let mut guard = REDIS_CLIENT.lock().unwrap();
*guard = Some(Arc::clone(&client)); *guard = Some(Arc::clone(&client));
} }
Ok(client) Ok(client)
} }
// Create a new Redis client // Create a new Redis client
fn create_redis_client() -> RedisResult<Arc<RedisClientWrapper>> { fn create_redis_client() -> RedisResult<Arc<RedisClientWrapper>> {
// Get Redis configuration from environment variables // First try: Connect via Unix socket
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
let home_dir = env::var("HOME").unwrap_or_else(|_| String::from("/root")); let home_dir = env::var("HOME").unwrap_or_else(|_| String::from("/root"));
let socket_path = format!("{}/hero/var/myredis.sock", home_dir); let socket_path = format!("{}/hero/var/myredis.sock", home_dir);
if Path::new(&socket_path).exists() { if Path::new(&socket_path).exists() {
// Try to connect via Unix socket // Try to connect via Unix socket
let socket_builder = builder.clone().socket_path(&socket_path); let socket_url = format!("unix://{}", socket_path);
match Client::open(socket_url) {
match socket_builder.build() { Ok(client) => {
Ok((client, db)) => { let db = get_redis_db();
let wrapper = Arc::new(RedisClientWrapper::new(client, db)); let wrapper = Arc::new(RedisClientWrapper::new(client, db));
// Initialize the client // Initialize the client
if let Err(err) = wrapper.initialize() { if let Err(err) = wrapper.initialize() {
eprintln!( eprintln!("Socket exists at {} but connection failed: {}", socket_path, err);
"Socket exists at {} but connection failed: {}",
socket_path, err
);
} else { } else {
return Ok(wrapper); return Ok(wrapper);
} }
} },
Err(err) => { Err(err) => {
eprintln!( eprintln!("Socket exists at {} but connection failed: {}", socket_path, err);
"Socket exists at {} but connection failed: {}",
socket_path, err
);
} }
} }
} }
// Second try: Connect via TCP // Second try: Connect via TCP to localhost
match builder.clone().build() { let tcp_url = "redis://127.0.0.1/";
Ok((client, db)) => { match Client::open(tcp_url) {
Ok(client) => {
let db = get_redis_db();
let wrapper = Arc::new(RedisClientWrapper::new(client, db)); let wrapper = Arc::new(RedisClientWrapper::new(client, db));
// Initialize the client // Initialize the client
wrapper.initialize()?; wrapper.initialize()?;
Ok(wrapper) 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(); let mut client_guard = REDIS_CLIENT.lock().unwrap();
*client_guard = None; *client_guard = None;
} }
// Create a new client, only return error if it fails // Create a new client, only return error if it fails
// We don't need to return the client itself // We don't need to return the client itself
get_redis_client()?; get_redis_client()?;
@ -344,18 +175,4 @@ where
{ {
let client = get_redis_client()?; let client = get_redis_client()?;
client.execute(cmd) 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 super::*;
use redis::RedisResult;
use std::env; use std::env;
use redis::RedisResult;
#[cfg(test)] #[cfg(test)]
mod redis_client_tests { mod redis_client_tests {
use super::*; use super::*;
#[test] #[test]
fn test_env_vars() { fn test_env_vars() {
// Save original REDISDB value to restore later // Save original REDISDB value to restore later
let original_redisdb = env::var("REDISDB").ok(); let original_redisdb = env::var("REDISDB").ok();
// Set test environment variables // Set test environment variables
env::set_var("REDISDB", "5"); env::set_var("REDISDB", "5");
// Test with invalid value // Test with invalid value
env::set_var("REDISDB", "invalid"); env::set_var("REDISDB", "invalid");
// Test with unset value // Test with unset value
env::remove_var("REDISDB"); env::remove_var("REDISDB");
// Restore original REDISDB value // Restore original REDISDB value
if let Some(redisdb) = original_redisdb { if let Some(redisdb) = original_redisdb {
env::set_var("REDISDB", redisdb); env::set_var("REDISDB", redisdb);
@ -27,21 +27,21 @@ mod redis_client_tests {
env::remove_var("REDISDB"); env::remove_var("REDISDB");
} }
} }
#[test] #[test]
fn test_redis_client_creation_mock() { fn test_redis_client_creation_mock() {
// This is a simplified test that doesn't require an actual Redis server // This is a simplified test that doesn't require an actual Redis server
// It just verifies that the function handles environment variables correctly // It just verifies that the function handles environment variables correctly
// Save original HOME value to restore later // Save original HOME value to restore later
let original_home = env::var("HOME").ok(); let original_home = env::var("HOME").ok();
// Set HOME to a test value // Set HOME to a test value
env::set_var("HOME", "/tmp"); env::set_var("HOME", "/tmp");
// The actual client creation would be tested in integration tests // The actual client creation would be tested in integration tests
// with a real Redis server or a mock // with a real Redis server or a mock
// Restore original HOME value // Restore original HOME value
if let Some(home) = original_home { if let Some(home) = original_home {
env::set_var("HOME", home); env::set_var("HOME", home);
@ -49,12 +49,12 @@ mod redis_client_tests {
env::remove_var("HOME"); env::remove_var("HOME");
} }
} }
#[test] #[test]
fn test_reset_mock() { fn test_reset_mock() {
// This is a simplified test that doesn't require an actual Redis server // 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 // In a real test, we would need to mock the Redis client
// Just verify that the reset function doesn't panic // Just verify that the reset function doesn't panic
// This is a minimal test - in a real scenario, we would use mocking // This is a minimal test - in a real scenario, we would use mocking
// to verify that the client is properly reset // to verify that the client is properly reset
@ -63,77 +63,6 @@ mod redis_client_tests {
// So we don't assert anything here // 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 // Integration tests that require a real Redis server
@ -141,7 +70,7 @@ mod redis_client_tests {
#[cfg(test)] #[cfg(test)]
mod redis_integration_tests { mod redis_integration_tests {
use super::*; use super::*;
// Helper function to check if Redis is available // Helper function to check if Redis is available
fn is_redis_available() -> bool { fn is_redis_available() -> bool {
match get_redis_client() { match get_redis_client() {
@ -149,200 +78,49 @@ mod redis_integration_tests {
Err(_) => false, Err(_) => false,
} }
} }
#[test] #[test]
fn test_redis_client_integration() { fn test_redis_client_integration() {
if !is_redis_available() { if !is_redis_available() {
println!("Skipping Redis integration tests - Redis server not available"); println!("Skipping Redis integration tests - Redis server not available");
return; return;
} }
println!("Running Redis integration tests..."); println!("Running Redis integration tests...");
// Test basic operations // Test basic operations
test_basic_redis_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() { fn test_basic_redis_operations() {
if !is_redis_available() { if !is_redis_available() {
return; return;
} }
// Test setting and getting values // Test setting and getting values
let client_result = get_redis_client(); let client_result = get_redis_client();
if client_result.is_err() { if client_result.is_err() {
// Skip the test if we can't connect to Redis // Skip the test if we can't connect to Redis
return; return;
} }
// Create SET command // Create SET command
let mut set_cmd = redis::cmd("SET"); let mut set_cmd = redis::cmd("SET");
set_cmd.arg("test_key").arg("test_value"); set_cmd.arg("test_key").arg("test_value");
// Execute SET command // Execute SET command
let set_result: RedisResult<()> = execute(&mut set_cmd); let set_result: RedisResult<()> = execute(&mut set_cmd);
assert!(set_result.is_ok()); assert!(set_result.is_ok());
// Create GET command // Create GET command
let mut get_cmd = redis::cmd("GET"); let mut get_cmd = redis::cmd("GET");
get_cmd.arg("test_key"); get_cmd.arg("test_key");
// Execute GET command and check the result // Execute GET command and check the result
if let Ok(value) = execute::<String>(&mut get_cmd) { if let Ok(value) = execute::<String>(&mut get_cmd) {
assert_eq!(value, "test_value"); 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")); 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. //! This module provides Rhai wrappers for the functions in the Git module.
use crate::git::{GitError, GitRepo, GitTree}; use rhai::{Engine, EvalAltResult, Array, Dynamic};
use rhai::{Array, Dynamic, Engine, EvalAltResult}; use crate::git::{GitTree, GitRepo, GitError};
/// Register Git module functions with the Rhai engine /// Register Git module functions with the Rhai engine
/// ///
@ -18,12 +18,12 @@ pub fn register_git_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>
// Register GitTree constructor // Register GitTree constructor
engine.register_type::<GitTree>(); engine.register_type::<GitTree>();
engine.register_fn("git_tree_new", git_tree_new); engine.register_fn("git_tree_new", git_tree_new);
// Register GitTree methods // Register GitTree methods
engine.register_fn("list", git_tree_list); engine.register_fn("list", git_tree_list);
engine.register_fn("find", git_tree_find); engine.register_fn("find", git_tree_find);
engine.register_fn("get", git_tree_get); engine.register_fn("get", git_tree_get);
// Register GitRepo methods // Register GitRepo methods
engine.register_type::<GitRepo>(); engine.register_type::<GitRepo>();
engine.register_fn("path", git_repo_path); engine.register_fn("path", git_repo_path);
@ -32,10 +32,7 @@ pub fn register_git_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>
engine.register_fn("reset", git_repo_reset); engine.register_fn("reset", git_repo_reset);
engine.register_fn("commit", git_repo_commit); engine.register_fn("commit", git_repo_commit);
engine.register_fn("push", git_repo_push); engine.register_fn("push", git_repo_push);
// Register git_clone function for testing
engine.register_fn("git_clone", git_clone);
Ok(()) Ok(())
} }
@ -44,7 +41,7 @@ fn git_error_to_rhai_error<T>(result: Result<T, GitError>) -> Result<T, Box<Eval
result.map_err(|e| { result.map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime( Box::new(EvalAltResult::ErrorRuntime(
format!("Git error: {}", e).into(), format!("Git error: {}", e).into(),
rhai::Position::NONE, rhai::Position::NONE
)) ))
}) })
} }
@ -65,13 +62,13 @@ pub fn git_tree_new(base_path: &str) -> Result<GitTree, Box<EvalAltResult>> {
/// Lists all git repositories under the base path. /// Lists all git repositories under the base path.
pub fn git_tree_list(git_tree: &mut GitTree) -> Result<Array, Box<EvalAltResult>> { pub fn git_tree_list(git_tree: &mut GitTree) -> Result<Array, Box<EvalAltResult>> {
let repos = git_error_to_rhai_error(git_tree.list())?; let repos = git_error_to_rhai_error(git_tree.list())?;
// Convert Vec<String> to Rhai Array // Convert Vec<String> to Rhai Array
let mut array = Array::new(); let mut array = Array::new();
for repo in repos { for repo in repos {
array.push(Dynamic::from(repo)); array.push(Dynamic::from(repo));
} }
Ok(array) Ok(array)
} }
@ -81,13 +78,13 @@ pub fn git_tree_list(git_tree: &mut GitTree) -> Result<Array, Box<EvalAltResult>
/// Assumes the underlying GitTree::find Rust method now returns Result<Vec<GitRepo>, GitError>. /// Assumes the underlying GitTree::find Rust method now returns Result<Vec<GitRepo>, GitError>.
pub fn git_tree_find(git_tree: &mut GitTree, pattern: &str) -> Result<Array, Box<EvalAltResult>> { 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))?; let repos: Vec<GitRepo> = git_error_to_rhai_error(git_tree.find(pattern))?;
// Convert Vec<GitRepo> to Rhai Array // Convert Vec<GitRepo> to Rhai Array
let mut array = Array::new(); let mut array = Array::new();
for repo in repos { for repo in repos {
array.push(Dynamic::from(repo)); array.push(Dynamic::from(repo));
} }
Ok(array) Ok(array)
} }
@ -98,10 +95,7 @@ pub fn git_tree_find(git_tree: &mut GitTree, pattern: &str) -> Result<Array, Box
/// This wrapper ensures that for Rhai, 'get' returns a single GitRepo or an error /// 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), /// if zero or multiple repositories are found (for local names/patterns),
/// or if a URL operation fails or unexpectedly yields not exactly one result. /// or if a URL operation fails or unexpectedly yields not exactly one result.
pub fn git_tree_get( pub fn git_tree_get(git_tree: &mut GitTree, name_or_url: &str) -> Result<GitRepo, Box<EvalAltResult>> {
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))?; let mut repos_vec: Vec<GitRepo> = git_error_to_rhai_error(git_tree.get(name_or_url))?;
match repos_vec.len() { match repos_vec.len() {
@ -157,10 +151,7 @@ pub fn git_repo_reset(git_repo: &mut GitRepo) -> Result<GitRepo, Box<EvalAltResu
/// Wrapper for GitRepo::commit /// Wrapper for GitRepo::commit
/// ///
/// Commits changes in the repository. /// Commits changes in the repository.
pub fn git_repo_commit( pub fn git_repo_commit(git_repo: &mut GitRepo, message: &str) -> Result<GitRepo, Box<EvalAltResult>> {
git_repo: &mut GitRepo,
message: &str,
) -> Result<GitRepo, Box<EvalAltResult>> {
git_error_to_rhai_error(git_repo.commit(message)) git_error_to_rhai_error(git_repo.commit(message))
} }
@ -169,15 +160,4 @@ pub fn git_repo_commit(
/// Pushes changes to the remote repository. /// Pushes changes to the remote repository.
pub fn git_repo_push(git_repo: &mut GitRepo) -> Result<GitRepo, Box<EvalAltResult>> { pub fn git_repo_push(git_repo: &mut GitRepo) -> Result<GitRepo, Box<EvalAltResult>> {
git_error_to_rhai_error(git_repo.push()) 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,75 @@
//! This module provides integration with the Rhai scripting language, //! This module provides integration with the Rhai scripting language,
//! allowing SAL functions to be called from Rhai scripts. //! allowing SAL functions to be called from Rhai scripts.
mod buildah;
mod error; mod error;
mod git;
mod nerdctl;
mod os; mod os;
mod postgresclient;
mod process; mod process;
mod redisclient; mod buildah;
mod rfs; mod nerdctl;
mod git;
mod text; mod text;
mod rfs;
mod zinit;
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;
// Re-export common Rhai types for convenience // 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 // Re-export error module
pub use error::*; pub use error::*;
// Re-export specific functions from modules to avoid name conflicts // Re-export specific functions from modules to avoid name conflicts
pub use os::{ 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, 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::{ pub use process::{
kill,
process_get,
process_list,
register_process_module, register_process_module,
// Run functions // Run functions
// Process management functions // Process management functions
which, which, kill, process_list, process_get
}; };
// Re-export buildah functions // Re-export buildah functions
pub use buildah::bah_new;
pub use buildah::register_bah_module; pub use buildah::register_bah_module;
pub use buildah::bah_new;
// Re-export nerdctl functions // Re-export nerdctl functions
pub use nerdctl::register_nerdctl_module; pub use nerdctl::register_nerdctl_module;
pub use nerdctl::{ 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 // Container functions
nerdctl_run, nerdctl_run, nerdctl_run_with_name, nerdctl_run_with_port,
nerdctl_run_with_name, nerdctl_exec, nerdctl_copy, nerdctl_stop, nerdctl_remove, nerdctl_list,
nerdctl_run_with_port, // Image functions
nerdctl_stop, nerdctl_images, nerdctl_image_remove, nerdctl_image_push, nerdctl_image_tag,
nerdctl_image_pull, nerdctl_image_commit, nerdctl_image_build
}; };
// Re-export RFS module // Re-export RFS module
pub use rfs::register as register_rfs_module; pub use rfs::register as register_rfs_module;
// Re-export git module // Re-export git module
pub use crate::git::{GitRepo, GitTree};
pub use git::register_git_module; pub use git::register_git_module;
pub use crate::git::{GitTree, GitRepo};
// Re-export zinit module
pub use zinit::register_zinit_module;
// Re-export text module // Re-export text module
pub use text::register_text_module; pub use text::register_text_module;
// Re-export text functions directly from text module // Re-export text functions directly from text module
pub use crate::text::{ pub use crate::text::{
// Dedent functions
dedent,
// Fix functions // Fix functions
name_fix, name_fix, path_fix,
path_fix, // Dedent functions
prefix, dedent, prefix
}; };
// Re-export TextReplacer functions // Re-export TextReplacer functions
@ -116,7 +88,7 @@ pub use os::copy as os_copy;
/// ///
/// # Example /// # Example
/// ///
/// ```ignore /// ```
/// use rhai::Engine; /// use rhai::Engine;
/// use sal::rhai; /// use sal::rhai;
/// ///
@ -124,45 +96,35 @@ pub use os::copy as os_copy;
/// rhai::register(&mut engine); /// rhai::register(&mut engine);
/// ///
/// // Now you can use SAL functions in Rhai scripts /// // Now you can use SAL functions in Rhai scripts
/// // You can evaluate Rhai scripts with SAL functions /// let result = engine.eval::<bool>("exist('some_file.txt')").unwrap();
/// let result = engine.eval::<i64>("exist('some_file.txt')").unwrap();
/// ``` /// ```
pub fn register(engine: &mut Engine) -> Result<(), Box<rhai::EvalAltResult>> { pub fn register(engine: &mut Engine) -> Result<(), Box<rhai::EvalAltResult>> {
// Register OS module functions // Register OS module functions
os::register_os_module(engine)?; os::register_os_module(engine)?;
// Register Process module functions // Register Process module functions
process::register_process_module(engine)?; process::register_process_module(engine)?;
// Register Buildah module functions // Register Buildah module functions
buildah::register_bah_module(engine)?; buildah::register_bah_module(engine)?;
// Register Nerdctl module functions // Register Nerdctl module functions
nerdctl::register_nerdctl_module(engine)?; nerdctl::register_nerdctl_module(engine)?;
// Register Git module functions // Register Git module functions
git::register_git_module(engine)?; git::register_git_module(engine)?;
// Register Zinit module functions
zinit::register_zinit_module(engine)?;
// Register Text module functions // Register Text module functions
text::register_text_module(engine)?; text::register_text_module(engine)?;
// Register RFS module functions // Register RFS module functions
rfs::register(engine)?; 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 // Future modules can be registered here
Ok(()) 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,8 +2,8 @@
//! //!
//! This module provides Rhai wrappers for the functions in the Process module. //! This module provides Rhai wrappers for the functions in the Process module.
use crate::process::{self, CommandResult, ProcessError, ProcessInfo, RunError}; use rhai::{Engine, EvalAltResult, Array, Dynamic};
use rhai::{Array, Dynamic, Engine, EvalAltResult, Map}; use crate::process::{self, CommandResult, ProcessInfo, RunError, ProcessError};
use std::clone::Clone; use std::clone::Clone;
/// Register Process module functions with the Rhai engine /// Register Process module functions with the Rhai engine
@ -47,11 +47,6 @@ pub fn register_process_module(engine: &mut Engine) -> Result<(), Box<EvalAltRes
engine.register_fn("process_list", process_list); engine.register_fn("process_list", process_list);
engine.register_fn("process_get", process_get); engine.register_fn("process_get", process_get);
// 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);
Ok(()) Ok(())
} }
@ -60,7 +55,7 @@ fn run_error_to_rhai_error<T>(result: Result<T, RunError>) -> Result<T, Box<Eval
result.map_err(|e| { result.map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime( Box::new(EvalAltResult::ErrorRuntime(
format!("Run error: {}", e).into(), format!("Run error: {}", e).into(),
rhai::Position::NONE, rhai::Position::NONE
)) ))
}) })
} }
@ -115,13 +110,11 @@ impl RhaiCommandBuilder {
} }
} }
fn process_error_to_rhai_error<T>( fn process_error_to_rhai_error<T>(result: Result<T, ProcessError>) -> Result<T, Box<EvalAltResult>> {
result: Result<T, ProcessError>,
) -> Result<T, Box<EvalAltResult>> {
result.map_err(|e| { result.map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime( Box::new(EvalAltResult::ErrorRuntime(
format!("Process error: {}", e).into(), format!("Process error: {}", e).into(),
rhai::Position::NONE, rhai::Position::NONE
)) ))
}) })
} }
@ -136,7 +129,7 @@ fn process_error_to_rhai_error<T>(
pub fn which(cmd: &str) -> Dynamic { pub fn which(cmd: &str) -> Dynamic {
match process::which(cmd) { match process::which(cmd) {
Some(path) => path.into(), Some(path) => path.into(),
None => Dynamic::UNIT, None => Dynamic::UNIT
} }
} }
@ -152,13 +145,13 @@ pub fn kill(pattern: &str) -> Result<String, Box<EvalAltResult>> {
/// List processes matching a pattern (or all if pattern is empty). /// List processes matching a pattern (or all if pattern is empty).
pub fn process_list(pattern: &str) -> Result<Array, Box<EvalAltResult>> { pub fn process_list(pattern: &str) -> Result<Array, Box<EvalAltResult>> {
let processes = process_error_to_rhai_error(process::process_list(pattern))?; let processes = process_error_to_rhai_error(process::process_list(pattern))?;
// Convert Vec<ProcessInfo> to Rhai Array // Convert Vec<ProcessInfo> to Rhai Array
let mut array = Array::new(); let mut array = Array::new();
for process in processes { for process in processes {
array.push(Dynamic::from(process)); array.push(Dynamic::from(process));
} }
Ok(array) Ok(array)
} }
@ -167,46 +160,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). /// 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>> { pub fn process_get(pattern: &str) -> Result<ProcessInfo, Box<EvalAltResult>> {
process_error_to_rhai_error(process::process_get(pattern)) 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)] #[cfg(test)]
mod tests { mod tests {
use super::super::register;
use rhai::Engine; use rhai::Engine;
use super::super::register;
use std::fs; use std::fs;
use std::path::Path; use std::path::Path;
#[test] #[test]
fn test_register() { fn test_register() {
let mut engine = Engine::new(); let mut engine = Engine::new();
assert!(register(&mut engine).is_ok()); assert!(register(&mut engine).is_ok());
} }
// OS Module Tests // OS Module Tests
#[test] #[test]
fn test_exist_function() { fn test_exist_function() {
let mut engine = Engine::new(); let mut engine = Engine::new();
register(&mut engine).unwrap(); register(&mut engine).unwrap();
// Test with a file that definitely exists // Test with a file that definitely exists
let result = engine.eval::<bool>(r#"exist("Cargo.toml")"#).unwrap(); let result = engine.eval::<bool>(r#"exist("Cargo.toml")"#).unwrap();
assert!(result); assert!(result);
// Test with a file that definitely doesn't exist // Test with a file that definitely doesn't exist
let result = engine let result = engine.eval::<bool>(r#"exist("non_existent_file.xyz")"#).unwrap();
.eval::<bool>(r#"exist("non_existent_file.xyz")"#)
.unwrap();
assert!(!result); assert!(!result);
} }
#[test] #[test]
fn test_mkdir_and_delete() { fn test_mkdir_and_delete() {
let mut engine = Engine::new(); let mut engine = Engine::new();
register(&mut engine).unwrap(); register(&mut engine).unwrap();
let test_dir = "test_rhai_dir"; let test_dir = "test_rhai_dir";
// Clean up from previous test runs if necessary // Clean up from previous test runs if necessary
if Path::new(test_dir).exists() { if Path::new(test_dir).exists() {
fs::remove_dir_all(test_dir).unwrap(); fs::remove_dir_all(test_dir).unwrap();
} }
// Create directory using Rhai // Create directory using Rhai
let script = format!(r#"mkdir("{}")"#, test_dir); let script = format!(r#"mkdir("{}")"#, test_dir);
let result = engine.eval::<String>(&script).unwrap(); let result = engine.eval::<String>(&script).unwrap();
assert!(result.contains("Successfully created directory")); assert!(result.contains("Successfully created directory"));
assert!(Path::new(test_dir).exists()); assert!(Path::new(test_dir).exists());
// Delete directory using Rhai // Delete directory using Rhai
let script = format!(r#"delete("{}")"#, test_dir); let script = format!(r#"delete("{}")"#, test_dir);
let result = engine.eval::<String>(&script).unwrap(); let result = engine.eval::<String>(&script).unwrap();
assert!(result.contains("Successfully deleted directory")); assert!(result.contains("Successfully deleted directory"));
assert!(!Path::new(test_dir).exists()); assert!(!Path::new(test_dir).exists());
} }
#[test] #[test]
fn test_file_size() { fn test_file_size() {
let mut engine = Engine::new(); let mut engine = Engine::new();
register(&mut engine).unwrap(); register(&mut engine).unwrap();
// Create a test file // Create a test file
let test_file = "test_rhai_file.txt"; let test_file = "test_rhai_file.txt";
let test_content = "Hello, Rhai!"; let test_content = "Hello, Rhai!";
fs::write(test_file, test_content).unwrap(); fs::write(test_file, test_content).unwrap();
// Get file size using Rhai // Get file size using Rhai
let script = format!(r#"file_size("{}")"#, test_file); let script = format!(r#"file_size("{}")"#, test_file);
let size = engine.eval::<i64>(&script).unwrap(); let size = engine.eval::<i64>(&script).unwrap();
assert_eq!(size, test_content.len() as i64); assert_eq!(size, test_content.len() as i64);
// Clean up // Clean up
fs::remove_file(test_file).unwrap(); fs::remove_file(test_file).unwrap();
} }
#[test] #[test]
fn test_error_handling() { fn test_error_handling() {
let mut engine = Engine::new(); let mut engine = Engine::new();
register(&mut engine).unwrap(); register(&mut engine).unwrap();
// Try to get the size of a non-existent file // Try to get the size of a non-existent file
let result = engine.eval::<i64>(r#"file_size("non_existent_file.xyz")"#); let result = engine.eval::<i64>(r#"file_size("non_existent_file.xyz")"#);
assert!(result.is_err()); assert!(result.is_err());
let err = result.unwrap_err(); let err = result.unwrap_err();
let err_str = err.to_string(); let err_str = err.to_string();
println!("Error string: {}", err_str); println!("Error string: {}", err_str);
// The actual error message is "No files found matching..." // The actual error message is "No files found matching..."
assert!( assert!(err_str.contains("No files found matching") ||
err_str.contains("No files found matching") err_str.contains("File not found") ||
|| err_str.contains("File not found") err_str.contains("File system error"));
|| err_str.contains("File system error")
);
} }
// Process Module Tests // Process Module Tests
#[test] #[test]
fn test_which_function() { fn test_which_function() {
let mut engine = Engine::new(); let mut engine = Engine::new();
register(&mut engine).unwrap(); register(&mut engine).unwrap();
// Test with a command that definitely exists (like "ls" on Unix or "cmd" on Windows) // Test with a command that definitely exists (like "ls" on Unix or "cmd" on Windows)
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
let cmd = "cmd"; let cmd = "cmd";
#[cfg(any(target_os = "macos", target_os = "linux"))] #[cfg(any(target_os = "macos", target_os = "linux"))]
let cmd = "ls"; let cmd = "ls";
let script = format!(r#"which("{}")"#, cmd); let script = format!(r#"which("{}")"#, cmd);
let result = engine.eval::<String>(&script).unwrap(); let result = engine.eval::<String>(&script).unwrap();
assert!(!result.is_empty()); assert!(!result.is_empty());
// Test with a command that definitely doesn't exist // Test with a command that definitely doesn't exist
let script = r#"which("non_existent_command_xyz123")"#; let script = r#"which("non_existent_command_xyz123")"#;
let result = engine.eval::<()>(&script).unwrap(); let result = engine.eval::<()>(&script).unwrap();
assert_eq!(result, ()); assert_eq!(result, ());
} }
#[test] #[test]
fn test_run_with_options() { fn test_run_with_options() {
let mut engine = Engine::new(); let mut engine = Engine::new();
register(&mut engine).unwrap(); register(&mut engine).unwrap();
// Test running a command with custom options // Test running a command with custom options
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
let script = r#" let script = r#"
@ -136,7 +132,7 @@ mod tests {
let result = run("echo Hello World", options); let result = run("echo Hello World", options);
result.success && result.stdout.contains("Hello World") result.success && result.stdout.contains("Hello World")
"#; "#;
#[cfg(any(target_os = "macos", target_os = "linux"))] #[cfg(any(target_os = "macos", target_os = "linux"))]
let script = r#" let script = r#"
let options = new_run_options(); let options = new_run_options();
@ -146,7 +142,7 @@ mod tests {
let result = run("echo 'Hello World'", options); let result = run("echo 'Hello World'", options);
result.success && result.stdout.contains("Hello World") result.success && result.stdout.contains("Hello World")
"#; "#;
let result = engine.eval::<bool>(script).unwrap(); let result = engine.eval::<bool>(script).unwrap();
assert!(result); assert!(result);
} }
@ -155,101 +151,92 @@ mod tests {
fn test_run_command() { fn test_run_command() {
let mut engine = Engine::new(); let mut engine = Engine::new();
register(&mut engine).unwrap(); register(&mut engine).unwrap();
// Test a simple echo command // Test a simple echo command
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
let script = r#" let script = r#"
let result = run_command("echo Hello World"); let result = run_command("echo Hello World");
result.success && result.stdout.contains("Hello World") result.success && result.stdout.contains("Hello World")
"#; "#;
#[cfg(any(target_os = "macos", target_os = "linux"))] #[cfg(any(target_os = "macos", target_os = "linux"))]
let script = r#" let script = r#"
let result = run_command("echo 'Hello World'"); let result = run_command("echo 'Hello World'");
result.success && result.stdout.contains("Hello World") result.success && result.stdout.contains("Hello World")
"#; "#;
let result = engine.eval::<bool>(script).unwrap(); let result = engine.eval::<bool>(script).unwrap();
assert!(result); assert!(result);
} }
#[test] #[test]
fn test_run_silent() { fn test_run_silent() {
let mut engine = Engine::new(); let mut engine = Engine::new();
register(&mut engine).unwrap(); register(&mut engine).unwrap();
// Test a simple echo command with silent execution // Test a simple echo command with silent execution
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
let script = r#" let script = r#"
let result = run_silent("echo Hello World"); let result = run_silent("echo Hello World");
result.success && result.stdout.contains("Hello World") result.success && result.stdout.contains("Hello World")
"#; "#;
#[cfg(any(target_os = "macos", target_os = "linux"))] #[cfg(any(target_os = "macos", target_os = "linux"))]
let script = r#" let script = r#"
let result = run_silent("echo 'Hello World'"); let result = run_silent("echo 'Hello World'");
result.success && result.stdout.contains("Hello World") result.success && result.stdout.contains("Hello World")
"#; "#;
let result = engine.eval::<bool>(script).unwrap(); let result = engine.eval::<bool>(script).unwrap();
assert!(result); assert!(result);
} }
#[test] #[test]
fn test_process_list() { fn test_process_list() {
let mut engine = Engine::new(); let mut engine = Engine::new();
register(&mut engine).unwrap(); register(&mut engine).unwrap();
// Test listing processes (should return a non-empty array) // Test listing processes (should return a non-empty array)
let script = r#" let script = r#"
let processes = process_list(""); let processes = process_list("");
processes.len() > 0 processes.len() > 0
"#; "#;
let result = engine.eval::<bool>(script).unwrap(); let result = engine.eval::<bool>(script).unwrap();
assert!(result); assert!(result);
} }
// Git Module Tests // Git Module Tests
#[test] #[test]
fn test_git_module_registration() { fn test_git_module_registration() {
let mut engine = Engine::new(); let mut engine = Engine::new();
register(&mut engine).unwrap(); register(&mut engine).unwrap();
// Test that git functions are registered by trying to use them // Test that git functions are registered
let script = r#" let script = r#"
// Try to use git_clone function // Check if git_clone function exists
let result = true; let fn_exists = is_def_fn("git_clone");
fn_exists
try {
// This should fail but not crash
git_clone("test-url");
} catch(err) {
// Expected error
result = err.contains("Git error");
}
result
"#; "#;
let result = engine.eval::<bool>(script).unwrap(); let result = engine.eval::<bool>(script).unwrap();
assert!(result); assert!(result);
} }
#[test] #[test]
fn test_git_parse_url() { fn test_git_parse_url() {
let mut engine = Engine::new(); let mut engine = Engine::new();
register(&mut engine).unwrap(); register(&mut engine).unwrap();
// Test parsing a git URL // Test parsing a git URL
let script = r#" let script = r#"
// We can't directly test git_clone without actually cloning, // We can't directly test git_clone without actually cloning,
// but we can test that the function exists and doesn't error // but we can test that the function exists and doesn't error
// when called with invalid parameters // when called with invalid parameters
let result = false; let result = false;
try { try {
// This should fail but not crash // This should fail but not crash
git_clone("invalid-url"); git_clone("invalid-url");
@ -257,11 +244,11 @@ mod tests {
// Expected error // Expected error
result = err.contains("Git error"); result = err.contains("Git error");
} }
result result
"#; "#;
let result = engine.eval::<bool>(script).unwrap(); let result = engine.eval::<bool>(script).unwrap();
assert!(result); assert!(result);
} }
} }

345
src/rhai/zinit.rs Normal file
View File

@ -0,0 +1,345 @@
//! Rhai wrappers for Zinit client module functions
//!
//! This module provides Rhai wrappers for the functions in the Zinit client module.
use rhai::{Engine, EvalAltResult, Array, Dynamic, Map};
use crate::zinit_client as client;
use tokio::runtime::Runtime;
use serde_json::{json, Value};
use crate::rhai::error::ToRhaiError;
/// Register Zinit 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_zinit_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
// Register Zinit client functions
engine.register_fn("zinit_list", zinit_list);
engine.register_fn("zinit_status", zinit_status);
engine.register_fn("zinit_start", zinit_start);
engine.register_fn("zinit_stop", zinit_stop);
engine.register_fn("zinit_restart", zinit_restart);
engine.register_fn("zinit_monitor", zinit_monitor);
engine.register_fn("zinit_forget", zinit_forget);
engine.register_fn("zinit_kill", zinit_kill);
engine.register_fn("zinit_create_service", zinit_create_service);
engine.register_fn("zinit_delete_service", zinit_delete_service);
engine.register_fn("zinit_get_service", zinit_get_service);
engine.register_fn("zinit_logs", zinit_logs);
engine.register_fn("zinit_logs_all", zinit_logs_all);
Ok(())
}
impl<T> ToRhaiError<T> for Result<T, zinit_client::ClientError> {
fn to_rhai_error(self) -> Result<T, Box<EvalAltResult>> {
self.map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime(
format!("Zinit error: {}", e).into(),
rhai::Position::NONE
))
})
}
}
// Helper function to get a runtime
fn get_runtime() -> Result<Runtime, Box<EvalAltResult>> {
tokio::runtime::Runtime::new().map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime(
format!("Failed to create Tokio runtime: {}", e).into(),
rhai::Position::NONE
))
})
}
//
// Zinit Client Function Wrappers
//
/// Wrapper for zinit_client::list
///
/// Lists all services managed by Zinit.
pub fn zinit_list(socket_path: &str) -> Result<Map, Box<EvalAltResult>> {
let rt = get_runtime()?;
let result = rt.block_on(async {
client::list(socket_path).await
});
let services = result.to_rhai_error()?;
// Convert HashMap<String, String> to Rhai Map
let mut map = Map::new();
for (name, state) in services {
map.insert(name.into(), Dynamic::from(state));
}
Ok(map)
}
/// Wrapper for zinit_client::status
///
/// Gets the status of a specific service.
pub fn zinit_status(socket_path: &str, name: &str) -> Result<Map, Box<EvalAltResult>> {
let rt = get_runtime()?;
let result = rt.block_on(async {
client::status(socket_path, name).await
});
let status = result.to_rhai_error()?;
// Convert Status to Rhai Map
let mut map = Map::new();
map.insert("name".into(), Dynamic::from(status.name));
map.insert("pid".into(), Dynamic::from(status.pid));
map.insert("state".into(), Dynamic::from(status.state));
map.insert("target".into(), Dynamic::from(status.target));
// Convert dependencies
let mut deps_map = Map::new();
for (dep, state) in status.after {
deps_map.insert(dep.into(), Dynamic::from(state));
}
map.insert("after".into(), Dynamic::from_map(deps_map));
Ok(map)
}
/// Wrapper for zinit_client::start
///
/// Starts a service.
pub fn zinit_start(socket_path: &str, name: &str) -> Result<bool, Box<EvalAltResult>> {
let rt = get_runtime()?;
let result = rt.block_on(async {
client::start(socket_path, name).await
});
result.to_rhai_error()?;
Ok(true)
}
/// Wrapper for zinit_client::stop
///
/// Stops a service.
pub fn zinit_stop(socket_path: &str, name: &str) -> Result<bool, Box<EvalAltResult>> {
let rt = get_runtime()?;
let result = rt.block_on(async {
client::stop(socket_path, name).await
});
result.to_rhai_error()?;
Ok(true)
}
/// Wrapper for zinit_client::restart
///
/// Restarts a service.
pub fn zinit_restart(socket_path: &str, name: &str) -> Result<bool, Box<EvalAltResult>> {
let rt = get_runtime()?;
let result = rt.block_on(async {
client::restart(socket_path, name).await
});
result.to_rhai_error()?;
Ok(true)
}
/// Wrapper for zinit_client::monitor
///
/// Starts monitoring a service.
pub fn zinit_monitor(socket_path: &str, name: &str) -> Result<bool, Box<EvalAltResult>> {
let rt = get_runtime()?;
let result = rt.block_on(async {
let client = client::get_zinit_client(socket_path).await?;
client.monitor(name).await
});
result.to_rhai_error()?;
Ok(true)
}
/// Wrapper for zinit_client::forget
///
/// Stops monitoring a service.
pub fn zinit_forget(socket_path: &str, name: &str) -> Result<bool, Box<EvalAltResult>> {
let rt = get_runtime()?;
let result = rt.block_on(async {
let client = client::get_zinit_client(socket_path).await?;
client.forget(name).await
});
result.to_rhai_error()?;
Ok(true)
}
/// Wrapper for zinit_client::kill
///
/// Sends a signal to a service.
pub fn zinit_kill(socket_path: &str, name: &str, signal: &str) -> Result<bool, Box<EvalAltResult>> {
let rt = get_runtime()?;
let result = rt.block_on(async {
let client = client::get_zinit_client(socket_path).await?;
client.kill(name, signal).await
});
result.to_rhai_error()?;
Ok(true)
}
/// Wrapper for zinit_client::create_service
///
/// Creates a new service.
pub fn zinit_create_service(socket_path: &str, name: &str, exec: &str, oneshot: bool) -> Result<String, Box<EvalAltResult>> {
let rt = get_runtime()?;
// Create service configuration
let content = serde_json::from_value(json!({
"exec": exec,
"oneshot": oneshot
})).map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime(
format!("Failed to create service configuration: {}", e).into(),
rhai::Position::NONE
))
})?;
let result = rt.block_on(async {
let client = client::get_zinit_client(socket_path).await?;
client.create_service(name, content).await
});
result.to_rhai_error()
}
/// Wrapper for zinit_client::delete_service
///
/// Deletes a service.
pub fn zinit_delete_service(socket_path: &str, name: &str) -> Result<String, Box<EvalAltResult>> {
let rt = get_runtime()?;
let result = rt.block_on(async {
let client = client::get_zinit_client(socket_path).await?;
client.delete_service(name).await
});
result.to_rhai_error()
}
/// Wrapper for zinit_client::get_service
///
/// Gets a service configuration.
pub fn zinit_get_service(socket_path: &str, name: &str) -> Result<Dynamic, Box<EvalAltResult>> {
let rt = get_runtime()?;
let result = rt.block_on(async {
let client = client::get_zinit_client(socket_path).await?;
client.get_service(name).await
});
let value = result.to_rhai_error()?;
// Convert Value to Dynamic
match value {
Value::Object(map) => {
let mut rhai_map = Map::new();
for (k, v) in map {
rhai_map.insert(k.into(), value_to_dynamic(v));
}
Ok(Dynamic::from_map(rhai_map))
},
_ => Err(Box::new(EvalAltResult::ErrorRuntime(
"Expected object from get_service".into(),
rhai::Position::NONE
)))
}
}
/// Wrapper for zinit_client::logs with a filter
///
/// Gets logs for a specific service.
pub fn zinit_logs(socket_path: &str, filter: &str) -> Result<Array, Box<EvalAltResult>> {
let rt = get_runtime()?;
let filter_string = Some(filter.to_string());
let result = rt.block_on(async {
let client = client::get_zinit_client(socket_path).await?;
client.logs(filter_string).await
});
let logs = result.to_rhai_error()?;
// Convert Vec<String> to Rhai Array
let mut array = Array::new();
for log in logs {
array.push(Dynamic::from(log));
}
Ok(array)
}
/// Wrapper for zinit_client::logs without a filter
///
/// Gets all logs.
pub fn zinit_logs_all(socket_path: &str) -> Result<Array, Box<EvalAltResult>> {
let rt = get_runtime()?;
let result = rt.block_on(async {
let client = client::get_zinit_client(socket_path).await?;
client.logs(None).await
});
let logs = result.to_rhai_error()?;
// Convert Vec<String> to Rhai Array
let mut array = Array::new();
for log in logs {
array.push(Dynamic::from(log));
}
Ok(array)
}
// Helper function to convert serde_json::Value to rhai::Dynamic
fn value_to_dynamic(value: Value) -> Dynamic {
match value {
Value::Null => Dynamic::UNIT,
Value::Bool(b) => Dynamic::from(b),
Value::Number(n) => {
if let Some(i) = n.as_i64() {
Dynamic::from(i)
} else if let Some(f) = n.as_f64() {
Dynamic::from(f)
} else {
Dynamic::from(n.to_string())
}
},
Value::String(s) => Dynamic::from(s),
Value::Array(arr) => {
let mut rhai_arr = Array::new();
for item in arr {
rhai_arr.push(value_to_dynamic(item));
}
Dynamic::from(rhai_arr)
},
Value::Object(map) => {
let mut rhai_map = Map::new();
for (k, v) in map {
rhai_map.insert(k.into(), value_to_dynamic(v));
}
Dynamic::from_map(rhai_map)
}
}
}

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;

View File

@ -1,152 +0,0 @@
// 01_mount_operations.rhai
// Tests for RFS mount 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 rfs is available
fn is_rfs_available() {
try {
let result = run("which rfs");
return result.success;
} catch(err) {
return false;
}
}
// Helper function to clean up mounts
fn cleanup_mounts() {
try {
rfs_unmount_all();
print("All mounts cleaned up");
} catch(err) {
print(`Error cleaning up mounts: ${err}`);
}
}
print("=== Testing RFS Mount Operations ===");
// Check if rfs is available
let rfs_available = is_rfs_available();
if !rfs_available {
print("rfs is not available. Skipping RFS tests.");
// Exit gracefully without error
return;
}
print("✓ rfs is available");
// Clean up any existing mounts
cleanup_mounts();
// Create test directories
let source_dir = "rhai_test_rfs_source";
let target_dir = "rhai_test_rfs_target";
mkdir(source_dir);
mkdir(target_dir);
// Create a test file in the source directory
let test_file = `${source_dir}/test.txt`;
file_write(test_file, "Hello from RFS test");
try {
// Test mounting a local directory
print("Testing rfs_mount() with local directory...");
let options = #{
"readonly": "true"
};
let mount = rfs_mount(source_dir, target_dir, "local", options);
// Verify mount properties
assert_true(mount.id != "", "Mount ID should not be empty");
assert_eq(mount.source, source_dir, "Mount source should match");
assert_eq(mount.target, target_dir, "Mount target should match");
assert_eq(mount.fs_type, "local", "Mount type should be local");
print(`✓ rfs_mount(): Mounted ${mount.source} to ${mount.target} with ID: ${mount.id}`);
// Test listing mounts
print("Testing rfs_list_mounts()...");
let mounts = rfs_list_mounts();
assert_true(mounts.len() > 0, "There should be at least one mount");
// Find our mount in the list
let found = false;
for m in mounts {
if m.target == target_dir {
found = true;
break;
}
}
assert_true(found, "Our mount should be in the list");
print(`✓ rfs_list_mounts(): Found ${mounts.len()} mounts`);
// Test getting mount info
print("Testing rfs_get_mount_info()...");
let mount_info = rfs_get_mount_info(target_dir);
assert_eq(mount_info.target, target_dir, "Mount info target should match");
assert_eq(mount_info.source, source_dir, "Mount info source should match");
print(`✓ rfs_get_mount_info(): Got info for mount at ${mount_info.target}`);
// Verify the mounted file is accessible
let mounted_file = `${target_dir}/test.txt`;
assert_true(exist(mounted_file), "Mounted file should exist");
let mounted_content = file_read(mounted_file);
assert_eq(mounted_content, "Hello from RFS test", "Mounted file content should match");
print("✓ Mounted file is accessible and content matches");
// Test unmounting a specific mount
print("Testing rfs_unmount()...");
rfs_unmount(target_dir);
// Verify the mount is gone
try {
rfs_get_mount_info(target_dir);
assert_true(false, "Mount should not exist after unmounting");
} catch(err) {
print("✓ rfs_unmount(): Mount successfully unmounted");
}
// Mount again to test unmount_all
print("Testing mounting again for unmount_all...");
let mount2 = rfs_mount(source_dir, target_dir, "local", options);
assert_true(mount2.id != "", "Mount ID should not be empty");
// Test unmounting all mounts
print("Testing rfs_unmount_all()...");
rfs_unmount_all();
// Verify all mounts are gone
let mounts_after = rfs_list_mounts();
assert_true(mounts_after.len() == 0, "There should be no mounts after unmount_all");
print("✓ rfs_unmount_all(): All mounts successfully unmounted");
print("All mount operations tests completed successfully!");
} catch(err) {
print(`Error: ${err}`);
// Clean up in case of error
cleanup_mounts();
throw err;
} finally {
// Clean up test directories
delete(source_dir);
delete(target_dir);
print("✓ Cleanup: Test directories removed");
}

View File

@ -1,117 +0,0 @@
// 02_filesystem_layer_operations.rhai
// Tests for RFS filesystem layer 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 rfs is available
fn is_rfs_available() {
try {
let result = run("which rfs");
return result.success;
} catch(err) {
return false;
}
}
print("=== Testing RFS Filesystem Layer Operations ===");
// Check if rfs is available
let rfs_available = is_rfs_available();
if !rfs_available {
print("rfs is not available. Skipping RFS tests.");
// Exit gracefully without error
return;
}
print("✓ rfs is available");
// Create test directories
let source_dir = "rhai_test_rfs_source";
let unpack_dir = "rhai_test_rfs_unpack";
mkdir(source_dir);
mkdir(unpack_dir);
// Create test files in the source directory
file_write(`${source_dir}/file1.txt`, "Content of file 1");
file_write(`${source_dir}/file2.txt`, "Content of file 2");
// Create a subdirectory with files
mkdir(`${source_dir}/subdir`);
file_write(`${source_dir}/subdir/file3.txt`, "Content of file 3");
// Output file for the filesystem layer
let output_file = "rhai_test_rfs_layer.fl";
try {
// Test packing a directory
print("Testing rfs_pack()...");
// Use a file store spec for testing
let store_specs = "file:path=.";
rfs_pack(source_dir, output_file, store_specs);
// Verify the output file exists
assert_true(exist(output_file), "Output file should exist");
print(`✓ rfs_pack(): Directory packed to ${output_file}`);
// Test listing contents of the filesystem layer
print("Testing rfs_list_contents()...");
let contents = rfs_list_contents(output_file);
// Verify the contents include our files
assert_true(contents.contains("file1.txt"), "Contents should include file1.txt");
assert_true(contents.contains("file2.txt"), "Contents should include file2.txt");
assert_true(contents.contains("subdir/file3.txt"), "Contents should include subdir/file3.txt");
print("✓ rfs_list_contents(): Layer contents listed successfully");
// Test verifying the filesystem layer
print("Testing rfs_verify()...");
let is_valid = rfs_verify(output_file);
assert_true(is_valid, "Filesystem layer should be valid");
print("✓ rfs_verify(): Layer verified successfully");
// Test unpacking the filesystem layer
print("Testing rfs_unpack()...");
rfs_unpack(output_file, unpack_dir);
// Verify the unpacked files exist and have the correct content
assert_true(exist(`${unpack_dir}/file1.txt`), "Unpacked file1.txt should exist");
assert_true(exist(`${unpack_dir}/file2.txt`), "Unpacked file2.txt should exist");
assert_true(exist(`${unpack_dir}/subdir/file3.txt`), "Unpacked subdir/file3.txt should exist");
let content1 = file_read(`${unpack_dir}/file1.txt`);
let content2 = file_read(`${unpack_dir}/file2.txt`);
let content3 = file_read(`${unpack_dir}/subdir/file3.txt`);
assert_eq(content1, "Content of file 1", "Content of file1.txt should match");
assert_eq(content2, "Content of file 2", "Content of file2.txt should match");
assert_eq(content3, "Content of file 3", "Content of file3.txt should match");
print("✓ rfs_unpack(): Layer unpacked successfully");
print("All filesystem layer operations tests completed successfully!");
} catch(err) {
print(`Error: ${err}`);
throw err;
} finally {
// Clean up test directories and files
delete(source_dir);
delete(unpack_dir);
delete(output_file);
print("✓ Cleanup: Test directories and files removed");
}

View File

@ -1,168 +0,0 @@
// run_all_tests.rhai
// Runs all RFS module tests
print("=== Running RFS Module Tests ===");
// Custom assert function
fn assert_true(condition, message) {
if !condition {
print(`ASSERTION FAILED: ${message}`);
throw message;
}
}
// Helper function to check if rfs is available
fn is_rfs_available() {
try {
let result = run("which rfs");
return result.success;
} catch(e) {
return false;
}
}
// Helper function to clean up mounts
fn cleanup_mounts() {
try {
rfs_unmount_all();
} catch(e) {
// Ignore errors during cleanup
}
}
// Run each test directly
let passed = 0;
let failed = 0;
let skipped = 0;
let total = 0;
// Check if rfs is available
let rfs_available = is_rfs_available();
if !rfs_available {
print("rfs is not available. Skipping all RFS tests.");
skipped = 2; // Skip both tests
total = 2;
} else {
// Test 1: Mount Operations
print("\n--- Running Mount Operations Tests ---");
try {
// Clean up any existing mounts
cleanup_mounts();
// Create test directories
let source_dir = "rhai_test_rfs_source";
let target_dir = "rhai_test_rfs_target";
mkdir(source_dir);
mkdir(target_dir);
// Create a test file in the source directory
let test_file = `${source_dir}/test.txt`;
file_write(test_file, "Hello from RFS test");
// Mount the directory
let options = #{
"readonly": "true"
};
let mount = rfs_mount(source_dir, target_dir, "local", options);
assert_true(mount.id != "", "Mount ID should not be empty");
// List mounts
let mounts = rfs_list_mounts();
assert_true(mounts.len() > 0, "There should be at least one mount");
// Unmount
rfs_unmount(target_dir);
// Clean up
delete(source_dir);
delete(target_dir);
print("--- Mount Operations Tests completed successfully ---");
passed += 1;
} catch(err) {
print(`!!! Error in Mount Operations Tests: ${err}`);
failed += 1;
// Clean up in case of error
cleanup_mounts();
try {
delete("rhai_test_rfs_source");
delete("rhai_test_rfs_target");
} catch(e) {
// Ignore errors during cleanup
}
}
total += 1;
// Test 2: Filesystem Layer Operations
print("\n--- Running Filesystem Layer Operations Tests ---");
try {
// Create test directories
let source_dir = "rhai_test_rfs_source";
let unpack_dir = "rhai_test_rfs_unpack";
mkdir(source_dir);
mkdir(unpack_dir);
// Create test files in the source directory
file_write(`${source_dir}/file1.txt`, "Content of file 1");
// Output file for the filesystem layer
let output_file = "rhai_test_rfs_layer.fl";
// Pack the directory
let store_specs = "file:path=.";
rfs_pack(source_dir, output_file, store_specs);
// List contents
let contents = rfs_list_contents(output_file);
assert_true(contents.contains("file1.txt"), "Contents should include file1.txt");
// Verify the layer
let is_valid = rfs_verify(output_file);
assert_true(is_valid, "Filesystem layer should be valid");
// Unpack the layer
rfs_unpack(output_file, unpack_dir);
// Clean up
delete(source_dir);
delete(unpack_dir);
delete(output_file);
print("--- Filesystem Layer Operations Tests completed successfully ---");
passed += 1;
} catch(err) {
print(`!!! Error in Filesystem Layer Operations Tests: ${err}`);
failed += 1;
// Clean up in case of error
try {
delete("rhai_test_rfs_source");
delete("rhai_test_rfs_unpack");
delete("rhai_test_rfs_layer.fl");
} 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,95 +0,0 @@
#!/bin/bash
# Run all Rhai tests
# This script runs all the Rhai tests in the rhai_tests directory
# Set the base directory
BASE_DIR="src/rhai_tests"
# Define colors for output
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[0;33m'
NC='\033[0m' # No Color
# Initialize counters
TOTAL_MODULES=0
PASSED_MODULES=0
FAILED_MODULES=0
# Function to run tests in a directory
run_tests_in_dir() {
local dir=$1
local module_name=$(basename $dir)
echo -e "${YELLOW}Running tests for module: ${module_name}${NC}"
# Check if the directory has a run_all_tests.rhai script
if [ -f "${dir}/run_all_tests.rhai" ]; then
echo "Using module's run_all_tests.rhai script"
herodo --path "${dir}/run_all_tests.rhai"
if [ $? -eq 0 ]; then
echo -e "${GREEN}✓ All tests passed for module: ${module_name}${NC}"
PASSED_MODULES=$((PASSED_MODULES + 1))
else
echo -e "${RED}✗ Tests failed for module: ${module_name}${NC}"
FAILED_MODULES=$((FAILED_MODULES + 1))
fi
else
# Run all .rhai files in the directory
local test_files=$(find "${dir}" -name "*.rhai" | sort)
local all_passed=true
for test_file in $test_files; do
echo "Running test: $(basename $test_file)"
herodo --path "$test_file"
if [ $? -ne 0 ]; then
all_passed=false
fi
done
if $all_passed; then
echo -e "${GREEN}✓ All tests passed for module: ${module_name}${NC}"
PASSED_MODULES=$((PASSED_MODULES + 1))
else
echo -e "${RED}✗ Tests failed for module: ${module_name}${NC}"
FAILED_MODULES=$((FAILED_MODULES + 1))
fi
fi
TOTAL_MODULES=$((TOTAL_MODULES + 1))
echo ""
}
# Main function
main() {
echo "=======================================
Running Rhai Tests
======================================="
# Find all module directories
for dir in $(find "${BASE_DIR}" -mindepth 1 -maxdepth 1 -type d | sort); do
run_tests_in_dir "$dir"
done
# Print summary
echo "=======================================
Test Summary
======================================="
echo "Total modules tested: ${TOTAL_MODULES}"
echo "Passed: ${PASSED_MODULES}"
echo "Failed: ${FAILED_MODULES}"
if [ $FAILED_MODULES -gt 0 ]; then
echo -e "${RED}Some tests failed!${NC}"
exit 1
else
echo -e "${GREEN}All tests passed!${NC}"
exit 0
fi
}
# Run the main function
main

View File

@ -1,108 +0,0 @@
// 01_text_indentation.rhai
// Tests for text indentation functions in the Text module
// 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;
}
}
print("=== Testing Text Indentation Functions ===");
// Test dedent function
print("Testing dedent()...");
// Test case 1: Basic indentation
let indented_text1 = " line 1\n line 2\n line 3";
let expected_dedented1 = "line 1\nline 2\n line 3";
let dedented1 = dedent(indented_text1);
assert_eq(dedented1, expected_dedented1, "Basic indentation should be removed correctly");
print("✓ dedent(): Basic indentation removed correctly");
// Test case 2: Mixed indentation
let indented_text2 = " line 1\n line 2\n line 3";
let expected_dedented2 = "line 1\n line 2\nline 3";
let dedented2 = dedent(indented_text2);
assert_eq(dedented2, expected_dedented2, "Mixed indentation should be handled correctly");
print("✓ dedent(): Mixed indentation handled correctly");
// Test case 3: Empty lines
let indented_text3 = " line 1\n\n line 3";
let expected_dedented3 = "line 1\n\nline 3";
let dedented3 = dedent(indented_text3);
assert_eq(dedented3, expected_dedented3, "Empty lines should be preserved");
print("✓ dedent(): Empty lines preserved correctly");
// Test case 4: No indentation
let text4 = "line 1\nline 2\nline 3";
let dedented4 = dedent(text4);
assert_eq(dedented4, text4, "Text without indentation should remain unchanged");
print("✓ dedent(): Text without indentation remains unchanged");
// Test case 5: Single line
let indented_text5 = " single line";
let expected_dedented5 = "single line";
let dedented5 = dedent(indented_text5);
assert_eq(dedented5, expected_dedented5, "Single line indentation should be removed");
print("✓ dedent(): Single line indentation removed correctly");
// Test prefix function
print("\nTesting prefix()...");
// Test case 1: Basic prefix
let text1 = "line 1\nline 2\nline 3";
let expected_prefixed1 = " line 1\n line 2\n line 3";
let prefixed1 = prefix(text1, " ");
assert_eq(prefixed1, expected_prefixed1, "Basic prefix should be added correctly");
print("✓ prefix(): Basic prefix added correctly");
// Test case 2: Empty prefix
let text2 = "line 1\nline 2\nline 3";
let prefixed2 = prefix(text2, "");
assert_eq(prefixed2, text2, "Empty prefix should not change the text");
print("✓ prefix(): Empty prefix doesn't change the text");
// Test case 3: Prefix with empty lines
let text3 = "line 1\n\nline 3";
let expected_prefixed3 = " line 1\n \n line 3";
let prefixed3 = prefix(text3, " ");
assert_eq(prefixed3, expected_prefixed3, "Prefix should be added to empty lines");
print("✓ prefix(): Prefix added to empty lines correctly");
// Test case 4: Single line
let text4 = "single line";
let expected_prefixed4 = " single line";
let prefixed4 = prefix(text4, " ");
assert_eq(prefixed4, expected_prefixed4, "Prefix should be added to single line");
print("✓ prefix(): Prefix added to single line correctly");
// Test case 5: Non-space prefix
let text5 = "line 1\nline 2\nline 3";
let expected_prefixed5 = ">>> line 1\n>>> line 2\n>>> line 3";
let prefixed5 = prefix(text5, ">>> ");
assert_eq(prefixed5, expected_prefixed5, "Non-space prefix should be added correctly");
print("✓ prefix(): Non-space prefix added correctly");
// Test combining dedent and prefix
print("\nTesting combination of dedent() and prefix()...");
let indented_text = " line 1\n line 2\n line 3";
let dedented = dedent(indented_text);
let prefixed = prefix(dedented, " ");
let expected_result = " line 1\n line 2\n line 3";
assert_eq(prefixed, expected_result, "Combination of dedent and prefix should work correctly");
print("✓ dedent() + prefix(): Combination works correctly");
print("\nAll text indentation tests completed successfully!");

View File

@ -1,100 +0,0 @@
// 02_name_path_fix.rhai
// Tests for filename and path normalization functions in the Text module
// 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;
}
}
print("=== Testing Filename and Path Normalization Functions ===");
// Test name_fix function
print("Testing name_fix()...");
// Test case 1: Basic name fixing
let name1 = "Hello World";
let expected_fixed1 = "hello_world";
let fixed1 = name_fix(name1);
assert_eq(fixed1, expected_fixed1, "Spaces should be replaced with underscores and converted to lowercase");
print("✓ name_fix(): Basic name fixing works correctly");
// Test case 2: Special characters
let name2 = "File-Name.txt";
let expected_fixed2 = "file_name.txt";
let fixed2 = name_fix(name2);
assert_eq(fixed2, expected_fixed2, "Hyphens should be replaced with underscores");
print("✓ name_fix(): Special characters handled correctly");
// Test case 3: Multiple special characters
let name3 = "Test!@#$%^&*()";
let expected_fixed3 = "test_";
let fixed3 = name_fix(name3);
assert_eq(fixed3, expected_fixed3, "Multiple special characters should be collapsed into a single underscore");
print("✓ name_fix(): Multiple special characters handled correctly");
// Test case 4: Non-ASCII characters
let name4 = "Café";
let expected_fixed4 = "caf";
let fixed4 = name_fix(name4);
assert_eq(fixed4, expected_fixed4, "Non-ASCII characters should be removed");
print("✓ name_fix(): Non-ASCII characters removed correctly");
// Test case 5: Uppercase conversion
let name5 = "UPPERCASE";
let expected_fixed5 = "uppercase";
let fixed5 = name_fix(name5);
assert_eq(fixed5, expected_fixed5, "Uppercase should be converted to lowercase");
print("✓ name_fix(): Uppercase conversion works correctly");
// Test path_fix function
print("\nTesting path_fix()...");
// Test case 1: Path ending with /
let path1 = "/path/to/directory/";
let expected_fixed_path1 = "/path/to/directory/";
let fixed_path1 = path_fix(path1);
assert_eq(fixed_path1, expected_fixed_path1, "Path ending with / should remain unchanged");
print("✓ path_fix(): Path ending with / remains unchanged");
// Test case 2: Single filename
let path2 = "filename.txt";
let expected_fixed_path2 = "filename.txt";
let fixed_path2 = path_fix(path2);
assert_eq(fixed_path2, expected_fixed_path2, "Single filename should be fixed");
print("✓ path_fix(): Single filename fixed correctly");
// Test case 3: Path with filename
let path3 = "/path/to/File Name.txt";
let expected_fixed_path3 = "/path/to/file_name.txt";
let fixed_path3 = path_fix(path3);
assert_eq(fixed_path3, expected_fixed_path3, "Only the filename part of the path should be fixed");
print("✓ path_fix(): Path with filename fixed correctly");
// Test case 4: Relative path
let path4 = "./relative/path/to/DOCUMENT-123.pdf";
let expected_fixed_path4 = "./relative/path/to/document_123.pdf";
let fixed_path4 = path_fix(path4);
assert_eq(fixed_path4, expected_fixed_path4, "Relative path should be handled correctly");
print("✓ path_fix(): Relative path handled correctly");
// Test case 5: Path with special characters in filename
let path5 = "/path/with/[special]<chars>.txt";
let expected_fixed_path5 = "/path/with/_special_chars_.txt";
let fixed_path5 = path_fix(path5);
assert_eq(fixed_path5, expected_fixed_path5, "Special characters in filename should be handled correctly");
print("✓ path_fix(): Path with special characters in filename handled correctly");
print("\nAll filename and path normalization tests completed successfully!");

View File

@ -1,134 +0,0 @@
// 03_text_replacer.rhai
// Tests for text replacement functions in the Text module
// 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;
}
}
print("=== Testing Text Replacement Functions ===");
// Test TextReplacer with simple replacements
print("Testing TextReplacer with simple replacements...");
// Test case 1: Basic replacement
let replacer1 = text_replacer_new()
.pattern("foo")
.replacement("bar")
.build();
let input1 = "foo bar foo";
let expected_output1 = "bar bar bar";
let output1 = replacer1.replace(input1);
assert_eq(output1, expected_output1, "Basic replacement should work correctly");
print("✓ TextReplacer: Basic replacement works correctly");
// Test case 2: Multiple replacements
let replacer2 = text_replacer_new()
.pattern("foo")
.replacement("bar")
.and()
.pattern("baz")
.replacement("qux")
.build();
let input2 = "foo baz foo";
let expected_output2 = "bar qux bar";
let output2 = replacer2.replace(input2);
assert_eq(output2, expected_output2, "Multiple replacements should work correctly");
print("✓ TextReplacer: Multiple replacements work correctly");
// Test TextReplacer with regex replacements
print("\nTesting TextReplacer with regex replacements...");
// Test case 3: Basic regex replacement
let replacer3 = text_replacer_new()
.pattern("f.o")
.replacement("bar")
.regex(true)
.build();
let input3 = "foo fao fio";
let output3 = replacer3.replace(input3);
// The regex "f.o" matches "foo", "fao", and "fio"
let expected_output3 = "bar bar bar";
assert_eq(output3, expected_output3, "Basic regex replacement should work correctly");
print("✓ TextReplacer: Basic regex replacement works correctly");
// Test case 4: Case-insensitive regex replacement
let replacer4 = text_replacer_new()
.pattern("foo")
.replacement("bar")
.regex(true)
.case_insensitive(true)
.build();
let input4 = "FOO foo Foo";
let expected_output4 = "bar bar bar";
let output4 = replacer4.replace(input4);
assert_eq(output4, expected_output4, "Case-insensitive regex replacement should work correctly");
print("✓ TextReplacer: Case-insensitive regex replacement works correctly");
// Test TextReplacer with file operations
print("\nTesting TextReplacer with file operations...");
// Create a temporary file for testing
let test_dir = "rhai_test_text_replacer";
mkdir(test_dir);
let test_file = `${test_dir}/test_file.txt`;
let test_output_file = `${test_dir}/test_output_file.txt`;
// Write test content to the file
let test_content = "This is a test file with foo and bar.";
file_write(test_file, test_content);
// Test case 5: Replace in file and get result as string
let replacer5 = text_replacer_new()
.pattern("foo")
.replacement("baz")
.build();
let expected_output5 = "This is a test file with baz and bar.";
let output5 = replacer5.replace_file(test_file);
assert_eq(output5, expected_output5, "replace_file should return the replaced content");
print("✓ TextReplacer: replace_file works correctly");
// Test case 6: Replace in file and write to a new file
replacer5.replace_file_to(test_file, test_output_file);
let output_content = file_read(test_output_file);
assert_eq(output_content, expected_output5, "replace_file_to should write the replaced content to a new file");
print("✓ TextReplacer: replace_file_to works correctly");
// Test case 7: Replace in file and write back to the same file
// First, update the test file with the replaced content
file_write(test_file, expected_output5);
let replacer6 = text_replacer_new()
.pattern("baz")
.replacement("qux")
.build();
replacer6.replace_file_in_place(test_file);
let updated_content = file_read(test_file);
let expected_output6 = "This is a test file with qux and bar.";
assert_eq(updated_content, expected_output6, "replace_file_in_place should update the file in place");
print("✓ TextReplacer: replace_file_in_place works correctly");
// Clean up
delete(test_dir);
print("✓ Cleanup: Test directory removed");
print("\nAll text replacement tests completed successfully!");

View File

@ -1,102 +0,0 @@
// 04_template_builder.rhai
// Tests for template rendering functions in the Text module
// 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;
}
}
print("=== Testing Template Rendering Functions ===");
// Create a temporary directory for testing
let test_dir = "rhai_test_template";
mkdir(test_dir);
// Test TemplateBuilder with string template
print("Testing TemplateBuilder with string template...");
// Test case 1: Basic template with string variable
let template1 = "Hello, {{ name }}!";
let builder1 = template_builder_open(template1);
builder1.add_var("name", "World");
let expected_output1 = "Hello, World!";
let output1 = builder1.render();
assert_eq(output1, expected_output1, "Basic template with string variable should render correctly");
print("✓ TemplateBuilder: Basic template with string variable renders correctly");
// Test case 2: Template with multiple variables of different types
let template2 = "{{ name }} is {{ age }} years old and {{ is_active ? 'active' : 'inactive' }}.";
let builder2 = template_builder_open(template2);
builder2.add_var("name", "John");
builder2.add_var("age", 30);
builder2.add_var("is_active", true);
let expected_output2 = "John is 30 years old and active.";
let output2 = builder2.render();
assert_eq(output2, expected_output2, "Template with multiple variables should render correctly");
print("✓ TemplateBuilder: Template with multiple variables renders correctly");
// Test case 3: Template with array variable
let template3 = "Items: {% for item in items %}{{ item }}{% if !loop.last %}, {% endif %}{% endfor %}";
let builder3 = template_builder_open(template3);
let items = ["apple", "banana", "cherry"];
builder3.add_var("items", items);
let expected_output3 = "Items: apple, banana, cherry";
let output3 = builder3.render();
assert_eq(output3, expected_output3, "Template with array variable should render correctly");
print("✓ TemplateBuilder: Template with array variable renders correctly");
// Test case 4: Template with map variable
let template4 = "User: {{ user.name }}, Age: {{ user.age }}";
let builder4 = template_builder_open(template4);
let user = #{
name: "Alice",
age: 25
};
builder4.add_vars(user);
let expected_output4 = "User: Alice, Age: 25";
let output4 = builder4.render();
assert_eq(output4, expected_output4, "Template with map variable should render correctly");
print("✓ TemplateBuilder: Template with map variable renders correctly");
// Test TemplateBuilder with file operations
print("\nTesting TemplateBuilder with file operations...");
// Create a template file
let template_file = `${test_dir}/template.txt`;
let template_content = "Hello, {{ name }}! You are {{ age }} years old.";
file_write(template_file, template_content);
// Test case 5: Template from file
let builder5 = template_builder_open(template_file);
builder5.add_var("name", "Bob");
builder5.add_var("age", 40);
let expected_output5 = "Hello, Bob! You are 40 years old.";
let output5 = builder5.render();
assert_eq(output5, expected_output5, "Template from file should render correctly");
print("✓ TemplateBuilder: Template from file renders correctly");
// Test case 6: Render to file
let output_file = `${test_dir}/output.txt`;
builder5.render_to_file(output_file);
let output_content = file_read(output_file);
assert_eq(output_content, expected_output5, "render_to_file should write the rendered content to a file");
print("✓ TemplateBuilder: render_to_file works correctly");
// Clean up
delete(test_dir);
print("✓ Cleanup: Test directory removed");
print("\nAll template rendering tests completed successfully!");

View File

@ -1,138 +0,0 @@
// run_all_tests.rhai
// Runs all Text module tests
print("=== Running Text Module Tests ===");
// 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;
}
}
// Run each test directly
let passed = 0;
let failed = 0;
let total = 0;
// Test 1: Text Indentation
print("\n--- Running Text Indentation Tests ---");
try {
// Test dedent function
print("Testing dedent()...");
let indented_text = " line 1\n line 2\n line 3";
let dedented = dedent(indented_text);
assert_eq(dedented, "line 1\nline 2\n line 3", "Basic indentation should be removed correctly");
print("✓ dedent(): Basic indentation removed correctly");
// Test prefix function
print("Testing prefix()...");
let text = "line 1\nline 2\nline 3";
let prefixed = prefix(text, " ");
assert_eq(prefixed, " line 1\n line 2\n line 3", "Basic prefix should be added correctly");
print("✓ prefix(): Basic prefix added correctly");
print("--- Text Indentation Tests completed successfully ---");
passed += 1;
} catch(err) {
print(`!!! Error in Text Indentation Tests: ${err}`);
failed += 1;
}
total += 1;
// Test 2: Filename and Path Normalization
print("\n--- Running Filename and Path Normalization Tests ---");
try {
// Test name_fix function
print("Testing name_fix()...");
let name = "Hello World";
let fixed_name = name_fix(name);
assert_eq(fixed_name, "hello_world", "Spaces should be replaced with underscores and converted to lowercase");
print("✓ name_fix(): Basic name fixing works correctly");
// Test path_fix function
print("Testing path_fix()...");
let path = "/path/to/File Name.txt";
let fixed_path = path_fix(path);
assert_eq(fixed_path, "/path/to/file_name.txt", "Only the filename part of the path should be fixed");
print("✓ path_fix(): Path with filename fixed correctly");
print("--- Filename and Path Normalization Tests completed successfully ---");
passed += 1;
} catch(err) {
print(`!!! Error in Filename and Path Normalization Tests: ${err}`);
failed += 1;
}
total += 1;
// Test 3: Text Replacement
print("\n--- Running Text Replacement Tests ---");
try {
// Test TextReplacer with simple replacements
print("Testing TextReplacer with simple replacements...");
let replacer = text_replacer_new()
.pattern("foo")
.replacement("bar")
.build();
let input = "foo bar foo";
let output = replacer.replace(input);
assert_eq(output, "bar bar bar", "Basic replacement should work correctly");
print("✓ TextReplacer: Basic replacement works correctly");
// Create a temporary file for testing
let test_dir = "rhai_test_text_replacer";
mkdir(test_dir);
let test_file = `${test_dir}/test_file.txt`;
// Write test content to the file
let test_content = "This is a test file with foo and bar.";
file_write(test_file, test_content);
// Test replace_file
let expected_output = "This is a test file with bar and bar.";
let output = replacer.replace_file(test_file);
assert_eq(output, expected_output, "replace_file should return the replaced content");
print("✓ TextReplacer: replace_file works correctly");
// Clean up
delete(test_dir);
print("✓ Cleanup: Test directory removed");
print("--- Text Replacement Tests completed successfully ---");
passed += 1;
} catch(err) {
print(`!!! Error in Text Replacement Tests: ${err}`);
failed += 1;
}
total += 1;
// Skip Template Rendering Tests for now
print("\n--- Skipping Template Rendering Tests ---");
print("Template rendering tests are skipped due to compatibility issues.");
total += 1;
print("\n=== Test Summary ===");
print(`Passed: ${passed}`);
print(`Failed: ${failed}`);
print(`Total: ${total}`);
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,32 +1,30 @@
/** /**
* Dedent a multiline string by removing common leading whitespace. * Dedent a multiline string by removing common leading whitespace.
* *
* This function analyzes all non-empty lines in the input text to determine * This function analyzes all non-empty lines in the input text to determine
* the minimum indentation level, then removes that amount of whitespace * the minimum indentation level, then removes that amount of whitespace
* from the beginning of each line. This is useful for working with * from the beginning of each line. This is useful for working with
* multi-line strings in code that have been indented to match the * multi-line strings in code that have been indented to match the
* surrounding code structure. * surrounding code structure.
* *
* # Arguments * # Arguments
* *
* * `text` - The multiline string to dedent * * `text` - The multiline string to dedent
* *
* # Returns * # Returns
* *
* * `String` - The dedented string * * `String` - The dedented string
* *
* # Examples * # Examples
* *
* ``` * ```
* use sal::text::dedent;
*
* let indented = " line 1\n line 2\n line 3"; * let indented = " line 1\n line 2\n line 3";
* let dedented = dedent(indented); * let dedented = dedent(indented);
* assert_eq!(dedented, "line 1\nline 2\n line 3"); * assert_eq!(dedented, "line 1\nline 2\n line 3");
* ``` * ```
* *
* # Notes * # Notes
* *
* - Empty lines are preserved but have all leading whitespace removed * - Empty lines are preserved but have all leading whitespace removed
* - Tabs are counted as 4 spaces for indentation purposes * - Tabs are counted as 4 spaces for indentation purposes
*/ */
@ -34,8 +32,7 @@ pub fn dedent(text: &str) -> String {
let lines: Vec<&str> = text.lines().collect(); let lines: Vec<&str> = text.lines().collect();
// Find the minimum indentation level (ignore empty lines) // Find the minimum indentation level (ignore empty lines)
let min_indent = lines let min_indent = lines.iter()
.iter()
.filter(|line| !line.trim().is_empty()) .filter(|line| !line.trim().is_empty())
.map(|line| { .map(|line| {
let mut spaces = 0; let mut spaces = 0;
@ -54,8 +51,7 @@ pub fn dedent(text: &str) -> String {
.unwrap_or(0); .unwrap_or(0);
// Remove that many spaces from the beginning of each line // Remove that many spaces from the beginning of each line
lines lines.iter()
.iter()
.map(|line| { .map(|line| {
if line.trim().is_empty() { if line.trim().is_empty() {
return String::new(); return String::new();
@ -63,22 +59,22 @@ pub fn dedent(text: &str) -> String {
let mut count = 0; let mut count = 0;
let mut chars = line.chars().peekable(); let mut chars = line.chars().peekable();
// Skip initial spaces up to min_indent // Skip initial spaces up to min_indent
while count < min_indent && chars.peek().is_some() { while count < min_indent && chars.peek().is_some() {
match chars.peek() { match chars.peek() {
Some(' ') => { Some(' ') => {
chars.next(); chars.next();
count += 1; count += 1;
} },
Some('\t') => { Some('\t') => {
chars.next(); chars.next();
count += 4; count += 4;
} },
_ => break, _ => break,
} }
} }
// Return the remaining characters // Return the remaining characters
chars.collect::<String>() chars.collect::<String>()
}) })
@ -86,25 +82,24 @@ pub fn dedent(text: &str) -> String {
.join("\n") .join("\n")
} }
/** /**
* Prefix a multiline string with a specified prefix. * Prefix a multiline string with a specified prefix.
* *
* This function adds the specified prefix to the beginning of each line in the input text. * This function adds the specified prefix to the beginning of each line in the input text.
* *
* # Arguments * # Arguments
* *
* * `text` - The multiline string to prefix * * `text` - The multiline string to prefix
* * `prefix` - The prefix to add to each line * * `prefix` - The prefix to add to each line
* *
* # Returns * # Returns
* *
* * `String` - The prefixed string * * `String` - The prefixed string
* *
* # Examples * # Examples
* *
* ``` * ```
* use sal::text::prefix;
*
* let text = "line 1\nline 2\nline 3"; * let text = "line 1\nline 2\nline 3";
* let prefixed = prefix(text, " "); * let prefixed = prefix(text, " ");
* assert_eq!(prefixed, " line 1\n line 2\n line 3"); * assert_eq!(prefixed, " line 1\n line 2\n line 3");

View File

@ -1,6 +1,7 @@
use regex::Regex; use regex::Regex;
use std::fs; use std::fs;
use std::io::{self, Read}; use std::io::{self, Read, Seek, SeekFrom};
use std::path::Path; use std::path::Path;
/// Represents the type of replacement to perform. /// Represents the type of replacement to perform.
@ -45,36 +46,36 @@ impl TextReplacer {
/// Applies all configured replacement operations to the input text /// Applies all configured replacement operations to the input text
pub fn replace(&self, input: &str) -> String { pub fn replace(&self, input: &str) -> String {
let mut result = input.to_string(); let mut result = input.to_string();
// Apply each replacement operation in sequence // Apply each replacement operation in sequence
for op in &self.operations { for op in &self.operations {
result = op.apply(&result); result = op.apply(&result);
} }
result result
} }
/// Reads a file, applies all replacements, and returns the result as a string /// Reads a file, applies all replacements, and returns the result as a string
pub fn replace_file<P: AsRef<Path>>(&self, path: P) -> io::Result<String> { pub fn replace_file<P: AsRef<Path>>(&self, path: P) -> io::Result<String> {
let mut file = fs::File::open(path)?; let mut file = fs::File::open(path)?;
let mut content = String::new(); let mut content = String::new();
file.read_to_string(&mut content)?; file.read_to_string(&mut content)?;
Ok(self.replace(&content)) Ok(self.replace(&content))
} }
/// Reads a file, applies all replacements, and writes the result back to the file /// Reads a file, applies all replacements, and writes the result back to the file
pub fn replace_file_in_place<P: AsRef<Path>>(&self, path: P) -> io::Result<()> { pub fn replace_file_in_place<P: AsRef<Path>>(&self, path: P) -> io::Result<()> {
let content = self.replace_file(&path)?; let content = self.replace_file(&path)?;
fs::write(path, content)?; fs::write(path, content)?;
Ok(()) Ok(())
} }
/// Reads a file, applies all replacements, and writes the result to a new file /// Reads a file, applies all replacements, and writes the result to a new file
pub fn replace_file_to<P1: AsRef<Path>, P2: AsRef<Path>>( pub fn replace_file_to<P1: AsRef<Path>, P2: AsRef<Path>>(
&self, &self,
input_path: P1, input_path: P1,
output_path: P2, output_path: P2
) -> io::Result<()> { ) -> io::Result<()> {
let content = self.replace_file(&input_path)?; let content = self.replace_file(&input_path)?;
fs::write(output_path, content)?; fs::write(output_path, content)?;
@ -110,13 +111,13 @@ impl TextReplacerBuilder {
self.use_regex = yes; self.use_regex = yes;
self self
} }
/// Sets whether the replacement should be case-insensitive /// Sets whether the replacement should be case-insensitive
pub fn case_insensitive(mut self, yes: bool) -> Self { pub fn case_insensitive(mut self, yes: bool) -> Self {
self.case_insensitive = yes; self.case_insensitive = yes;
self self
} }
/// Adds another replacement operation to the chain and resets the builder for a new operation /// Adds another replacement operation to the chain and resets the builder for a new operation
pub fn and(mut self) -> Self { pub fn and(mut self) -> Self {
self.add_current_operation(); self.add_current_operation();
@ -129,20 +130,20 @@ impl TextReplacerBuilder {
let replacement = self.replacement.take().unwrap_or_default(); let replacement = self.replacement.take().unwrap_or_default();
let use_regex = self.use_regex; let use_regex = self.use_regex;
let case_insensitive = self.case_insensitive; let case_insensitive = self.case_insensitive;
// Reset current settings // Reset current settings
self.use_regex = false; self.use_regex = false;
self.case_insensitive = false; self.case_insensitive = false;
// Create the replacement mode // Create the replacement mode
let mode = if use_regex { let mode = if use_regex {
let mut regex_pattern = pattern; let mut regex_pattern = pattern;
// If case insensitive, add the flag to the regex pattern // If case insensitive, add the flag to the regex pattern
if case_insensitive && !regex_pattern.starts_with("(?i)") { if case_insensitive && !regex_pattern.starts_with("(?i)") {
regex_pattern = format!("(?i){}", regex_pattern); regex_pattern = format!("(?i){}", regex_pattern);
} }
match Regex::new(&regex_pattern) { match Regex::new(&regex_pattern) {
Ok(re) => ReplaceMode::Regex(re), Ok(re) => ReplaceMode::Regex(re),
Err(_) => return false, // Failed to compile regex Err(_) => return false, // Failed to compile regex
@ -155,10 +156,12 @@ impl TextReplacerBuilder {
} }
ReplaceMode::Literal(pattern) ReplaceMode::Literal(pattern)
}; };
self.operations self.operations.push(ReplacementOperation {
.push(ReplacementOperation { mode, replacement }); mode,
replacement,
});
true true
} else { } else {
false false
@ -169,12 +172,12 @@ impl TextReplacerBuilder {
pub fn build(mut self) -> Result<TextReplacer, String> { pub fn build(mut self) -> Result<TextReplacer, String> {
// If there's a pending replacement operation, add it // If there's a pending replacement operation, add it
self.add_current_operation(); self.add_current_operation();
// Ensure we have at least one replacement operation // Ensure we have at least one replacement operation
if self.operations.is_empty() { if self.operations.is_empty() {
return Err("No replacement operations configured".to_string()); return Err("No replacement operations configured".to_string());
} }
Ok(TextReplacer { Ok(TextReplacer {
operations: self.operations, operations: self.operations,
}) })
@ -184,7 +187,7 @@ impl TextReplacerBuilder {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use std::io::{Seek, SeekFrom, Write}; use std::io::Write;
use tempfile::NamedTempFile; use tempfile::NamedTempFile;
#[test] #[test]
@ -216,7 +219,7 @@ mod tests {
assert_eq!(output, "qux bar qux baz"); assert_eq!(output, "qux bar qux baz");
} }
#[test] #[test]
fn test_multiple_replacements() { fn test_multiple_replacements() {
let replacer = TextReplacer::builder() let replacer = TextReplacer::builder()
@ -233,7 +236,7 @@ mod tests {
assert_eq!(output, "qux baz qux"); assert_eq!(output, "qux baz qux");
} }
#[test] #[test]
fn test_case_insensitive_regex() { fn test_case_insensitive_regex() {
let replacer = TextReplacer::builder() let replacer = TextReplacer::builder()
@ -249,44 +252,44 @@ mod tests {
assert_eq!(output, "bar bar bar"); assert_eq!(output, "bar bar bar");
} }
#[test] #[test]
fn test_file_operations() -> io::Result<()> { fn test_file_operations() -> io::Result<()> {
// Create a temporary file // Create a temporary file
let mut temp_file = NamedTempFile::new()?; let mut temp_file = NamedTempFile::new()?;
writeln!(temp_file, "foo bar foo baz")?; writeln!(temp_file, "foo bar foo baz")?;
// Flush the file to ensure content is written // Flush the file to ensure content is written
temp_file.as_file_mut().flush()?; temp_file.as_file_mut().flush()?;
let replacer = TextReplacer::builder() let replacer = TextReplacer::builder()
.pattern("foo") .pattern("foo")
.replacement("qux") .replacement("qux")
.build() .build()
.unwrap(); .unwrap();
// Test replace_file // Test replace_file
let result = replacer.replace_file(temp_file.path())?; let result = replacer.replace_file(temp_file.path())?;
assert_eq!(result, "qux bar qux baz\n"); assert_eq!(result, "qux bar qux baz\n");
// Test replace_file_in_place // Test replace_file_in_place
replacer.replace_file_in_place(temp_file.path())?; replacer.replace_file_in_place(temp_file.path())?;
// Verify the file was updated - need to seek to beginning of file first // Verify the file was updated - need to seek to beginning of file first
let mut content = String::new(); let mut content = String::new();
temp_file.as_file_mut().seek(SeekFrom::Start(0))?; temp_file.as_file_mut().seek(SeekFrom::Start(0))?;
temp_file.as_file_mut().read_to_string(&mut content)?; temp_file.as_file_mut().read_to_string(&mut content)?;
assert_eq!(content, "qux bar qux baz\n"); assert_eq!(content, "qux bar qux baz\n");
// Test replace_file_to with a new temporary file // Test replace_file_to with a new temporary file
let output_file = NamedTempFile::new()?; let output_file = NamedTempFile::new()?;
replacer.replace_file_to(temp_file.path(), output_file.path())?; replacer.replace_file_to(temp_file.path(), output_file.path())?;
// Verify the output file has the replaced content // Verify the output file has the replaced content
let mut output_content = String::new(); let mut output_content = String::new();
fs::File::open(output_file.path())?.read_to_string(&mut output_content)?; fs::File::open(output_file.path())?.read_to_string(&mut output_content)?;
assert_eq!(output_content, "qux bar qux baz\n"); assert_eq!(output_content, "qux bar qux baz\n");
Ok(()) Ok(())
} }
} }

View File

@ -32,7 +32,7 @@ impl TemplateBuilder {
/// ``` /// ```
pub fn open<P: AsRef<Path>>(template_path: P) -> io::Result<Self> { pub fn open<P: AsRef<Path>>(template_path: P) -> io::Result<Self> {
let path_str = template_path.as_ref().to_string_lossy().to_string(); let path_str = template_path.as_ref().to_string_lossy().to_string();
// Verify the template file exists // Verify the template file exists
if !Path::new(&path_str).exists() { if !Path::new(&path_str).exists() {
return Err(io::Error::new( return Err(io::Error::new(
@ -40,14 +40,14 @@ impl TemplateBuilder {
format!("Template file not found: {}", path_str), format!("Template file not found: {}", path_str),
)); ));
} }
Ok(Self { Ok(Self {
template_path: path_str, template_path: path_str,
context: Context::new(), context: Context::new(),
tera: None, tera: None,
}) })
} }
/// Adds a variable to the template context. /// Adds a variable to the template context.
/// ///
/// # Arguments /// # Arguments
@ -61,15 +61,12 @@ impl TemplateBuilder {
/// ///
/// # Example /// # Example
/// ///
/// ```no_run /// ```
/// use sal::text::TemplateBuilder; /// use sal::text::TemplateBuilder;
/// ///
/// fn main() -> Result<(), Box<dyn std::error::Error>> { /// let builder = TemplateBuilder::open("templates/example.html")?
/// let builder = TemplateBuilder::open("templates/example.html")? /// .add_var("title", "Hello World")
/// .add_var("title", "Hello World") /// .add_var("username", "John Doe");
/// .add_var("username", "John Doe");
/// Ok(())
/// }
/// ``` /// ```
pub fn add_var<S, V>(mut self, name: S, value: V) -> Self pub fn add_var<S, V>(mut self, name: S, value: V) -> Self
where where
@ -79,7 +76,7 @@ impl TemplateBuilder {
self.context.insert(name.as_ref(), &value); self.context.insert(name.as_ref(), &value);
self self
} }
/// Adds multiple variables to the template context from a HashMap. /// Adds multiple variables to the template context from a HashMap.
/// ///
/// # Arguments /// # Arguments
@ -92,19 +89,16 @@ impl TemplateBuilder {
/// ///
/// # Example /// # Example
/// ///
/// ```no_run /// ```
/// use sal::text::TemplateBuilder; /// use sal::text::TemplateBuilder;
/// use std::collections::HashMap; /// use std::collections::HashMap;
/// ///
/// fn main() -> Result<(), Box<dyn std::error::Error>> { /// let mut vars = HashMap::new();
/// let mut vars = HashMap::new(); /// vars.insert("title", "Hello World");
/// vars.insert("title", "Hello World"); /// vars.insert("username", "John Doe");
/// vars.insert("username", "John Doe");
/// ///
/// let builder = TemplateBuilder::open("templates/example.html")? /// let builder = TemplateBuilder::open("templates/example.html")?
/// .add_vars(vars); /// .add_vars(vars);
/// Ok(())
/// }
/// ``` /// ```
pub fn add_vars<S, V>(mut self, vars: HashMap<S, V>) -> Self pub fn add_vars<S, V>(mut self, vars: HashMap<S, V>) -> Self
where where
@ -116,7 +110,7 @@ impl TemplateBuilder {
} }
self self
} }
/// Initializes the Tera template engine with the template file. /// Initializes the Tera template engine with the template file.
/// ///
/// This method is called automatically by render() if not called explicitly. /// This method is called automatically by render() if not called explicitly.
@ -128,24 +122,24 @@ impl TemplateBuilder {
if self.tera.is_none() { if self.tera.is_none() {
// Create a new Tera instance with just this template // Create a new Tera instance with just this template
let mut tera = Tera::default(); let mut tera = Tera::default();
// Read the template content // Read the template content
let template_content = fs::read_to_string(&self.template_path) let template_content = fs::read_to_string(&self.template_path)
.map_err(|e| tera::Error::msg(format!("Failed to read template file: {}", e)))?; .map_err(|e| tera::Error::msg(format!("Failed to read template file: {}", e)))?;
// Add the template to Tera // Add the template to Tera
let template_name = Path::new(&self.template_path) let template_name = Path::new(&self.template_path)
.file_name() .file_name()
.and_then(|n| n.to_str()) .and_then(|n| n.to_str())
.unwrap_or("template"); .unwrap_or("template");
tera.add_raw_template(template_name, &template_content)?; tera.add_raw_template(template_name, &template_content)?;
self.tera = Some(tera); self.tera = Some(tera);
} }
Ok(()) Ok(())
} }
/// Renders the template with the current context. /// Renders the template with the current context.
/// ///
/// # Returns /// # Returns
@ -154,34 +148,31 @@ impl TemplateBuilder {
/// ///
/// # Example /// # Example
/// ///
/// ```no_run /// ```
/// use sal::text::TemplateBuilder; /// use sal::text::TemplateBuilder;
/// ///
/// fn main() -> Result<(), Box<dyn std::error::Error>> { /// let result = TemplateBuilder::open("templates/example.html")?
/// let result = TemplateBuilder::open("templates/example.html")? /// .add_var("title", "Hello World")
/// .add_var("title", "Hello World") /// .add_var("username", "John Doe")
/// .add_var("username", "John Doe") /// .render()?;
/// .render()?;
/// ///
/// println!("Rendered template: {}", result); /// println!("Rendered template: {}", result);
/// Ok(())
/// }
/// ``` /// ```
pub fn render(&mut self) -> Result<String, tera::Error> { pub fn render(&mut self) -> Result<String, tera::Error> {
// Initialize Tera if not already done // Initialize Tera if not already done
self.initialize_tera()?; self.initialize_tera()?;
// Get the template name // Get the template name
let template_name = Path::new(&self.template_path) let template_name = Path::new(&self.template_path)
.file_name() .file_name()
.and_then(|n| n.to_str()) .and_then(|n| n.to_str())
.unwrap_or("template"); .unwrap_or("template");
// Render the template // Render the template
let tera = self.tera.as_ref().unwrap(); let tera = self.tera.as_ref().unwrap();
tera.render(template_name, &self.context) tera.render(template_name, &self.context)
} }
/// Renders the template and writes the result to a file. /// Renders the template and writes the result to a file.
/// ///
/// # Arguments /// # Arguments
@ -194,25 +185,19 @@ impl TemplateBuilder {
/// ///
/// # Example /// # Example
/// ///
/// ```no_run /// ```
/// use sal::text::TemplateBuilder; /// use sal::text::TemplateBuilder;
/// ///
/// fn main() -> Result<(), Box<dyn std::error::Error>> { /// TemplateBuilder::open("templates/example.html")?
/// TemplateBuilder::open("templates/example.html")? /// .add_var("title", "Hello World")
/// .add_var("title", "Hello World") /// .add_var("username", "John Doe")
/// .add_var("username", "John Doe") /// .render_to_file("output.html")?;
/// .render_to_file("output.html")?;
/// Ok(())
/// }
/// ``` /// ```
pub fn render_to_file<P: AsRef<Path>>(&mut self, output_path: P) -> io::Result<()> { pub fn render_to_file<P: AsRef<Path>>(&mut self, output_path: P) -> io::Result<()> {
let rendered = self.render().map_err(|e| { let rendered = self.render().map_err(|e| {
io::Error::new( io::Error::new(io::ErrorKind::Other, format!("Template rendering error: {}", e))
io::ErrorKind::Other,
format!("Template rendering error: {}", e),
)
})?; })?;
fs::write(output_path, rendered) fs::write(output_path, rendered)
} }
} }
@ -222,68 +207,70 @@ mod tests {
use super::*; use super::*;
use std::io::Write; use std::io::Write;
use tempfile::NamedTempFile; use tempfile::NamedTempFile;
#[test] #[test]
fn test_template_rendering() -> Result<(), Box<dyn std::error::Error>> { fn test_template_rendering() -> Result<(), Box<dyn std::error::Error>> {
// Create a temporary template file // Create a temporary template file
let temp_file = NamedTempFile::new()?; let temp_file = NamedTempFile::new()?;
let template_content = "Hello, {{ name }}! Welcome to {{ place }}.\n"; let template_content = "Hello, {{ name }}! Welcome to {{ place }}.\n";
fs::write(temp_file.path(), template_content)?; fs::write(temp_file.path(), template_content)?;
// Create a template builder and add variables // Create a template builder and add variables
let mut builder = TemplateBuilder::open(temp_file.path())?; let mut builder = TemplateBuilder::open(temp_file.path())?;
builder = builder.add_var("name", "John").add_var("place", "Rust"); builder = builder
.add_var("name", "John")
.add_var("place", "Rust");
// Render the template // Render the template
let result = builder.render()?; let result = builder.render()?;
assert_eq!(result, "Hello, John! Welcome to Rust.\n"); assert_eq!(result, "Hello, John! Welcome to Rust.\n");
Ok(()) Ok(())
} }
#[test] #[test]
fn test_template_with_multiple_vars() -> Result<(), Box<dyn std::error::Error>> { fn test_template_with_multiple_vars() -> Result<(), Box<dyn std::error::Error>> {
// Create a temporary template file // Create a temporary template file
let temp_file = NamedTempFile::new()?; let temp_file = NamedTempFile::new()?;
let template_content = "{% if show_greeting %}Hello, {{ name }}!{% endif %}\n{% for item in items %}{{ item }}{% if not loop.last %}, {% endif %}{% endfor %}\n"; let template_content = "{% if show_greeting %}Hello, {{ name }}!{% endif %}\n{% for item in items %}{{ item }}{% if not loop.last %}, {% endif %}{% endfor %}\n";
fs::write(temp_file.path(), template_content)?; fs::write(temp_file.path(), template_content)?;
// Create a template builder and add variables // Create a template builder and add variables
let mut builder = TemplateBuilder::open(temp_file.path())?; let mut builder = TemplateBuilder::open(temp_file.path())?;
// Add variables including a boolean and a vector // Add variables including a boolean and a vector
builder = builder builder = builder
.add_var("name", "Alice") .add_var("name", "Alice")
.add_var("show_greeting", true) .add_var("show_greeting", true)
.add_var("items", vec!["apple", "banana", "cherry"]); .add_var("items", vec!["apple", "banana", "cherry"]);
// Render the template // Render the template
let result = builder.render()?; let result = builder.render()?;
assert_eq!(result, "Hello, Alice!\napple, banana, cherry\n"); assert_eq!(result, "Hello, Alice!\napple, banana, cherry\n");
Ok(()) Ok(())
} }
#[test] #[test]
fn test_template_with_hashmap_vars() -> Result<(), Box<dyn std::error::Error>> { fn test_template_with_hashmap_vars() -> Result<(), Box<dyn std::error::Error>> {
// Create a temporary template file // Create a temporary template file
let mut temp_file = NamedTempFile::new()?; let mut temp_file = NamedTempFile::new()?;
writeln!(temp_file, "{{{{ greeting }}}}, {{{{ name }}}}!")?; writeln!(temp_file, "{{{{ greeting }}}}, {{{{ name }}}}!")?;
temp_file.flush()?; temp_file.flush()?;
// Create a HashMap of variables // Create a HashMap of variables
let mut vars = HashMap::new(); let mut vars = HashMap::new();
vars.insert("greeting", "Hi"); vars.insert("greeting", "Hi");
vars.insert("name", "Bob"); vars.insert("name", "Bob");
// Create a template builder and add variables from HashMap // Create a template builder and add variables from HashMap
let mut builder = TemplateBuilder::open(temp_file.path())?; let mut builder = TemplateBuilder::open(temp_file.path())?;
builder = builder.add_vars(vars); builder = builder.add_vars(vars);
// Render the template // Render the template
let result = builder.render()?; let result = builder.render()?;
assert_eq!(result, "Hi, Bob!\n"); assert_eq!(result, "Hi, Bob!\n");
Ok(()) Ok(())
} }
#[test] #[test]
@ -292,19 +279,20 @@ mod tests {
let temp_file = NamedTempFile::new()?; let temp_file = NamedTempFile::new()?;
let template_content = "{{ message }}\n"; let template_content = "{{ message }}\n";
fs::write(temp_file.path(), template_content)?; fs::write(temp_file.path(), template_content)?;
// Create an output file // Create an output file
let output_file = NamedTempFile::new()?; let output_file = NamedTempFile::new()?;
// Create a template builder, add a variable, and render to file // Create a template builder, add a variable, and render to file
let mut builder = TemplateBuilder::open(temp_file.path())?; let mut builder = TemplateBuilder::open(temp_file.path())?;
builder = builder.add_var("message", "This is a test"); builder = builder.add_var("message", "This is a test");
builder.render_to_file(output_file.path())?; builder.render_to_file(output_file.path())?;
// Read the output file and verify its contents // Read the output file and verify its contents
let content = fs::read_to_string(output_file.path())?; let content = fs::read_to_string(output_file.path())?;
assert_eq!(content, "This is a test\n"); assert_eq!(content, "This is a test\n");
Ok(()) Ok(())
} }
} }

View File

@ -2,35 +2,21 @@
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::super::container_types::Container; use super::super::container_types::{Container, ContainerStatus, ResourceUsage};
use std::process::Command; use super::super::NerdctlError;
use std::error::Error;
use std::thread; use std::thread;
use std::time::Duration; use std::time::Duration;
// Helper function to check if nerdctl is available
fn is_nerdctl_available() -> bool {
match Command::new("which").arg("nerdctl").output() {
Ok(output) => output.status.success(),
Err(_) => false,
}
}
#[test] #[test]
fn test_container_builder_pattern() { fn test_container_builder_pattern() {
// Skip test if nerdctl is not available
if !is_nerdctl_available() {
println!("Skipping test: nerdctl is not available");
return;
}
// Create a container with builder pattern // Create a container with builder pattern
let container = Container::new("test-container") let container = Container::new("test-container").unwrap()
.unwrap()
.with_port("8080:80") .with_port("8080:80")
.with_volume("/tmp:/data") .with_volume("/tmp:/data")
.with_env("TEST_ENV", "test_value") .with_env("TEST_ENV", "test_value")
.with_detach(true); .with_detach(true);
// Verify container properties // Verify container properties
assert_eq!(container.name, "test-container"); assert_eq!(container.name, "test-container");
assert_eq!(container.ports.len(), 1); assert_eq!(container.ports.len(), 1);
@ -41,36 +27,23 @@ mod tests {
assert_eq!(container.env_vars.get("TEST_ENV").unwrap(), "test_value"); assert_eq!(container.env_vars.get("TEST_ENV").unwrap(), "test_value");
assert_eq!(container.detach, true); assert_eq!(container.detach, true);
} }
#[test] #[test]
fn test_container_from_image() { fn test_container_from_image() {
// Skip test if nerdctl is not available
if !is_nerdctl_available() {
println!("Skipping test: nerdctl is not available");
return;
}
// Create a container from image // Create a container from image
let container = Container::from_image("test-container", "alpine:latest").unwrap(); let container = Container::from_image("test-container", "alpine:latest").unwrap();
// Verify container properties // Verify container properties
assert_eq!(container.name, "test-container"); assert_eq!(container.name, "test-container");
assert_eq!(container.image.as_ref().unwrap(), "alpine:latest"); assert_eq!(container.image.as_ref().unwrap(), "alpine:latest");
} }
#[test] #[test]
fn test_container_health_check() { fn test_container_health_check() {
// Skip test if nerdctl is not available
if !is_nerdctl_available() {
println!("Skipping test: nerdctl is not available");
return;
}
// Create a container with health check // Create a container with health check
let container = Container::new("test-container") let container = Container::new("test-container").unwrap()
.unwrap()
.with_health_check("curl -f http://localhost/ || exit 1"); .with_health_check("curl -f http://localhost/ || exit 1");
// Verify health check // Verify health check
assert!(container.health_check.is_some()); assert!(container.health_check.is_some());
let health_check = container.health_check.unwrap(); let health_check = container.health_check.unwrap();
@ -80,26 +53,19 @@ mod tests {
assert!(health_check.retries.is_none()); assert!(health_check.retries.is_none());
assert!(health_check.start_period.is_none()); assert!(health_check.start_period.is_none());
} }
#[test] #[test]
fn test_container_health_check_options() { fn test_container_health_check_options() {
// Skip test if nerdctl is not available
if !is_nerdctl_available() {
println!("Skipping test: nerdctl is not available");
return;
}
// Create a container with health check options // Create a container with health check options
let container = Container::new("test-container") let container = Container::new("test-container").unwrap()
.unwrap()
.with_health_check_options( .with_health_check_options(
"curl -f http://localhost/ || exit 1", "curl -f http://localhost/ || exit 1",
Some("30s"), Some("30s"),
Some("10s"), Some("10s"),
Some(3), Some(3),
Some("5s"), Some("5s")
); );
// Verify health check options // Verify health check options
assert!(container.health_check.is_some()); assert!(container.health_check.is_some());
let health_check = container.health_check.unwrap(); let health_check = container.health_check.unwrap();
@ -109,7 +75,7 @@ mod tests {
assert_eq!(health_check.retries.unwrap(), 3); assert_eq!(health_check.retries.unwrap(), 3);
assert_eq!(health_check.start_period.as_ref().unwrap(), "5s"); assert_eq!(health_check.start_period.as_ref().unwrap(), "5s");
} }
#[test] #[test]
#[ignore] // Ignore by default as it requires nerdctl to be installed and running #[ignore] // Ignore by default as it requires nerdctl to be installed and running
fn test_container_runtime_and_resources() { fn test_container_runtime_and_resources() {
@ -120,47 +86,42 @@ mod tests {
println!("Error: {:?}", nerdctl_check.err()); println!("Error: {:?}", nerdctl_check.err());
return; return;
} }
// Create a unique container name for this test // Create a unique container name for this test
let container_name = format!( let container_name = format!("test-runtime-{}", std::time::SystemTime::now()
"test-runtime-{}", .duration_since(std::time::UNIX_EPOCH)
std::time::SystemTime::now() .unwrap()
.duration_since(std::time::UNIX_EPOCH) .as_secs());
.unwrap()
.as_secs()
);
// Create and build a container that will use resources // Create and build a container that will use resources
// Use a simple container with a basic command to avoid dependency on external images // Use a simple container with a basic command to avoid dependency on external images
let container_result = Container::from_image(&container_name, "busybox:latest") let container_result = Container::from_image(&container_name, "busybox:latest").unwrap()
.unwrap()
.with_detach(true) .with_detach(true)
.build(); .build();
// Check if the build was successful // Check if the build was successful
if container_result.is_err() { if container_result.is_err() {
println!("Failed to build container: {:?}", container_result.err()); println!("Failed to build container: {:?}", container_result.err());
return; return;
} }
let container = container_result.unwrap(); let container = container_result.unwrap();
println!("Container created successfully: {}", container_name); println!("Container created successfully: {}", container_name);
// Start the container with a simple command // Start the container with a simple command
let start_result = let start_result = container.exec("sh -c 'for i in $(seq 1 10); do echo $i; sleep 1; done'");
container.exec("sh -c 'for i in $(seq 1 10); do echo $i; sleep 1; done'");
if start_result.is_err() { if start_result.is_err() {
println!("Failed to start container: {:?}", start_result.err()); println!("Failed to start container: {:?}", start_result.err());
// Try to clean up // Try to clean up
let _ = container.remove(); let _ = container.remove();
return; return;
} }
println!("Container started successfully"); println!("Container started successfully");
// Wait for the container to start and consume resources // Wait for the container to start and consume resources
thread::sleep(Duration::from_secs(3)); thread::sleep(Duration::from_secs(3));
// Check container status // Check container status
let status_result = container.status(); let status_result = container.status();
if status_result.is_err() { if status_result.is_err() {
@ -170,10 +131,10 @@ mod tests {
let _ = container.remove(); let _ = container.remove();
return; return;
} }
let status = status_result.unwrap(); let status = status_result.unwrap();
println!("Container status: {:?}", status); println!("Container status: {:?}", status);
// Verify the container is running // Verify the container is running
if status.status != "running" { if status.status != "running" {
println!("Container is not running, status: {}", status.status); println!("Container is not running, status: {}", status.status);
@ -181,7 +142,7 @@ mod tests {
let _ = container.remove(); let _ = container.remove();
return; return;
} }
// Check resource usage // Check resource usage
let resources_result = container.resources(); let resources_result = container.resources();
if resources_result.is_err() { if resources_result.is_err() {
@ -191,55 +152,42 @@ mod tests {
let _ = container.remove(); let _ = container.remove();
return; return;
} }
let resources = resources_result.unwrap(); let resources = resources_result.unwrap();
println!("Container resources: {:?}", resources); println!("Container resources: {:?}", resources);
// Verify the container is using memory (if we can get the information) // Verify the container is using memory (if we can get the information)
if resources.memory_usage == "0B" || resources.memory_usage == "unknown" { if resources.memory_usage == "0B" || resources.memory_usage == "unknown" {
println!( println!("Warning: Container memory usage is {}", resources.memory_usage);
"Warning: Container memory usage is {}",
resources.memory_usage
);
} else { } else {
println!("Container is using memory: {}", resources.memory_usage); println!("Container is using memory: {}", resources.memory_usage);
} }
// Clean up - stop and remove the container // Clean up - stop and remove the container
println!("Stopping container..."); println!("Stopping container...");
let stop_result = container.stop(); let stop_result = container.stop();
if stop_result.is_err() { if stop_result.is_err() {
println!("Warning: Failed to stop container: {:?}", stop_result.err()); println!("Warning: Failed to stop container: {:?}", stop_result.err());
} }
println!("Removing container..."); println!("Removing container...");
let remove_result = container.remove(); let remove_result = container.remove();
if remove_result.is_err() { if remove_result.is_err() {
println!( println!("Warning: Failed to remove container: {:?}", remove_result.err());
"Warning: Failed to remove container: {:?}",
remove_result.err()
);
} }
println!("Test completed successfully"); println!("Test completed successfully");
} }
#[test] #[test]
fn test_container_with_custom_command() { fn test_container_with_custom_command() {
// Skip test if nerdctl is not available
if !is_nerdctl_available() {
println!("Skipping test: nerdctl is not available");
return;
}
// Create a container with a custom command // Create a container with a custom command
let container = Container::new("test-command-container") let container = Container::new("test-command-container").unwrap()
.unwrap()
.with_port("8080:80") .with_port("8080:80")
.with_volume("/tmp:/data") .with_volume("/tmp:/data")
.with_env("TEST_ENV", "test_value") .with_env("TEST_ENV", "test_value")
.with_detach(true); .with_detach(true);
// Verify container properties // Verify container properties
assert_eq!(container.name, "test-command-container"); assert_eq!(container.name, "test-command-container");
assert_eq!(container.ports.len(), 1); assert_eq!(container.ports.len(), 1);
@ -249,10 +197,10 @@ mod tests {
assert_eq!(container.env_vars.len(), 1); assert_eq!(container.env_vars.len(), 1);
assert_eq!(container.env_vars.get("TEST_ENV").unwrap(), "test_value"); assert_eq!(container.env_vars.get("TEST_ENV").unwrap(), "test_value");
assert_eq!(container.detach, true); assert_eq!(container.detach, true);
// Convert the container to a command string that would be used to run it // Convert the container to a command string that would be used to run it
let command_args = container_to_command_args(&container); let command_args = container_to_command_args(&container);
// Verify the command arguments contain all the expected options // Verify the command arguments contain all the expected options
assert!(command_args.contains(&"--name".to_string())); assert!(command_args.contains(&"--name".to_string()));
assert!(command_args.contains(&"test-command-container".to_string())); assert!(command_args.contains(&"test-command-container".to_string()));
@ -263,45 +211,45 @@ mod tests {
assert!(command_args.contains(&"-e".to_string())); assert!(command_args.contains(&"-e".to_string()));
assert!(command_args.contains(&"TEST_ENV=test_value".to_string())); assert!(command_args.contains(&"TEST_ENV=test_value".to_string()));
assert!(command_args.contains(&"-d".to_string())); assert!(command_args.contains(&"-d".to_string()));
println!("Command args: {:?}", command_args); println!("Command args: {:?}", command_args);
} }
// Helper function to convert a container to command arguments // Helper function to convert a container to command arguments
fn container_to_command_args(container: &Container) -> Vec<String> { fn container_to_command_args(container: &Container) -> Vec<String> {
let mut args = Vec::new(); let mut args = Vec::new();
args.push("run".to_string()); args.push("run".to_string());
if container.detach { if container.detach {
args.push("-d".to_string()); args.push("-d".to_string());
} }
args.push("--name".to_string()); args.push("--name".to_string());
args.push(container.name.clone()); args.push(container.name.clone());
// Add port mappings // Add port mappings
for port in &container.ports { for port in &container.ports {
args.push("-p".to_string()); args.push("-p".to_string());
args.push(port.clone()); args.push(port.clone());
} }
// Add volume mounts // Add volume mounts
for volume in &container.volumes { for volume in &container.volumes {
args.push("-v".to_string()); args.push("-v".to_string());
args.push(volume.clone()); args.push(volume.clone());
} }
// Add environment variables // Add environment variables
for (key, value) in &container.env_vars { for (key, value) in &container.env_vars {
args.push("-e".to_string()); args.push("-e".to_string());
args.push(format!("{}={}", key, value)); args.push(format!("{}={}", key, value));
} }
// Add image if available // Add image if available
if let Some(image) = &container.image { if let Some(image) = &container.image {
args.push(image.clone()); args.push(image.clone());
} }
args args
} }
} }

View File

@ -1,9 +1,9 @@
use std::collections::HashMap;
use super::{ use super::{
cmd::execute_rfs_command,
error::RfsError, error::RfsError,
cmd::execute_rfs_command,
types::{Mount, MountType, StoreSpec}, types::{Mount, MountType, StoreSpec},
}; };
use std::collections::HashMap;
/// Builder for RFS mount operations /// Builder for RFS mount operations
#[derive(Clone)] #[derive(Clone)]
@ -17,7 +17,6 @@ pub struct RfsBuilder {
/// Mount options /// Mount options
options: HashMap<String, String>, options: HashMap<String, String>,
/// Mount ID /// Mount ID
#[allow(dead_code)]
mount_id: Option<String>, mount_id: Option<String>,
/// Debug mode /// Debug mode
debug: bool, debug: bool,
@ -45,7 +44,7 @@ impl RfsBuilder {
debug: false, debug: false,
} }
} }
/// Add a mount option /// Add a mount option
/// ///
/// # Arguments /// # Arguments
@ -60,7 +59,7 @@ impl RfsBuilder {
self.options.insert(key.to_string(), value.to_string()); self.options.insert(key.to_string(), value.to_string());
self self
} }
/// Add multiple mount options /// Add multiple mount options
/// ///
/// # Arguments /// # Arguments
@ -76,7 +75,7 @@ impl RfsBuilder {
} }
self self
} }
/// Set debug mode /// Set debug mode
/// ///
/// # Arguments /// # Arguments
@ -90,7 +89,7 @@ impl RfsBuilder {
self.debug = debug; self.debug = debug;
self self
} }
/// Mount the filesystem /// Mount the filesystem
/// ///
/// # Returns /// # Returns
@ -100,7 +99,7 @@ impl RfsBuilder {
// Build the command string // Build the command string
let mut cmd = String::from("mount -t "); let mut cmd = String::from("mount -t ");
cmd.push_str(&self.mount_type.to_string()); cmd.push_str(&self.mount_type.to_string());
// Add options if any // Add options if any
if !self.options.is_empty() { if !self.options.is_empty() {
cmd.push_str(" -o "); cmd.push_str(" -o ");
@ -115,39 +114,35 @@ impl RfsBuilder {
first = false; first = false;
} }
} }
// Add source and target // Add source and target
cmd.push_str(" "); cmd.push_str(" ");
cmd.push_str(&self.source); cmd.push_str(&self.source);
cmd.push_str(" "); cmd.push_str(" ");
cmd.push_str(&self.target); cmd.push_str(&self.target);
// Split the command into arguments // Split the command into arguments
let args: Vec<&str> = cmd.split_whitespace().collect(); let args: Vec<&str> = cmd.split_whitespace().collect();
// Execute the command // Execute the command
let result = execute_rfs_command(&args)?; let result = execute_rfs_command(&args)?;
// Parse the output to get the mount ID // Parse the output to get the mount ID
let mount_id = result.stdout.trim().to_string(); let mount_id = result.stdout.trim().to_string();
if mount_id.is_empty() { if mount_id.is_empty() {
return Err(RfsError::MountFailed("Failed to get mount ID".to_string())); return Err(RfsError::MountFailed("Failed to get mount ID".to_string()));
} }
// Create and return the Mount struct // Create and return the Mount struct
Ok(Mount { Ok(Mount {
id: mount_id, id: mount_id,
source: self.source, source: self.source,
target: self.target, target: self.target,
fs_type: self.mount_type.to_string(), fs_type: self.mount_type.to_string(),
options: self options: self.options.iter().map(|(k, v)| format!("{}={}", k, v)).collect(),
.options
.iter()
.map(|(k, v)| format!("{}={}", k, v))
.collect(),
}) })
} }
/// Unmount the filesystem /// Unmount the filesystem
/// ///
/// # Returns /// # Returns
@ -156,15 +151,12 @@ impl RfsBuilder {
pub fn unmount(&self) -> Result<(), RfsError> { pub fn unmount(&self) -> Result<(), RfsError> {
// Execute the unmount command // Execute the unmount command
let result = execute_rfs_command(&["unmount", &self.target])?; let result = execute_rfs_command(&["unmount", &self.target])?;
// Check for errors // Check for errors
if !result.success { if !result.success {
return Err(RfsError::UnmountFailed(format!( return Err(RfsError::UnmountFailed(format!("Failed to unmount {}: {}", self.target, result.stderr)));
"Failed to unmount {}: {}",
self.target, result.stderr
)));
} }
Ok(()) Ok(())
} }
} }
@ -201,7 +193,7 @@ impl PackBuilder {
debug: false, debug: false,
} }
} }
/// Add a store specification /// Add a store specification
/// ///
/// # Arguments /// # Arguments
@ -215,7 +207,7 @@ impl PackBuilder {
self.store_specs.push(store_spec); self.store_specs.push(store_spec);
self self
} }
/// Add multiple store specifications /// Add multiple store specifications
/// ///
/// # Arguments /// # Arguments
@ -229,7 +221,7 @@ impl PackBuilder {
self.store_specs.extend(store_specs); self.store_specs.extend(store_specs);
self self
} }
/// Set debug mode /// Set debug mode
/// ///
/// # Arguments /// # Arguments
@ -243,7 +235,7 @@ impl PackBuilder {
self.debug = debug; self.debug = debug;
self self
} }
/// Pack the directory /// Pack the directory
/// ///
/// # Returns /// # Returns
@ -253,7 +245,7 @@ impl PackBuilder {
// Build the command string // Build the command string
let mut cmd = String::from("pack -m "); let mut cmd = String::from("pack -m ");
cmd.push_str(&self.output); cmd.push_str(&self.output);
// Add store specs if any // Add store specs if any
if !self.store_specs.is_empty() { if !self.store_specs.is_empty() {
cmd.push_str(" -s "); cmd.push_str(" -s ");
@ -267,25 +259,22 @@ impl PackBuilder {
first = false; first = false;
} }
} }
// Add directory // Add directory
cmd.push_str(" "); cmd.push_str(" ");
cmd.push_str(&self.directory); cmd.push_str(&self.directory);
// Split the command into arguments // Split the command into arguments
let args: Vec<&str> = cmd.split_whitespace().collect(); let args: Vec<&str> = cmd.split_whitespace().collect();
// Execute the command // Execute the command
let result = execute_rfs_command(&args)?; let result = execute_rfs_command(&args)?;
// Check for errors // Check for errors
if !result.success { if !result.success {
return Err(RfsError::PackFailed(format!( return Err(RfsError::PackFailed(format!("Failed to pack {}: {}", self.directory, result.stderr)));
"Failed to pack {}: {}",
self.directory, result.stderr
)));
} }
Ok(()) Ok(())
} }
} }

View File

@ -1,7 +1,7 @@
use super::error::RfsError;
use crate::process::{run_command, CommandResult}; use crate::process::{run_command, CommandResult};
use std::cell::RefCell; use super::error::RfsError;
use std::thread_local; use std::thread_local;
use std::cell::RefCell;
// Thread-local storage for debug flag // Thread-local storage for debug flag
thread_local! { thread_local! {
@ -9,7 +9,6 @@ thread_local! {
} }
/// Set the thread-local debug flag /// Set the thread-local debug flag
#[allow(dead_code)]
pub fn set_thread_local_debug(debug: bool) { pub fn set_thread_local_debug(debug: bool) {
DEBUG.with(|d| { DEBUG.with(|d| {
*d.borrow_mut() = debug; *d.borrow_mut() = debug;
@ -18,7 +17,9 @@ pub fn set_thread_local_debug(debug: bool) {
/// Get the current thread-local debug flag /// Get the current thread-local debug flag
pub fn thread_local_debug() -> bool { pub fn thread_local_debug() -> bool {
DEBUG.with(|d| *d.borrow()) DEBUG.with(|d| {
*d.borrow()
})
} }
/// Execute an RFS command with the given arguments /// Execute an RFS command with the given arguments
@ -32,30 +33,30 @@ pub fn thread_local_debug() -> bool {
/// * `Result<CommandResult, RfsError>` - Command result or error /// * `Result<CommandResult, RfsError>` - Command result or error
pub fn execute_rfs_command(args: &[&str]) -> Result<CommandResult, RfsError> { pub fn execute_rfs_command(args: &[&str]) -> Result<CommandResult, RfsError> {
let debug = thread_local_debug(); let debug = thread_local_debug();
// Construct the command string // Construct the command string
let mut cmd = String::from("rfs"); let mut cmd = String::from("rfs");
for arg in args { for arg in args {
cmd.push(' '); cmd.push(' ');
cmd.push_str(arg); cmd.push_str(arg);
} }
if debug { if debug {
println!("Executing RFS command: {}", cmd); println!("Executing RFS command: {}", cmd);
} }
// Execute the command // Execute the command
let result = run_command(&cmd) let result = run_command(&cmd)
.map_err(|e| RfsError::CommandFailed(format!("Failed to execute RFS command: {}", e)))?; .map_err(|e| RfsError::CommandFailed(format!("Failed to execute RFS command: {}", e)))?;
if debug { if debug {
println!("RFS command result: {:?}", result); println!("RFS command result: {:?}", result);
} }
// Check if the command was successful // Check if the command was successful
if !result.success && !result.stderr.is_empty() { if !result.success && !result.stderr.is_empty() {
return Err(RfsError::CommandFailed(result.stderr)); return Err(RfsError::CommandFailed(result.stderr));
} }
Ok(result) Ok(result)
} }

203
src/zinit_client/mod.rs Normal file
View File

@ -0,0 +1,203 @@
use std::sync::{Arc, Mutex, Once};
use std::sync::atomic::{AtomicBool, Ordering};
use lazy_static::lazy_static;
use zinit_client::{Client as ZinitClient, ClientError, Status};
use std::collections::HashMap;
use serde_json::{Map, Value};
// Global Zinit client instance using lazy_static
lazy_static! {
static ref ZINIT_CLIENT: Mutex<Option<Arc<ZinitClientWrapper>>> = Mutex::new(None);
static ref INIT: Once = Once::new();
}
// Wrapper for Zinit client to handle connection
pub struct ZinitClientWrapper {
client: ZinitClient,
initialized: AtomicBool,
}
impl ZinitClientWrapper {
// Create a new Zinit client wrapper
fn new(client: ZinitClient) -> Self {
ZinitClientWrapper {
client,
initialized: AtomicBool::new(false),
}
}
// Initialize the client
async fn initialize(&self) -> Result<(), ClientError> {
if self.initialized.load(Ordering::Relaxed) {
return Ok(());
}
// Try to list services to check if the connection works
let _ = self.client.list().await.map_err(|e| {
eprintln!("Failed to initialize Zinit client: {}", e);
e
})?;
self.initialized.store(true, Ordering::Relaxed);
Ok(())
}
// List all services
pub async fn list(&self) -> Result<HashMap<String, String>, ClientError> {
self.client.list().await
}
// Get status of a service
pub async fn status(&self, name: &str) -> Result<Status, ClientError> {
self.client.status(name).await
}
// Start a service
pub async fn start(&self, name: &str) -> Result<(), ClientError> {
self.client.start(name).await
}
// Stop a service
pub async fn stop(&self, name: &str) -> Result<(), ClientError> {
self.client.stop(name).await
}
// Restart a service
pub async fn restart(&self, name: &str) -> Result<(), ClientError> {
self.client.restart(name).await
}
// Monitor a service
pub async fn monitor(&self, name: &str) -> Result<(), ClientError> {
self.client.monitor(name).await
}
// Forget a service
pub async fn forget(&self, name: &str) -> Result<(), ClientError> {
self.client.forget(name).await
}
// Send a signal to a service
pub async fn kill(&self, name: &str, signal: &str) -> Result<(), ClientError> {
self.client.kill(name, signal).await
}
// Create a new service
pub async fn create_service(&self, name: &str, content: Map<String, Value>) -> Result<String, ClientError> {
self.client.create_service(name, content).await
}
// Delete a service
pub async fn delete_service(&self, name: &str) -> Result<String, ClientError> {
self.client.delete_service(name).await
}
// Get a service configuration
pub async fn get_service(&self, name: &str) -> Result<Value, ClientError> {
self.client.get_service(name).await
}
// Shutdown the system
pub async fn shutdown(&self) -> Result<(), ClientError> {
self.client.shutdown().await
}
// Reboot the system
pub async fn reboot(&self) -> Result<(), ClientError> {
self.client.reboot().await
}
// Start HTTP server
pub async fn start_http_server(&self, address: &str) -> Result<String, ClientError> {
self.client.start_http_server(address).await
}
// Stop HTTP server
pub async fn stop_http_server(&self) -> Result<(), ClientError> {
self.client.stop_http_server().await
}
// Get logs
pub async fn logs(&self, filter: Option<String>) -> Result<Vec<String>, ClientError> {
self.client.logs(filter).await
}
}
// Get the Zinit client instance
pub async fn get_zinit_client(socket_path: &str) -> Result<Arc<ZinitClientWrapper>, ClientError> {
// Check if we already have a client
{
let guard = ZINIT_CLIENT.lock().unwrap();
if let Some(ref client) = &*guard {
return Ok(Arc::clone(client));
}
}
// Create a new client
let client = create_zinit_client(socket_path).await?;
// Store the client globally
{
let mut guard = ZINIT_CLIENT.lock().unwrap();
*guard = Some(Arc::clone(&client));
}
Ok(client)
}
// Create a new Zinit client
async fn create_zinit_client(socket_path: &str) -> Result<Arc<ZinitClientWrapper>, ClientError> {
// Connect via Unix socket
let client = ZinitClient::unix_socket(socket_path).await?;
let wrapper = Arc::new(ZinitClientWrapper::new(client));
// Initialize the client
wrapper.initialize().await?;
Ok(wrapper)
}
// Reset the Zinit client
pub async fn reset(socket_path: &str) -> Result<(), ClientError> {
// Clear the existing client
{
let mut client_guard = ZINIT_CLIENT.lock().unwrap();
*client_guard = None;
}
// Create a new client, only return error if it fails
get_zinit_client(socket_path).await?;
Ok(())
}
// Convenience functions for common operations
// List all services
pub async fn list(socket_path: &str) -> Result<HashMap<String, String>, ClientError> {
let client = get_zinit_client(socket_path).await?;
client.list().await
}
// Get status of a service
pub async fn status(socket_path: &str, name: &str) -> Result<Status, ClientError> {
let client = get_zinit_client(socket_path).await?;
client.status(name).await
}
// Start a service
pub async fn start(socket_path: &str, name: &str) -> Result<(), ClientError> {
let client = get_zinit_client(socket_path).await?;
client.start(name).await
}
// Stop a service
pub async fn stop(socket_path: &str, name: &str) -> Result<(), ClientError> {
let client = get_zinit_client(socket_path).await?;
client.stop(name).await
}
// Restart a service
pub async fn restart(socket_path: &str, name: &str) -> Result<(), ClientError> {
let client = get_zinit_client(socket_path).await?;
client.restart(name).await
}