Compare commits
110 Commits
2cd9faf4fa
...
developmen
Author | SHA1 | Date | |
---|---|---|---|
|
0e63efda61 | ||
|
568a5b0a49 | ||
|
d431705501 | ||
|
b4e370b668 | ||
717cd7b16f | |||
|
e125bb6511 | ||
|
8012a66250 | ||
|
6dead402a2 | ||
|
c94467c205 | ||
|
b737cd6337 | ||
|
455f84528b | ||
|
3e3d0a1d45 | ||
|
511729c477 | ||
|
74217364fa | ||
|
d22fd686b7 | ||
|
c4cdb8126c | ||
|
a35edc2030 | ||
|
a7a7353aa1 | ||
|
4a8d3bfd24 | ||
|
3e617c2489 | ||
|
4d51518f31 | ||
|
e031b03e04 | ||
ba9103685f | |||
dee38eb6c2 | |||
49c879359b | |||
c0df07d6df | |||
6a1e70c484 | |||
e7e8e7daf8 | |||
8a8ead17cb | |||
0e7dba9466 | |||
f0d7636cda | |||
3a6bde02d5 | |||
3a7b323f9a | |||
66d5c8588a | |||
29a06d2bb4 | |||
|
bb39f3e3f2 | ||
|
5194f5245d | ||
a9255de679 | |||
7d05567ad2 | |||
|
c0e11c6510 | ||
|
fedf957079 | ||
|
65e404e517 | ||
944f22be23 | |||
887e66bb17 | |||
|
e5a4a1b634
|
||
|
7f55cf4fba
|
||
|
c26e0e5ad8 | ||
|
365814b424
|
||
|
cc4e087f1d | ||
|
229fef217f | ||
|
dd84ce3f48 | ||
|
7b8b8c662e | ||
|
d29a8fbb67
|
||
|
771df07c25 | ||
|
9a23c4cc09 | ||
|
2014c63b78
|
||
|
2adda10664
|
||
|
7b1908b676
|
||
|
e9b867a36e
|
||
|
78c0fd7871
|
||
|
e44ee83e74
|
||
0c425470a5 | |||
|
3e64a53a83 | ||
|
3225b3f029 | ||
|
3417e2c1ff | ||
|
7add64562e | ||
|
809599d60c | ||
|
25f2ae6fa9 | ||
|
dfe6c91273
|
||
a4438d63e0 | |||
393c4270d4 | |||
495fe92321 | |||
577d80b282 | |||
3f8aecb786 | |||
|
c7a5699798 | ||
|
3a0900fc15 | ||
|
916eabfa42 | ||
a8ed0900fd | |||
e47e163285 | |||
8aa2b2da26 | |||
992481ce1b | |||
516d0177e7 | |||
|
8285fdb7b9 | ||
|
1ebd591f19 | ||
7298645368 | |||
f669bdb84f | |||
654f91b849 | |||
619ce57776 | |||
2695b5f5f7 | |||
7828f82f58 | |||
7a346a1dd1 | |||
07390c3cae | |||
|
138dce66fa | ||
|
49e85ff8e6 | ||
|
f386890a8a | ||
663367ea57 | |||
|
114d63e590 | ||
|
22f87b320e | ||
|
f002445c9e | ||
|
61bd58498a | ||
|
d3c645e8e6 | ||
98ab2e1536 | |||
|
83662736c0 | ||
|
4897eb9133 | ||
|
1286939608 | ||
|
32217b6545 | ||
|
4578b10acb | ||
|
4e166f7750 | ||
|
d6905916ee | ||
|
76582706ab |
73
.github/workflows/rhai-tests.yml
vendored
Normal file
73
.github/workflows/rhai-tests.yml
vendored
Normal file
@@ -0,0 +1,73 @@
|
||||
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()
|
43
.gitignore
vendored
43
.gitignore
vendored
@@ -19,3 +19,46 @@ Cargo.lock
|
||||
# Added by cargo
|
||||
|
||||
/target
|
||||
/rhai_test_template
|
||||
/rhai_test_download
|
||||
/rhai_test_fs
|
||||
run_rhai_tests.log
|
||||
new_location
|
||||
log.txt
|
||||
file.txt
|
||||
fix_doc*
|
||||
|
||||
# Dependencies
|
||||
/node_modules
|
||||
|
||||
# Production
|
||||
/build
|
||||
|
||||
# Generated files
|
||||
.docusaurus
|
||||
.cache-loader
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
bun.lockb
|
||||
bun.lock
|
||||
|
||||
yarn.lock
|
||||
|
||||
build.sh
|
||||
build_dev.sh
|
||||
develop.sh
|
||||
|
||||
docusaurus.config.ts
|
||||
|
||||
sidebars.ts
|
||||
|
||||
tsconfig.json
|
||||
|
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"gitea": {
|
||||
"command": "/Users/despiegk/hero/bin/mcpgitea",
|
||||
"args": [
|
||||
"-t", "stdio",
|
||||
"--host", "https://gitea.com",
|
||||
"--token", "5bd13c898368a2edbfcef43f898a34857b51b37a"
|
||||
],
|
||||
"env": {
|
||||
"GITEA_HOST": "https://git.ourworld.tf/",
|
||||
"GITEA_ACCESS_TOKEN": "5bd13c898368a2edbfcef43f898a34857b51b37a"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
97
Cargo.toml
97
Cargo.toml
@@ -4,40 +4,83 @@ version = "0.1.0"
|
||||
edition = "2021"
|
||||
authors = ["PlanetFirst <info@incubaid.com>"]
|
||||
description = "System Abstraction Layer - A library for easy interaction with operating system features"
|
||||
repository = "https://git.ourworld.tf/herocode/sal"
|
||||
repository = "https://git.threefold.info/herocode/sal"
|
||||
license = "Apache-2.0"
|
||||
keywords = ["system", "os", "abstraction", "platform", "filesystem"]
|
||||
categories = ["os", "filesystem", "api-bindings"]
|
||||
readme = "README.md"
|
||||
|
||||
[dependencies]
|
||||
tera = "1.19.0" # Template engine for text rendering
|
||||
# Cross-platform functionality
|
||||
[workspace]
|
||||
members = [".", "vault", "git", "redisclient", "mycelium", "text", "os", "net", "zinit_client", "process", "virt", "postgresclient", "rhai", "herodo"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.metadata]
|
||||
# Workspace-level metadata
|
||||
rust-version = "1.70.0"
|
||||
|
||||
[workspace.dependencies]
|
||||
# Core shared dependencies with consistent versions
|
||||
anyhow = "1.0.98"
|
||||
base64 = "0.22.1"
|
||||
dirs = "6.0.0"
|
||||
env_logger = "0.11.8"
|
||||
futures = "0.3.30"
|
||||
glob = "0.3.1"
|
||||
lazy_static = "1.4.0"
|
||||
libc = "0.2"
|
||||
cfg-if = "1.0"
|
||||
thiserror = "1.0" # For error handling
|
||||
redis = "0.22.0" # Redis client
|
||||
lazy_static = "1.4.0" # For lazy initialization of static variables
|
||||
regex = "1.8.1" # For regex pattern matching
|
||||
serde = { version = "1.0", features = ["derive"] } # For serialization/deserialization
|
||||
serde_json = "1.0" # For JSON handling
|
||||
glob = "0.3.1" # For file pattern matching
|
||||
tempfile = "3.5" # For temporary file operations
|
||||
log = "0.4" # Logging facade
|
||||
rhai = { version = "1.12.0", features = ["sync"] } # Embedded scripting language
|
||||
rand = "0.8.5" # Random number generation
|
||||
clap = "2.33" # Command-line argument parsing
|
||||
log = "0.4"
|
||||
once_cell = "1.18.0"
|
||||
rand = "0.8.5"
|
||||
regex = "1.8.1"
|
||||
reqwest = { version = "0.12.15", features = ["json"] }
|
||||
rhai = { version = "1.12.0", features = ["sync"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
tempfile = "3.5"
|
||||
thiserror = "2.0.12"
|
||||
tokio = { version = "1.45.0", features = ["full"] }
|
||||
url = "2.4"
|
||||
uuid = { version = "1.16.0", features = ["v4"] }
|
||||
|
||||
# Optional features for specific OS functionality
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
nix = "0.26" # Unix-specific functionality
|
||||
# Database dependencies
|
||||
postgres = "0.19.10"
|
||||
r2d2_postgres = "0.18.2"
|
||||
redis = "0.31.0"
|
||||
tokio-postgres = "0.7.13"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows = { version = "0.48", features = ["Win32_Foundation", "Win32_System_Threading", "Win32_Storage_FileSystem"] }
|
||||
# Crypto dependencies
|
||||
chacha20poly1305 = "0.10.1"
|
||||
k256 = { version = "0.13.4", features = ["ecdsa", "ecdh"] }
|
||||
sha2 = "0.10.7"
|
||||
hex = "0.4"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.5" # For tests that need temporary files/directories
|
||||
# Ethereum dependencies
|
||||
ethers = { version = "2.0.7", features = ["legacy"] }
|
||||
|
||||
[[bin]]
|
||||
name = "herodo"
|
||||
path = "src/bin/herodo.rs"
|
||||
# Platform-specific dependencies
|
||||
nix = "0.30.1"
|
||||
windows = { version = "0.61.1", features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_System_Threading",
|
||||
"Win32_Storage_FileSystem",
|
||||
] }
|
||||
|
||||
# Specialized dependencies
|
||||
zinit-client = "0.3.0"
|
||||
urlencoding = "2.1.3"
|
||||
tokio-test = "0.4.4"
|
||||
|
||||
[dependencies]
|
||||
thiserror = "2.0.12" # For error handling in the main Error enum
|
||||
sal-git = { path = "git" }
|
||||
sal-redisclient = { path = "redisclient" }
|
||||
sal-mycelium = { path = "mycelium" }
|
||||
sal-text = { path = "text" }
|
||||
sal-os = { path = "os" }
|
||||
sal-net = { path = "net" }
|
||||
sal-zinit-client = { path = "zinit_client" }
|
||||
sal-process = { path = "process" }
|
||||
sal-virt = { path = "virt" }
|
||||
sal-postgresclient = { path = "postgresclient" }
|
||||
sal-vault = { path = "vault" }
|
||||
sal-rhai = { path = "rhai" }
|
||||
|
241
README.md
241
README.md
@@ -1,73 +1,228 @@
|
||||
# SAL (System Abstraction Layer)
|
||||
|
||||
A Rust library that provides a unified interface for interacting with operating system features across different platforms. It abstracts away platform-specific details, allowing developers to write cross-platform code with ease.
|
||||
**Version: 0.1.0**
|
||||
|
||||
## Features
|
||||
SAL is a comprehensive Rust library designed to provide a unified and simplified interface for a wide array of system-level operations and interactions. It abstracts platform-specific details, enabling developers to write robust, cross-platform code with greater ease. SAL also includes `herodo`, a powerful command-line tool for executing Rhai scripts that leverage SAL's capabilities for automation and system management tasks.
|
||||
|
||||
- **File System Operations**: Simplified file and directory management
|
||||
- **Process Management**: Create, monitor, and control processes
|
||||
- **System Information**: Access system details and metrics
|
||||
- **Git Integration**: Interface with Git repositories
|
||||
- **Redis Client**: Robust Redis connection management and command execution
|
||||
- **Text Processing**: Utilities for text manipulation and formatting
|
||||
## 🏗️ **Cargo Workspace Structure**
|
||||
|
||||
## Modules
|
||||
SAL is organized as a **Cargo workspace** with 16 specialized crates:
|
||||
|
||||
### Redis Client
|
||||
- **Root Package**: `sal` - Umbrella crate that re-exports all modules
|
||||
- **13 Library Crates**: Specialized SAL modules (git, text, os, net, etc.)
|
||||
- **1 Binary Crate**: `herodo` - Rhai script execution engine
|
||||
- **1 Integration Crate**: `rhai` - Rhai scripting integration layer
|
||||
|
||||
The Redis client module provides a robust wrapper around the Redis client library for Rust, offering:
|
||||
This workspace structure provides excellent build performance, dependency management, and maintainability.
|
||||
|
||||
- Automatic connection management and reconnection
|
||||
- Support for both Unix socket and TCP connections
|
||||
- Database selection via environment variables
|
||||
- Thread-safe global client instance
|
||||
- Simple command execution interface
|
||||
### **🚀 Workspace Benefits**
|
||||
- **Unified Dependency Management**: Shared dependencies across all crates with consistent versions
|
||||
- **Optimized Build Performance**: Parallel compilation and shared build artifacts
|
||||
- **Simplified Testing**: Run tests across all modules with a single command
|
||||
- **Modular Architecture**: Each module is independently maintainable while sharing common infrastructure
|
||||
- **Production Ready**: 100% test coverage with comprehensive Rhai integration tests
|
||||
|
||||
[View Redis Client Documentation](src/redisclient/README.md)
|
||||
## Core Features
|
||||
|
||||
### OS Module
|
||||
SAL offers a broad spectrum of functionalities, including:
|
||||
|
||||
Provides platform-independent interfaces for operating system functionality.
|
||||
- **System Operations**: File and directory management, environment variable access, system information retrieval, and OS-specific commands.
|
||||
- **Process Management**: Create, monitor, control, and interact with system processes.
|
||||
- **Containerization Tools**:
|
||||
- Integration with **Buildah** for building OCI/Docker-compatible container images.
|
||||
- Integration with **nerdctl** for managing containers (run, stop, list, build, etc.).
|
||||
- **Version Control**: Programmatic interaction with Git repositories (clone, commit, push, pull, status, etc.).
|
||||
- **Database Clients**:
|
||||
- **Redis**: Robust client for interacting with Redis servers.
|
||||
- **PostgreSQL**: Client for executing queries and managing PostgreSQL databases.
|
||||
- **Scripting Engine**: In-built support for the **Rhai** scripting language, allowing SAL functionalities to be scripted and automated, primarily through the `herodo` tool.
|
||||
- **Networking & Services**:
|
||||
- **Mycelium**: Tools for Mycelium network peer management and message passing.
|
||||
- **Zinit**: Client for interacting with the Zinit process supervision system.
|
||||
- **RFS (Remote/Virtual Filesystem)**: Mount, manage, pack, and unpack various types of filesystems (local, SSH, S3, WebDAV).
|
||||
- **Text Processing**: A suite of utilities for text manipulation, formatting, and regular expressions.
|
||||
- **Cryptography (`vault`)**: Functions for common cryptographic operations.
|
||||
|
||||
### Git Module
|
||||
## `herodo`: The SAL Scripting Tool
|
||||
|
||||
Tools for interacting with Git repositories programmatically.
|
||||
`herodo` is a command-line utility bundled with SAL that executes Rhai scripts. It empowers users to automate tasks and orchestrate complex workflows by leveraging SAL's diverse modules directly from scripts.
|
||||
|
||||
### Process Module
|
||||
### Usage
|
||||
|
||||
Utilities for process creation, monitoring, and management.
|
||||
```bash
|
||||
# Execute a single Rhai script
|
||||
herodo script.rhai
|
||||
|
||||
### Text Module
|
||||
# Execute a script with arguments
|
||||
herodo script.rhai arg1 arg2
|
||||
|
||||
Text processing utilities for common operations.
|
||||
# Execute all .rhai scripts in a directory
|
||||
herodo /path/to/scripts/
|
||||
```
|
||||
|
||||
## Usage
|
||||
If a directory is provided, `herodo` will execute all `.rhai` scripts within that directory (and its subdirectories) in alphabetical order.
|
||||
|
||||
Add this to your `Cargo.toml`:
|
||||
### Scriptable SAL Modules via `herodo`
|
||||
|
||||
The following SAL modules and functionalities are exposed to the Rhai scripting environment through `herodo`:
|
||||
|
||||
- **OS (`os`)**: Comprehensive file system operations, file downloading & installation, and system package management. [Documentation](os/README.md)
|
||||
- **Process (`process`)**: Robust command and script execution, plus process management (listing, finding, killing, checking command existence). [Documentation](process/README.md)
|
||||
- **Text (`text`)**: String manipulation, prefixing, path/name fixing, text replacement, and templating. [Documentation](text/README.md)
|
||||
- **Net (`net`)**: Network operations, HTTP requests, and connectivity utilities. [Documentation](net/README.md)
|
||||
- **Git (`git`)**: High-level repository management and generic Git command execution with Redis-backed authentication (clone, pull, push, commit, etc.). [Documentation](git/README.md)
|
||||
- **Vault (`vault`)**: Cryptographic operations, keypair management, encryption, decryption, hashing, etc. [Documentation](vault/README.md)
|
||||
- **Redis Client (`redisclient`)**: Execute Redis commands (`redis_get`, `redis_set`, `redis_execute`, etc.). [Documentation](redisclient/README.md)
|
||||
- **PostgreSQL Client (`postgresclient`)**: Execute SQL queries against PostgreSQL databases. [Documentation](postgresclient/README.md)
|
||||
- **Zinit (`zinit_client`)**: Client for Zinit process supervisor (service management, logs). [Documentation](zinit_client/README.md)
|
||||
- **Mycelium (`mycelium`)**: Client for Mycelium decentralized networking API (node info, peer management, messaging). [Documentation](mycelium/README.md)
|
||||
- **Virtualization (`virt`)**:
|
||||
- **Buildah**: OCI/Docker image building functions. [Documentation](virt/README.md)
|
||||
- **nerdctl**: Container lifecycle management (`nerdctl_run`, `nerdctl_stop`, `nerdctl_images`, `nerdctl_image_build`, etc.)
|
||||
- **RFS**: Mount various filesystems (local, SSH, S3, etc.), pack/unpack filesystem layers.
|
||||
|
||||
### Example `herodo` Rhai Script
|
||||
|
||||
```rhai
|
||||
// file: /opt/scripts/example_task.rhai
|
||||
|
||||
// OS operations
|
||||
println("Checking for /tmp/my_app_data...");
|
||||
if !exist("/tmp/my_app_data") {
|
||||
mkdir("/tmp/my_app_data");
|
||||
println("Created directory /tmp/my_app_data");
|
||||
}
|
||||
|
||||
// Redis operations
|
||||
println("Setting Redis key 'app_status' to 'running'");
|
||||
redis_set("app_status", "running");
|
||||
let status = redis_get("app_status");
|
||||
println("Current app_status from Redis: " + status);
|
||||
|
||||
// Process execution
|
||||
println("Listing files in /tmp:");
|
||||
let output = run("ls -la /tmp");
|
||||
println(output.stdout);
|
||||
|
||||
println("Script finished.");
|
||||
```
|
||||
|
||||
Run with: `herodo /opt/scripts/example_task.rhai`
|
||||
|
||||
For more examples, check the individual module test directories (e.g., `text/tests/rhai/`, `os/tests/rhai/`, etc.) in this repository.
|
||||
|
||||
## Using SAL as a Rust Library
|
||||
|
||||
Add SAL as a dependency to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
sal = "0.1.0"
|
||||
sal = "0.1.0" # Or the latest version
|
||||
```
|
||||
|
||||
Basic example:
|
||||
### Rust Example: Using Redis Client
|
||||
|
||||
```rust
|
||||
use sal::redisclient::execute;
|
||||
use redis::cmd;
|
||||
use sal::redisclient::{get_global_client, execute_cmd_with_args};
|
||||
use redis::RedisResult;
|
||||
|
||||
async fn example_redis_interaction() -> RedisResult<()> {
|
||||
// Get a connection from the global pool
|
||||
let mut conn = get_global_client().await?.get_async_connection().await?;
|
||||
|
||||
// Set a value
|
||||
execute_cmd_with_args(&mut conn, "SET", vec!["my_key", "my_value"]).await?;
|
||||
println!("Set 'my_key' to 'my_value'");
|
||||
|
||||
// Get a value
|
||||
let value: String = execute_cmd_with_args(&mut conn, "GET", vec!["my_key"]).await?;
|
||||
println!("Retrieved value for 'my_key': {}", value);
|
||||
|
||||
fn main() -> redis::RedisResult<()> {
|
||||
// Execute a Redis command
|
||||
let mut cmd = redis::cmd("SET");
|
||||
cmd.arg("example_key").arg("example_value");
|
||||
execute(&mut cmd)?;
|
||||
|
||||
// Retrieve the value
|
||||
let mut get_cmd = redis::cmd("GET");
|
||||
get_cmd.arg("example_key");
|
||||
let value: String = execute(&mut get_cmd)?;
|
||||
println!("Value: {}", value);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
if let Err(e) = example_redis_interaction().await {
|
||||
eprintln!("Redis Error: {}", e);
|
||||
}
|
||||
}
|
||||
```
|
||||
*(Note: The Redis client API might have evolved; please refer to `src/redisclient/mod.rs` and its documentation for the most current usage.)*
|
||||
|
||||
## 📦 **Workspace Modules Overview**
|
||||
|
||||
SAL is organized as a Cargo workspace with the following crates:
|
||||
|
||||
### **Core Library Modules**
|
||||
- **`sal-os`**: Core OS interactions, file system operations, environment access
|
||||
- **`sal-process`**: Process creation, management, and control
|
||||
- **`sal-text`**: Utilities for text processing and manipulation
|
||||
- **`sal-net`**: Network operations, HTTP requests, and connectivity utilities
|
||||
|
||||
### **Integration Modules**
|
||||
- **`sal-git`**: Git repository management and operations
|
||||
- **`sal-vault`**: Cryptographic functions and keypair management
|
||||
- **`sal-rhai`**: Integration layer for the Rhai scripting engine, used by `herodo`
|
||||
|
||||
### **Client Modules**
|
||||
- **`sal-redisclient`**: Client for Redis database interactions
|
||||
- **`sal-postgresclient`**: Client for PostgreSQL database interactions
|
||||
- **`sal-zinit-client`**: Client for Zinit process supervisor
|
||||
- **`sal-mycelium`**: Client for Mycelium network operations
|
||||
|
||||
### **Specialized Modules**
|
||||
- **`sal-virt`**: Virtualization-related utilities (buildah, nerdctl, rfs)
|
||||
|
||||
### **Root Package & Binary**
|
||||
- **`sal`**: Root umbrella crate that re-exports all modules
|
||||
- **`herodo`**: Command-line binary for executing Rhai scripts
|
||||
|
||||
## 🔨 **Building SAL**
|
||||
|
||||
Build the entire workspace (all crates) using Cargo:
|
||||
|
||||
```bash
|
||||
# Build all workspace members
|
||||
cargo build --workspace
|
||||
|
||||
# Build for release
|
||||
cargo build --workspace --release
|
||||
|
||||
# Build specific crate
|
||||
cargo build -p sal-text
|
||||
cargo build -p herodo
|
||||
```
|
||||
|
||||
The `herodo` executable will be located at `target/debug/herodo` or `target/release/herodo`.
|
||||
|
||||
## 🧪 **Running Tests**
|
||||
|
||||
### **Rust Unit Tests**
|
||||
```bash
|
||||
# Run all workspace tests
|
||||
cargo test --workspace
|
||||
|
||||
# Run tests for specific crate
|
||||
cargo test -p sal-text
|
||||
cargo test -p sal-os
|
||||
|
||||
# Run only library tests (faster)
|
||||
cargo test --workspace --lib
|
||||
```
|
||||
|
||||
### **Rhai Integration Tests**
|
||||
Run comprehensive Rhai script tests that exercise `herodo` and SAL's scripted functionalities:
|
||||
|
||||
```bash
|
||||
# Run all Rhai integration tests (16 modules)
|
||||
./run_rhai_tests.sh
|
||||
|
||||
# Results: 16/16 modules pass with 100% success rate
|
||||
```
|
||||
|
||||
The Rhai tests validate real-world functionality across all SAL modules and provide comprehensive integration testing.
|
||||
|
||||
## License
|
||||
|
||||
SAL is licensed under the Apache License 2.0. See the [LICENSE](LICENSE) file for details.
|
||||
|
@@ -6,10 +6,12 @@ cd "$(dirname "${BASH_SOURCE[0]}")"
|
||||
|
||||
rm -f ./target/debug/herodo
|
||||
|
||||
# Build the herodo project
|
||||
echo "Building herodo..."
|
||||
cargo build --bin herodo
|
||||
# cargo build --release --bin herodo
|
||||
# Build the herodo project from the herodo package
|
||||
echo "Building herodo from herodo package..."
|
||||
cd herodo
|
||||
cargo build
|
||||
# cargo build --release
|
||||
cd ..
|
||||
|
||||
# Check if the build was successful
|
||||
if [ $? -ne 0 ]; then
|
||||
@@ -20,8 +22,14 @@ fi
|
||||
# Echo a success message
|
||||
echo "Build successful!"
|
||||
|
||||
mkdir -p ~/hero/bin/
|
||||
cp target/debug/herodo ~/hero/bin/herodo
|
||||
if [ "$EUID" -eq 0 ]; then
|
||||
echo "Running as root, copying to /usr/local/bin/"
|
||||
cp target/debug/herodo /usr/local/bin/herodo
|
||||
else
|
||||
echo "Running as non-root user, copying to ~/hero/bin/"
|
||||
mkdir -p ~/hero/bin/
|
||||
cp target/debug/herodo ~/hero/bin/herodo
|
||||
fi
|
||||
|
||||
# Check if a script name was provided
|
||||
if [ $# -eq 1 ]; then
|
||||
|
105
docs/docs/rhai/buildah_module_tests.md
Normal file
105
docs/docs/rhai/buildah_module_tests.md
Normal file
@@ -0,0 +1,105 @@
|
||||
# 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
|
71
docs/docs/rhai/ci_workflow.md
Normal file
71
docs/docs/rhai/ci_workflow.md
Normal file
@@ -0,0 +1,71 @@
|
||||
# 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.
|
81
docs/docs/rhai/git_module_tests.md
Normal file
81
docs/docs/rhai/git_module_tests.md
Normal file
@@ -0,0 +1,81 @@
|
||||
# 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 git/tests/rhai/run_all_tests.rhai
|
||||
```
|
||||
|
||||
To run individual test scripts:
|
||||
|
||||
```bash
|
||||
herodo --path git/tests/rhai/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
|
85
docs/docs/rhai/index.md
Normal file
85
docs/docs/rhai/index.md
Normal file
@@ -0,0 +1,85 @@
|
||||
# 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.
|
386
docs/docs/rhai/mycelium_tutorial.md
Normal file
386
docs/docs/rhai/mycelium_tutorial.md
Normal file
@@ -0,0 +1,386 @@
|
||||
# Mycelium Tutorial for Rhai
|
||||
|
||||
This tutorial explains how to use the Mycelium networking functionality in Rhai scripts. Mycelium is a peer-to-peer networking system that allows nodes to communicate with each other, and the Rhai bindings provide an easy way to interact with Mycelium from your scripts.
|
||||
|
||||
## Introduction
|
||||
|
||||
The Mycelium module for Rhai provides the following capabilities:
|
||||
|
||||
- Getting node information
|
||||
- Managing peers (listing, adding, removing)
|
||||
- Viewing routing information
|
||||
- Sending and receiving messages between nodes
|
||||
|
||||
This tutorial will walk you through using these features with example scripts.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before using the Mycelium functionality in Rhai, you need:
|
||||
|
||||
1. A running Mycelium node accessible via HTTP
|
||||
> See https://github.com/threefoldtech/mycelium
|
||||
2. The Rhai runtime with Mycelium module enabled
|
||||
|
||||
## Basic Mycelium Operations
|
||||
|
||||
Let's start by exploring the basic operations available in Mycelium using the `mycelium_basic.rhai` example.
|
||||
|
||||
### Getting Node Information
|
||||
|
||||
To get information about your Mycelium node:
|
||||
|
||||
```rhai
|
||||
// API URL for Mycelium
|
||||
let api_url = "http://localhost:8989";
|
||||
|
||||
// Get node information
|
||||
print("Getting node information:");
|
||||
try {
|
||||
let node_info = mycelium_get_node_info(api_url);
|
||||
print(`Node subnet: ${node_info.nodeSubnet}`);
|
||||
print(`Node public key: ${node_info.nodePubkey}`);
|
||||
} catch(err) {
|
||||
print(`Error getting node info: ${err}`);
|
||||
}
|
||||
```
|
||||
|
||||
This code:
|
||||
1. Sets the API URL for your Mycelium node
|
||||
2. Calls `mycelium_get_node_info()` to retrieve information about the node
|
||||
3. Prints the node's subnet and public key
|
||||
|
||||
### Managing Peers
|
||||
|
||||
#### Listing Peers
|
||||
|
||||
To list all peers connected to your Mycelium node:
|
||||
|
||||
```rhai
|
||||
// List all peers
|
||||
print("\nListing all peers:");
|
||||
try {
|
||||
let peers = mycelium_list_peers(api_url);
|
||||
|
||||
if peers.is_empty() {
|
||||
print("No peers connected.");
|
||||
} else {
|
||||
for peer in peers {
|
||||
print(`Peer Endpoint: ${peer.endpoint.proto}://${peer.endpoint.socketAddr}`);
|
||||
print(` Type: ${peer.type}`);
|
||||
print(` Connection State: ${peer.connectionState}`);
|
||||
print(` Bytes sent: ${peer.txBytes}`);
|
||||
print(` Bytes received: ${peer.rxBytes}`);
|
||||
}
|
||||
}
|
||||
} catch(err) {
|
||||
print(`Error listing peers: ${err}`);
|
||||
}
|
||||
```
|
||||
|
||||
This code:
|
||||
1. Calls `mycelium_list_peers()` to get all connected peers
|
||||
2. Iterates through the peers and prints their details
|
||||
|
||||
#### Adding a Peer
|
||||
|
||||
To add a new peer to your Mycelium node:
|
||||
|
||||
```rhai
|
||||
// Add a new peer
|
||||
print("\nAdding a new peer:");
|
||||
let new_peer_address = "tcp://65.21.231.58:9651";
|
||||
try {
|
||||
let result = mycelium_add_peer(api_url, new_peer_address);
|
||||
print(`Peer added: ${result.success}`);
|
||||
} catch(err) {
|
||||
print(`Error adding peer: ${err}`);
|
||||
}
|
||||
```
|
||||
|
||||
This code:
|
||||
1. Specifies a peer address to add
|
||||
2. Calls `mycelium_add_peer()` to add the peer to your node
|
||||
3. Prints whether the operation was successful
|
||||
|
||||
#### Removing a Peer
|
||||
|
||||
To remove a peer from your Mycelium node:
|
||||
|
||||
```rhai
|
||||
// Remove a peer
|
||||
print("\nRemoving a peer:");
|
||||
let peer_id = "tcp://65.21.231.58:9651"; // This is the peer we added earlier
|
||||
try {
|
||||
let result = mycelium_remove_peer(api_url, peer_id);
|
||||
print(`Peer removed: ${result.success}`);
|
||||
} catch(err) {
|
||||
print(`Error removing peer: ${err}`);
|
||||
}
|
||||
```
|
||||
|
||||
This code:
|
||||
1. Specifies the peer ID to remove
|
||||
2. Calls `mycelium_remove_peer()` to remove the peer
|
||||
3. Prints whether the operation was successful
|
||||
|
||||
### Viewing Routing Information
|
||||
|
||||
#### Listing Selected Routes
|
||||
|
||||
To list the selected routes in your Mycelium node:
|
||||
|
||||
```rhai
|
||||
// List selected routes
|
||||
print("\nListing selected routes:");
|
||||
try {
|
||||
let routes = mycelium_list_selected_routes(api_url);
|
||||
|
||||
if routes.is_empty() {
|
||||
print("No selected routes.");
|
||||
} else {
|
||||
for route in routes {
|
||||
print(`Subnet: ${route.subnet}`);
|
||||
print(` Next hop: ${route.nextHop}`);
|
||||
print(` Metric: ${route.metric}`);
|
||||
}
|
||||
}
|
||||
} catch(err) {
|
||||
print(`Error listing routes: ${err}`);
|
||||
}
|
||||
```
|
||||
|
||||
This code:
|
||||
1. Calls `mycelium_list_selected_routes()` to get all selected routes
|
||||
2. Iterates through the routes and prints their details
|
||||
|
||||
#### Listing Fallback Routes
|
||||
|
||||
To list the fallback routes in your Mycelium node:
|
||||
|
||||
```rhai
|
||||
// List fallback routes
|
||||
print("\nListing fallback routes:");
|
||||
try {
|
||||
let routes = mycelium_list_fallback_routes(api_url);
|
||||
|
||||
if routes.is_empty() {
|
||||
print("No fallback routes.");
|
||||
} else {
|
||||
for route in routes {
|
||||
print(`Subnet: ${route.subnet}`);
|
||||
print(` Next hop: ${route.nextHop}`);
|
||||
print(` Metric: ${route.metric}`);
|
||||
}
|
||||
}
|
||||
} catch(err) {
|
||||
print(`Error listing fallback routes: ${err}`);
|
||||
}
|
||||
```
|
||||
|
||||
This code:
|
||||
1. Calls `mycelium_list_fallback_routes()` to get all fallback routes
|
||||
2. Iterates through the routes and prints their details
|
||||
|
||||
## Sending Messages
|
||||
|
||||
Now let's look at how to send messages using the `mycelium_send_message.rhai` example.
|
||||
|
||||
```rhai
|
||||
// API URL for Mycelium
|
||||
let api_url = "http://localhost:1111";
|
||||
|
||||
// Send a message
|
||||
print("\nSending a message:");
|
||||
let destination = "5af:ae6b:dcd8:ffdb:b71:7dde:d3:1033"; // Replace with the actual destination IP address
|
||||
let topic = "test_topic";
|
||||
let message = "Hello from Rhai sender!";
|
||||
let deadline_secs = -10; // Seconds we wait for a reply
|
||||
|
||||
try {
|
||||
print(`Attempting to send message to ${destination} on topic '${topic}'`);
|
||||
let result = mycelium_send_message(api_url, destination, topic, message, deadline_secs);
|
||||
print(`result: ${result}`);
|
||||
print(`Message sent: ${result.success}`);
|
||||
if result.id != "" {
|
||||
print(`Message ID: ${result.id}`);
|
||||
}
|
||||
} catch(err) {
|
||||
print(`Error sending message: ${err}`);
|
||||
}
|
||||
```
|
||||
|
||||
This code:
|
||||
1. Sets the API URL for your Mycelium node
|
||||
2. Specifies the destination IP address, topic, message content, and deadline
|
||||
3. Calls `mycelium_send_message()` to send the message
|
||||
4. Prints the result, including the message ID if successful
|
||||
|
||||
### Important Parameters for Sending Messages
|
||||
|
||||
- `api_url`: The URL of your Mycelium node's API
|
||||
- `destination`: The IP address of the destination node
|
||||
- `topic`: The topic to send the message on (must match what the receiver is listening for)
|
||||
- `message`: The content of the message
|
||||
- `deadline_secs`: Time in seconds to wait for a reply. Use a negative value if you don't want to wait for a reply.
|
||||
|
||||
## Receiving Messages
|
||||
|
||||
Now let's look at how to receive messages using the `mycelium_receive_message.rhai` example.
|
||||
|
||||
```rhai
|
||||
// API URL for Mycelium
|
||||
let api_url = "http://localhost:2222";
|
||||
|
||||
// Receive messages
|
||||
print("\nReceiving messages:");
|
||||
let receive_topic = "test_topic";
|
||||
let wait_deadline_secs = 100;
|
||||
|
||||
print(`Listening for messages on topic '${receive_topic}'...`);
|
||||
try {
|
||||
let messages = mycelium_receive_messages(api_url, receive_topic, wait_deadline_secs);
|
||||
|
||||
if messages.is_empty() {
|
||||
// print("No new messages received in this poll.");
|
||||
} else {
|
||||
print("Received a message:");
|
||||
print(` Message id: ${messages.id}`);
|
||||
print(` Message from: ${messages.srcIp}`);
|
||||
print(` Topic: ${messages.topic}`);
|
||||
print(` Payload: ${messages.payload}`);
|
||||
}
|
||||
} catch(err) {
|
||||
print(`Error receiving messages: ${err}`);
|
||||
}
|
||||
|
||||
print("Finished attempting to receive messages.");
|
||||
```
|
||||
|
||||
This code:
|
||||
1. Sets the API URL for your Mycelium node
|
||||
2. Specifies the topic to listen on and how long to wait for messages
|
||||
3. Calls `mycelium_receive_messages()` to receive messages
|
||||
4. Processes and prints any received messages
|
||||
|
||||
### Important Parameters for Receiving Messages
|
||||
|
||||
- `api_url`: The URL of your Mycelium node's API
|
||||
- `receive_topic`: The topic to listen for messages on (must match what the sender is using)
|
||||
- `wait_deadline_secs`: Time in seconds to wait for messages to arrive. The function will block for this duration if no messages are immediately available.
|
||||
|
||||
## Complete Messaging Example
|
||||
|
||||
To set up a complete messaging system, you would typically run two instances of Mycelium (node A sender, node B receiver).
|
||||
|
||||
1. Run the `mycelium_receive_message.rhai` script to listen for messages. **Fill in the API address of node B**.
|
||||
2. Run the `mycelium_send_message.rhai` script to send messages. **Fill in the API address of node A, and fill in the overlay address of node B as destination**.
|
||||
|
||||
### Setting Up the Receiver
|
||||
|
||||
First, start a Mycelium node and run the receiver script:
|
||||
|
||||
```rhai
|
||||
// API URL for Mycelium
|
||||
let api_url = "http://localhost:2222"; // Your receiver node's API URL
|
||||
|
||||
// Receive messages
|
||||
let receive_topic = "test_topic";
|
||||
let wait_deadline_secs = 100; // Wait up to 100 seconds for messages
|
||||
|
||||
print(`Listening for messages on topic '${receive_topic}'...`);
|
||||
try {
|
||||
let messages = mycelium_receive_messages(api_url, receive_topic, wait_deadline_secs);
|
||||
|
||||
if messages.is_empty() {
|
||||
print("No new messages received in this poll.");
|
||||
} else {
|
||||
print("Received a message:");
|
||||
print(` Message id: ${messages.id}`);
|
||||
print(` Message from: ${messages.srcIp}`);
|
||||
print(` Topic: ${messages.topic}`);
|
||||
print(` Payload: ${messages.payload}`);
|
||||
}
|
||||
} catch(err) {
|
||||
print(`Error receiving messages: ${err}`);
|
||||
}
|
||||
```
|
||||
|
||||
### Setting Up the Sender
|
||||
|
||||
Then, on another Mycelium node, run the sender script:
|
||||
|
||||
```rhai
|
||||
// API URL for Mycelium
|
||||
let api_url = "http://localhost:1111"; // Your sender node's API URL
|
||||
|
||||
// Send a message
|
||||
let destination = "5af:ae6b:dcd8:ffdb:b71:7dde:d3:1033"; // The receiver node's IP address
|
||||
let topic = "test_topic"; // Must match the receiver's topic
|
||||
let message = "Hello from Rhai sender!";
|
||||
let deadline_secs = -10; // Don't wait for a reply
|
||||
|
||||
try {
|
||||
print(`Attempting to send message to ${destination} on topic '${topic}'`);
|
||||
let result = mycelium_send_message(api_url, destination, topic, message, deadline_secs);
|
||||
print(`Message sent: ${result.success}`);
|
||||
if result.id != "" {
|
||||
print(`Message ID: ${result.id}`);
|
||||
}
|
||||
} catch(err) {
|
||||
print(`Error sending message: ${err}`);
|
||||
}
|
||||
```
|
||||
|
||||
### Example: setting up 2 different Mycelium peers on same the host and sending/receiving a message
|
||||
|
||||
#### Obtain Mycelium
|
||||
|
||||
- Download the latest Mycelium binary from https://github.com/threefoldtech/mycelium/releases/
|
||||
- Or compile from source
|
||||
|
||||
#### Setup
|
||||
- Create two different private key files. Each key file should contain exactely 32 bytes. In this example we'll save these files as `sender.bin` and `receiver.bin`. Note: generate your own 32-byte key files, the values below are just used as examples.
|
||||
> `echo '9f3d72c1a84be6f027bba94cde015ee839cedb2ac4f2822bfc94449e3e2a1c6a' > sender.bin`
|
||||
|
||||
> `echo 'e81c5a76f42bd9a3c73fe0bb2196acdfb6348e99d0b01763a2e57ce3a4e8f5dd' > receiver.bin`
|
||||
|
||||
#### Start the nodes
|
||||
- **Sender**: this node will have the API server hosted on `127.0.0.1:1111` and the JSON-RPC server on `127.0.0.1:8991`.
|
||||
> `sudo ./mycelium --key-file sender.bin --disable-peer-discovery --disable-quic --no-tun --api-addr 127.0.0.1:1111 --jsonrpc-addr 127.0.0.1:8991`
|
||||
|
||||
- **Receiver**: this node will have the API server hosted on `127.0.0.1:2222` and the JSON-RPC server on `127.0.0.1:8992`.
|
||||
> `sudo ./mycelium --key-file receiver.bin --disable-peer-discovery --disable-quic --no-tun --api-addr 127.0.0.1:2222 --jsonrpc-addr 127.0.0.1:8992 --peers tcp://<UNDERLAY_IP_SENDER>:9651`
|
||||
- Obtain the Mycelium overlay IP by running `./mycelium --key-file receiver.bin --api-addr 127.0.0.1:2222 inspect`. **Replace this IP as destination in the [mycelium_send_message.rhai](../../../examples/mycelium/mycelium_send_message.rhai) example**.
|
||||
|
||||
#### Execute the examples
|
||||
- First build by executing `./build_herdo.sh` from the SAL root directory
|
||||
- `cd target/debug`
|
||||
|
||||
- Run the sender script: `sudo ./herodo --path ../../examples/mycelium/mycelium_send_message.rhai`
|
||||
```
|
||||
Executing: ../../examples/mycelium/mycelium_send_message.rhai
|
||||
|
||||
Sending a message:
|
||||
Attempting to send message to 50e:6d75:4568:366e:f75:2ac3:bbb1:3fdd on topic 'test_topic'
|
||||
result: #{"id": "bfd47dc689a7b826"}
|
||||
Message sent:
|
||||
Message ID: bfd47dc689a7b826
|
||||
Script executed successfull
|
||||
```
|
||||
|
||||
- Run the receiver script: `sudo ./herodo --path ../../examples/mycelium/mycelium_receive_message.rhai`
|
||||
```
|
||||
Executing: ../../examples/mycelium/mycelium_receive_message.rhai
|
||||
|
||||
Receiving messages:
|
||||
Listening for messages on topic 'test_topic'...
|
||||
Received a message:
|
||||
Message id: bfd47dc689a7b826
|
||||
Message from: 45d:26e1:a413:9d08:80ce:71c6:a931:4315
|
||||
Topic: dGVzdF90b3BpYw==
|
||||
Payload: SGVsbG8gZnJvbSBSaGFpIHNlbmRlciE=
|
||||
Finished attempting to receive messages.
|
||||
Script executed successfully
|
||||
```
|
||||
> Decoding the payload `SGVsbG8gZnJvbSBSaGFpIHNlbmRlciE=` results in the expected `Hello from Rhai sender!` message. Mission succesful!
|
||||
|
116
docs/docs/rhai/nerdctl_module_tests.md
Normal file
116
docs/docs/rhai/nerdctl_module_tests.md
Normal file
@@ -0,0 +1,116 @@
|
||||
# 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
|
105
docs/docs/rhai/os_module_tests.md
Normal file
105
docs/docs/rhai/os_module_tests.md
Normal file
@@ -0,0 +1,105 @@
|
||||
# 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
|
188
docs/docs/rhai/postgresclient_module_tests.md
Normal file
188
docs/docs/rhai/postgresclient_module_tests.md
Normal file
@@ -0,0 +1,188 @@
|
||||
# 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}`);
|
||||
}
|
||||
}
|
||||
```
|
79
docs/docs/rhai/process_module_tests.md
Normal file
79
docs/docs/rhai/process_module_tests.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# 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
|
125
docs/docs/rhai/redisclient_module_tests.md
Normal file
125
docs/docs/rhai/redisclient_module_tests.md
Normal file
@@ -0,0 +1,125 @@
|
||||
# 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
|
113
docs/docs/rhai/rfs_module_tests.md
Normal file
113
docs/docs/rhai/rfs_module_tests.md
Normal file
@@ -0,0 +1,113 @@
|
||||
# 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
|
76
docs/docs/rhai/running_tests.md
Normal file
76
docs/docs/rhai/running_tests.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# 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
|
129
docs/docs/rhai/text_module_tests.md
Normal file
129
docs/docs/rhai/text_module_tests.md
Normal file
@@ -0,0 +1,129 @@
|
||||
# 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
|
@@ -1,4 +1,4 @@
|
||||
// File: /root/code/git.ourworld.tf/herocode/sal/examples/container_example.rs
|
||||
// File: /root/code/git.threefold.info/herocode/sal/examples/container_example.rs
|
||||
|
||||
use std::error::Error;
|
||||
use sal::virt::nerdctl::Container;
|
||||
|
@@ -2,7 +2,7 @@
|
||||
// Demonstrates file system operations using SAL
|
||||
|
||||
// Create a test directory
|
||||
let test_dir = "rhai_test_dir";
|
||||
let test_dir = "/tmp/rhai_test_dir";
|
||||
println(`Creating directory: ${test_dir}`);
|
||||
let mkdir_result = mkdir(test_dir);
|
||||
println(`Directory creation result: ${mkdir_result}`);
|
||||
@@ -61,4 +61,4 @@ for file in files {
|
||||
// delete(test_dir);
|
||||
// println("Cleanup complete");
|
||||
|
||||
"File operations script completed successfully!"
|
||||
"File operations script completed successfully!"
|
||||
|
@@ -121,16 +121,16 @@ println(`Using local image: ${local_image_name}`);
|
||||
|
||||
// Tag the image with the localhost prefix for nerdctl compatibility
|
||||
println(`Tagging image as ${local_image_name}...`);
|
||||
let tag_result = bah_image_tag(final_image_name, local_image_name);
|
||||
let tag_result = image_tag(final_image_name, local_image_name);
|
||||
|
||||
// Print a command to check if the image exists in buildah
|
||||
println("\nTo verify the image was created with buildah, run:");
|
||||
println("buildah images");
|
||||
|
||||
// Note: If nerdctl cannot find the image, you may need to push it to a registry
|
||||
println("\nNote: If nerdctl cannot find the image, you may need to push it to a registry:");
|
||||
println("buildah push localhost/custom-golang-nginx:latest docker://localhost:5000/custom-golang-nginx:latest");
|
||||
println("nerdctl pull localhost:5000/custom-golang-nginx:latest");
|
||||
// println("\nNote: If nerdctl cannot find the image, you may need to push it to a registry:");
|
||||
// println("buildah push localhost/custom-golang-nginx:latest docker://localhost:5000/custom-golang-nginx:latest");
|
||||
// println("nerdctl pull localhost:5000/custom-golang-nginx:latest");
|
||||
|
||||
let container = nerdctl_container_from_image("golang-nginx-demo", local_image_name)
|
||||
.with_detach(true)
|
||||
|
44
examples/containers/buildah_run.rhai
Normal file
44
examples/containers/buildah_run.rhai
Normal file
@@ -0,0 +1,44 @@
|
||||
|
||||
// Now use nerdctl to run a container from the new image
|
||||
println("\nStarting container from the new image using nerdctl...");
|
||||
|
||||
// Create a container using the builder pattern
|
||||
// Use localhost/ prefix to ensure nerdctl uses the local image
|
||||
let local_image_name = "localhost/custom-golang-nginx:latest";
|
||||
println(`Using local image: ${local_image_name}`);
|
||||
|
||||
// Import the image from buildah to nerdctl
|
||||
println("Importing image from buildah to nerdctl...");
|
||||
process_run("buildah", ["push", "custom-golang-nginx:latest", "docker-daemon:localhost/custom-golang-nginx:latest"]);
|
||||
|
||||
let tag_result = nerdctl_image_tag("custom-golang-nginx:latest", local_image_name);
|
||||
|
||||
// Tag the image with the localhost prefix for nerdctl compatibility
|
||||
// println(`Tagging image as ${local_image_name}...`);
|
||||
// let tag_result = bah_image_tag(final_image_name, local_image_name);
|
||||
|
||||
// Print a command to check if the image exists in buildah
|
||||
println("\nTo verify the image was created with buildah, run:");
|
||||
println("buildah images");
|
||||
|
||||
// Note: If nerdctl cannot find the image, you may need to push it to a registry
|
||||
// println("\nNote: If nerdctl cannot find the image, you may need to push it to a registry:");
|
||||
// println("buildah push localhost/custom-golang-nginx:latest docker://localhost:5000/custom-golang-nginx:latest");
|
||||
// println("nerdctl pull localhost:5000/custom-golang-nginx:latest");
|
||||
|
||||
let container = nerdctl_container_from_image("golang-nginx-demo", local_image_name)
|
||||
.with_detach(true)
|
||||
.with_port("8081:80") // Map port 80 in the container to 8080 on the host
|
||||
.with_restart_policy("unless-stopped")
|
||||
.build();
|
||||
|
||||
// Start the container
|
||||
let start_result = container.start();
|
||||
|
||||
println("\nWorkflow completed successfully!");
|
||||
println("The web server should be running at http://localhost:8081");
|
||||
println("You can check container logs with: nerdctl logs golang-nginx-demo");
|
||||
println("To stop the container: nerdctl stop golang-nginx-demo");
|
||||
println("To remove the container: nerdctl rm golang-nginx-demo");
|
||||
|
||||
"Buildah and nerdctl workflow completed successfully!"
|
@@ -1,42 +0,0 @@
|
||||
|
||||
|
||||
|
||||
fn nerdctl_download(){
|
||||
let name="nerdctl";
|
||||
let url="https://github.com/containerd/nerdctl/releases/download/v2.0.4/nerdctl-2.0.4-linux-amd64.tar.gz";
|
||||
download(url,`/tmp/${name}`,20000);
|
||||
copy(`/tmp/${name}/*`,"/root/hero/bin/");
|
||||
delete(`/tmp/${name}`);
|
||||
|
||||
let name="containerd";
|
||||
let url="https://github.com/containerd/containerd/releases/download/v2.0.4/containerd-2.0.4-linux-amd64.tar.gz";
|
||||
download(url,`/tmp/${name}`,20000);
|
||||
copy(`/tmp/${name}/bin/*`,"/root/hero/bin/");
|
||||
delete(`/tmp/${name}`);
|
||||
|
||||
run("apt-get -y install buildah runc");
|
||||
|
||||
let url="https://github.com/threefoldtech/rfs/releases/download/v2.0.6/rfs";
|
||||
download_file(url,`/tmp/rfs`,10000);
|
||||
chmod_exec("/tmp/rfs");
|
||||
mv(`/tmp/rfs`,"/root/hero/bin/");
|
||||
|
||||
}
|
||||
|
||||
fn ipfs_download(){
|
||||
let name="ipfs";
|
||||
let url="https://github.com/ipfs/kubo/releases/download/v0.34.1/kubo_v0.34.1_linux-amd64.tar.gz";
|
||||
download(url,`/tmp/${name}`,20);
|
||||
copy(`/tmp/${name}/kubo/ipfs`,"/root/hero/bin/ipfs");
|
||||
// delete(`/tmp/${name}`);
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
nerdctl_download();
|
||||
// ipfs_download();
|
||||
|
||||
"done"
|
64
examples/hero_vault/README.md
Normal file
64
examples/hero_vault/README.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# Hero Vault Cryptography Examples
|
||||
|
||||
This directory contains examples demonstrating the Hero Vault cryptography functionality integrated into the SAL project.
|
||||
|
||||
## Overview
|
||||
|
||||
Hero Vault provides cryptographic operations including:
|
||||
|
||||
- Key space management (creation, loading, encryption, decryption)
|
||||
- Keypair management (creation, selection, listing)
|
||||
- Digital signatures (signing and verification)
|
||||
- Symmetric encryption (key generation, encryption, decryption)
|
||||
- Ethereum wallet functionality
|
||||
- Smart contract interactions
|
||||
- Key-value store with encryption
|
||||
|
||||
## Example Files
|
||||
|
||||
- `example.rhai` - Basic example demonstrating key management, signing, and encryption
|
||||
- `advanced_example.rhai` - Advanced example with error handling, conditional logic, and more complex operations
|
||||
- `key_persistence_example.rhai` - Demonstrates creating and saving a key space to disk
|
||||
- `load_existing_space.rhai` - Shows how to load a previously created key space and use its keypairs
|
||||
- `contract_example.rhai` - Demonstrates loading a contract ABI and interacting with smart contracts
|
||||
- `agung_send_transaction.rhai` - Demonstrates sending native tokens on the Agung network
|
||||
- `agung_contract_with_args.rhai` - Shows how to interact with contracts with arguments on Agung
|
||||
|
||||
## Running the Examples
|
||||
|
||||
You can run the examples using the `herodo` tool that comes with the SAL project:
|
||||
|
||||
```bash
|
||||
# Run a single example
|
||||
herodo --path example.rhai
|
||||
|
||||
# Run all examples using the provided script
|
||||
./run_examples.sh
|
||||
```
|
||||
|
||||
## Key Space Storage
|
||||
|
||||
Key spaces are stored in the `~/.hero-vault/key-spaces/` directory by default. Each key space is stored in a separate JSON file named after the key space (e.g., `my_space.json`).
|
||||
|
||||
## Ethereum Functionality
|
||||
|
||||
The Hero Vault module provides comprehensive Ethereum wallet functionality:
|
||||
|
||||
- Creating and managing wallets for different networks
|
||||
- Sending ETH transactions
|
||||
- Checking balances
|
||||
- Interacting with smart contracts (read and write functions)
|
||||
- Support for multiple networks (Ethereum, Gnosis, Peaq, Agung, etc.)
|
||||
|
||||
## Security
|
||||
|
||||
Key spaces are encrypted with ChaCha20Poly1305 using a key derived from the provided password. The encryption ensures that the key material is secure at rest.
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use Strong Passwords**: Since the security of your key spaces depends on the strength of your passwords, use strong, unique passwords.
|
||||
2. **Backup Key Spaces**: Regularly backup your key spaces directory to prevent data loss.
|
||||
3. **Script Organization**: Split your scripts into logical units, with separate scripts for key creation and key usage.
|
||||
4. **Error Handling**: Always check the return values of functions to ensure operations succeeded before proceeding.
|
||||
5. **Network Selection**: When working with Ethereum functionality, be explicit about which network you're targeting to avoid confusion.
|
||||
6. **Gas Management**: For Ethereum transactions, consider gas costs and set appropriate gas limits.
|
233
examples/hero_vault/advanced_example.rhai
Normal file
233
examples/hero_vault/advanced_example.rhai
Normal file
@@ -0,0 +1,233 @@
|
||||
// Advanced Rhai script example for Hero Vault Cryptography Module
|
||||
// This script demonstrates conditional logic, error handling, and more complex operations
|
||||
|
||||
// Function to create a key space with error handling
|
||||
fn setup_key_space(name, password) {
|
||||
print("Attempting: Create key space: " + name);
|
||||
let result = create_key_space(name, password);
|
||||
|
||||
if result {
|
||||
print("✅ Create key space succeeded!");
|
||||
return true;
|
||||
} else {
|
||||
print("❌ Create key space failed!");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Function to create and select a keypair
|
||||
fn setup_keypair(name, password) {
|
||||
print("Attempting: Create keypair: " + name);
|
||||
let result = create_keypair(name, password);
|
||||
|
||||
if result {
|
||||
print("✅ Create keypair succeeded!");
|
||||
|
||||
print("Attempting: Select keypair: " + name);
|
||||
let selected = select_keypair(name);
|
||||
|
||||
if selected {
|
||||
print("✅ Select keypair succeeded!");
|
||||
return true;
|
||||
} else {
|
||||
print("❌ Select keypair failed!");
|
||||
}
|
||||
} else {
|
||||
print("❌ Create keypair failed!");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Function to sign multiple messages
|
||||
fn sign_messages(messages) {
|
||||
let signatures = [];
|
||||
|
||||
for message in messages {
|
||||
print("Signing message: " + message);
|
||||
print("Attempting: Sign message");
|
||||
let signature = sign(message);
|
||||
|
||||
if signature != "" {
|
||||
print("✅ Sign message succeeded!");
|
||||
signatures.push(#{
|
||||
message: message,
|
||||
signature: signature
|
||||
});
|
||||
} else {
|
||||
print("❌ Sign message failed!");
|
||||
}
|
||||
}
|
||||
|
||||
return signatures;
|
||||
}
|
||||
|
||||
// Function to verify signatures
|
||||
fn verify_signatures(signed_messages) {
|
||||
let results = [];
|
||||
|
||||
for item in signed_messages {
|
||||
let message = item.message;
|
||||
let signature = item.signature;
|
||||
|
||||
print("Verifying signature for: " + message);
|
||||
print("Attempting: Verify signature");
|
||||
let is_valid = verify(message, signature);
|
||||
|
||||
if is_valid {
|
||||
print("✅ Verify signature succeeded!");
|
||||
} else {
|
||||
print("❌ Verify signature failed!");
|
||||
}
|
||||
|
||||
results.push(#{
|
||||
message: message,
|
||||
valid: is_valid
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
// Function to encrypt multiple messages
|
||||
fn encrypt_messages(messages) {
|
||||
// Generate a symmetric key
|
||||
print("Attempting: Generate symmetric key");
|
||||
let key = generate_key();
|
||||
|
||||
if key == "" {
|
||||
print("❌ Generate symmetric key failed!");
|
||||
return [];
|
||||
}
|
||||
|
||||
print("✅ Generate symmetric key succeeded!");
|
||||
print("Using key: " + key);
|
||||
let encrypted_messages = [];
|
||||
|
||||
for message in messages {
|
||||
print("Encrypting message: " + message);
|
||||
print("Attempting: Encrypt message");
|
||||
let encrypted = encrypt(key, message);
|
||||
|
||||
if encrypted != "" {
|
||||
print("✅ Encrypt message succeeded!");
|
||||
encrypted_messages.push(#{
|
||||
original: message,
|
||||
encrypted: encrypted,
|
||||
key: key
|
||||
});
|
||||
} else {
|
||||
print("❌ Encrypt message failed!");
|
||||
}
|
||||
}
|
||||
|
||||
return encrypted_messages;
|
||||
}
|
||||
|
||||
// Function to decrypt messages
|
||||
fn decrypt_messages(encrypted_messages) {
|
||||
let decrypted_messages = [];
|
||||
|
||||
for item in encrypted_messages {
|
||||
let encrypted = item.encrypted;
|
||||
let key = item.key;
|
||||
let original = item.original;
|
||||
|
||||
print("Decrypting message...");
|
||||
print("Attempting: Decrypt message");
|
||||
let decrypted = decrypt(key, encrypted);
|
||||
|
||||
if decrypted != false {
|
||||
let success = decrypted == original;
|
||||
|
||||
decrypted_messages.push(#{
|
||||
decrypted: decrypted,
|
||||
original: original,
|
||||
success: success
|
||||
});
|
||||
|
||||
if success {
|
||||
print("Decryption matched original ✅");
|
||||
} else {
|
||||
print("Decryption did not match original ❌");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return decrypted_messages;
|
||||
}
|
||||
|
||||
// Main script execution
|
||||
print("=== Advanced Cryptography Script ===");
|
||||
|
||||
// Set up key space
|
||||
let space_name = "advanced_space";
|
||||
let password = "secure_password123";
|
||||
|
||||
if setup_key_space(space_name, password) {
|
||||
print("\n--- Key space setup complete ---\n");
|
||||
|
||||
// Set up keypair
|
||||
if setup_keypair("advanced_keypair", password) {
|
||||
print("\n--- Keypair setup complete ---\n");
|
||||
|
||||
// Define messages to sign
|
||||
let messages = [
|
||||
"This is the first message to sign",
|
||||
"Here's another message that needs signing",
|
||||
"And a third message for good measure"
|
||||
];
|
||||
|
||||
// Sign messages
|
||||
print("\n--- Signing Messages ---\n");
|
||||
let signed_messages = sign_messages(messages);
|
||||
|
||||
// Verify signatures
|
||||
print("\n--- Verifying Signatures ---\n");
|
||||
let verification_results = verify_signatures(signed_messages);
|
||||
|
||||
// Count successful verifications
|
||||
let successful_verifications = verification_results.filter(|r| r.valid).len();
|
||||
print("Successfully verified " + successful_verifications + " out of " + verification_results.len() + " signatures");
|
||||
|
||||
// Encrypt messages
|
||||
print("\n--- Encrypting Messages ---\n");
|
||||
let encrypted_messages = encrypt_messages(messages);
|
||||
|
||||
// Decrypt messages
|
||||
print("\n--- Decrypting Messages ---\n");
|
||||
let decryption_results = decrypt_messages(encrypted_messages);
|
||||
|
||||
// Count successful decryptions
|
||||
let successful_decryptions = decryption_results.filter(|r| r.success).len();
|
||||
print("Successfully decrypted " + successful_decryptions + " out of " + decryption_results.len() + " messages");
|
||||
|
||||
// Create Ethereum wallet
|
||||
print("\n--- Creating Ethereum Wallet ---\n");
|
||||
print("Attempting: Create Ethereum wallet");
|
||||
let wallet_created = create_ethereum_wallet();
|
||||
|
||||
if wallet_created {
|
||||
print("✅ Create Ethereum wallet succeeded!");
|
||||
|
||||
print("Attempting: Get Ethereum address");
|
||||
let address = get_ethereum_address();
|
||||
|
||||
if address != "" {
|
||||
print("✅ Get Ethereum address succeeded!");
|
||||
print("Ethereum wallet address: " + address);
|
||||
} else {
|
||||
print("❌ Get Ethereum address failed!");
|
||||
}
|
||||
} else {
|
||||
print("❌ Create Ethereum wallet failed!");
|
||||
}
|
||||
|
||||
print("\n=== Script execution completed successfully! ===");
|
||||
} else {
|
||||
print("Failed to set up keypair. Aborting script.");
|
||||
}
|
||||
} else {
|
||||
print("Failed to set up key space. Aborting script.");
|
||||
}
|
152
examples/hero_vault/agung_contract_with_args.rhai
Normal file
152
examples/hero_vault/agung_contract_with_args.rhai
Normal file
@@ -0,0 +1,152 @@
|
||||
// Example Rhai script for testing contract functions with arguments on Agung network
|
||||
// This script demonstrates how to use call_contract_read and call_contract_write with arguments
|
||||
|
||||
// Step 1: Set up wallet and network
|
||||
let space_name = "agung_contract_args_demo";
|
||||
let password = "secure_password123";
|
||||
let private_key = "51c194d20bcd25360a3aa94426b3b60f738007e42f22e1bc97821c65c353e6d2";
|
||||
let network_name = "agung";
|
||||
|
||||
print("=== Testing Contract Functions With Arguments on Agung Network ===\n");
|
||||
|
||||
// Create a key space
|
||||
print("Creating key space: " + space_name);
|
||||
if create_key_space(space_name, password) {
|
||||
print("✓ Key space created successfully");
|
||||
|
||||
// Create a keypair
|
||||
print("\nCreating keypair...");
|
||||
if create_keypair("contract_key", password) {
|
||||
print("✓ Created contract keypair");
|
||||
|
||||
// Create a wallet from the private key for the Agung network
|
||||
print("\nCreating wallet from private key for Agung network...");
|
||||
if create_wallet_from_private_key_for_network(private_key, network_name) {
|
||||
print("✓ Wallet created successfully");
|
||||
|
||||
// Get the wallet address
|
||||
let wallet_address = get_wallet_address_for_network(network_name);
|
||||
print("Wallet address: " + wallet_address);
|
||||
|
||||
// Check wallet balance
|
||||
print("\nChecking wallet balance...");
|
||||
let balance = get_balance(network_name, wallet_address);
|
||||
if balance != "" {
|
||||
print("Wallet balance: " + balance + " wei");
|
||||
|
||||
// Define a simple ERC-20 token contract ABI (partial)
|
||||
let token_abi = `[
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "name",
|
||||
"outputs": [{"name": "", "type": "string"}],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "symbol",
|
||||
"outputs": [{"name": "", "type": "string"}],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "decimals",
|
||||
"outputs": [{"name": "", "type": "uint8"}],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [{"name": "_owner", "type": "address"}],
|
||||
"name": "balanceOf",
|
||||
"outputs": [{"name": "balance", "type": "uint256"}],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [{"name": "_to", "type": "address"}, {"name": "_value", "type": "uint256"}],
|
||||
"name": "transfer",
|
||||
"outputs": [{"name": "", "type": "bool"}],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
}
|
||||
]`;
|
||||
|
||||
// For this example, we'll use a test token contract on Agung
|
||||
let token_address = "0x7267B587E4416537060C6bF0B06f6Fd421106650";
|
||||
|
||||
print("\nLoading contract ABI...");
|
||||
let contract = load_contract_abi(network_name, token_address, token_abi);
|
||||
|
||||
if contract != "" {
|
||||
print("✓ Contract loaded successfully");
|
||||
|
||||
// First, let's try to read some data from the contract
|
||||
print("\nReading contract data...");
|
||||
|
||||
// Try to get token name (no arguments)
|
||||
let token_name = call_contract_read(contract, "name");
|
||||
print("Token name: " + token_name);
|
||||
|
||||
// Try to get token symbol (no arguments)
|
||||
let token_symbol = call_contract_read(contract, "symbol");
|
||||
print("Token symbol: " + token_symbol);
|
||||
|
||||
// Try to get token decimals (no arguments)
|
||||
let token_decimals = call_contract_read(contract, "decimals");
|
||||
print("Token decimals: " + token_decimals);
|
||||
|
||||
// Try to get token balance (with address argument)
|
||||
print("\nCalling balanceOf with address argument...");
|
||||
let balance = call_contract_read(contract, "balanceOf", [wallet_address]);
|
||||
print("Token balance: " + balance);
|
||||
|
||||
// Now, let's try to execute a write function with arguments
|
||||
print("\nExecuting contract write function with arguments...");
|
||||
|
||||
// Define a recipient address and amount for the transfer
|
||||
// Using a random valid address on the network
|
||||
let recipient = "0xEEdf3468E8F232A7a03D49b674bA44740C8BD8Be";
|
||||
let amount = 1000000; // Changed from string to number for uint256 compatibility
|
||||
|
||||
print("Attempting to transfer " + amount + " tokens to " + recipient);
|
||||
|
||||
// Call the transfer function with arguments
|
||||
let tx_hash = call_contract_write(contract, "transfer", [recipient, amount]);
|
||||
|
||||
if tx_hash != "" {
|
||||
print("✓ Transaction sent successfully");
|
||||
print("Transaction hash: " + tx_hash);
|
||||
print("You can view the transaction at: " + get_network_explorer_url(network_name) + "/tx/" + tx_hash);
|
||||
} else {
|
||||
print("✗ Failed to send transaction");
|
||||
print("This could be due to insufficient funds, contract issues, or other errors.");
|
||||
}
|
||||
} else {
|
||||
print("✗ Failed to load contract");
|
||||
}
|
||||
} else {
|
||||
print("✗ Failed to get wallet balance");
|
||||
}
|
||||
} else {
|
||||
print("✗ Failed to create wallet from private key");
|
||||
}
|
||||
} else {
|
||||
print("✗ Failed to create keypair");
|
||||
}
|
||||
} else {
|
||||
print("✗ Failed to create key space");
|
||||
}
|
||||
|
||||
print("\nContract function with arguments test completed");
|
104
examples/hero_vault/agung_send_transaction.rhai
Normal file
104
examples/hero_vault/agung_send_transaction.rhai
Normal file
@@ -0,0 +1,104 @@
|
||||
// Script to create an Agung wallet from a private key and send tokens
|
||||
// This script demonstrates how to create a wallet from a private key and send tokens
|
||||
|
||||
// Define the private key and recipient address
|
||||
let private_key = "0x9ecfd58eca522b0e7c109bf945966ee208cd6d593b1dc3378aedfdc60b64f512";
|
||||
let recipient_address = "0xf400f9c3F7317e19523a5DB698Ce67e7a7E083e2";
|
||||
|
||||
print("=== Agung Wallet Transaction Demo ===");
|
||||
print(`From private key: ${private_key}`);
|
||||
print(`To address: ${recipient_address}`);
|
||||
|
||||
// First, create a key space and keypair (required for the wallet infrastructure)
|
||||
let space_name = "agung_transaction_demo";
|
||||
let password = "demo_password";
|
||||
|
||||
// Create a new key space
|
||||
if !create_key_space(space_name, password) {
|
||||
print("Failed to create key space");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a keypair
|
||||
if !create_keypair("demo_keypair", password) {
|
||||
print("Failed to create keypair");
|
||||
return;
|
||||
}
|
||||
|
||||
// Select the keypair
|
||||
if !select_keypair("demo_keypair") {
|
||||
print("Failed to select keypair");
|
||||
return;
|
||||
}
|
||||
|
||||
print("\nCreated and selected keypair successfully");
|
||||
|
||||
// Clear any existing Agung wallets to avoid conflicts
|
||||
if clear_wallets_for_network("agung") {
|
||||
print("Cleared existing Agung wallets");
|
||||
} else {
|
||||
print("Failed to clear existing Agung wallets");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a wallet from the private key directly
|
||||
print("\n=== Creating Wallet from Private Key ===");
|
||||
|
||||
// Create a wallet from the private key for the Agung network
|
||||
if create_wallet_from_private_key_for_network(private_key, "agung") {
|
||||
print("Successfully created wallet from private key for Agung network");
|
||||
|
||||
// Get the wallet address
|
||||
let wallet_address = get_wallet_address_for_network("agung");
|
||||
print(`Wallet address: ${wallet_address}`);
|
||||
|
||||
// Create a provider for the Agung network
|
||||
let provider_id = create_agung_provider();
|
||||
if provider_id != "" {
|
||||
print("Successfully created Agung provider");
|
||||
|
||||
// Check the wallet balance first
|
||||
let wallet_address = get_wallet_address_for_network("agung");
|
||||
let balance_wei = get_balance("agung", wallet_address);
|
||||
|
||||
if balance_wei == "" {
|
||||
print("Failed to get wallet balance");
|
||||
print("This could be due to network issues or other errors.");
|
||||
return;
|
||||
}
|
||||
|
||||
print(`Current wallet balance: ${balance_wei} wei`);
|
||||
|
||||
// Convert 1 AGNG to wei (1 AGNG = 10^18 wei)
|
||||
// Use string representation for large numbers
|
||||
let amount_wei_str = "1000000000000000000"; // 1 AGNG in wei as a string
|
||||
|
||||
// Check if we have enough balance
|
||||
if parse_int(balance_wei) < parse_int(amount_wei_str) {
|
||||
print(`Insufficient balance to send ${amount_wei_str} wei (1 AGNG)`);
|
||||
print(`Current balance: ${balance_wei} wei`);
|
||||
print("Please fund the wallet before attempting to send a transaction");
|
||||
return;
|
||||
}
|
||||
|
||||
print(`Attempting to send ${amount_wei_str} wei (1 AGNG) to ${recipient_address}`);
|
||||
|
||||
// Send the transaction using the blocking implementation
|
||||
let tx_hash = send_eth("agung", recipient_address, amount_wei_str);
|
||||
|
||||
if tx_hash != "" {
|
||||
print(`Transaction sent with hash: ${tx_hash}`);
|
||||
print(`You can view the transaction at: ${get_network_explorer_url("agung")}/tx/${tx_hash}`);
|
||||
} else {
|
||||
print("Transaction failed");
|
||||
print("This could be due to insufficient funds, network issues, or other errors.");
|
||||
print("Check the logs for more details.");
|
||||
}
|
||||
} else {
|
||||
print("Failed to create Agung provider");
|
||||
}
|
||||
} else {
|
||||
print("Failed to create wallet from private key");
|
||||
}
|
||||
|
||||
print("\nAgung transaction demo completed");
|
98
examples/hero_vault/contract_example.rhai
Normal file
98
examples/hero_vault/contract_example.rhai
Normal file
@@ -0,0 +1,98 @@
|
||||
// Example Rhai script for interacting with smart contracts using Hero Vault
|
||||
// This script demonstrates loading a contract ABI and interacting with a contract
|
||||
|
||||
// Step 1: Set up wallet and network
|
||||
let space_name = "contract_demo_space";
|
||||
let password = "secure_password123";
|
||||
|
||||
print("Creating key space: " + space_name);
|
||||
if create_key_space(space_name, password) {
|
||||
print("✓ Key space created successfully");
|
||||
|
||||
// Create a keypair
|
||||
print("\nCreating keypair...");
|
||||
if create_keypair("contract_key", password) {
|
||||
print("✓ Created contract keypair");
|
||||
}
|
||||
|
||||
// Step 2: Create an Ethereum wallet for Gnosis Chain
|
||||
print("\nCreating Ethereum wallet...");
|
||||
if create_ethereum_wallet() {
|
||||
print("✓ Ethereum wallet created");
|
||||
|
||||
let address = get_ethereum_address();
|
||||
print("Ethereum address: " + address);
|
||||
|
||||
// Step 3: Define a simple ERC-20 ABI (partial)
|
||||
let erc20_abi = `[
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "name",
|
||||
"outputs": [{"name": "", "type": "string"}],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "symbol",
|
||||
"outputs": [{"name": "", "type": "string"}],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "decimals",
|
||||
"outputs": [{"name": "", "type": "uint8"}],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [{"name": "owner", "type": "address"}],
|
||||
"name": "balanceOf",
|
||||
"outputs": [{"name": "", "type": "uint256"}],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
}
|
||||
]`;
|
||||
|
||||
// Step 4: Load the contract ABI
|
||||
print("\nLoading contract ABI...");
|
||||
let contract = load_contract_abi("Gnosis", "0x4ECaBa5870353805a9F068101A40E0f32ed605C6", erc20_abi);
|
||||
if contract != "" {
|
||||
print("✓ Contract loaded successfully");
|
||||
|
||||
// Step 5: Call read-only functions
|
||||
print("\nCalling read-only functions...");
|
||||
|
||||
// Get token name
|
||||
let token_name = call_contract_read(contract, "name");
|
||||
print("Token name: " + token_name);
|
||||
|
||||
// Get token symbol
|
||||
let token_symbol = call_contract_read(contract, "symbol");
|
||||
print("Token symbol: " + token_symbol);
|
||||
|
||||
// Get token decimals
|
||||
let token_decimals = call_contract_read(contract, "decimals");
|
||||
print("Token decimals: " + token_decimals);
|
||||
|
||||
// For now, we're just demonstrating the basic structure
|
||||
} else {
|
||||
print("✗ Failed to load contract");
|
||||
}
|
||||
} else {
|
||||
print("✗ Failed to create Ethereum wallet");
|
||||
}
|
||||
} else {
|
||||
print("✗ Failed to create key space");
|
||||
}
|
||||
|
||||
print("\nContract example completed");
|
85
examples/hero_vault/example.rhai
Normal file
85
examples/hero_vault/example.rhai
Normal file
@@ -0,0 +1,85 @@
|
||||
// Example Rhai script for Hero Vault Cryptography Module
|
||||
// This script demonstrates key management, signing, and encryption
|
||||
|
||||
// Step 1: Create and manage a key space
|
||||
let space_name = "demo_space";
|
||||
let password = "secure_password123";
|
||||
|
||||
print("Creating key space: " + space_name);
|
||||
if create_key_space(space_name, password) {
|
||||
print("✓ Key space created successfully");
|
||||
|
||||
// Step 2: Create and use keypairs
|
||||
print("\nCreating keypairs...");
|
||||
if create_keypair("signing_key", password) {
|
||||
print("✓ Created signing keypair");
|
||||
}
|
||||
|
||||
if create_keypair("encryption_key", password) {
|
||||
print("✓ Created encryption keypair");
|
||||
}
|
||||
|
||||
// List all keypairs
|
||||
let keypairs = list_keypairs();
|
||||
print("Available keypairs: " + keypairs);
|
||||
|
||||
// Step 3: Sign a message
|
||||
print("\nPerforming signing operations...");
|
||||
if select_keypair("signing_key") {
|
||||
print("✓ Selected signing keypair");
|
||||
|
||||
let message = "This is a secure message that needs to be signed";
|
||||
print("Message: " + message);
|
||||
|
||||
let signature = sign(message);
|
||||
print("Signature: " + signature);
|
||||
|
||||
// Verify the signature
|
||||
let is_valid = verify(message, signature);
|
||||
if is_valid {
|
||||
print("Signature verification: ✓ Valid");
|
||||
} else {
|
||||
print("Signature verification: ✗ Invalid");
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4: Encrypt and decrypt data
|
||||
print("\nPerforming encryption operations...");
|
||||
|
||||
// Generate a symmetric key
|
||||
let sym_key = generate_key();
|
||||
print("Generated symmetric key: " + sym_key);
|
||||
|
||||
// Encrypt a message
|
||||
let secret = "This is a top secret message that must be encrypted";
|
||||
print("Original message: " + secret);
|
||||
|
||||
let encrypted_data = encrypt(sym_key, secret);
|
||||
print("Encrypted data: " + encrypted_data);
|
||||
|
||||
// Decrypt the message
|
||||
let decrypted_data = decrypt(sym_key, encrypted_data);
|
||||
print("Decrypted message: " + decrypted_data);
|
||||
|
||||
// Verify decryption was successful
|
||||
if decrypted_data == secret {
|
||||
print("✓ Encryption/decryption successful");
|
||||
} else {
|
||||
print("✗ Encryption/decryption failed");
|
||||
}
|
||||
|
||||
// Step 5: Create an Ethereum wallet
|
||||
print("\nCreating Ethereum wallet...");
|
||||
if select_keypair("encryption_key") {
|
||||
print("✓ Selected keypair for Ethereum wallet");
|
||||
|
||||
if create_ethereum_wallet() {
|
||||
print("✓ Ethereum wallet created");
|
||||
|
||||
let address = get_ethereum_address();
|
||||
print("Ethereum address: " + address);
|
||||
}
|
||||
}
|
||||
|
||||
print("\nScript execution completed successfully!");
|
||||
}
|
65
examples/hero_vault/key_persistence_example.rhai
Normal file
65
examples/hero_vault/key_persistence_example.rhai
Normal file
@@ -0,0 +1,65 @@
|
||||
// Example Rhai script demonstrating key space persistence for Hero Vault
|
||||
// This script shows how to create, save, and load key spaces
|
||||
|
||||
// Step 1: Create a key space
|
||||
let space_name = "persistent_space";
|
||||
let password = "secure_password123";
|
||||
|
||||
print("Creating key space: " + space_name);
|
||||
if create_key_space(space_name, password) {
|
||||
print("✓ Key space created successfully");
|
||||
|
||||
// Step 2: Create keypairs in this space
|
||||
print("\nCreating keypairs...");
|
||||
if create_keypair("persistent_key1", password) {
|
||||
print("✓ Created first keypair");
|
||||
}
|
||||
|
||||
if create_keypair("persistent_key2", password) {
|
||||
print("✓ Created second keypair");
|
||||
}
|
||||
|
||||
// List all keypairs
|
||||
let keypairs = list_keypairs();
|
||||
print("Available keypairs: " + keypairs);
|
||||
|
||||
// Step 3: Clear the session (simulate closing and reopening the CLI)
|
||||
print("\nClearing session (simulating restart)...");
|
||||
// Note: In a real script, you would exit here and run a new script
|
||||
// For demonstration purposes, we'll continue in the same script
|
||||
|
||||
// Step 4: Load the key space from disk
|
||||
print("\nLoading key space from disk...");
|
||||
if load_key_space(space_name, password) {
|
||||
print("✓ Key space loaded successfully");
|
||||
|
||||
// Verify the keypairs are still available
|
||||
let loaded_keypairs = list_keypairs();
|
||||
print("Keypairs after loading: " + loaded_keypairs);
|
||||
|
||||
// Step 5: Use a keypair from the loaded space
|
||||
print("\nSelecting and using a keypair...");
|
||||
if select_keypair("persistent_key1") {
|
||||
print("✓ Selected keypair");
|
||||
|
||||
let message = "This message was signed using a keypair from a loaded key space";
|
||||
let signature = sign(message);
|
||||
print("Message: " + message);
|
||||
print("Signature: " + signature);
|
||||
|
||||
// Verify the signature
|
||||
let is_valid = verify(message, signature);
|
||||
if is_valid {
|
||||
print("Signature verification: ✓ Valid");
|
||||
} else {
|
||||
print("Signature verification: ✗ Invalid");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
print("✗ Failed to load key space");
|
||||
}
|
||||
} else {
|
||||
print("✗ Failed to create key space");
|
||||
}
|
||||
|
||||
print("\nScript execution completed!");
|
65
examples/hero_vault/load_existing_space.rhai
Normal file
65
examples/hero_vault/load_existing_space.rhai
Normal file
@@ -0,0 +1,65 @@
|
||||
// Example Rhai script demonstrating loading an existing key space for Hero Vault
|
||||
// This script shows how to load a previously created key space and use its keypairs
|
||||
|
||||
// Define the key space name and password
|
||||
let space_name = "persistent_space";
|
||||
let password = "secure_password123";
|
||||
|
||||
print("Loading existing key space: " + space_name);
|
||||
|
||||
// Load the key space from disk
|
||||
if load_key_space(space_name, password) {
|
||||
print("✓ Key space loaded successfully");
|
||||
|
||||
// List available keypairs
|
||||
let keypairs = list_keypairs();
|
||||
print("Available keypairs: " + keypairs);
|
||||
|
||||
// Use both keypairs to sign different messages
|
||||
if select_keypair("persistent_key1") {
|
||||
print("\nUsing persistent_key1:");
|
||||
let message1 = "Message signed with the first keypair";
|
||||
let signature1 = sign(message1);
|
||||
print("Message: " + message1);
|
||||
print("Signature: " + signature1);
|
||||
|
||||
let is_valid1 = verify(message1, signature1);
|
||||
if is_valid1 {
|
||||
print("Verification: ✓ Valid");
|
||||
} else {
|
||||
print("Verification: ✗ Invalid");
|
||||
}
|
||||
}
|
||||
|
||||
if select_keypair("persistent_key2") {
|
||||
print("\nUsing persistent_key2:");
|
||||
let message2 = "Message signed with the second keypair";
|
||||
let signature2 = sign(message2);
|
||||
print("Message: " + message2);
|
||||
print("Signature: " + signature2);
|
||||
|
||||
let is_valid2 = verify(message2, signature2);
|
||||
if is_valid2 {
|
||||
print("Verification: ✓ Valid");
|
||||
} else {
|
||||
print("Verification: ✗ Invalid");
|
||||
}
|
||||
}
|
||||
|
||||
// Create an Ethereum wallet using one of the keypairs
|
||||
print("\nCreating Ethereum wallet from persistent keypair:");
|
||||
if select_keypair("persistent_key1") {
|
||||
if create_ethereum_wallet() {
|
||||
print("✓ Ethereum wallet created");
|
||||
|
||||
let address = get_ethereum_address();
|
||||
print("Ethereum address: " + address);
|
||||
} else {
|
||||
print("✗ Failed to create Ethereum wallet");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
print("✗ Failed to load key space. Make sure you've run key_persistence_example.rhai first.");
|
||||
}
|
||||
|
||||
print("\nScript execution completed!");
|
133
examples/mycelium/mycelium_basic.rhai
Normal file
133
examples/mycelium/mycelium_basic.rhai
Normal file
@@ -0,0 +1,133 @@
|
||||
// Basic example of using the Mycelium client in Rhai
|
||||
|
||||
// API URL for Mycelium
|
||||
let api_url = "http://localhost:8989";
|
||||
|
||||
// Get node information
|
||||
print("Getting node information:");
|
||||
try {
|
||||
let node_info = mycelium_get_node_info(api_url);
|
||||
print(`Node subnet: ${node_info.nodeSubnet}`);
|
||||
print(`Node public key: ${node_info.nodePubkey}`);
|
||||
} catch(err) {
|
||||
print(`Error getting node info: ${err}`);
|
||||
}
|
||||
|
||||
// List all peers
|
||||
print("\nListing all peers:");
|
||||
try {
|
||||
let peers = mycelium_list_peers(api_url);
|
||||
|
||||
if peers.is_empty() {
|
||||
print("No peers connected.");
|
||||
} else {
|
||||
for peer in peers {
|
||||
print(`Peer Endpoint: ${peer.endpoint.proto}://${peer.endpoint.socketAddr}`);
|
||||
print(` Type: ${peer.type}`);
|
||||
print(` Connection State: ${peer.connectionState}`);
|
||||
print(` Bytes sent: ${peer.txBytes}`);
|
||||
print(` Bytes received: ${peer.rxBytes}`);
|
||||
}
|
||||
}
|
||||
} catch(err) {
|
||||
print(`Error listing peers: ${err}`);
|
||||
}
|
||||
|
||||
// Add a new peer
|
||||
print("\nAdding a new peer:");
|
||||
let new_peer_address = "tcp://65.21.231.58:9651";
|
||||
try {
|
||||
let result = mycelium_add_peer(api_url, new_peer_address);
|
||||
print(`Peer added: ${result.success}`);
|
||||
} catch(err) {
|
||||
print(`Error adding peer: ${err}`);
|
||||
}
|
||||
|
||||
// List selected routes
|
||||
print("\nListing selected routes:");
|
||||
try {
|
||||
let routes = mycelium_list_selected_routes(api_url);
|
||||
|
||||
if routes.is_empty() {
|
||||
print("No selected routes.");
|
||||
} else {
|
||||
for route in routes {
|
||||
print(`Subnet: ${route.subnet}`);
|
||||
print(` Next hop: ${route.nextHop}`);
|
||||
print(` Metric: ${route.metric}`);
|
||||
}
|
||||
}
|
||||
} catch(err) {
|
||||
print(`Error listing routes: ${err}`);
|
||||
}
|
||||
|
||||
// List fallback routes
|
||||
print("\nListing fallback routes:");
|
||||
try {
|
||||
let routes = mycelium_list_fallback_routes(api_url);
|
||||
|
||||
if routes.is_empty() {
|
||||
print("No fallback routes.");
|
||||
} else {
|
||||
for route in routes {
|
||||
print(`Subnet: ${route.subnet}`);
|
||||
print(` Next hop: ${route.nextHop}`);
|
||||
print(` Metric: ${route.metric}`);
|
||||
}
|
||||
}
|
||||
} catch(err) {
|
||||
print(`Error listing fallback routes: ${err}`);
|
||||
}
|
||||
|
||||
// Send a message
|
||||
// TO SEND A MESSAGE FILL IN THE DESTINATION IP ADDRESS
|
||||
// -----------------------------------------------------//
|
||||
// print("\nSending a message:");
|
||||
// let destination = < FILL IN CORRECT DEST IP >
|
||||
// let topic = "test";
|
||||
// let message = "Hello from Rhai!";
|
||||
// let deadline_secs = 60;
|
||||
|
||||
// try {
|
||||
// let result = mycelium_send_message(api_url, destination, topic, message, deadline_secs);
|
||||
// print(`Message sent: ${result.success}`);
|
||||
// if result.id {
|
||||
// print(`Message ID: ${result.id}`);
|
||||
// }
|
||||
// } catch(err) {
|
||||
// print(`Error sending message: ${err}`);
|
||||
// }
|
||||
|
||||
// Receive messages
|
||||
// RECEIVING MESSAGES SHOULD BE DONE ON THE DESTINATION NODE FROM THE CALL ABOVE
|
||||
// -----------------------------------------------------------------------------//
|
||||
// print("\nReceiving messages:");
|
||||
// let receive_topic = "test";
|
||||
// let count = 5;
|
||||
|
||||
// try {
|
||||
// let messages = mycelium_receive_messages(api_url, receive_topic, count);
|
||||
|
||||
// if messages.is_empty() {
|
||||
// print("No messages received.");
|
||||
// } else {
|
||||
// for msg in messages {
|
||||
// print(`Message from: ${msg.source}`);
|
||||
// print(` Topic: ${msg.topic}`);
|
||||
// print(` Content: ${msg.content}`);
|
||||
// print(` Timestamp: ${msg.timestamp}`);
|
||||
// }
|
||||
// }
|
||||
// } catch(err) {
|
||||
// print(`Error receiving messages: ${err}`);
|
||||
// }
|
||||
|
||||
// Remove a peer
|
||||
print("\nRemoving a peer:");
|
||||
let peer_id = "tcp://65.21.231.58:9651"; // This is the peer we added earlier
|
||||
try {
|
||||
let result = mycelium_remove_peer(api_url, peer_id);
|
||||
print(`Peer removed: ${result.success}`);
|
||||
} catch(err) {
|
||||
print(`Error removing peer: ${err}`);
|
||||
}
|
31
examples/mycelium/mycelium_receive_message.rhai
Normal file
31
examples/mycelium/mycelium_receive_message.rhai
Normal file
@@ -0,0 +1,31 @@
|
||||
// Script to receive Mycelium messages
|
||||
|
||||
// API URL for Mycelium
|
||||
let api_url = "http://localhost:2222";
|
||||
|
||||
// Receive messages
|
||||
// This script will listen for messages on a specific topic.
|
||||
// Ensure the sender script is using the same topic.
|
||||
// -----------------------------------------------------------------------------//
|
||||
print("\nReceiving messages:");
|
||||
let receive_topic = "test_topic";
|
||||
let wait_deadline_secs = 100;
|
||||
|
||||
print(`Listening for messages on topic '${receive_topic}'...`);
|
||||
try {
|
||||
let messages = mycelium_receive_messages(api_url, receive_topic, wait_deadline_secs);
|
||||
|
||||
if messages.is_empty() {
|
||||
// print("No new messages received in this poll.");
|
||||
} else {
|
||||
print("Received a message:");
|
||||
print(` Message id: ${messages.id}`);
|
||||
print(` Message from: ${messages.srcIp}`);
|
||||
print(` Topic: ${messages.topic}`);
|
||||
print(` Payload: ${messages.payload}`);
|
||||
}
|
||||
} catch(err) {
|
||||
print(`Error receiving messages: ${err}`);
|
||||
}
|
||||
|
||||
print("Finished attempting to receive messages.");
|
25
examples/mycelium/mycelium_send_message.rhai
Normal file
25
examples/mycelium/mycelium_send_message.rhai
Normal file
@@ -0,0 +1,25 @@
|
||||
// Script to send a Mycelium message
|
||||
|
||||
// API URL for Mycelium
|
||||
let api_url = "http://localhost:1111";
|
||||
|
||||
// Send a message
|
||||
// TO SEND A MESSAGE FILL IN THE DESTINATION IP ADDRESS
|
||||
// -----------------------------------------------------//
|
||||
print("\nSending a message:");
|
||||
let destination = "50e:6d75:4568:366e:f75:2ac3:bbb1:3fdd"; // IMPORTANT: Replace with the actual destination IP address
|
||||
let topic = "test_topic";
|
||||
let message = "Hello from Rhai sender!";
|
||||
let deadline_secs = -10; // Seconds we wait for a reply
|
||||
|
||||
try {
|
||||
print(`Attempting to send message to ${destination} on topic '${topic}'`);
|
||||
let result = mycelium_send_message(api_url, destination, topic, message, deadline_secs);
|
||||
print(`result: ${result}`);
|
||||
print(`Message sent: ${result.success}`);
|
||||
if result.id != "" {
|
||||
print(`Message ID: ${result.id}`);
|
||||
}
|
||||
} catch(err) {
|
||||
print(`Error sending message: ${err}`);
|
||||
}
|
83
examples/network/network_connectivity.rhai
Normal file
83
examples/network/network_connectivity.rhai
Normal file
@@ -0,0 +1,83 @@
|
||||
// Example of using the network modules in SAL
|
||||
// Shows TCP port checking, HTTP URL validation, and SSH command execution
|
||||
|
||||
// Import system module for display
|
||||
import "os" as os;
|
||||
|
||||
// Function to print section header
|
||||
fn section(title) {
|
||||
print("\n");
|
||||
print("==== " + title + " ====");
|
||||
print("\n");
|
||||
}
|
||||
|
||||
// TCP connectivity checks
|
||||
section("TCP Connectivity");
|
||||
|
||||
// Create a TCP connector
|
||||
let tcp = sal::net::TcpConnector::new();
|
||||
|
||||
// Check if a port is open
|
||||
let host = "localhost";
|
||||
let port = 22;
|
||||
print(`Checking if port ${port} is open on ${host}...`);
|
||||
let is_open = tcp.check_port(host, port);
|
||||
print(`Port ${port} is ${is_open ? "open" : "closed"}`);
|
||||
|
||||
// Check multiple ports
|
||||
let ports = [22, 80, 443];
|
||||
print(`Checking multiple ports on ${host}...`);
|
||||
let port_results = tcp.check_ports(host, ports);
|
||||
for result in port_results {
|
||||
print(`Port ${result.0} is ${result.1 ? "open" : "closed"}`);
|
||||
}
|
||||
|
||||
// HTTP connectivity checks
|
||||
section("HTTP Connectivity");
|
||||
|
||||
// Create an HTTP connector
|
||||
let http = sal::net::HttpConnector::new();
|
||||
|
||||
// Check if a URL is reachable
|
||||
let url = "https://www.example.com";
|
||||
print(`Checking if ${url} is reachable...`);
|
||||
let is_reachable = http.check_url(url);
|
||||
print(`${url} is ${is_reachable ? "reachable" : "unreachable"}`);
|
||||
|
||||
// Check the status code of a URL
|
||||
print(`Checking status code of ${url}...`);
|
||||
let status = http.check_status(url);
|
||||
if status {
|
||||
print(`Status code: ${status.unwrap()}`);
|
||||
} else {
|
||||
print("Failed to get status code");
|
||||
}
|
||||
|
||||
// Only attempt SSH if port 22 is open
|
||||
if is_open {
|
||||
// SSH connectivity checks
|
||||
section("SSH Connectivity");
|
||||
|
||||
// Create an SSH connection to localhost (if SSH server is running)
|
||||
print("Attempting to connect to SSH server on localhost...");
|
||||
|
||||
// Using the builder pattern
|
||||
let ssh = sal::net::SshConnectionBuilder::new()
|
||||
.host("localhost")
|
||||
.port(22)
|
||||
.user(os::get_env("USER") || "root")
|
||||
.build();
|
||||
|
||||
// Execute a simple command
|
||||
print("Executing 'uname -a' command...");
|
||||
let result = ssh.execute("uname -a");
|
||||
if result.0 == 0 {
|
||||
print("Command output:");
|
||||
print(result.1);
|
||||
} else {
|
||||
print(`Command failed with exit code: ${result.0}`);
|
||||
print(result.1);
|
||||
}
|
||||
}
|
||||
|
||||
print("\nNetwork connectivity checks completed.");
|
82
examples/network/network_rhai.rhai
Normal file
82
examples/network/network_rhai.rhai
Normal file
@@ -0,0 +1,82 @@
|
||||
// Example of using the network modules in SAL through Rhai
|
||||
// Shows TCP port checking, HTTP URL validation, and SSH command execution
|
||||
|
||||
// Function to print section header
|
||||
fn section(title) {
|
||||
print("\n");
|
||||
print("==== " + title + " ====");
|
||||
print("\n");
|
||||
}
|
||||
|
||||
// TCP connectivity checks
|
||||
section("TCP Connectivity");
|
||||
|
||||
// Create a TCP connector
|
||||
let tcp = net::new_tcp_connector();
|
||||
|
||||
// Check if a port is open
|
||||
let host = "localhost";
|
||||
let port = 22;
|
||||
print(`Checking if port ${port} is open on ${host}...`);
|
||||
let is_open = tcp.check_port(host, port);
|
||||
print(`Port ${port} is ${is_open ? "open" : "closed"}`);
|
||||
|
||||
// Check multiple ports
|
||||
let ports = [22, 80, 443];
|
||||
print(`Checking multiple ports on ${host}...`);
|
||||
let port_results = tcp.check_ports(host, ports);
|
||||
for result in port_results {
|
||||
print(`Port ${result.port} is ${result.is_open ? "open" : "closed"}`);
|
||||
}
|
||||
|
||||
// HTTP connectivity checks
|
||||
section("HTTP Connectivity");
|
||||
|
||||
// Create an HTTP connector
|
||||
let http = net::new_http_connector();
|
||||
|
||||
// Check if a URL is reachable
|
||||
let url = "https://www.example.com";
|
||||
print(`Checking if ${url} is reachable...`);
|
||||
let is_reachable = http.check_url(url);
|
||||
print(`${url} is ${is_reachable ? "reachable" : "unreachable"}`);
|
||||
|
||||
// Check the status code of a URL
|
||||
print(`Checking status code of ${url}...`);
|
||||
let status = http.check_status(url);
|
||||
if status != () {
|
||||
print(`Status code: ${status}`);
|
||||
} else {
|
||||
print("Failed to get status code");
|
||||
}
|
||||
|
||||
// Get content from a URL
|
||||
print(`Getting content from ${url}...`);
|
||||
let content = http.get_content(url);
|
||||
print(`Content length: ${content.len()} characters`);
|
||||
print(`First 100 characters: ${content.substr(0, 100)}...`);
|
||||
|
||||
// Only attempt SSH if port 22 is open
|
||||
if is_open {
|
||||
// SSH connectivity checks
|
||||
section("SSH Connectivity");
|
||||
|
||||
// Create an SSH connection to localhost (if SSH server is running)
|
||||
print("Attempting to connect to SSH server on localhost...");
|
||||
|
||||
// Using the builder pattern
|
||||
let ssh = net::new_ssh_builder()
|
||||
.host("localhost")
|
||||
.port(22)
|
||||
.user(os::get_env("USER") || "root")
|
||||
.timeout(10)
|
||||
.build();
|
||||
|
||||
// Execute a simple command
|
||||
print("Executing 'uname -a' command...");
|
||||
let result = ssh.execute("uname -a");
|
||||
print(`Command exit code: ${result.code}`);
|
||||
print(`Command output: ${result.output}`);
|
||||
}
|
||||
|
||||
print("\nNetwork connectivity checks completed.");
|
145
examples/postgresclient/auth_example.rhai
Normal file
145
examples/postgresclient/auth_example.rhai
Normal file
@@ -0,0 +1,145 @@
|
||||
// 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();
|
132
examples/postgresclient/basic_operations.rhai
Normal file
132
examples/postgresclient/basic_operations.rhai
Normal file
@@ -0,0 +1,132 @@
|
||||
// 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();
|
@@ -2,7 +2,7 @@ print("Running a command using run().log().do()...");
|
||||
|
||||
// The .log() method will print the command string to the console before execution.
|
||||
// This is useful for debugging or tracing which commands are being run.
|
||||
let result = run("echo This command is logged").log().do();
|
||||
let result = run("echo This command is logged").log().execute();
|
||||
|
||||
print(`Command finished.`);
|
||||
print(`Success: ${result.success}`);
|
||||
|
131
examples/redisclient/auth_example.rhai
Normal file
131
examples/redisclient/auth_example.rhai
Normal file
@@ -0,0 +1,131 @@
|
||||
// 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();
|
78
examples/zinit/zinit_basic.rhai
Normal file
78
examples/zinit/zinit_basic.rhai
Normal file
@@ -0,0 +1,78 @@
|
||||
// Basic example of using the Zinit client in Rhai
|
||||
|
||||
// Socket path for Zinit
|
||||
let socket_path = "/tmp/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);
|
||||
}
|
||||
// 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}`);
|
||||
}
|
41
examples/zinit/zinit_basic2.rhai
Normal file
41
examples/zinit/zinit_basic2.rhai
Normal file
@@ -0,0 +1,41 @@
|
||||
// Basic example of using the Zinit client in Rhai
|
||||
|
||||
// Socket path for Zinit
|
||||
let socket_path = "/tmp/zinit.sock";
|
||||
|
||||
// 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;
|
||||
|
||||
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);
|
||||
}
|
||||
// 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}`);
|
21
git/Cargo.toml
Normal file
21
git/Cargo.toml
Normal file
@@ -0,0 +1,21 @@
|
||||
[package]
|
||||
name = "sal-git"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
authors = ["PlanetFirst <info@incubaid.com>"]
|
||||
description = "SAL Git - Git repository management and operations"
|
||||
repository = "https://git.threefold.info/herocode/sal"
|
||||
license = "Apache-2.0"
|
||||
|
||||
[dependencies]
|
||||
# Use workspace dependencies for consistency
|
||||
regex = { workspace = true }
|
||||
redis = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
rhai = { workspace = true }
|
||||
log = { workspace = true }
|
||||
url = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = { workspace = true }
|
116
git/README.md
Normal file
116
git/README.md
Normal file
@@ -0,0 +1,116 @@
|
||||
# SAL `git` Module
|
||||
|
||||
The `git` module in SAL provides comprehensive functionalities for interacting with Git repositories. It offers both high-level abstractions for common Git workflows and a flexible executor for running arbitrary Git commands with integrated authentication.
|
||||
|
||||
This module is central to SAL's capabilities for managing source code, enabling automation of development tasks, and integrating with version control systems.
|
||||
|
||||
## Core Components
|
||||
|
||||
The module is primarily composed of two main parts:
|
||||
|
||||
1. **Repository and Tree Management (`git.rs`)**: Defines `GitTree` and `GitRepo` structs for a more structured, object-oriented approach to Git operations.
|
||||
2. **Command Execution with Authentication (`git_executor.rs`)**: Provides `GitExecutor` for running any Git command, with a focus on handling authentication via configurations stored in Redis.
|
||||
|
||||
### 1. Repository and Tree Management (`GitTree` & `GitRepo`)
|
||||
|
||||
These components allow for programmatic management of Git repositories.
|
||||
|
||||
* **`GitTree`**: Represents a directory (base path) that can contain multiple Git repositories.
|
||||
* `new(base_path)`: Creates a new `GitTree` instance for the given base path.
|
||||
* `list()`: Lists all Git repositories found under the base path.
|
||||
* `find(pattern)`: Finds repositories within the tree that match a given name pattern (supports wildcards).
|
||||
* `get(path_or_url)`: Retrieves `GitRepo` instances. If a local path/pattern is given, it finds existing repositories. If a Git URL is provided, it will clone the repository into a structured path (`base_path/server/account/repo`) if it doesn't already exist.
|
||||
|
||||
* **`GitRepo`**: Represents a single Git repository.
|
||||
* `new(path)`: Creates a `GitRepo` instance for the repository at the given path.
|
||||
* `path()`: Returns the local file system path to the repository.
|
||||
* `has_changes()`: Checks if the repository has uncommitted local changes.
|
||||
* `pull()`: Pulls the latest changes from the remote. Fails if local changes exist.
|
||||
* `reset()`: Performs a hard reset (`git reset --hard HEAD`) and cleans untracked files (`git clean -fd`).
|
||||
* `commit(message)`: Stages all changes (`git add .`) and commits them with the given message.
|
||||
* `push()`: Pushes committed changes to the remote repository.
|
||||
|
||||
* **`GitError`**: A comprehensive enum for errors related to `GitTree` and `GitRepo` operations (e.g., Git not installed, invalid URL, repository not found, local changes exist).
|
||||
|
||||
* **`parse_git_url(url)`**: A utility function to parse HTTPS and SSH Git URLs into server, account, and repository name components.
|
||||
|
||||
### 2. Command Execution with Authentication (`GitExecutor`)
|
||||
|
||||
`GitExecutor` is designed for flexible execution of any Git command, with a special emphasis on handling authentication for remote operations.
|
||||
|
||||
* **`GitExecutor::new()` / `GitExecutor::default()`**: Creates a new executor instance.
|
||||
* **`GitExecutor::init()`**: Initializes the executor by attempting to load authentication configurations from Redis (key: `herocontext:git`). If Redis is unavailable or the config is missing, it proceeds without specific auth configurations, relying on system defaults.
|
||||
* **`GitExecutor::execute(args: &[&str])`**: The primary method to run a Git command (e.g., `executor.execute(&["clone", "https://github.com/user/repo.git", "myrepo"])`).
|
||||
* It intelligently attempts to apply authentication based on the command and the loaded configuration.
|
||||
|
||||
#### Authentication Configuration (`herocontext:git` in Redis)
|
||||
|
||||
The `GitExecutor` can load its authentication settings from a JSON object stored in Redis under the key `herocontext:git`. The structure is as follows:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "ok", // or "error"
|
||||
"auth": {
|
||||
"github.com": {
|
||||
"sshagent": true // Use SSH agent for github.com
|
||||
},
|
||||
"gitlab.example.com": {
|
||||
"key": "/path/to/ssh/key_for_gitlab" // Use specific SSH key
|
||||
},
|
||||
"dev.azure.com": {
|
||||
"username": "your_username",
|
||||
"password": "your_pat_or_password" // Use HTTPS credentials
|
||||
}
|
||||
// ... other server configurations
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
* **Authentication Methods Supported**:
|
||||
* **SSH Agent**: If `sshagent: true` is set for a server, and an SSH agent is loaded with identities.
|
||||
* **SSH Key**: If `key: "/path/to/key"` is specified, `GIT_SSH_COMMAND` is used to point to this key.
|
||||
* **Username/Password (HTTPS)**: If `username` and `password` are provided, HTTPS URLs are rewritten to include these credentials (e.g., `https://user:pass@server/repo.git`).
|
||||
|
||||
* **`GitExecutorError`**: An enum for errors specific to `GitExecutor`, including command failures, Redis errors, JSON parsing issues, and authentication problems (e.g., `SshAgentNotLoaded`, `InvalidAuthConfig`).
|
||||
|
||||
## Usage with `herodo`
|
||||
|
||||
The `herodo` CLI tool likely leverages `GitExecutor` to provide its scriptable Git functionalities. This allows Rhai scripts executed by `herodo` to perform Git operations using the centrally managed authentication configurations from Redis, promoting secure and consistent access to Git repositories.
|
||||
|
||||
## Error Handling
|
||||
|
||||
Both `git.rs` and `git_executor.rs` define their own specific error enums (`GitError` and `GitExecutorError` respectively) to provide detailed information about issues encountered during Git operations. These errors cover a wide range of scenarios from command execution failures to authentication problems and invalid configurations.
|
||||
|
||||
## Configuration
|
||||
|
||||
The git module supports configuration through environment variables:
|
||||
|
||||
### Environment Variables
|
||||
|
||||
- **`REDIS_URL`**: Redis connection URL (default: `redis://127.0.0.1/`)
|
||||
- **`SAL_REDIS_URL`**: Alternative Redis URL (fallback if REDIS_URL not set)
|
||||
- **`GIT_DEFAULT_BASE_PATH`**: Default base path for git operations (default: system temp directory)
|
||||
|
||||
### Example Configuration
|
||||
|
||||
```bash
|
||||
# Set Redis connection
|
||||
export REDIS_URL="redis://localhost:6379/0"
|
||||
|
||||
# Set default git base path
|
||||
export GIT_DEFAULT_BASE_PATH="/tmp/git_repos"
|
||||
|
||||
# Run your application
|
||||
herodo your_script.rhai
|
||||
```
|
||||
|
||||
### Security Considerations
|
||||
|
||||
- Passwords are never embedded in URLs or logged
|
||||
- Temporary credential helpers are used for HTTPS authentication
|
||||
- Redis URLs with passwords are masked in logs
|
||||
- All temporary files are cleaned up after use
|
||||
|
||||
## Summary
|
||||
|
||||
The `git` module offers a powerful and flexible interface to Git, catering to both simple, high-level repository interactions and complex, authenticated command execution scenarios. Its integration with Redis for authentication configuration makes it particularly well-suited for automated systems and tools like `herodo`.
|
@@ -1,9 +1,9 @@
|
||||
use std::process::Command;
|
||||
use std::path::Path;
|
||||
use std::fs;
|
||||
use regex::Regex;
|
||||
use std::fmt;
|
||||
use std::error::Error;
|
||||
use std::fmt;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
// Define a custom error type for git operations
|
||||
#[derive(Debug)]
|
||||
@@ -35,7 +35,7 @@ impl fmt::Display for GitError {
|
||||
GitError::CommandExecutionError(e) => write!(f, "Error executing command: {}", e),
|
||||
GitError::NoRepositoriesFound => write!(f, "No repositories found"),
|
||||
GitError::RepositoryNotFound(pattern) => write!(f, "No repositories found matching '{}'", pattern),
|
||||
GitError::MultipleRepositoriesFound(pattern, count) =>
|
||||
GitError::MultipleRepositoriesFound(pattern, count) =>
|
||||
write!(f, "Multiple repositories ({}) found matching '{}'. Use '*' suffix for multiple matches.", count, pattern),
|
||||
GitError::NotAGitRepository(path) => write!(f, "Not a git repository at {}", path),
|
||||
GitError::LocalChangesExist(path) => write!(f, "Repository at {} has local changes", path),
|
||||
@@ -57,48 +57,48 @@ impl Error for GitError {
|
||||
}
|
||||
|
||||
/// Parses a git URL to extract the server, account, and repository name.
|
||||
///
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `url` - The URL of the git repository to parse. Can be in HTTPS format
|
||||
///
|
||||
/// * `url` - The URL of the git repository to parse. Can be in HTTPS format
|
||||
/// (https://github.com/username/repo.git) or SSH format (git@github.com:username/repo.git).
|
||||
///
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
///
|
||||
/// A tuple containing:
|
||||
/// * `server` - The server name (e.g., "github.com")
|
||||
/// * `account` - The account or organization name (e.g., "username")
|
||||
/// * `repo` - The repository name (e.g., "repo")
|
||||
///
|
||||
///
|
||||
/// If the URL cannot be parsed, all three values will be empty strings.
|
||||
pub fn parse_git_url(url: &str) -> (String, String, String) {
|
||||
// HTTP(S) URL format: https://github.com/username/repo.git
|
||||
let https_re = Regex::new(r"https?://([^/]+)/([^/]+)/([^/\.]+)(?:\.git)?").unwrap();
|
||||
|
||||
|
||||
// SSH URL format: git@github.com:username/repo.git
|
||||
let ssh_re = Regex::new(r"git@([^:]+):([^/]+)/([^/\.]+)(?:\.git)?").unwrap();
|
||||
|
||||
|
||||
if let Some(caps) = https_re.captures(url) {
|
||||
let server = caps.get(1).map_or("", |m| m.as_str()).to_string();
|
||||
let account = caps.get(2).map_or("", |m| m.as_str()).to_string();
|
||||
let repo = caps.get(3).map_or("", |m| m.as_str()).to_string();
|
||||
|
||||
|
||||
return (server, account, repo);
|
||||
} else if let Some(caps) = ssh_re.captures(url) {
|
||||
let server = caps.get(1).map_or("", |m| m.as_str()).to_string();
|
||||
let account = caps.get(2).map_or("", |m| m.as_str()).to_string();
|
||||
let repo = caps.get(3).map_or("", |m| m.as_str()).to_string();
|
||||
|
||||
|
||||
return (server, account, repo);
|
||||
}
|
||||
|
||||
|
||||
(String::new(), String::new(), String::new())
|
||||
}
|
||||
|
||||
/// Checks if git is installed on the system.
|
||||
///
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
///
|
||||
/// * `Ok(())` - If git is installed
|
||||
/// * `Err(GitError)` - If git is not installed
|
||||
fn check_git_installed() -> Result<(), GitError> {
|
||||
@@ -117,55 +117,53 @@ pub struct GitTree {
|
||||
|
||||
impl GitTree {
|
||||
/// Creates a new GitTree with the specified base path.
|
||||
///
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
///
|
||||
/// * `base_path` - The base path where all git repositories are located
|
||||
///
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
///
|
||||
/// * `Ok(GitTree)` - A new GitTree instance
|
||||
/// * `Err(GitError)` - If the base path is invalid or cannot be created
|
||||
pub fn new(base_path: &str) -> Result<Self, GitError> {
|
||||
// Check if git is installed
|
||||
check_git_installed()?;
|
||||
|
||||
|
||||
// Validate the base path
|
||||
let path = Path::new(base_path);
|
||||
if !path.exists() {
|
||||
fs::create_dir_all(path).map_err(|e| {
|
||||
GitError::FileSystemError(e)
|
||||
})?;
|
||||
fs::create_dir_all(path).map_err(|e| GitError::FileSystemError(e))?;
|
||||
} else if !path.is_dir() {
|
||||
return Err(GitError::InvalidBasePath(base_path.to_string()));
|
||||
}
|
||||
|
||||
|
||||
Ok(GitTree {
|
||||
base_path: base_path.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/// Lists all git repositories under the base path.
|
||||
///
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
///
|
||||
/// * `Ok(Vec<String>)` - A vector of paths to git repositories
|
||||
/// * `Err(GitError)` - If the operation failed
|
||||
pub fn list(&self) -> Result<Vec<String>, GitError> {
|
||||
let base_path = Path::new(&self.base_path);
|
||||
|
||||
|
||||
if !base_path.exists() || !base_path.is_dir() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
|
||||
let mut repos = Vec::new();
|
||||
|
||||
|
||||
// Find all directories with .git subdirectories
|
||||
let output = Command::new("find")
|
||||
.args(&[&self.base_path, "-type", "d", "-name", ".git"])
|
||||
.output()
|
||||
.map_err(GitError::CommandExecutionError)?;
|
||||
|
||||
|
||||
if output.status.success() {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
for line in stdout.lines() {
|
||||
@@ -178,22 +176,25 @@ impl GitTree {
|
||||
}
|
||||
} else {
|
||||
let error = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(GitError::GitCommandFailed(format!("Failed to find git repositories: {}", error)));
|
||||
return Err(GitError::GitCommandFailed(format!(
|
||||
"Failed to find git repositories: {}",
|
||||
error
|
||||
)));
|
||||
}
|
||||
|
||||
|
||||
Ok(repos)
|
||||
}
|
||||
|
||||
|
||||
/// Finds repositories matching a pattern or partial path.
|
||||
///
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
///
|
||||
/// * `pattern` - The pattern to match against repository paths
|
||||
/// - If the pattern ends with '*', all matching repositories are returned
|
||||
/// - Otherwise, exactly one matching repository must be found
|
||||
///
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
///
|
||||
/// * `Ok(Vec<String>)` - A vector of paths to matching repositories
|
||||
/// * `Err(GitError)` - If no matching repositories are found,
|
||||
/// or if multiple repositories match a non-wildcard pattern
|
||||
@@ -212,7 +213,7 @@ impl GitTree {
|
||||
matched_repos.push(GitRepo::new(full_path));
|
||||
}
|
||||
} else if pattern.ends_with('*') {
|
||||
let prefix = &pattern[0..pattern.len()-1];
|
||||
let prefix = &pattern[0..pattern.len() - 1];
|
||||
for name in repo_names {
|
||||
if name.starts_with(prefix) {
|
||||
let full_path = format!("{}/{}", self.base_path, name);
|
||||
@@ -233,17 +234,17 @@ impl GitTree {
|
||||
|
||||
Ok(matched_repos)
|
||||
}
|
||||
|
||||
|
||||
/// Gets one or more GitRepo objects based on a path pattern or URL.
|
||||
///
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
///
|
||||
/// * `path_or_url` - The path pattern to match against repository paths or a git URL
|
||||
/// - If it's a URL, the repository will be cloned if it doesn't exist
|
||||
/// - If it's a path pattern, it will find matching repositories
|
||||
///
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
///
|
||||
/// * `Ok(Vec<GitRepo>)` - A vector of GitRepo objects
|
||||
/// * `Err(GitError)` - If no matching repositories are found or the clone operation failed
|
||||
pub fn get(&self, path_or_url: &str) -> Result<Vec<GitRepo>, GitError> {
|
||||
@@ -254,32 +255,35 @@ impl GitTree {
|
||||
if server.is_empty() || account.is_empty() || repo.is_empty() {
|
||||
return Err(GitError::InvalidUrl(path_or_url.to_string()));
|
||||
}
|
||||
|
||||
|
||||
// Create the target directory
|
||||
let clone_path = format!("{}/{}/{}/{}", self.base_path, server, account, repo);
|
||||
let clone_dir = Path::new(&clone_path);
|
||||
|
||||
|
||||
// Check if repo already exists
|
||||
if clone_dir.exists() {
|
||||
return Ok(vec![GitRepo::new(clone_path)]);
|
||||
}
|
||||
|
||||
|
||||
// Create parent directory
|
||||
if let Some(parent) = clone_dir.parent() {
|
||||
fs::create_dir_all(parent).map_err(GitError::FileSystemError)?;
|
||||
}
|
||||
|
||||
|
||||
// Clone the repository
|
||||
let output = Command::new("git")
|
||||
.args(&["clone", "--depth", "1", path_or_url, &clone_path])
|
||||
.output()
|
||||
.map_err(GitError::CommandExecutionError)?;
|
||||
|
||||
|
||||
if output.status.success() {
|
||||
Ok(vec![GitRepo::new(clone_path)])
|
||||
} else {
|
||||
let error = String::from_utf8_lossy(&output.stderr);
|
||||
Err(GitError::GitCommandFailed(format!("Git clone error: {}", error)))
|
||||
Err(GitError::GitCommandFailed(format!(
|
||||
"Git clone error: {}",
|
||||
error
|
||||
)))
|
||||
}
|
||||
} else {
|
||||
// It's a path pattern, find matching repositories using the updated self.find()
|
||||
@@ -297,27 +301,27 @@ pub struct GitRepo {
|
||||
|
||||
impl GitRepo {
|
||||
/// Creates a new GitRepo with the specified path.
|
||||
///
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `path` - The path to the git repository
|
||||
pub fn new(path: String) -> Self {
|
||||
GitRepo { path }
|
||||
}
|
||||
|
||||
|
||||
/// Gets the path of the repository.
|
||||
///
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
///
|
||||
/// * The path to the git repository
|
||||
pub fn path(&self) -> &str {
|
||||
&self.path
|
||||
}
|
||||
|
||||
|
||||
/// Checks if the repository has uncommitted changes.
|
||||
///
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
///
|
||||
/// * `Ok(bool)` - True if the repository has uncommitted changes, false otherwise
|
||||
/// * `Err(GitError)` - If the operation failed
|
||||
pub fn has_changes(&self) -> Result<bool, GitError> {
|
||||
@@ -325,14 +329,14 @@ impl GitRepo {
|
||||
.args(&["-C", &self.path, "status", "--porcelain"])
|
||||
.output()
|
||||
.map_err(GitError::CommandExecutionError)?;
|
||||
|
||||
|
||||
Ok(!output.stdout.is_empty())
|
||||
}
|
||||
|
||||
|
||||
/// Pulls the latest changes from the remote repository.
|
||||
///
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
///
|
||||
/// * `Ok(Self)` - The GitRepo object for method chaining
|
||||
/// * `Err(GitError)` - If the pull operation failed
|
||||
pub fn pull(&self) -> Result<Self, GitError> {
|
||||
@@ -341,7 +345,7 @@ impl GitRepo {
|
||||
if !git_dir.exists() || !git_dir.is_dir() {
|
||||
return Err(GitError::NotAGitRepository(self.path.clone()));
|
||||
}
|
||||
|
||||
|
||||
// Check for local changes
|
||||
if self.has_changes()? {
|
||||
return Err(GitError::LocalChangesExist(self.path.clone()));
|
||||
@@ -357,14 +361,17 @@ impl GitRepo {
|
||||
Ok(self.clone())
|
||||
} else {
|
||||
let error = String::from_utf8_lossy(&output.stderr);
|
||||
Err(GitError::GitCommandFailed(format!("Git pull error: {}", error)))
|
||||
Err(GitError::GitCommandFailed(format!(
|
||||
"Git pull error: {}",
|
||||
error
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Resets any local changes in the repository.
|
||||
///
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
///
|
||||
/// * `Ok(Self)` - The GitRepo object for method chaining
|
||||
/// * `Err(GitError)` - If the reset operation failed
|
||||
pub fn reset(&self) -> Result<Self, GitError> {
|
||||
@@ -373,7 +380,7 @@ impl GitRepo {
|
||||
if !git_dir.exists() || !git_dir.is_dir() {
|
||||
return Err(GitError::NotAGitRepository(self.path.clone()));
|
||||
}
|
||||
|
||||
|
||||
// Reset any local changes
|
||||
let reset_output = Command::new("git")
|
||||
.args(&["-C", &self.path, "reset", "--hard", "HEAD"])
|
||||
@@ -382,7 +389,10 @@ impl GitRepo {
|
||||
|
||||
if !reset_output.status.success() {
|
||||
let error = String::from_utf8_lossy(&reset_output.stderr);
|
||||
return Err(GitError::GitCommandFailed(format!("Git reset error: {}", error)));
|
||||
return Err(GitError::GitCommandFailed(format!(
|
||||
"Git reset error: {}",
|
||||
error
|
||||
)));
|
||||
}
|
||||
|
||||
// Clean untracked files
|
||||
@@ -393,20 +403,23 @@ impl GitRepo {
|
||||
|
||||
if !clean_output.status.success() {
|
||||
let error = String::from_utf8_lossy(&clean_output.stderr);
|
||||
return Err(GitError::GitCommandFailed(format!("Git clean error: {}", error)));
|
||||
return Err(GitError::GitCommandFailed(format!(
|
||||
"Git clean error: {}",
|
||||
error
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(self.clone())
|
||||
}
|
||||
|
||||
|
||||
/// Commits changes in the repository.
|
||||
///
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
///
|
||||
/// * `message` - The commit message
|
||||
///
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
///
|
||||
/// * `Ok(Self)` - The GitRepo object for method chaining
|
||||
/// * `Err(GitError)` - If the commit operation failed
|
||||
pub fn commit(&self, message: &str) -> Result<Self, GitError> {
|
||||
@@ -429,7 +442,10 @@ impl GitRepo {
|
||||
|
||||
if !add_output.status.success() {
|
||||
let error = String::from_utf8_lossy(&add_output.stderr);
|
||||
return Err(GitError::GitCommandFailed(format!("Git add error: {}", error)));
|
||||
return Err(GitError::GitCommandFailed(format!(
|
||||
"Git add error: {}",
|
||||
error
|
||||
)));
|
||||
}
|
||||
|
||||
// Commit the changes
|
||||
@@ -440,16 +456,19 @@ impl GitRepo {
|
||||
|
||||
if !commit_output.status.success() {
|
||||
let error = String::from_utf8_lossy(&commit_output.stderr);
|
||||
return Err(GitError::GitCommandFailed(format!("Git commit error: {}", error)));
|
||||
return Err(GitError::GitCommandFailed(format!(
|
||||
"Git commit error: {}",
|
||||
error
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(self.clone())
|
||||
}
|
||||
|
||||
|
||||
/// Pushes changes to the remote repository.
|
||||
///
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
///
|
||||
/// * `Ok(Self)` - The GitRepo object for method chaining
|
||||
/// * `Err(GitError)` - If the push operation failed
|
||||
pub fn push(&self) -> Result<Self, GitError> {
|
||||
@@ -469,7 +488,10 @@ impl GitRepo {
|
||||
Ok(self.clone())
|
||||
} else {
|
||||
let error = String::from_utf8_lossy(&push_output.stderr);
|
||||
Err(GitError::GitCommandFailed(format!("Git push error: {}", error)))
|
||||
Err(GitError::GitCommandFailed(format!(
|
||||
"Git push error: {}",
|
||||
error
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,11 +1,47 @@
|
||||
use std::process::{Command, Output};
|
||||
use std::error::Error;
|
||||
use std::fmt;
|
||||
use std::collections::HashMap;
|
||||
use redis::Cmd;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::error::Error;
|
||||
use std::fmt;
|
||||
use std::process::{Command, Output};
|
||||
|
||||
use crate::redisclient;
|
||||
// Simple redis client functionality with configurable connection
|
||||
fn execute_redis_command(cmd: &mut redis::Cmd) -> redis::RedisResult<String> {
|
||||
// Get Redis URL from environment variables with fallback
|
||||
let redis_url = get_redis_url();
|
||||
log::debug!("Connecting to Redis at: {}", mask_redis_url(&redis_url));
|
||||
|
||||
let client = redis::Client::open(redis_url)?;
|
||||
let mut con = client.get_connection()?;
|
||||
cmd.query(&mut con)
|
||||
}
|
||||
|
||||
/// Get Redis URL from environment variables with secure fallbacks
|
||||
fn get_redis_url() -> String {
|
||||
std::env::var("REDIS_URL")
|
||||
.or_else(|_| std::env::var("SAL_REDIS_URL"))
|
||||
.unwrap_or_else(|_| "redis://127.0.0.1/".to_string())
|
||||
}
|
||||
|
||||
/// Mask sensitive information in Redis URL for logging
|
||||
fn mask_redis_url(url: &str) -> String {
|
||||
if let Ok(parsed) = url::Url::parse(url) {
|
||||
if parsed.password().is_some() {
|
||||
format!(
|
||||
"{}://{}:***@{}:{}/{}",
|
||||
parsed.scheme(),
|
||||
parsed.username(),
|
||||
parsed.host_str().unwrap_or("unknown"),
|
||||
parsed.port().unwrap_or(6379),
|
||||
parsed.path().trim_start_matches('/')
|
||||
)
|
||||
} else {
|
||||
url.to_string()
|
||||
}
|
||||
} else {
|
||||
"redis://***masked***".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
// Define a custom error type for GitExecutor operations
|
||||
#[derive(Debug)]
|
||||
@@ -24,12 +60,16 @@ impl fmt::Display for GitExecutorError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
GitExecutorError::GitCommandFailed(e) => write!(f, "Git command failed: {}", e),
|
||||
GitExecutorError::CommandExecutionError(e) => write!(f, "Command execution error: {}", e),
|
||||
GitExecutorError::CommandExecutionError(e) => {
|
||||
write!(f, "Command execution error: {}", e)
|
||||
}
|
||||
GitExecutorError::RedisError(e) => write!(f, "Redis error: {}", e),
|
||||
GitExecutorError::JsonError(e) => write!(f, "JSON error: {}", e),
|
||||
GitExecutorError::AuthenticationError(e) => write!(f, "Authentication error: {}", e),
|
||||
GitExecutorError::SshAgentNotLoaded => write!(f, "SSH agent is not loaded"),
|
||||
GitExecutorError::InvalidAuthConfig(e) => write!(f, "Invalid authentication configuration: {}", e),
|
||||
GitExecutorError::InvalidAuthConfig(e) => {
|
||||
write!(f, "Invalid authentication configuration: {}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -112,7 +152,7 @@ impl GitExecutor {
|
||||
Err(e) => {
|
||||
// If Redis error, we'll proceed without config
|
||||
// This is not a fatal error as we might use default git behavior
|
||||
eprintln!("Warning: Failed to load git config from Redis: {}", e);
|
||||
log::warn!("Failed to load git config from Redis: {}", e);
|
||||
self.config = None;
|
||||
Ok(())
|
||||
}
|
||||
@@ -126,18 +166,20 @@ impl GitExecutor {
|
||||
cmd.arg("GET").arg("herocontext:git");
|
||||
|
||||
// Execute the command
|
||||
let result: redis::RedisResult<String> = redisclient::execute(&mut cmd);
|
||||
|
||||
let result: redis::RedisResult<String> = execute_redis_command(&mut cmd);
|
||||
|
||||
match result {
|
||||
Ok(json_str) => {
|
||||
// Parse the JSON string into GitConfig
|
||||
let config: GitConfig = serde_json::from_str(&json_str)?;
|
||||
|
||||
|
||||
// Validate the config
|
||||
if config.status == GitConfigStatus::Error {
|
||||
return Err(GitExecutorError::InvalidAuthConfig("Config status is error".to_string()));
|
||||
return Err(GitExecutorError::InvalidAuthConfig(
|
||||
"Config status is error".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
Err(e) => Err(GitExecutorError::RedisError(e)),
|
||||
@@ -146,10 +188,8 @@ impl GitExecutor {
|
||||
|
||||
// Check if SSH agent is loaded
|
||||
fn is_ssh_agent_loaded(&self) -> bool {
|
||||
let output = Command::new("ssh-add")
|
||||
.arg("-l")
|
||||
.output();
|
||||
|
||||
let output = Command::new("ssh-add").arg("-l").output();
|
||||
|
||||
match output {
|
||||
Ok(output) => output.status.success() && !output.stdout.is_empty(),
|
||||
Err(_) => false,
|
||||
@@ -159,7 +199,7 @@ impl GitExecutor {
|
||||
// Get authentication configuration for a git URL
|
||||
fn get_auth_for_url(&self, url: &str) -> Option<&GitServerAuth> {
|
||||
if let Some(config) = &self.config {
|
||||
let (server, _, _) = crate::git::git::parse_git_url(url);
|
||||
let (server, _, _) = crate::parse_git_url(url);
|
||||
if !server.is_empty() {
|
||||
return config.auth.get(&server);
|
||||
}
|
||||
@@ -173,7 +213,7 @@ impl GitExecutor {
|
||||
if let Some(true) = auth.sshagent {
|
||||
if auth.key.is_some() || auth.username.is_some() || auth.password.is_some() {
|
||||
return Err(GitExecutorError::InvalidAuthConfig(
|
||||
"When sshagent is true, key, username, and password must be empty".to_string()
|
||||
"When sshagent is true, key, username, and password must be empty".to_string(),
|
||||
));
|
||||
}
|
||||
// Check if SSH agent is actually loaded
|
||||
@@ -181,30 +221,31 @@ impl GitExecutor {
|
||||
return Err(GitExecutorError::SshAgentNotLoaded);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Rule: If key is set, other fields should be empty
|
||||
if let Some(_) = &auth.key {
|
||||
if auth.sshagent.unwrap_or(false) || auth.username.is_some() || auth.password.is_some() {
|
||||
if auth.sshagent.unwrap_or(false) || auth.username.is_some() || auth.password.is_some()
|
||||
{
|
||||
return Err(GitExecutorError::InvalidAuthConfig(
|
||||
"When key is set, sshagent, username, and password must be empty".to_string()
|
||||
"When key is set, sshagent, username, and password must be empty".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Rule: If username is set, password should be set and other fields empty
|
||||
if let Some(_) = &auth.username {
|
||||
if auth.sshagent.unwrap_or(false) || auth.key.is_some() {
|
||||
return Err(GitExecutorError::InvalidAuthConfig(
|
||||
"When username is set, sshagent and key must be empty".to_string()
|
||||
"When username is set, sshagent and key must be empty".to_string(),
|
||||
));
|
||||
}
|
||||
if auth.password.is_none() {
|
||||
return Err(GitExecutorError::InvalidAuthConfig(
|
||||
"When username is set, password must also be set".to_string()
|
||||
"When username is set, password must also be set".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -212,18 +253,18 @@ impl GitExecutor {
|
||||
pub fn execute(&self, args: &[&str]) -> Result<Output, GitExecutorError> {
|
||||
// Extract the git URL if this is a command that needs authentication
|
||||
let url_arg = self.extract_git_url_from_args(args);
|
||||
|
||||
|
||||
// If we have a URL and authentication config, use it
|
||||
if let Some(url) = url_arg {
|
||||
if let Some(auth) = self.get_auth_for_url(&url) {
|
||||
// Validate the authentication configuration
|
||||
self.validate_auth_config(auth)?;
|
||||
|
||||
|
||||
// Execute with the appropriate authentication method
|
||||
return self.execute_with_auth(args, auth);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// No special authentication needed, execute normally
|
||||
self.execute_git_command(args)
|
||||
}
|
||||
@@ -231,7 +272,11 @@ impl GitExecutor {
|
||||
// Extract git URL from command arguments
|
||||
fn extract_git_url_from_args<'a>(&self, args: &[&'a str]) -> Option<&'a str> {
|
||||
// Commands that might contain a git URL
|
||||
if args.contains(&"clone") || args.contains(&"fetch") || args.contains(&"pull") || args.contains(&"push") {
|
||||
if args.contains(&"clone")
|
||||
|| args.contains(&"fetch")
|
||||
|| args.contains(&"pull")
|
||||
|| args.contains(&"push")
|
||||
{
|
||||
// The URL is typically the last argument for clone, or after remote for others
|
||||
for (i, &arg) in args.iter().enumerate() {
|
||||
if arg == "clone" && i + 1 < args.len() {
|
||||
@@ -249,7 +294,11 @@ impl GitExecutor {
|
||||
}
|
||||
|
||||
// Execute git command with authentication
|
||||
fn execute_with_auth(&self, args: &[&str], auth: &GitServerAuth) -> Result<Output, GitExecutorError> {
|
||||
fn execute_with_auth(
|
||||
&self,
|
||||
args: &[&str],
|
||||
auth: &GitServerAuth,
|
||||
) -> Result<Output, GitExecutorError> {
|
||||
// Handle different authentication methods
|
||||
if let Some(true) = auth.sshagent {
|
||||
// Use SSH agent (already validated that it's loaded)
|
||||
@@ -263,7 +312,9 @@ impl GitExecutor {
|
||||
self.execute_with_credentials(args, username, password)
|
||||
} else {
|
||||
// This should never happen due to validation
|
||||
Err(GitExecutorError::AuthenticationError("Password is required when username is set".to_string()))
|
||||
Err(GitExecutorError::AuthenticationError(
|
||||
"Password is required when username is set".to_string(),
|
||||
))
|
||||
}
|
||||
} else {
|
||||
// No authentication method specified, use default
|
||||
@@ -275,13 +326,13 @@ impl GitExecutor {
|
||||
fn execute_with_ssh_key(&self, args: &[&str], key: &str) -> Result<Output, GitExecutorError> {
|
||||
// Create a command with GIT_SSH_COMMAND to specify the key
|
||||
let ssh_command = format!("ssh -i {} -o IdentitiesOnly=yes", key);
|
||||
|
||||
|
||||
let mut command = Command::new("git");
|
||||
command.env("GIT_SSH_COMMAND", ssh_command);
|
||||
command.args(args);
|
||||
|
||||
|
||||
let output = command.output()?;
|
||||
|
||||
|
||||
if output.status.success() {
|
||||
Ok(output)
|
||||
} else {
|
||||
@@ -290,42 +341,68 @@ impl GitExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
// Execute git command with username/password
|
||||
fn execute_with_credentials(&self, args: &[&str], username: &str, password: &str) -> Result<Output, GitExecutorError> {
|
||||
// For HTTPS authentication, we need to modify the URL to include credentials
|
||||
// Create a new vector to hold our modified arguments
|
||||
let modified_args: Vec<String> = args.iter().map(|&arg| {
|
||||
if arg.starts_with("https://") {
|
||||
// Replace https:// with https://username:password@
|
||||
format!("https://{}:{}@{}",
|
||||
username,
|
||||
password,
|
||||
&arg[8..]) // Skip the "https://" part
|
||||
} else {
|
||||
arg.to_string()
|
||||
}
|
||||
}).collect();
|
||||
|
||||
// Execute the command
|
||||
let mut command = Command::new("git");
|
||||
|
||||
// Add the modified arguments to the command
|
||||
for arg in &modified_args {
|
||||
command.arg(arg.as_str());
|
||||
// Execute git command with username/password using secure credential helper
|
||||
fn execute_with_credentials(
|
||||
&self,
|
||||
args: &[&str],
|
||||
username: &str,
|
||||
password: &str,
|
||||
) -> Result<Output, GitExecutorError> {
|
||||
// Use git credential helper approach for security
|
||||
// Create a temporary credential helper script
|
||||
let temp_dir = std::env::temp_dir();
|
||||
let helper_script = temp_dir.join(format!("git_helper_{}", std::process::id()));
|
||||
|
||||
// Create credential helper script content
|
||||
let script_content = format!(
|
||||
"#!/bin/bash\necho username={}\necho password={}\n",
|
||||
username, password
|
||||
);
|
||||
|
||||
// Write the helper script
|
||||
std::fs::write(&helper_script, script_content)
|
||||
.map_err(|e| GitExecutorError::CommandExecutionError(e))?;
|
||||
|
||||
// Make it executable
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let mut perms = std::fs::metadata(&helper_script)
|
||||
.map_err(|e| GitExecutorError::CommandExecutionError(e))?
|
||||
.permissions();
|
||||
perms.set_mode(0o755);
|
||||
std::fs::set_permissions(&helper_script, perms)
|
||||
.map_err(|e| GitExecutorError::CommandExecutionError(e))?;
|
||||
}
|
||||
|
||||
// Execute the command and handle the result
|
||||
// Execute git command with credential helper
|
||||
let mut command = Command::new("git");
|
||||
command.args(args);
|
||||
command.env("GIT_ASKPASS", &helper_script);
|
||||
command.env("GIT_TERMINAL_PROMPT", "0"); // Disable terminal prompts
|
||||
|
||||
log::debug!("Executing git command with credential helper");
|
||||
let output = command.output()?;
|
||||
if output.status.success() { Ok(output) } else { Err(GitExecutorError::GitCommandFailed(String::from_utf8_lossy(&output.stderr).to_string())) }
|
||||
|
||||
// Clean up the temporary helper script
|
||||
let _ = std::fs::remove_file(&helper_script);
|
||||
|
||||
if output.status.success() {
|
||||
Ok(output)
|
||||
} else {
|
||||
let error = String::from_utf8_lossy(&output.stderr);
|
||||
log::error!("Git command failed: {}", error);
|
||||
Err(GitExecutorError::GitCommandFailed(error.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
// Basic git command execution
|
||||
fn execute_git_command(&self, args: &[&str]) -> Result<Output, GitExecutorError> {
|
||||
let mut command = Command::new("git");
|
||||
command.args(args);
|
||||
|
||||
|
||||
let output = command.output()?;
|
||||
|
||||
|
||||
if output.status.success() {
|
||||
Ok(output)
|
||||
} else {
|
||||
@@ -340,4 +417,4 @@ impl Default for GitExecutor {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,5 +1,6 @@
|
||||
mod git;
|
||||
mod git_executor;
|
||||
pub mod rhai;
|
||||
|
||||
pub use git::*;
|
||||
pub use git_executor::*;
|
||||
pub use git_executor::*;
|
@@ -2,8 +2,8 @@
|
||||
//!
|
||||
//! This module provides Rhai wrappers for the functions in the Git module.
|
||||
|
||||
use rhai::{Engine, EvalAltResult, Array, Dynamic};
|
||||
use crate::git::{GitTree, GitRepo, GitError};
|
||||
use crate::{GitError, GitRepo, GitTree};
|
||||
use rhai::{Array, Dynamic, Engine, EvalAltResult};
|
||||
|
||||
/// 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
|
||||
engine.register_type::<GitTree>();
|
||||
engine.register_fn("git_tree_new", git_tree_new);
|
||||
|
||||
|
||||
// Register GitTree methods
|
||||
engine.register_fn("list", git_tree_list);
|
||||
engine.register_fn("find", git_tree_find);
|
||||
engine.register_fn("get", git_tree_get);
|
||||
|
||||
|
||||
// Register GitRepo methods
|
||||
engine.register_type::<GitRepo>();
|
||||
engine.register_fn("path", git_repo_path);
|
||||
@@ -32,7 +32,10 @@ pub fn register_git_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>
|
||||
engine.register_fn("reset", git_repo_reset);
|
||||
engine.register_fn("commit", git_repo_commit);
|
||||
engine.register_fn("push", git_repo_push);
|
||||
|
||||
|
||||
// Register git_clone function for testing
|
||||
engine.register_fn("git_clone", git_clone);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -41,7 +44,7 @@ fn git_error_to_rhai_error<T>(result: Result<T, GitError>) -> Result<T, Box<Eval
|
||||
result.map_err(|e| {
|
||||
Box::new(EvalAltResult::ErrorRuntime(
|
||||
format!("Git error: {}", e).into(),
|
||||
rhai::Position::NONE
|
||||
rhai::Position::NONE,
|
||||
))
|
||||
})
|
||||
}
|
||||
@@ -62,13 +65,13 @@ pub fn git_tree_new(base_path: &str) -> Result<GitTree, Box<EvalAltResult>> {
|
||||
/// Lists all git repositories under the base path.
|
||||
pub fn git_tree_list(git_tree: &mut GitTree) -> Result<Array, Box<EvalAltResult>> {
|
||||
let repos = git_error_to_rhai_error(git_tree.list())?;
|
||||
|
||||
|
||||
// Convert Vec<String> to Rhai Array
|
||||
let mut array = Array::new();
|
||||
for repo in repos {
|
||||
array.push(Dynamic::from(repo));
|
||||
}
|
||||
|
||||
|
||||
Ok(array)
|
||||
}
|
||||
|
||||
@@ -78,13 +81,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>.
|
||||
pub fn git_tree_find(git_tree: &mut GitTree, pattern: &str) -> Result<Array, Box<EvalAltResult>> {
|
||||
let repos: Vec<GitRepo> = git_error_to_rhai_error(git_tree.find(pattern))?;
|
||||
|
||||
|
||||
// Convert Vec<GitRepo> to Rhai Array
|
||||
let mut array = Array::new();
|
||||
for repo in repos {
|
||||
array.push(Dynamic::from(repo));
|
||||
}
|
||||
|
||||
|
||||
Ok(array)
|
||||
}
|
||||
|
||||
@@ -95,7 +98,10 @@ 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
|
||||
/// if zero or multiple repositories are found (for local names/patterns),
|
||||
/// or if a URL operation fails or unexpectedly yields not exactly one result.
|
||||
pub fn git_tree_get(git_tree: &mut GitTree, name_or_url: &str) -> Result<GitRepo, Box<EvalAltResult>> {
|
||||
pub fn git_tree_get(
|
||||
git_tree: &mut GitTree,
|
||||
name_or_url: &str,
|
||||
) -> Result<GitRepo, Box<EvalAltResult>> {
|
||||
let mut repos_vec: Vec<GitRepo> = git_error_to_rhai_error(git_tree.get(name_or_url))?;
|
||||
|
||||
match repos_vec.len() {
|
||||
@@ -151,7 +157,10 @@ pub fn git_repo_reset(git_repo: &mut GitRepo) -> Result<GitRepo, Box<EvalAltResu
|
||||
/// Wrapper for GitRepo::commit
|
||||
///
|
||||
/// Commits changes in the repository.
|
||||
pub fn git_repo_commit(git_repo: &mut GitRepo, message: &str) -> Result<GitRepo, Box<EvalAltResult>> {
|
||||
pub fn git_repo_commit(
|
||||
git_repo: &mut GitRepo,
|
||||
message: &str,
|
||||
) -> Result<GitRepo, Box<EvalAltResult>> {
|
||||
git_error_to_rhai_error(git_repo.commit(message))
|
||||
}
|
||||
|
||||
@@ -160,4 +169,39 @@ pub fn git_repo_commit(git_repo: &mut GitRepo, message: &str) -> Result<GitRepo,
|
||||
/// Pushes changes to the remote repository.
|
||||
pub fn git_repo_push(git_repo: &mut GitRepo) -> Result<GitRepo, Box<EvalAltResult>> {
|
||||
git_error_to_rhai_error(git_repo.push())
|
||||
}
|
||||
}
|
||||
|
||||
/// Clone a git repository to a temporary location
|
||||
///
|
||||
/// This function clones a repository from the given URL to a temporary directory
|
||||
/// and returns the GitRepo object for further operations.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `url` - The URL of the git repository to clone
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Ok(GitRepo)` - The cloned repository object
|
||||
/// * `Err(Box<EvalAltResult>)` - If the clone operation failed
|
||||
pub fn git_clone(url: &str) -> Result<GitRepo, Box<EvalAltResult>> {
|
||||
// Get base path from environment or use default temp directory
|
||||
let base_path = std::env::var("GIT_DEFAULT_BASE_PATH").unwrap_or_else(|_| {
|
||||
std::env::temp_dir()
|
||||
.join("sal_git_clones")
|
||||
.to_string_lossy()
|
||||
.to_string()
|
||||
});
|
||||
|
||||
// Create GitTree and clone the repository
|
||||
let git_tree = git_error_to_rhai_error(GitTree::new(&base_path))?;
|
||||
let repos = git_error_to_rhai_error(git_tree.get(url))?;
|
||||
|
||||
// Return the first (and should be only) repository
|
||||
repos.into_iter().next().ok_or_else(|| {
|
||||
Box::new(EvalAltResult::ErrorRuntime(
|
||||
"Git error: No repository was cloned".into(),
|
||||
rhai::Position::NONE,
|
||||
))
|
||||
})
|
||||
}
|
197
git/tests/git_executor_security_tests.rs
Normal file
197
git/tests/git_executor_security_tests.rs
Normal file
@@ -0,0 +1,197 @@
|
||||
use sal_git::*;
|
||||
use std::env;
|
||||
|
||||
#[test]
|
||||
fn test_git_executor_initialization() {
|
||||
let mut executor = GitExecutor::new();
|
||||
|
||||
// Test that executor can be initialized without panicking
|
||||
// Even if Redis is not available, init should handle it gracefully
|
||||
let result = executor.init();
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"GitExecutor init should handle Redis unavailability gracefully"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_redis_connection_fallback() {
|
||||
// Test that GitExecutor handles Redis connection failures gracefully
|
||||
// Set an invalid Redis URL to force connection failure
|
||||
env::set_var("REDIS_URL", "redis://invalid-host:9999/0");
|
||||
|
||||
let mut executor = GitExecutor::new();
|
||||
let result = executor.init();
|
||||
|
||||
// Should succeed even with invalid Redis URL (graceful fallback)
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"GitExecutor should handle Redis connection failures gracefully"
|
||||
);
|
||||
|
||||
// Cleanup
|
||||
env::remove_var("REDIS_URL");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_environment_variable_precedence() {
|
||||
// Test REDIS_URL takes precedence over SAL_REDIS_URL
|
||||
env::set_var("REDIS_URL", "redis://primary:6379/0");
|
||||
env::set_var("SAL_REDIS_URL", "redis://fallback:6379/1");
|
||||
|
||||
// Create executor - should use REDIS_URL (primary)
|
||||
let mut executor = GitExecutor::new();
|
||||
let result = executor.init();
|
||||
|
||||
// Should succeed (even if connection fails, init handles it gracefully)
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"GitExecutor should handle environment variables correctly"
|
||||
);
|
||||
|
||||
// Test with only SAL_REDIS_URL
|
||||
env::remove_var("REDIS_URL");
|
||||
let mut executor2 = GitExecutor::new();
|
||||
let result2 = executor2.init();
|
||||
assert!(
|
||||
result2.is_ok(),
|
||||
"GitExecutor should use SAL_REDIS_URL as fallback"
|
||||
);
|
||||
|
||||
// Cleanup
|
||||
env::remove_var("SAL_REDIS_URL");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_git_command_argument_validation() {
|
||||
let executor = GitExecutor::new();
|
||||
|
||||
// Test with empty arguments
|
||||
let result = executor.execute(&[]);
|
||||
assert!(result.is_err(), "Empty git command should fail");
|
||||
|
||||
// Test with invalid git command
|
||||
let result = executor.execute(&["invalid-command"]);
|
||||
assert!(result.is_err(), "Invalid git command should fail");
|
||||
|
||||
// Test with malformed URL (should fail due to URL validation, not injection)
|
||||
let result = executor.execute(&["clone", "not-a-url"]);
|
||||
assert!(result.is_err(), "Invalid URL should be rejected");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_git_executor_with_valid_commands() {
|
||||
let executor = GitExecutor::new();
|
||||
|
||||
// Test git version command (should work if git is available)
|
||||
let result = executor.execute(&["--version"]);
|
||||
|
||||
match result {
|
||||
Ok(output) => {
|
||||
// If git is available, version should be in output
|
||||
let output_str = String::from_utf8_lossy(&output.stdout);
|
||||
assert!(
|
||||
output_str.contains("git version"),
|
||||
"Git version output should contain 'git version'"
|
||||
);
|
||||
}
|
||||
Err(_) => {
|
||||
// If git is not available, that's acceptable in test environment
|
||||
println!("Note: Git not available in test environment");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_credential_helper_environment_setup() {
|
||||
use std::process::Command;
|
||||
|
||||
// Test that we can create and execute a simple credential helper script
|
||||
let temp_dir = std::env::temp_dir();
|
||||
let helper_script = temp_dir.join("test_git_helper");
|
||||
|
||||
// Create a test credential helper script
|
||||
let script_content = "#!/bin/bash\necho username=testuser\necho password=testpass\n";
|
||||
|
||||
// Write the helper script
|
||||
let write_result = std::fs::write(&helper_script, script_content);
|
||||
assert!(
|
||||
write_result.is_ok(),
|
||||
"Should be able to write credential helper script"
|
||||
);
|
||||
|
||||
// Make it executable (Unix only)
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let mut perms = std::fs::metadata(&helper_script).unwrap().permissions();
|
||||
perms.set_mode(0o755);
|
||||
let perm_result = std::fs::set_permissions(&helper_script, perms);
|
||||
assert!(
|
||||
perm_result.is_ok(),
|
||||
"Should be able to set script permissions"
|
||||
);
|
||||
}
|
||||
|
||||
// Test that the script can be executed
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let output = Command::new(&helper_script).output();
|
||||
match output {
|
||||
Ok(output) => {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
assert!(
|
||||
stdout.contains("username=testuser"),
|
||||
"Script should output username"
|
||||
);
|
||||
assert!(
|
||||
stdout.contains("password=testpass"),
|
||||
"Script should output password"
|
||||
);
|
||||
}
|
||||
Err(_) => {
|
||||
println!("Note: Could not execute credential helper script (shell not available)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up
|
||||
let _ = std::fs::remove_file(&helper_script);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_redis_url_masking() {
|
||||
// Test that sensitive Redis URLs are properly masked for logging
|
||||
// This tests the internal URL masking functionality
|
||||
|
||||
// Test URLs with passwords
|
||||
let test_cases = vec![
|
||||
("redis://user:password@localhost:6379/0", true),
|
||||
("redis://localhost:6379/0", false),
|
||||
("redis://user@localhost:6379/0", false),
|
||||
("invalid-url", false),
|
||||
];
|
||||
|
||||
for (url, has_password) in test_cases {
|
||||
// Set the Redis URL and create executor
|
||||
std::env::set_var("REDIS_URL", url);
|
||||
|
||||
let mut executor = GitExecutor::new();
|
||||
let result = executor.init();
|
||||
|
||||
// Should always succeed (graceful handling of connection failures)
|
||||
assert!(result.is_ok(), "GitExecutor should handle URL: {}", url);
|
||||
|
||||
// The actual masking happens internally during logging
|
||||
// We can't easily test the log output, but we verify the executor handles it
|
||||
if has_password {
|
||||
println!(
|
||||
"Note: Tested URL with password (should be masked in logs): {}",
|
||||
url
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
std::env::remove_var("REDIS_URL");
|
||||
}
|
178
git/tests/git_executor_tests.rs
Normal file
178
git/tests/git_executor_tests.rs
Normal file
@@ -0,0 +1,178 @@
|
||||
use sal_git::*;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[test]
|
||||
fn test_git_executor_new() {
|
||||
let executor = GitExecutor::new();
|
||||
// We can't directly access the config field since it's private,
|
||||
// but we can test that the executor was created successfully
|
||||
let _executor = executor;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_git_executor_default() {
|
||||
let executor = GitExecutor::default();
|
||||
let _executor = executor;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_git_config_status_serialization() {
|
||||
let status_ok = GitConfigStatus::Ok;
|
||||
let status_error = GitConfigStatus::Error;
|
||||
|
||||
let json_ok = serde_json::to_string(&status_ok).unwrap();
|
||||
let json_error = serde_json::to_string(&status_error).unwrap();
|
||||
|
||||
assert_eq!(json_ok, "\"ok\"");
|
||||
assert_eq!(json_error, "\"error\"");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_git_config_status_deserialization() {
|
||||
let status_ok: GitConfigStatus = serde_json::from_str("\"ok\"").unwrap();
|
||||
let status_error: GitConfigStatus = serde_json::from_str("\"error\"").unwrap();
|
||||
|
||||
assert_eq!(status_ok, GitConfigStatus::Ok);
|
||||
assert_eq!(status_error, GitConfigStatus::Error);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_git_server_auth_serialization() {
|
||||
let auth = GitServerAuth {
|
||||
sshagent: Some(true),
|
||||
key: None,
|
||||
username: None,
|
||||
password: None,
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&auth).unwrap();
|
||||
assert!(json.contains("\"sshagent\":true"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_git_server_auth_deserialization() {
|
||||
let json = r#"{"sshagent":true,"key":null,"username":null,"password":null}"#;
|
||||
let auth: GitServerAuth = serde_json::from_str(json).unwrap();
|
||||
|
||||
assert_eq!(auth.sshagent, Some(true));
|
||||
assert_eq!(auth.key, None);
|
||||
assert_eq!(auth.username, None);
|
||||
assert_eq!(auth.password, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_git_config_serialization() {
|
||||
let mut auth_map = HashMap::new();
|
||||
auth_map.insert(
|
||||
"github.com".to_string(),
|
||||
GitServerAuth {
|
||||
sshagent: Some(true),
|
||||
key: None,
|
||||
username: None,
|
||||
password: None,
|
||||
},
|
||||
);
|
||||
|
||||
let config = GitConfig {
|
||||
status: GitConfigStatus::Ok,
|
||||
auth: auth_map,
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&config).unwrap();
|
||||
assert!(json.contains("\"status\":\"ok\""));
|
||||
assert!(json.contains("\"github.com\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_git_config_deserialization() {
|
||||
let json = r#"{"status":"ok","auth":{"github.com":{"sshagent":true,"key":null,"username":null,"password":null}}}"#;
|
||||
let config: GitConfig = serde_json::from_str(json).unwrap();
|
||||
|
||||
assert_eq!(config.status, GitConfigStatus::Ok);
|
||||
assert!(config.auth.contains_key("github.com"));
|
||||
assert_eq!(config.auth["github.com"].sshagent, Some(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_git_executor_error_display() {
|
||||
let error = GitExecutorError::GitCommandFailed("command failed".to_string());
|
||||
assert_eq!(format!("{}", error), "Git command failed: command failed");
|
||||
|
||||
let error = GitExecutorError::SshAgentNotLoaded;
|
||||
assert_eq!(format!("{}", error), "SSH agent is not loaded");
|
||||
|
||||
let error = GitExecutorError::AuthenticationError("auth failed".to_string());
|
||||
assert_eq!(format!("{}", error), "Authentication error: auth failed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_git_executor_error_from_redis_error() {
|
||||
let redis_error = redis::RedisError::from((redis::ErrorKind::TypeError, "type error"));
|
||||
let git_error = GitExecutorError::from(redis_error);
|
||||
|
||||
match git_error {
|
||||
GitExecutorError::RedisError(_) => {}
|
||||
_ => panic!("Expected RedisError variant"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_git_executor_error_from_serde_error() {
|
||||
let serde_error = serde_json::from_str::<GitConfig>("invalid json").unwrap_err();
|
||||
let git_error = GitExecutorError::from(serde_error);
|
||||
|
||||
match git_error {
|
||||
GitExecutorError::JsonError(_) => {}
|
||||
_ => panic!("Expected JsonError variant"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_git_executor_error_from_io_error() {
|
||||
let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
|
||||
let git_error = GitExecutorError::from(io_error);
|
||||
|
||||
match git_error {
|
||||
GitExecutorError::CommandExecutionError(_) => {}
|
||||
_ => panic!("Expected CommandExecutionError variant"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_redis_url_configuration() {
|
||||
// Test default Redis URL
|
||||
std::env::remove_var("REDIS_URL");
|
||||
std::env::remove_var("SAL_REDIS_URL");
|
||||
|
||||
// This is testing the internal function, but we can't access it directly
|
||||
// Instead, we test that GitExecutor can be created without panicking
|
||||
let executor = GitExecutor::new();
|
||||
let _executor = executor; // Just verify it was created successfully
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_redis_url_from_environment() {
|
||||
// Test REDIS_URL environment variable
|
||||
std::env::set_var("REDIS_URL", "redis://test:6379/1");
|
||||
|
||||
// Create executor - should use the environment variable
|
||||
let executor = GitExecutor::new();
|
||||
let _executor = executor; // Just verify it was created successfully
|
||||
|
||||
// Clean up
|
||||
std::env::remove_var("REDIS_URL");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sal_redis_url_from_environment() {
|
||||
// Test SAL_REDIS_URL environment variable (fallback)
|
||||
std::env::remove_var("REDIS_URL");
|
||||
std::env::set_var("SAL_REDIS_URL", "redis://sal-test:6379/2");
|
||||
|
||||
// Create executor - should use the SAL_REDIS_URL
|
||||
let executor = GitExecutor::new();
|
||||
let _executor = executor; // Just verify it was created successfully
|
||||
|
||||
// Clean up
|
||||
std::env::remove_var("SAL_REDIS_URL");
|
||||
}
|
124
git/tests/git_integration_tests.rs
Normal file
124
git/tests/git_integration_tests.rs
Normal file
@@ -0,0 +1,124 @@
|
||||
use sal_git::*;
|
||||
use std::fs;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
fn test_clone_existing_repository() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let base_path = temp_dir.path().to_str().unwrap();
|
||||
|
||||
let git_tree = GitTree::new(base_path).unwrap();
|
||||
|
||||
// First clone
|
||||
let result1 = git_tree.get("https://github.com/octocat/Hello-World.git");
|
||||
|
||||
// Second clone of same repo - should return existing
|
||||
let result2 = git_tree.get("https://github.com/octocat/Hello-World.git");
|
||||
|
||||
match (result1, result2) {
|
||||
(Ok(repos1), Ok(repos2)) => {
|
||||
// git_tree.get() returns Vec<GitRepo>, should have exactly 1 repo
|
||||
assert_eq!(
|
||||
repos1.len(),
|
||||
1,
|
||||
"First clone should return exactly 1 repository"
|
||||
);
|
||||
assert_eq!(
|
||||
repos2.len(),
|
||||
1,
|
||||
"Second clone should return exactly 1 repository"
|
||||
);
|
||||
assert_eq!(
|
||||
repos1[0].path(),
|
||||
repos2[0].path(),
|
||||
"Both clones should point to same path"
|
||||
);
|
||||
|
||||
// Verify the path actually exists
|
||||
assert!(
|
||||
std::path::Path::new(repos1[0].path()).exists(),
|
||||
"Repository path should exist"
|
||||
);
|
||||
}
|
||||
(Err(e1), Err(e2)) => {
|
||||
// Both failed - acceptable if network/git issues
|
||||
println!("Note: Clone test skipped due to errors: {} / {}", e1, e2);
|
||||
}
|
||||
_ => {
|
||||
panic!(
|
||||
"Inconsistent results: one clone succeeded, other failed - this indicates a bug"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_repository_operations_on_cloned_repo() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let base_path = temp_dir.path().to_str().unwrap();
|
||||
|
||||
let git_tree = GitTree::new(base_path).unwrap();
|
||||
|
||||
match git_tree.get("https://github.com/octocat/Hello-World.git") {
|
||||
Ok(repos) if repos.len() == 1 => {
|
||||
let repo = &repos[0];
|
||||
|
||||
// Test has_changes on fresh clone
|
||||
match repo.has_changes() {
|
||||
Ok(has_changes) => assert!(!has_changes, "Fresh clone should have no changes"),
|
||||
Err(_) => println!("Note: has_changes test skipped due to git availability"),
|
||||
}
|
||||
|
||||
// Test path is valid
|
||||
assert!(repo.path().len() > 0);
|
||||
assert!(std::path::Path::new(repo.path()).exists());
|
||||
}
|
||||
_ => {
|
||||
println!(
|
||||
"Note: Repository operations test skipped due to network/environment constraints"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_repositories_in_git_tree() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let base_path = temp_dir.path().to_str().unwrap();
|
||||
|
||||
// Create some fake git repositories for testing
|
||||
let repo1_path = temp_dir.path().join("github.com/user1/repo1");
|
||||
let repo2_path = temp_dir.path().join("github.com/user2/repo2");
|
||||
|
||||
fs::create_dir_all(&repo1_path).unwrap();
|
||||
fs::create_dir_all(&repo2_path).unwrap();
|
||||
fs::create_dir_all(repo1_path.join(".git")).unwrap();
|
||||
fs::create_dir_all(repo2_path.join(".git")).unwrap();
|
||||
|
||||
let git_tree = GitTree::new(base_path).unwrap();
|
||||
let repos = git_tree.list().unwrap();
|
||||
|
||||
assert!(repos.len() >= 2, "Should find at least 2 repositories");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_git_repository_handling() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let fake_repo_path = temp_dir.path().join("fake_repo");
|
||||
fs::create_dir_all(&fake_repo_path).unwrap();
|
||||
|
||||
// Create a directory that looks like a repo but isn't (no .git directory)
|
||||
let repo = GitRepo::new(fake_repo_path.to_str().unwrap().to_string());
|
||||
|
||||
// Operations should fail gracefully on non-git directories
|
||||
// Note: has_changes might succeed if git is available and treats it as empty repo
|
||||
// So we test the operations that definitely require .git directory
|
||||
assert!(
|
||||
repo.pull().is_err(),
|
||||
"Pull should fail on non-git directory"
|
||||
);
|
||||
assert!(
|
||||
repo.reset().is_err(),
|
||||
"Reset should fail on non-git directory"
|
||||
);
|
||||
}
|
119
git/tests/git_tests.rs
Normal file
119
git/tests/git_tests.rs
Normal file
@@ -0,0 +1,119 @@
|
||||
use sal_git::*;
|
||||
use std::fs;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
fn test_parse_git_url_https() {
|
||||
let (server, account, repo) = parse_git_url("https://github.com/user/repo.git");
|
||||
assert_eq!(server, "github.com");
|
||||
assert_eq!(account, "user");
|
||||
assert_eq!(repo, "repo");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_git_url_https_without_git_extension() {
|
||||
let (server, account, repo) = parse_git_url("https://github.com/user/repo");
|
||||
assert_eq!(server, "github.com");
|
||||
assert_eq!(account, "user");
|
||||
assert_eq!(repo, "repo");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_git_url_ssh() {
|
||||
let (server, account, repo) = parse_git_url("git@github.com:user/repo.git");
|
||||
assert_eq!(server, "github.com");
|
||||
assert_eq!(account, "user");
|
||||
assert_eq!(repo, "repo");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_git_url_ssh_without_git_extension() {
|
||||
let (server, account, repo) = parse_git_url("git@github.com:user/repo");
|
||||
assert_eq!(server, "github.com");
|
||||
assert_eq!(account, "user");
|
||||
assert_eq!(repo, "repo");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_git_url_invalid() {
|
||||
let (server, account, repo) = parse_git_url("invalid-url");
|
||||
assert_eq!(server, "");
|
||||
assert_eq!(account, "");
|
||||
assert_eq!(repo, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_git_tree_new_creates_directory() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let base_path = temp_dir.path().join("git_repos");
|
||||
let base_path_str = base_path.to_str().unwrap();
|
||||
|
||||
let _git_tree = GitTree::new(base_path_str).unwrap();
|
||||
assert!(base_path.exists());
|
||||
assert!(base_path.is_dir());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_git_tree_new_existing_directory() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let base_path = temp_dir.path().join("existing_dir");
|
||||
fs::create_dir_all(&base_path).unwrap();
|
||||
let base_path_str = base_path.to_str().unwrap();
|
||||
|
||||
let _git_tree = GitTree::new(base_path_str).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_git_tree_new_invalid_path() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let file_path = temp_dir.path().join("file.txt");
|
||||
fs::write(&file_path, "content").unwrap();
|
||||
let file_path_str = file_path.to_str().unwrap();
|
||||
|
||||
let result = GitTree::new(file_path_str);
|
||||
assert!(result.is_err());
|
||||
if let Err(error) = result {
|
||||
match error {
|
||||
GitError::InvalidBasePath(_) => {}
|
||||
_ => panic!("Expected InvalidBasePath error"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_git_tree_list_empty_directory() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let base_path_str = temp_dir.path().to_str().unwrap();
|
||||
|
||||
let git_tree = GitTree::new(base_path_str).unwrap();
|
||||
let repos = git_tree.list().unwrap();
|
||||
assert!(repos.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_git_repo_new() {
|
||||
let repo = GitRepo::new("/path/to/repo".to_string());
|
||||
assert_eq!(repo.path(), "/path/to/repo");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_git_repo_clone() {
|
||||
let repo1 = GitRepo::new("/path/to/repo".to_string());
|
||||
let repo2 = repo1.clone();
|
||||
assert_eq!(repo1.path(), repo2.path());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_git_error_display() {
|
||||
let error = GitError::InvalidUrl("bad-url".to_string());
|
||||
assert_eq!(format!("{}", error), "Could not parse git URL: bad-url");
|
||||
|
||||
let error = GitError::NoRepositoriesFound;
|
||||
assert_eq!(format!("{}", error), "No repositories found");
|
||||
|
||||
let error = GitError::RepositoryNotFound("pattern".to_string());
|
||||
assert_eq!(
|
||||
format!("{}", error),
|
||||
"No repositories found matching 'pattern'"
|
||||
);
|
||||
}
|
70
git/tests/rhai/01_git_basic.rhai
Normal file
70
git/tests/rhai/01_git_basic.rhai
Normal file
@@ -0,0 +1,70 @@
|
||||
// 01_git_basic.rhai
|
||||
// Tests for basic Git functionality like creating a GitTree, listing repositories, finding repositories, and cloning repositories
|
||||
|
||||
// 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`);
|
||||
|
||||
// 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!");
|
61
git/tests/rhai/02_git_operations.rhai
Normal file
61
git/tests/rhai/02_git_operations.rhai
Normal file
@@ -0,0 +1,61 @@
|
||||
// 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 pulled_repo = repo.pull();
|
||||
print("✓ GitRepo.pull(): Pull operation completed successfully");
|
||||
} catch(err) {
|
||||
// Pull might fail if there are no changes or network issues
|
||||
print(`Note: GitRepo.pull() failed (expected): ${err}`);
|
||||
print("✓ GitRepo.pull(): Method exists and can be called");
|
||||
}
|
||||
|
||||
// Test GitRepo.reset()
|
||||
print("Testing GitRepo.reset()...");
|
||||
try {
|
||||
let reset_repo = repo.reset();
|
||||
print("✓ GitRepo.reset(): Reset operation completed successfully");
|
||||
} catch(err) {
|
||||
print(`Error in GitRepo.reset(): ${err}`);
|
||||
throw err;
|
||||
}
|
||||
|
||||
// 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!");
|
151
git/tests/rhai/run_all_tests.rhai
Normal file
151
git/tests/rhai/run_all_tests.rhai
Normal file
@@ -0,0 +1,151 @@
|
||||
// run_all_tests.rhai
|
||||
// Test runner for all Git module tests
|
||||
|
||||
// Custom assert function
|
||||
fn assert_true(condition, message) {
|
||||
if !condition {
|
||||
print(`ASSERTION FAILED: ${message}`);
|
||||
throw message;
|
||||
}
|
||||
}
|
||||
|
||||
// Test counters
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
print("=== Git Module Test Suite ===");
|
||||
print("Running comprehensive tests for Git module functionality...");
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Test 3: Git Error Handling and Real Functionality
|
||||
print("\n--- Running Git Error Handling and Real Functionality Tests ---");
|
||||
try {
|
||||
print("Testing git_clone with invalid URL...");
|
||||
try {
|
||||
git_clone("invalid-url-format");
|
||||
print("!!! Expected error but got success");
|
||||
failed += 1;
|
||||
} catch(err) {
|
||||
assert_true(err.contains("Git error"), "Expected Git error message");
|
||||
print("✓ git_clone properly handles invalid URLs");
|
||||
}
|
||||
|
||||
print("Testing git_clone with real repository...");
|
||||
try {
|
||||
let repo = git_clone("https://github.com/octocat/Hello-World.git");
|
||||
let path = repo.path();
|
||||
assert_true(path.len() > 0, "Repository path should not be empty");
|
||||
print(`✓ git_clone successfully cloned repository to: ${path}`);
|
||||
|
||||
// Test repository operations
|
||||
print("Testing repository operations...");
|
||||
let has_changes = repo.has_changes();
|
||||
print(`✓ Repository has_changes check: ${has_changes}`);
|
||||
|
||||
} catch(err) {
|
||||
// Network issues or git not available are acceptable failures
|
||||
if err.contains("Git error") || err.contains("command") || err.contains("Failed to clone") {
|
||||
print(`Note: git_clone test skipped due to environment: ${err}`);
|
||||
} else {
|
||||
print(`!!! Unexpected error in git_clone: ${err}`);
|
||||
failed += 1;
|
||||
}
|
||||
}
|
||||
|
||||
print("Testing GitTree with invalid path...");
|
||||
try {
|
||||
let git_tree = git_tree_new("/invalid/nonexistent/path");
|
||||
print("Note: GitTree creation succeeded (directory was created)");
|
||||
// Clean up if it was created
|
||||
try {
|
||||
delete("/invalid");
|
||||
} catch(cleanup_err) {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
} catch(err) {
|
||||
print(`✓ GitTree properly handles invalid paths: ${err}`);
|
||||
}
|
||||
|
||||
print("--- Git Error Handling Tests completed successfully ---");
|
||||
passed += 1;
|
||||
} catch(err) {
|
||||
print(`!!! Error in Git Error Handling Tests: ${err}`);
|
||||
failed += 1;
|
||||
}
|
||||
|
||||
// Summary
|
||||
print("\n=== Test Results ===");
|
||||
print(`Passed: ${passed}`);
|
||||
print(`Failed: ${failed}`);
|
||||
print(`Total: ${passed + failed}`);
|
||||
|
||||
if failed == 0 {
|
||||
print("🎉 All tests passed!");
|
||||
} else {
|
||||
print("❌ Some tests failed!");
|
||||
}
|
||||
|
||||
print("=== Git Module Test Suite Complete ===");
|
121
git/tests/rhai_advanced_tests.rs
Normal file
121
git/tests/rhai_advanced_tests.rs
Normal file
@@ -0,0 +1,121 @@
|
||||
use rhai::Engine;
|
||||
use sal_git::rhai::*;
|
||||
|
||||
#[test]
|
||||
fn test_git_clone_with_various_url_formats() {
|
||||
let mut engine = Engine::new();
|
||||
register_git_module(&mut engine).unwrap();
|
||||
|
||||
let test_cases = vec![
|
||||
(
|
||||
"https://github.com/octocat/Hello-World.git",
|
||||
"HTTPS with .git",
|
||||
),
|
||||
(
|
||||
"https://github.com/octocat/Hello-World",
|
||||
"HTTPS without .git",
|
||||
),
|
||||
// SSH would require key setup: ("git@github.com:octocat/Hello-World.git", "SSH format"),
|
||||
];
|
||||
|
||||
for (url, description) in test_cases {
|
||||
let script = format!(
|
||||
r#"
|
||||
let result = "";
|
||||
try {{
|
||||
let repo = git_clone("{}");
|
||||
let path = repo.path();
|
||||
if path.len() > 0 {{
|
||||
result = "success";
|
||||
}} else {{
|
||||
result = "no_path";
|
||||
}}
|
||||
}} catch(e) {{
|
||||
if e.contains("Git error") {{
|
||||
result = "git_error";
|
||||
}} else {{
|
||||
result = "unexpected_error";
|
||||
}}
|
||||
}}
|
||||
result
|
||||
"#,
|
||||
url
|
||||
);
|
||||
|
||||
let result = engine.eval::<String>(&script);
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"Failed to execute script for {}: {:?}",
|
||||
description,
|
||||
result
|
||||
);
|
||||
|
||||
let outcome = result.unwrap();
|
||||
// Accept success or git_error (network issues)
|
||||
assert!(
|
||||
outcome == "success" || outcome == "git_error",
|
||||
"Unexpected outcome for {}: {}",
|
||||
description,
|
||||
outcome
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_git_tree_operations_comprehensive() {
|
||||
let mut engine = Engine::new();
|
||||
register_git_module(&mut engine).unwrap();
|
||||
|
||||
let script = r#"
|
||||
let results = [];
|
||||
|
||||
try {
|
||||
// Test GitTree creation
|
||||
let git_tree = git_tree_new("/tmp/rhai_comprehensive_test");
|
||||
results.push("git_tree_created");
|
||||
|
||||
// Test list on empty directory
|
||||
let repos = git_tree.list();
|
||||
results.push("list_executed");
|
||||
|
||||
// Test find with pattern
|
||||
let found = git_tree.find("nonexistent");
|
||||
results.push("find_executed");
|
||||
|
||||
} catch(e) {
|
||||
results.push("error_occurred");
|
||||
}
|
||||
|
||||
results.len()
|
||||
"#;
|
||||
|
||||
let result = engine.eval::<i64>(&script);
|
||||
assert!(result.is_ok());
|
||||
assert!(result.unwrap() >= 3, "Should execute at least 3 operations");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_message_quality() {
|
||||
let mut engine = Engine::new();
|
||||
register_git_module(&mut engine).unwrap();
|
||||
|
||||
let script = r#"
|
||||
let error_msg = "";
|
||||
try {
|
||||
git_clone("invalid-url-format");
|
||||
} catch(e) {
|
||||
error_msg = e;
|
||||
}
|
||||
error_msg
|
||||
"#;
|
||||
|
||||
let result = engine.eval::<String>(&script);
|
||||
assert!(result.is_ok());
|
||||
|
||||
let error_msg = result.unwrap();
|
||||
assert!(
|
||||
error_msg.contains("Git error"),
|
||||
"Error should contain 'Git error'"
|
||||
);
|
||||
assert!(error_msg.len() > 10, "Error message should be descriptive");
|
||||
}
|
101
git/tests/rhai_tests.rs
Normal file
101
git/tests/rhai_tests.rs
Normal file
@@ -0,0 +1,101 @@
|
||||
use rhai::Engine;
|
||||
use sal_git::rhai::*;
|
||||
|
||||
#[test]
|
||||
fn test_register_git_module() {
|
||||
let mut engine = Engine::new();
|
||||
let result = register_git_module(&mut engine);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_git_tree_new_function_registered() {
|
||||
let mut engine = Engine::new();
|
||||
register_git_module(&mut engine).unwrap();
|
||||
|
||||
// Test that the function is registered by trying to call it
|
||||
// This will fail because /nonexistent doesn't exist, but it proves the function is registered
|
||||
let result = engine.eval::<String>(
|
||||
r#"
|
||||
let result = "";
|
||||
try {
|
||||
let git_tree = git_tree_new("/nonexistent");
|
||||
result = "success";
|
||||
} catch(e) {
|
||||
result = "error_caught";
|
||||
}
|
||||
result
|
||||
"#,
|
||||
);
|
||||
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), "error_caught");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_git_clone_function_registered() {
|
||||
let mut engine = Engine::new();
|
||||
register_git_module(&mut engine).unwrap();
|
||||
|
||||
// Test that git_clone function is registered by testing with invalid URL
|
||||
let result = engine.eval::<String>(
|
||||
r#"
|
||||
let result = "";
|
||||
try {
|
||||
git_clone("invalid-url-format");
|
||||
result = "unexpected_success";
|
||||
} catch(e) {
|
||||
// Should catch error for invalid URL
|
||||
if e.contains("Git error") {
|
||||
result = "error_caught_correctly";
|
||||
} else {
|
||||
result = "wrong_error_type";
|
||||
}
|
||||
}
|
||||
result
|
||||
"#,
|
||||
);
|
||||
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), "error_caught_correctly");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_git_clone_with_valid_public_repo() {
|
||||
let mut engine = Engine::new();
|
||||
register_git_module(&mut engine).unwrap();
|
||||
|
||||
// Test with a real public repository (small one for testing)
|
||||
let result = engine.eval::<String>(
|
||||
r#"
|
||||
let result = "";
|
||||
try {
|
||||
let repo = git_clone("https://github.com/octocat/Hello-World.git");
|
||||
// If successful, repo should have a valid path
|
||||
let path = repo.path();
|
||||
if path.len() > 0 {
|
||||
result = "clone_successful";
|
||||
} else {
|
||||
result = "clone_failed_no_path";
|
||||
}
|
||||
} catch(e) {
|
||||
// Network issues or git not available are acceptable failures
|
||||
if e.contains("Git error") || e.contains("command") {
|
||||
result = "acceptable_failure";
|
||||
} else {
|
||||
result = "unexpected_error";
|
||||
}
|
||||
}
|
||||
result
|
||||
"#,
|
||||
);
|
||||
|
||||
assert!(result.is_ok());
|
||||
let outcome = result.unwrap();
|
||||
// Accept either successful clone or acceptable failure (network/git issues)
|
||||
assert!(
|
||||
outcome == "clone_successful" || outcome == "acceptable_failure",
|
||||
"Unexpected outcome: {}",
|
||||
outcome
|
||||
);
|
||||
}
|
25
herodo/Cargo.toml
Normal file
25
herodo/Cargo.toml
Normal file
@@ -0,0 +1,25 @@
|
||||
[package]
|
||||
name = "herodo"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
authors = ["PlanetFirst <info@incubaid.com>"]
|
||||
description = "Herodo - A Rhai script executor for SAL (System Abstraction Layer)"
|
||||
repository = "https://git.threefold.info/herocode/sal"
|
||||
license = "Apache-2.0"
|
||||
keywords = ["rhai", "scripting", "automation", "sal", "system"]
|
||||
categories = ["command-line-utilities", "development-tools"]
|
||||
|
||||
[[bin]]
|
||||
name = "herodo"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
# Core dependencies for herodo binary
|
||||
env_logger = { workspace = true }
|
||||
rhai = { workspace = true }
|
||||
|
||||
# SAL library for Rhai module registration
|
||||
sal = { path = ".." }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = { workspace = true }
|
142
herodo/README.md
Normal file
142
herodo/README.md
Normal file
@@ -0,0 +1,142 @@
|
||||
# Herodo - Rhai Script Executor for SAL
|
||||
|
||||
**Version: 0.1.0**
|
||||
|
||||
Herodo is a command-line utility that executes Rhai scripts with full access to the SAL (System Abstraction Layer) library. It provides a powerful scripting environment for automation and system management tasks.
|
||||
|
||||
## Features
|
||||
|
||||
- **Single Script Execution**: Execute individual `.rhai` script files
|
||||
- **Directory Execution**: Execute all `.rhai` scripts in a directory (recursively)
|
||||
- **Sorted Execution**: Scripts are executed in alphabetical order for predictable behavior
|
||||
- **SAL Integration**: Full access to all SAL modules and functions
|
||||
- **Error Handling**: Clear error messages and proper exit codes
|
||||
- **Logging Support**: Built-in logging with `env_logger`
|
||||
|
||||
## Installation
|
||||
|
||||
Build the herodo binary:
|
||||
|
||||
```bash
|
||||
cd herodo
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
The executable will be available at `target/release/herodo`.
|
||||
|
||||
## Usage
|
||||
|
||||
### Execute a Single Script
|
||||
|
||||
```bash
|
||||
herodo path/to/script.rhai
|
||||
```
|
||||
|
||||
### Execute All Scripts in a Directory
|
||||
|
||||
```bash
|
||||
herodo path/to/scripts/
|
||||
```
|
||||
|
||||
When given a directory, herodo will:
|
||||
1. Recursively find all `.rhai` files
|
||||
2. Sort them alphabetically
|
||||
3. Execute them in order
|
||||
4. Stop on the first error
|
||||
|
||||
## Example Scripts
|
||||
|
||||
### Basic Script
|
||||
```rhai
|
||||
// hello.rhai
|
||||
println("Hello from Herodo!");
|
||||
let result = 42 * 2;
|
||||
println("Result: " + result);
|
||||
```
|
||||
|
||||
### Using SAL Functions
|
||||
```rhai
|
||||
// system_info.rhai
|
||||
println("=== System Information ===");
|
||||
|
||||
// Check if a file exists
|
||||
let config_exists = exist("/etc/hosts");
|
||||
println("Config file exists: " + config_exists);
|
||||
|
||||
// Download a file
|
||||
download("https://example.com/data.txt", "/tmp/data.txt");
|
||||
println("File downloaded successfully");
|
||||
|
||||
// Execute a system command
|
||||
let output = run("ls -la /tmp");
|
||||
println("Directory listing:");
|
||||
println(output.stdout);
|
||||
```
|
||||
|
||||
### Redis Operations
|
||||
```rhai
|
||||
// redis_example.rhai
|
||||
println("=== Redis Operations ===");
|
||||
|
||||
// Set a value
|
||||
redis_set("app_status", "running");
|
||||
println("Status set in Redis");
|
||||
|
||||
// Get the value
|
||||
let status = redis_get("app_status");
|
||||
println("Current status: " + status);
|
||||
```
|
||||
|
||||
## Available SAL Functions
|
||||
|
||||
Herodo provides access to all SAL modules through Rhai:
|
||||
|
||||
- **File System**: `exist()`, `mkdir()`, `delete()`, `file_size()`
|
||||
- **Downloads**: `download()`, `download_install()`
|
||||
- **Process Management**: `run()`, `kill()`, `process_list()`
|
||||
- **Redis**: `redis_set()`, `redis_get()`, `redis_del()`
|
||||
- **PostgreSQL**: Database operations and management
|
||||
- **Network**: HTTP requests, SSH operations, TCP connectivity
|
||||
- **Virtualization**: Container operations with Buildah and Nerdctl
|
||||
- **Text Processing**: String manipulation and template rendering
|
||||
- **And many more...**
|
||||
|
||||
## Error Handling
|
||||
|
||||
Herodo provides clear error messages and appropriate exit codes:
|
||||
|
||||
- **Exit Code 0**: All scripts executed successfully
|
||||
- **Exit Code 1**: Error occurred (file not found, script error, etc.)
|
||||
|
||||
## Logging
|
||||
|
||||
Enable detailed logging by setting the `RUST_LOG` environment variable:
|
||||
|
||||
```bash
|
||||
RUST_LOG=debug herodo script.rhai
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
Run the test suite:
|
||||
|
||||
```bash
|
||||
cd herodo
|
||||
cargo test
|
||||
```
|
||||
|
||||
The test suite includes:
|
||||
- Unit tests for core functionality
|
||||
- Integration tests with real script execution
|
||||
- Error handling scenarios
|
||||
- SAL module integration tests
|
||||
|
||||
## Dependencies
|
||||
|
||||
- **rhai**: Embedded scripting language
|
||||
- **env_logger**: Logging implementation
|
||||
- **sal**: System Abstraction Layer library
|
||||
|
||||
## License
|
||||
|
||||
Apache-2.0
|
@@ -1,9 +1,8 @@
|
||||
//! Herodo - A Rhai script executor for SAL
|
||||
//!
|
||||
//! This binary loads the Rhai engine, registers all SAL modules,
|
||||
//! This library loads the Rhai engine, registers all SAL modules,
|
||||
//! and executes Rhai scripts from a specified directory in sorted order.
|
||||
|
||||
// Removed unused imports
|
||||
use rhai::Engine;
|
||||
use std::error::Error;
|
||||
use std::fs;
|
||||
@@ -35,67 +34,49 @@ pub fn run(script_path: &str) -> Result<(), Box<dyn Error>> {
|
||||
engine.register_fn("println", |s: &str| println!("{}", s));
|
||||
|
||||
// Register all SAL modules with the engine
|
||||
crate::rhai::register(&mut engine)?;
|
||||
sal::rhai::register(&mut engine)?;
|
||||
|
||||
// Determine if the path is a file or directory
|
||||
// Collect script files to execute
|
||||
let script_files: Vec<PathBuf> = if path.is_file() {
|
||||
// Check if it's a .rhai file
|
||||
if path.extension().map_or(false, |ext| ext == "rhai") {
|
||||
vec![path.to_path_buf()]
|
||||
} else {
|
||||
eprintln!("Error: '{}' is not a Rhai script file", script_path);
|
||||
// Single file
|
||||
if let Some(extension) = path.extension() {
|
||||
if extension != "rhai" {
|
||||
eprintln!("Warning: '{}' does not have a .rhai extension", script_path);
|
||||
}
|
||||
}
|
||||
vec![path.to_path_buf()]
|
||||
} else if path.is_dir() {
|
||||
// Directory - collect all .rhai files recursively and sort them
|
||||
let mut files = Vec::new();
|
||||
collect_rhai_files(path, &mut files)?;
|
||||
|
||||
if files.is_empty() {
|
||||
eprintln!("No .rhai files found in directory: {}", script_path);
|
||||
process::exit(1);
|
||||
}
|
||||
} else if path.is_dir() {
|
||||
// Find all .rhai files in the directory recursively
|
||||
let mut files: Vec<PathBuf> = Vec::new();
|
||||
|
||||
// Helper function to recursively find .rhai files
|
||||
fn find_rhai_files(dir: &Path, files: &mut Vec<PathBuf>) -> std::io::Result<()> {
|
||||
if dir.is_dir() {
|
||||
for entry in fs::read_dir(dir)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
|
||||
if path.is_dir() {
|
||||
find_rhai_files(&path, files)?;
|
||||
} else if path.is_file() &&
|
||||
path.extension().map_or(false, |ext| ext == "rhai") {
|
||||
files.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Find all .rhai files recursively
|
||||
find_rhai_files(path, &mut files)?;
|
||||
|
||||
// Sort the script files by name
|
||||
|
||||
// Sort files for consistent execution order
|
||||
files.sort();
|
||||
|
||||
if files.is_empty() {
|
||||
println!("No Rhai scripts found in '{}'", script_path);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
|
||||
files
|
||||
} else {
|
||||
eprintln!("Error: '{}' is neither a file nor a directory", script_path);
|
||||
process::exit(1);
|
||||
};
|
||||
|
||||
println!("Found {} Rhai script{} to execute:",
|
||||
script_files.len(),
|
||||
if script_files.len() == 1 { "" } else { "s" });
|
||||
|
||||
println!(
|
||||
"Found {} Rhai script{} to execute:",
|
||||
script_files.len(),
|
||||
if script_files.len() == 1 { "" } else { "s" }
|
||||
);
|
||||
|
||||
// Execute each script in sorted order
|
||||
for script_file in script_files {
|
||||
println!("\nExecuting: {}", script_file.display());
|
||||
|
||||
|
||||
// Read the script content
|
||||
let script = fs::read_to_string(&script_file)?;
|
||||
|
||||
|
||||
// Execute the script
|
||||
match engine.eval::<rhai::Dynamic>(&script) {
|
||||
Ok(result) => {
|
||||
@@ -103,7 +84,7 @@ pub fn run(script_path: &str) -> Result<(), Box<dyn Error>> {
|
||||
if !result.is_unit() {
|
||||
println!("Result: {}", result);
|
||||
}
|
||||
},
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("Error executing script: {}", err);
|
||||
// Exit with error code when a script fails
|
||||
@@ -112,6 +93,37 @@ pub fn run(script_path: &str) -> Result<(), Box<dyn Error>> {
|
||||
}
|
||||
}
|
||||
|
||||
println!("\nAll scripts executed");
|
||||
println!("\nAll scripts executed successfully!");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Recursively collect all .rhai files from a directory
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `dir` - Directory to search
|
||||
/// * `files` - Vector to collect files into
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Result indicating success or failure
|
||||
fn collect_rhai_files(dir: &Path, files: &mut Vec<PathBuf>) -> Result<(), Box<dyn Error>> {
|
||||
for entry in fs::read_dir(dir)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
|
||||
if path.is_dir() {
|
||||
// Recursively search subdirectories
|
||||
collect_rhai_files(&path, files)?;
|
||||
} else if path.is_file() {
|
||||
// Check if it's a .rhai file
|
||||
if let Some(extension) = path.extension() {
|
||||
if extension == "rhai" {
|
||||
files.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
25
herodo/src/main.rs
Normal file
25
herodo/src/main.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
//! Herodo binary entry point
|
||||
//!
|
||||
//! This is the main entry point for the herodo binary.
|
||||
//! It parses command line arguments and executes Rhai scripts using the SAL library.
|
||||
|
||||
use env_logger;
|
||||
use std::env;
|
||||
use std::process;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Initialize the logger
|
||||
env_logger::init();
|
||||
|
||||
let args: Vec<String> = env::args().collect();
|
||||
|
||||
if args.len() != 2 {
|
||||
eprintln!("Usage: {} <script_path>", args[0]);
|
||||
process::exit(1);
|
||||
}
|
||||
|
||||
let script_path = &args[1];
|
||||
|
||||
// Call the run function from the herodo library
|
||||
herodo::run(script_path)
|
||||
}
|
222
herodo/tests/integration_tests.rs
Normal file
222
herodo/tests/integration_tests.rs
Normal file
@@ -0,0 +1,222 @@
|
||||
//! Integration tests for herodo script executor
|
||||
//!
|
||||
//! These tests verify that herodo can execute Rhai scripts correctly,
|
||||
//! handle errors appropriately, and integrate with SAL modules.
|
||||
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use tempfile::TempDir;
|
||||
|
||||
/// Test that herodo can execute a simple Rhai script
|
||||
#[test]
|
||||
fn test_simple_script_execution() {
|
||||
let temp_dir = TempDir::new().expect("Failed to create temp directory");
|
||||
let script_path = temp_dir.path().join("test.rhai");
|
||||
|
||||
// Create a simple test script
|
||||
fs::write(
|
||||
&script_path,
|
||||
r#"
|
||||
println("Hello from herodo test!");
|
||||
let result = 42;
|
||||
result
|
||||
"#,
|
||||
)
|
||||
.expect("Failed to write test script");
|
||||
|
||||
// Execute the script
|
||||
let result = herodo::run(script_path.to_str().unwrap());
|
||||
assert!(result.is_ok(), "Script execution should succeed");
|
||||
}
|
||||
|
||||
/// Test that herodo can execute multiple scripts in a directory
|
||||
#[test]
|
||||
fn test_directory_script_execution() {
|
||||
let temp_dir = TempDir::new().expect("Failed to create temp directory");
|
||||
|
||||
// Create multiple test scripts
|
||||
fs::write(
|
||||
temp_dir.path().join("01_first.rhai"),
|
||||
r#"
|
||||
println("First script executing");
|
||||
let first = 1;
|
||||
"#,
|
||||
)
|
||||
.expect("Failed to write first script");
|
||||
|
||||
fs::write(
|
||||
temp_dir.path().join("02_second.rhai"),
|
||||
r#"
|
||||
println("Second script executing");
|
||||
let second = 2;
|
||||
"#,
|
||||
)
|
||||
.expect("Failed to write second script");
|
||||
|
||||
fs::write(
|
||||
temp_dir.path().join("03_third.rhai"),
|
||||
r#"
|
||||
println("Third script executing");
|
||||
let third = 3;
|
||||
"#,
|
||||
)
|
||||
.expect("Failed to write third script");
|
||||
|
||||
// Execute all scripts in the directory
|
||||
let result = herodo::run(temp_dir.path().to_str().unwrap());
|
||||
assert!(result.is_ok(), "Directory script execution should succeed");
|
||||
}
|
||||
|
||||
/// Test that herodo handles non-existent paths correctly
|
||||
#[test]
|
||||
fn test_nonexistent_path_handling() {
|
||||
// This test verifies error handling but herodo::run calls process::exit
|
||||
// In a real scenario, we would need to refactor herodo to return errors
|
||||
// instead of calling process::exit for better testability
|
||||
|
||||
// For now, we test that the path validation logic works
|
||||
let nonexistent_path = "/this/path/does/not/exist";
|
||||
let path = Path::new(nonexistent_path);
|
||||
assert!(!path.exists(), "Test path should not exist");
|
||||
}
|
||||
|
||||
/// Test that herodo can execute scripts with SAL module functions
|
||||
#[test]
|
||||
fn test_sal_module_integration() {
|
||||
let temp_dir = TempDir::new().expect("Failed to create temp directory");
|
||||
let script_path = temp_dir.path().join("sal_test.rhai");
|
||||
|
||||
// Create a script that uses SAL functions
|
||||
fs::write(
|
||||
&script_path,
|
||||
r#"
|
||||
println("Testing SAL module integration");
|
||||
|
||||
// Test file existence check (should work with temp directory)
|
||||
let temp_exists = exist(".");
|
||||
println("Current directory exists: " + temp_exists);
|
||||
|
||||
// Test basic text operations
|
||||
let text = " hello world ";
|
||||
let trimmed = text.trim();
|
||||
println("Trimmed text: '" + trimmed + "'");
|
||||
|
||||
println("SAL integration test completed");
|
||||
"#,
|
||||
)
|
||||
.expect("Failed to write SAL test script");
|
||||
|
||||
// Execute the script
|
||||
let result = herodo::run(script_path.to_str().unwrap());
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"SAL integration script should execute successfully"
|
||||
);
|
||||
}
|
||||
|
||||
/// Test script execution with subdirectories
|
||||
#[test]
|
||||
fn test_recursive_directory_execution() {
|
||||
let temp_dir = TempDir::new().expect("Failed to create temp directory");
|
||||
|
||||
// Create subdirectory
|
||||
let sub_dir = temp_dir.path().join("subdir");
|
||||
fs::create_dir(&sub_dir).expect("Failed to create subdirectory");
|
||||
|
||||
// Create scripts in main directory
|
||||
fs::write(
|
||||
temp_dir.path().join("main.rhai"),
|
||||
r#"
|
||||
println("Main directory script");
|
||||
"#,
|
||||
)
|
||||
.expect("Failed to write main script");
|
||||
|
||||
// Create scripts in subdirectory
|
||||
fs::write(
|
||||
sub_dir.join("sub.rhai"),
|
||||
r#"
|
||||
println("Subdirectory script");
|
||||
"#,
|
||||
)
|
||||
.expect("Failed to write sub script");
|
||||
|
||||
// Execute all scripts recursively
|
||||
let result = herodo::run(temp_dir.path().to_str().unwrap());
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"Recursive directory execution should succeed"
|
||||
);
|
||||
}
|
||||
|
||||
/// Test that herodo handles empty directories gracefully
|
||||
#[test]
|
||||
fn test_empty_directory_handling() {
|
||||
let temp_dir = TempDir::new().expect("Failed to create temp directory");
|
||||
|
||||
// Create an empty subdirectory
|
||||
let empty_dir = temp_dir.path().join("empty");
|
||||
fs::create_dir(&empty_dir).expect("Failed to create empty directory");
|
||||
|
||||
// This should handle the empty directory case
|
||||
// Note: herodo::run will call process::exit(1) for empty directories
|
||||
// In a production refactor, this should return an error instead
|
||||
let path = empty_dir.to_str().unwrap();
|
||||
let path_obj = Path::new(path);
|
||||
assert!(
|
||||
path_obj.is_dir(),
|
||||
"Empty directory should exist and be a directory"
|
||||
);
|
||||
}
|
||||
|
||||
/// Test script with syntax errors
|
||||
#[test]
|
||||
fn test_syntax_error_handling() {
|
||||
let temp_dir = TempDir::new().expect("Failed to create temp directory");
|
||||
let script_path = temp_dir.path().join("syntax_error.rhai");
|
||||
|
||||
// Create a script with syntax errors
|
||||
fs::write(
|
||||
&script_path,
|
||||
r#"
|
||||
println("This script has syntax errors");
|
||||
let invalid syntax here;
|
||||
missing_function_call(;
|
||||
"#,
|
||||
)
|
||||
.expect("Failed to write syntax error script");
|
||||
|
||||
// Note: herodo::run will call process::exit(1) on script errors
|
||||
// In a production refactor, this should return an error instead
|
||||
// For now, we just verify the file exists and can be read
|
||||
assert!(script_path.exists(), "Syntax error script should exist");
|
||||
let content = fs::read_to_string(&script_path).expect("Should be able to read script");
|
||||
assert!(
|
||||
content.contains("syntax errors"),
|
||||
"Script should contain expected content"
|
||||
);
|
||||
}
|
||||
|
||||
/// Test file extension validation
|
||||
#[test]
|
||||
fn test_file_extension_validation() {
|
||||
let temp_dir = TempDir::new().expect("Failed to create temp directory");
|
||||
|
||||
// Create files with different extensions
|
||||
let rhai_file = temp_dir.path().join("valid.rhai");
|
||||
let txt_file = temp_dir.path().join("invalid.txt");
|
||||
|
||||
fs::write(&rhai_file, "println(\"Valid rhai file\");").expect("Failed to write rhai file");
|
||||
fs::write(&txt_file, "This is not a rhai file").expect("Failed to write txt file");
|
||||
|
||||
// Verify file extensions
|
||||
assert_eq!(rhai_file.extension().unwrap(), "rhai");
|
||||
assert_eq!(txt_file.extension().unwrap(), "txt");
|
||||
|
||||
// herodo should execute .rhai files and warn about non-.rhai files
|
||||
let result = herodo::run(rhai_file.to_str().unwrap());
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"Valid .rhai file should execute successfully"
|
||||
);
|
||||
}
|
268
herodo/tests/unit_tests.rs
Normal file
268
herodo/tests/unit_tests.rs
Normal file
@@ -0,0 +1,268 @@
|
||||
//! Unit tests for herodo library functions
|
||||
//!
|
||||
//! These tests focus on individual functions and components of the herodo library.
|
||||
|
||||
use std::fs;
|
||||
use tempfile::TempDir;
|
||||
|
||||
/// Test the collect_rhai_files function indirectly through directory operations
|
||||
#[test]
|
||||
fn test_rhai_file_collection_logic() {
|
||||
let temp_dir = TempDir::new().expect("Failed to create temp directory");
|
||||
|
||||
// Create various files
|
||||
fs::write(temp_dir.path().join("script1.rhai"), "// Script 1")
|
||||
.expect("Failed to write script1");
|
||||
fs::write(temp_dir.path().join("script2.rhai"), "// Script 2")
|
||||
.expect("Failed to write script2");
|
||||
fs::write(temp_dir.path().join("not_script.txt"), "Not a script")
|
||||
.expect("Failed to write txt file");
|
||||
fs::write(temp_dir.path().join("README.md"), "# README").expect("Failed to write README");
|
||||
|
||||
// Create subdirectory with more scripts
|
||||
let sub_dir = temp_dir.path().join("subdir");
|
||||
fs::create_dir(&sub_dir).expect("Failed to create subdirectory");
|
||||
fs::write(sub_dir.join("sub_script.rhai"), "// Sub script")
|
||||
.expect("Failed to write sub script");
|
||||
|
||||
// Count .rhai files manually
|
||||
let mut rhai_count = 0;
|
||||
for entry in fs::read_dir(temp_dir.path()).expect("Failed to read temp directory") {
|
||||
let entry = entry.expect("Failed to get directory entry");
|
||||
let path = entry.path();
|
||||
if path.is_file() && path.extension().map_or(false, |ext| ext == "rhai") {
|
||||
rhai_count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Should find 2 .rhai files in the main directory
|
||||
assert_eq!(
|
||||
rhai_count, 2,
|
||||
"Should find exactly 2 .rhai files in main directory"
|
||||
);
|
||||
|
||||
// Verify subdirectory has 1 .rhai file
|
||||
let mut sub_rhai_count = 0;
|
||||
for entry in fs::read_dir(&sub_dir).expect("Failed to read subdirectory") {
|
||||
let entry = entry.expect("Failed to get directory entry");
|
||||
let path = entry.path();
|
||||
if path.is_file() && path.extension().map_or(false, |ext| ext == "rhai") {
|
||||
sub_rhai_count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
sub_rhai_count, 1,
|
||||
"Should find exactly 1 .rhai file in subdirectory"
|
||||
);
|
||||
}
|
||||
|
||||
/// Test path validation logic
|
||||
#[test]
|
||||
fn test_path_validation() {
|
||||
let temp_dir = TempDir::new().expect("Failed to create temp directory");
|
||||
let script_path = temp_dir.path().join("test.rhai");
|
||||
|
||||
// Create a test script
|
||||
fs::write(&script_path, "println(\"test\");").expect("Failed to write test script");
|
||||
|
||||
// Test file path validation
|
||||
assert!(script_path.exists(), "Script file should exist");
|
||||
assert!(script_path.is_file(), "Script path should be a file");
|
||||
|
||||
// Test directory path validation
|
||||
assert!(temp_dir.path().exists(), "Temp directory should exist");
|
||||
assert!(temp_dir.path().is_dir(), "Temp path should be a directory");
|
||||
|
||||
// Test non-existent path
|
||||
let nonexistent = temp_dir.path().join("nonexistent.rhai");
|
||||
assert!(!nonexistent.exists(), "Non-existent path should not exist");
|
||||
}
|
||||
|
||||
/// Test file extension checking
|
||||
#[test]
|
||||
fn test_file_extension_checking() {
|
||||
let temp_dir = TempDir::new().expect("Failed to create temp directory");
|
||||
|
||||
// Create files with different extensions
|
||||
let rhai_file = temp_dir.path().join("script.rhai");
|
||||
let txt_file = temp_dir.path().join("document.txt");
|
||||
let no_ext_file = temp_dir.path().join("no_extension");
|
||||
|
||||
fs::write(&rhai_file, "// Rhai script").expect("Failed to write rhai file");
|
||||
fs::write(&txt_file, "Text document").expect("Failed to write txt file");
|
||||
fs::write(&no_ext_file, "No extension").expect("Failed to write no extension file");
|
||||
|
||||
// Test extension detection
|
||||
assert_eq!(rhai_file.extension().unwrap(), "rhai");
|
||||
assert_eq!(txt_file.extension().unwrap(), "txt");
|
||||
assert!(no_ext_file.extension().is_none());
|
||||
|
||||
// Test extension comparison
|
||||
assert!(rhai_file.extension().map_or(false, |ext| ext == "rhai"));
|
||||
assert!(!txt_file.extension().map_or(false, |ext| ext == "rhai"));
|
||||
assert!(!no_ext_file.extension().map_or(false, |ext| ext == "rhai"));
|
||||
}
|
||||
|
||||
/// Test script content reading
|
||||
#[test]
|
||||
fn test_script_content_reading() {
|
||||
let temp_dir = TempDir::new().expect("Failed to create temp directory");
|
||||
let script_path = temp_dir.path().join("content_test.rhai");
|
||||
|
||||
let expected_content = r#"
|
||||
println("Testing content reading");
|
||||
let value = 42;
|
||||
value * 2
|
||||
"#;
|
||||
|
||||
fs::write(&script_path, expected_content).expect("Failed to write script content");
|
||||
|
||||
// Read the content back
|
||||
let actual_content = fs::read_to_string(&script_path).expect("Failed to read script content");
|
||||
assert_eq!(
|
||||
actual_content, expected_content,
|
||||
"Script content should match"
|
||||
);
|
||||
|
||||
// Verify content contains expected elements
|
||||
assert!(
|
||||
actual_content.contains("println"),
|
||||
"Content should contain println"
|
||||
);
|
||||
assert!(
|
||||
actual_content.contains("let value = 42"),
|
||||
"Content should contain variable declaration"
|
||||
);
|
||||
assert!(
|
||||
actual_content.contains("value * 2"),
|
||||
"Content should contain expression"
|
||||
);
|
||||
}
|
||||
|
||||
/// Test directory traversal logic
|
||||
#[test]
|
||||
fn test_directory_traversal() {
|
||||
let temp_dir = TempDir::new().expect("Failed to create temp directory");
|
||||
|
||||
// Create nested directory structure
|
||||
let level1 = temp_dir.path().join("level1");
|
||||
let level2 = level1.join("level2");
|
||||
let level3 = level2.join("level3");
|
||||
|
||||
fs::create_dir_all(&level3).expect("Failed to create nested directories");
|
||||
|
||||
// Create scripts at different levels
|
||||
fs::write(temp_dir.path().join("root.rhai"), "// Root script")
|
||||
.expect("Failed to write root script");
|
||||
fs::write(level1.join("level1.rhai"), "// Level 1 script")
|
||||
.expect("Failed to write level1 script");
|
||||
fs::write(level2.join("level2.rhai"), "// Level 2 script")
|
||||
.expect("Failed to write level2 script");
|
||||
fs::write(level3.join("level3.rhai"), "// Level 3 script")
|
||||
.expect("Failed to write level3 script");
|
||||
|
||||
// Verify directory structure
|
||||
assert!(temp_dir.path().is_dir(), "Root temp directory should exist");
|
||||
assert!(level1.is_dir(), "Level 1 directory should exist");
|
||||
assert!(level2.is_dir(), "Level 2 directory should exist");
|
||||
assert!(level3.is_dir(), "Level 3 directory should exist");
|
||||
|
||||
// Verify scripts exist at each level
|
||||
assert!(
|
||||
temp_dir.path().join("root.rhai").exists(),
|
||||
"Root script should exist"
|
||||
);
|
||||
assert!(
|
||||
level1.join("level1.rhai").exists(),
|
||||
"Level 1 script should exist"
|
||||
);
|
||||
assert!(
|
||||
level2.join("level2.rhai").exists(),
|
||||
"Level 2 script should exist"
|
||||
);
|
||||
assert!(
|
||||
level3.join("level3.rhai").exists(),
|
||||
"Level 3 script should exist"
|
||||
);
|
||||
}
|
||||
|
||||
/// Test sorting behavior for script execution order
|
||||
#[test]
|
||||
fn test_script_sorting_order() {
|
||||
let temp_dir = TempDir::new().expect("Failed to create temp directory");
|
||||
|
||||
// Create scripts with names that should be sorted
|
||||
let scripts = vec![
|
||||
"03_third.rhai",
|
||||
"01_first.rhai",
|
||||
"02_second.rhai",
|
||||
"10_tenth.rhai",
|
||||
"05_fifth.rhai",
|
||||
];
|
||||
|
||||
for script in &scripts {
|
||||
fs::write(
|
||||
temp_dir.path().join(script),
|
||||
format!("// Script: {}", script),
|
||||
)
|
||||
.expect("Failed to write script");
|
||||
}
|
||||
|
||||
// Collect and sort the scripts manually to verify sorting logic
|
||||
let mut found_scripts = Vec::new();
|
||||
for entry in fs::read_dir(temp_dir.path()).expect("Failed to read directory") {
|
||||
let entry = entry.expect("Failed to get directory entry");
|
||||
let path = entry.path();
|
||||
if path.is_file() && path.extension().map_or(false, |ext| ext == "rhai") {
|
||||
found_scripts.push(path.file_name().unwrap().to_string_lossy().to_string());
|
||||
}
|
||||
}
|
||||
|
||||
found_scripts.sort();
|
||||
|
||||
// Verify sorting order
|
||||
let expected_order = vec![
|
||||
"01_first.rhai",
|
||||
"02_second.rhai",
|
||||
"03_third.rhai",
|
||||
"05_fifth.rhai",
|
||||
"10_tenth.rhai",
|
||||
];
|
||||
|
||||
assert_eq!(
|
||||
found_scripts, expected_order,
|
||||
"Scripts should be sorted in correct order"
|
||||
);
|
||||
}
|
||||
|
||||
/// Test empty directory handling
|
||||
#[test]
|
||||
fn test_empty_directory_detection() {
|
||||
let temp_dir = TempDir::new().expect("Failed to create temp directory");
|
||||
let empty_subdir = temp_dir.path().join("empty");
|
||||
|
||||
fs::create_dir(&empty_subdir).expect("Failed to create empty subdirectory");
|
||||
|
||||
// Verify directory is empty
|
||||
let entries: Vec<_> = fs::read_dir(&empty_subdir)
|
||||
.expect("Failed to read empty directory")
|
||||
.collect();
|
||||
|
||||
assert!(entries.is_empty(), "Directory should be empty");
|
||||
|
||||
// Count .rhai files in empty directory
|
||||
let mut rhai_count = 0;
|
||||
for entry in fs::read_dir(&empty_subdir).expect("Failed to read empty directory") {
|
||||
let entry = entry.expect("Failed to get directory entry");
|
||||
let path = entry.path();
|
||||
if path.is_file() && path.extension().map_or(false, |ext| ext == "rhai") {
|
||||
rhai_count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
rhai_count, 0,
|
||||
"Empty directory should contain no .rhai files"
|
||||
);
|
||||
}
|
47
installers/base.rhai
Normal file
47
installers/base.rhai
Normal file
@@ -0,0 +1,47 @@
|
||||
|
||||
|
||||
|
||||
fn mycelium(){
|
||||
let name="mycelium";
|
||||
let url="https://github.com/threefoldtech/mycelium/releases/download/v0.6.1/mycelium-x86_64-unknown-linux-musl.tar.gz";
|
||||
download(url,`/tmp/${name}`,5000);
|
||||
copy_bin(`/tmp/${name}/*`);
|
||||
delete(`/tmp/${name}`);
|
||||
|
||||
let name="containerd";
|
||||
|
||||
}
|
||||
|
||||
|
||||
fn zinit(){
|
||||
let name="zinit";
|
||||
let url="https://github.com/threefoldtech/zinit/releases/download/v0.2.25/zinit-linux-x86_64";
|
||||
download_file(url,`/tmp/${name}`,5000);
|
||||
screen_kill("zinit");
|
||||
copy_bin(`/tmp/${name}`);
|
||||
delete(`/tmp/${name}`);
|
||||
screen_new("zinit", "zinit init");
|
||||
sleep(1);
|
||||
let socket_path = "/tmp/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}`);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
platform_check_linux_x86();
|
||||
zinit();
|
||||
// mycelium();
|
||||
|
||||
"done"
|
7
installers/install_all.rhai
Normal file
7
installers/install_all.rhai
Normal file
@@ -0,0 +1,7 @@
|
||||
|
||||
|
||||
platform_check_linux_x86();
|
||||
|
||||
exec(`https://git.threefold.info/herocode/sal/raw/branch/main/installers/base.rhai`);
|
||||
//install all we need for nerdctl
|
||||
exec(`https://git.threefold.info/herocode/sal/raw/branch/main/installers/nerdctl.rhai`);
|
54
installers/nerdctl.rhai
Normal file
54
installers/nerdctl.rhai
Normal file
@@ -0,0 +1,54 @@
|
||||
|
||||
|
||||
|
||||
fn nerdctl_download(){
|
||||
let name="nerdctl";
|
||||
let url="https://github.com/containerd/nerdctl/releases/download/v2.1.2/nerdctl-2.1.2-linux-amd64.tar.gz";
|
||||
download(url,`/tmp/${name}`,10000);
|
||||
copy_bin(`/tmp/${name}/*`);
|
||||
delete(`/tmp/${name}`);
|
||||
|
||||
screen_kill("containerd");
|
||||
let name="containerd";
|
||||
let url="https://github.com/containerd/containerd/releases/download/v2.1.2/containerd-2.1.2-linux-amd64.tar.gz";
|
||||
download(url,`/tmp/${name}`,20000);
|
||||
// copy_bin(`/tmp/${name}/bin/*`);
|
||||
delete(`/tmp/${name}`);
|
||||
|
||||
let cfg = `
|
||||
[[registry]]
|
||||
location = "localhost:5000"
|
||||
insecure = true
|
||||
`;
|
||||
file_write("/etc/containers/registries.conf", dedent(cfg));
|
||||
screen_new("containerd", "containerd");
|
||||
sleep(1);
|
||||
nerdctl_remove_all();
|
||||
run("nerdctl run -d -p 5000:5000 --name registry registry:2").log().execute();
|
||||
|
||||
package_install("buildah");
|
||||
package_install("runc");
|
||||
|
||||
|
||||
// let url="https://github.com/threefoldtech/rfs/releases/download/v2.0.6/rfs";
|
||||
// download_file(url,`/tmp/rfs`,10000);
|
||||
// chmod_exec("/tmp/rfs");
|
||||
// mv(`/tmp/rfs`,"/root/hero/bin/");
|
||||
|
||||
}
|
||||
|
||||
fn ipfs_download(){
|
||||
let name="ipfs";
|
||||
let url="https://github.com/ipfs/kubo/releases/download/v0.34.1/kubo_v0.34.1_linux-amd64.tar.gz";
|
||||
download(url,`/tmp/${name}`,20);
|
||||
copy_bin(`/tmp/${name}/kubo/ipfs`);
|
||||
delete(`/tmp/${name}`);
|
||||
|
||||
|
||||
}
|
||||
|
||||
platform_check_linux_x86();
|
||||
nerdctl_download();
|
||||
// ipfs_download();
|
||||
|
||||
"done"
|
30
mycelium/Cargo.toml
Normal file
30
mycelium/Cargo.toml
Normal file
@@ -0,0 +1,30 @@
|
||||
[package]
|
||||
name = "sal-mycelium"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
authors = ["PlanetFirst <info@incubaid.com>"]
|
||||
description = "SAL Mycelium - Client interface for interacting with Mycelium node's HTTP API"
|
||||
repository = "https://git.threefold.info/herocode/sal"
|
||||
license = "Apache-2.0"
|
||||
|
||||
[dependencies]
|
||||
# HTTP client for async requests
|
||||
reqwest = { version = "0.12.15", features = ["json"] }
|
||||
# JSON handling
|
||||
serde_json = "1.0"
|
||||
# Base64 encoding/decoding for message payloads
|
||||
base64 = "0.22.1"
|
||||
# Async runtime
|
||||
tokio = { version = "1.45.0", features = ["full"] }
|
||||
# Rhai scripting support
|
||||
rhai = { version = "1.12.0", features = ["sync"] }
|
||||
# Logging
|
||||
log = "0.4"
|
||||
# URL encoding for API parameters
|
||||
urlencoding = "2.1.3"
|
||||
|
||||
[dev-dependencies]
|
||||
# For async testing
|
||||
tokio-test = "0.4.4"
|
||||
# For temporary files in tests
|
||||
tempfile = "3.5"
|
110
mycelium/README.md
Normal file
110
mycelium/README.md
Normal file
@@ -0,0 +1,110 @@
|
||||
# SAL Mycelium
|
||||
|
||||
A Rust client library for interacting with Mycelium node's HTTP API, with Rhai scripting support.
|
||||
|
||||
## Overview
|
||||
|
||||
SAL Mycelium provides async HTTP client functionality for managing Mycelium nodes, including:
|
||||
|
||||
- Node information retrieval
|
||||
- Peer management (list, add, remove)
|
||||
- Route inspection (selected and fallback routes)
|
||||
- Message operations (send and receive)
|
||||
|
||||
## Usage
|
||||
|
||||
### Rust API
|
||||
|
||||
```rust
|
||||
use sal_mycelium::*;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let api_url = "http://localhost:8989";
|
||||
|
||||
// Get node information
|
||||
let node_info = get_node_info(api_url).await?;
|
||||
println!("Node info: {:?}", node_info);
|
||||
|
||||
// List peers
|
||||
let peers = list_peers(api_url).await?;
|
||||
println!("Peers: {:?}", peers);
|
||||
|
||||
// Send a message
|
||||
use std::time::Duration;
|
||||
let result = send_message(
|
||||
api_url,
|
||||
"destination_ip",
|
||||
"topic",
|
||||
"Hello, Mycelium!",
|
||||
Some(Duration::from_secs(30))
|
||||
).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Rhai Scripting
|
||||
|
||||
```rhai
|
||||
// Get node information
|
||||
let api_url = "http://localhost:8989";
|
||||
let node_info = mycelium_get_node_info(api_url);
|
||||
print(`Node subnet: ${node_info.nodeSubnet}`);
|
||||
|
||||
// List peers
|
||||
let peers = mycelium_list_peers(api_url);
|
||||
print(`Found ${peers.len()} peers`);
|
||||
|
||||
// Send message (timeout in seconds, -1 for no timeout)
|
||||
let result = mycelium_send_message(api_url, "dest_ip", "topic", "message", 30);
|
||||
```
|
||||
|
||||
## API Functions
|
||||
|
||||
### Core Functions
|
||||
|
||||
- `get_node_info(api_url)` - Get node information
|
||||
- `list_peers(api_url)` - List connected peers
|
||||
- `add_peer(api_url, peer_address)` - Add a new peer
|
||||
- `remove_peer(api_url, peer_id)` - Remove a peer
|
||||
- `list_selected_routes(api_url)` - List selected routes
|
||||
- `list_fallback_routes(api_url)` - List fallback routes
|
||||
- `send_message(api_url, destination, topic, message, timeout)` - Send message
|
||||
- `receive_messages(api_url, topic, timeout)` - Receive messages
|
||||
|
||||
### Rhai Functions
|
||||
|
||||
All functions are available in Rhai with `mycelium_` prefix:
|
||||
- `mycelium_get_node_info(api_url)`
|
||||
- `mycelium_list_peers(api_url)`
|
||||
- `mycelium_add_peer(api_url, peer_address)`
|
||||
- `mycelium_remove_peer(api_url, peer_id)`
|
||||
- `mycelium_list_selected_routes(api_url)`
|
||||
- `mycelium_list_fallback_routes(api_url)`
|
||||
- `mycelium_send_message(api_url, destination, topic, message, timeout_secs)`
|
||||
- `mycelium_receive_messages(api_url, topic, timeout_secs)`
|
||||
|
||||
## Requirements
|
||||
|
||||
- A running Mycelium node with HTTP API enabled
|
||||
- Default API endpoint: `http://localhost:8989`
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
cargo test
|
||||
|
||||
# Run with a live Mycelium node for integration tests
|
||||
# (tests will skip if no node is available)
|
||||
cargo test -- --nocapture
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `reqwest` - HTTP client
|
||||
- `serde_json` - JSON handling
|
||||
- `base64` - Message encoding
|
||||
- `tokio` - Async runtime
|
||||
- `rhai` - Scripting support
|
327
mycelium/src/lib.rs
Normal file
327
mycelium/src/lib.rs
Normal file
@@ -0,0 +1,327 @@
|
||||
//! SAL Mycelium - Client interface for interacting with Mycelium node's HTTP API
|
||||
//!
|
||||
//! This crate provides a client interface for interacting with a Mycelium node's HTTP API.
|
||||
//! Mycelium is a decentralized networking project, and this SAL module allows Rust applications
|
||||
//! and `herodo` Rhai scripts to manage and communicate over a Mycelium network.
|
||||
//!
|
||||
//! The module enables operations such as:
|
||||
//! - Querying node status and information
|
||||
//! - Managing peer connections (listing, adding, removing)
|
||||
//! - Inspecting routing tables (selected and fallback routes)
|
||||
//! - Sending messages to other Mycelium nodes
|
||||
//! - Receiving messages from subscribed topics
|
||||
//!
|
||||
//! All interactions with the Mycelium API are performed asynchronously.
|
||||
|
||||
use base64::{engine::general_purpose, Engine as _};
|
||||
use reqwest::Client;
|
||||
use serde_json::Value;
|
||||
use std::time::Duration;
|
||||
|
||||
pub mod rhai;
|
||||
|
||||
/// Get information about the Mycelium node
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `api_url` - The URL of the Mycelium API
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<Value, String>` - The node information as a JSON value, or an error message
|
||||
pub async fn get_node_info(api_url: &str) -> Result<Value, String> {
|
||||
let client = Client::new();
|
||||
let url = format!("{}/api/v1/admin", api_url);
|
||||
|
||||
let response = client
|
||||
.get(&url)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to send request: {}", e))?;
|
||||
|
||||
let status = response.status();
|
||||
if !status.is_success() {
|
||||
return Err(format!("Request failed with status: {}", status));
|
||||
}
|
||||
|
||||
let result: Value = response
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to parse response: {}", e))?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// List all peers connected to the Mycelium node
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `api_url` - The URL of the Mycelium API
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<Value, String>` - The list of peers as a JSON value, or an error message
|
||||
pub async fn list_peers(api_url: &str) -> Result<Value, String> {
|
||||
let client = Client::new();
|
||||
let url = format!("{}/api/v1/admin/peers", api_url);
|
||||
|
||||
let response = client
|
||||
.get(&url)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to send request: {}", e))?;
|
||||
|
||||
let status = response.status();
|
||||
if !status.is_success() {
|
||||
return Err(format!("Request failed with status: {}", status));
|
||||
}
|
||||
|
||||
let result: Value = response
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to parse response: {}", e))?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Add a new peer to the Mycelium node
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `api_url` - The URL of the Mycelium API
|
||||
/// * `peer_address` - The address of the peer to add
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<Value, String>` - The result of the operation as a JSON value, or an error message
|
||||
pub async fn add_peer(api_url: &str, peer_address: &str) -> Result<Value, String> {
|
||||
let client = Client::new();
|
||||
let url = format!("{}/api/v1/admin/peers", api_url);
|
||||
|
||||
let response = client
|
||||
.post(&url)
|
||||
.json(&serde_json::json!({
|
||||
"endpoint": peer_address
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to send request: {}", e))?;
|
||||
|
||||
let status = response.status();
|
||||
if status == reqwest::StatusCode::NO_CONTENT {
|
||||
// Successfully added, but no content to parse
|
||||
return Ok(serde_json::json!({"success": true}));
|
||||
}
|
||||
if !status.is_success() {
|
||||
return Err(format!("Request failed with status: {}", status));
|
||||
}
|
||||
|
||||
// For other success statuses that might have a body
|
||||
let result: Value = response
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to parse response: {}", e))?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Remove a peer from the Mycelium node
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `api_url` - The URL of the Mycelium API
|
||||
/// * `peer_id` - The ID of the peer to remove
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<Value, String>` - The result of the operation as a JSON value, or an error message
|
||||
pub async fn remove_peer(api_url: &str, peer_id: &str) -> Result<Value, String> {
|
||||
let client = Client::new();
|
||||
let peer_id_url_encoded = urlencoding::encode(peer_id);
|
||||
let url = format!("{}/api/v1/admin/peers/{}", api_url, peer_id_url_encoded);
|
||||
|
||||
let response = client
|
||||
.delete(&url)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to send request: {}", e))?;
|
||||
|
||||
let status = response.status();
|
||||
if status == reqwest::StatusCode::NO_CONTENT {
|
||||
// Successfully removed, but no content to parse
|
||||
return Ok(serde_json::json!({"success": true}));
|
||||
}
|
||||
if !status.is_success() {
|
||||
return Err(format!("Request failed with status: {}", status));
|
||||
}
|
||||
|
||||
let result: Value = response
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to parse response: {}", e))?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// List all selected routes in the Mycelium node
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `api_url` - The URL of the Mycelium API
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<Value, String>` - The list of selected routes as a JSON value, or an error message
|
||||
pub async fn list_selected_routes(api_url: &str) -> Result<Value, String> {
|
||||
let client = Client::new();
|
||||
let url = format!("{}/api/v1/admin/routes/selected", api_url);
|
||||
|
||||
let response = client
|
||||
.get(&url)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to send request: {}", e))?;
|
||||
|
||||
let status = response.status();
|
||||
if !status.is_success() {
|
||||
return Err(format!("Request failed with status: {}", status));
|
||||
}
|
||||
|
||||
let result: Value = response
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to parse response: {}", e))?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// List all fallback routes in the Mycelium node
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `api_url` - The URL of the Mycelium API
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<Value, String>` - The list of fallback routes as a JSON value, or an error message
|
||||
pub async fn list_fallback_routes(api_url: &str) -> Result<Value, String> {
|
||||
let client = Client::new();
|
||||
let url = format!("{}/api/v1/admin/routes/fallback", api_url);
|
||||
|
||||
let response = client
|
||||
.get(&url)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to send request: {}", e))?;
|
||||
|
||||
let status = response.status();
|
||||
if !status.is_success() {
|
||||
return Err(format!("Request failed with status: {}", status));
|
||||
}
|
||||
|
||||
let result: Value = response
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to parse response: {}", e))?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Send a message to a destination via the Mycelium node
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `api_url` - The URL of the Mycelium API
|
||||
/// * `destination` - The destination address
|
||||
/// * `topic` - The message topic
|
||||
/// * `message` - The message content
|
||||
/// * `reply_deadline` - The deadline in seconds; pass `-1` to indicate we do not want to wait on a reply
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<Value, String>` - The result of the operation as a JSON value, or an error message
|
||||
pub async fn send_message(
|
||||
api_url: &str,
|
||||
destination: &str,
|
||||
topic: &str,
|
||||
message: &str,
|
||||
reply_deadline: Option<Duration>, // This is passed in URL query
|
||||
) -> Result<Value, String> {
|
||||
let client = Client::new();
|
||||
let url = format!("{}/api/v1/messages", api_url);
|
||||
|
||||
let mut request = client.post(&url);
|
||||
if let Some(deadline) = reply_deadline {
|
||||
request = request.query(&[("reply_timeout", deadline.as_secs())]);
|
||||
}
|
||||
|
||||
let response = request
|
||||
.json(&serde_json::json!({
|
||||
"dst": { "ip": destination },
|
||||
"topic": general_purpose::STANDARD.encode(topic),
|
||||
"payload": general_purpose::STANDARD.encode(message)
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to send request: {}", e))?;
|
||||
|
||||
let status = response.status();
|
||||
if !status.is_success() {
|
||||
return Err(format!("Request failed with status: {}", status));
|
||||
}
|
||||
|
||||
let result: Value = response
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to parse response: {}", e))?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Receive messages from a topic via the Mycelium node
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `api_url` - The URL of the Mycelium API
|
||||
/// * `topic` - The message topic
|
||||
/// * `wait_deadline` - Time we wait for receiving a message
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<Value, String>` - The received messages as a JSON value, or an error message
|
||||
pub async fn receive_messages(
|
||||
api_url: &str,
|
||||
topic: &str,
|
||||
wait_deadline: Option<Duration>,
|
||||
) -> Result<Value, String> {
|
||||
let client = Client::new();
|
||||
let url = format!("{}/api/v1/messages", api_url);
|
||||
|
||||
let mut request = client.get(&url);
|
||||
|
||||
if let Some(deadline) = wait_deadline {
|
||||
request = request.query(&[
|
||||
("topic", general_purpose::STANDARD.encode(topic)),
|
||||
("timeout", deadline.as_secs().to_string()),
|
||||
])
|
||||
} else {
|
||||
request = request.query(&[("topic", general_purpose::STANDARD.encode(topic))])
|
||||
};
|
||||
|
||||
let response = request
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to send request: {}", e))?;
|
||||
|
||||
let status = response.status();
|
||||
if !status.is_success() {
|
||||
return Err(format!("Request failed with status: {}", status));
|
||||
}
|
||||
|
||||
let result: Value = response
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to parse response: {}", e))?;
|
||||
|
||||
Ok(result)
|
||||
}
|
254
mycelium/src/rhai.rs
Normal file
254
mycelium/src/rhai.rs
Normal file
@@ -0,0 +1,254 @@
|
||||
//! Rhai wrappers for Mycelium client module functions
|
||||
//!
|
||||
//! This module provides Rhai wrappers for the functions in the Mycelium client module.
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use crate as client;
|
||||
use rhai::Position;
|
||||
use rhai::{Array, Dynamic, Engine, EvalAltResult, Map};
|
||||
use serde_json::Value;
|
||||
use tokio::runtime::Runtime;
|
||||
|
||||
/// Register Mycelium 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_mycelium_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
|
||||
// Register Mycelium client functions
|
||||
engine.register_fn("mycelium_get_node_info", mycelium_get_node_info);
|
||||
engine.register_fn("mycelium_list_peers", mycelium_list_peers);
|
||||
engine.register_fn("mycelium_add_peer", mycelium_add_peer);
|
||||
engine.register_fn("mycelium_remove_peer", mycelium_remove_peer);
|
||||
engine.register_fn(
|
||||
"mycelium_list_selected_routes",
|
||||
mycelium_list_selected_routes,
|
||||
);
|
||||
engine.register_fn(
|
||||
"mycelium_list_fallback_routes",
|
||||
mycelium_list_fallback_routes,
|
||||
);
|
||||
engine.register_fn("mycelium_send_message", mycelium_send_message);
|
||||
engine.register_fn("mycelium_receive_messages", mycelium_receive_messages);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// 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,
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Mycelium Client Function Wrappers
|
||||
//
|
||||
|
||||
/// Wrapper for mycelium::get_node_info
|
||||
///
|
||||
/// Gets information about the Mycelium node.
|
||||
pub fn mycelium_get_node_info(api_url: &str) -> Result<Dynamic, Box<EvalAltResult>> {
|
||||
let rt = get_runtime()?;
|
||||
|
||||
let result = rt.block_on(async { client::get_node_info(api_url).await });
|
||||
|
||||
let node_info = result.map_err(|e| {
|
||||
Box::new(EvalAltResult::ErrorRuntime(
|
||||
format!("Mycelium error: {}", e).into(),
|
||||
Position::NONE,
|
||||
))
|
||||
})?;
|
||||
|
||||
Ok(value_to_dynamic(node_info))
|
||||
}
|
||||
|
||||
/// Wrapper for mycelium::list_peers
|
||||
///
|
||||
/// Lists all peers connected to the Mycelium node.
|
||||
pub fn mycelium_list_peers(api_url: &str) -> Result<Dynamic, Box<EvalAltResult>> {
|
||||
let rt = get_runtime()?;
|
||||
|
||||
let result = rt.block_on(async { client::list_peers(api_url).await });
|
||||
|
||||
let peers = result.map_err(|e| {
|
||||
Box::new(EvalAltResult::ErrorRuntime(
|
||||
format!("Mycelium error: {}", e).into(),
|
||||
Position::NONE,
|
||||
))
|
||||
})?;
|
||||
|
||||
Ok(value_to_dynamic(peers))
|
||||
}
|
||||
|
||||
/// Wrapper for mycelium::add_peer
|
||||
///
|
||||
/// Adds a new peer to the Mycelium node.
|
||||
pub fn mycelium_add_peer(api_url: &str, peer_address: &str) -> Result<Dynamic, Box<EvalAltResult>> {
|
||||
let rt = get_runtime()?;
|
||||
|
||||
let result = rt.block_on(async { client::add_peer(api_url, peer_address).await });
|
||||
|
||||
let response = result.map_err(|e| {
|
||||
Box::new(EvalAltResult::ErrorRuntime(
|
||||
format!("Mycelium error: {}", e).into(),
|
||||
Position::NONE,
|
||||
))
|
||||
})?;
|
||||
|
||||
Ok(value_to_dynamic(response))
|
||||
}
|
||||
|
||||
/// Wrapper for mycelium::remove_peer
|
||||
///
|
||||
/// Removes a peer from the Mycelium node.
|
||||
pub fn mycelium_remove_peer(api_url: &str, peer_id: &str) -> Result<Dynamic, Box<EvalAltResult>> {
|
||||
let rt = get_runtime()?;
|
||||
|
||||
let result = rt.block_on(async { client::remove_peer(api_url, peer_id).await });
|
||||
|
||||
let response = result.map_err(|e| {
|
||||
Box::new(EvalAltResult::ErrorRuntime(
|
||||
format!("Mycelium error: {}", e).into(),
|
||||
Position::NONE,
|
||||
))
|
||||
})?;
|
||||
|
||||
Ok(value_to_dynamic(response))
|
||||
}
|
||||
|
||||
/// Wrapper for mycelium::list_selected_routes
|
||||
///
|
||||
/// Lists all selected routes in the Mycelium node.
|
||||
pub fn mycelium_list_selected_routes(api_url: &str) -> Result<Dynamic, Box<EvalAltResult>> {
|
||||
let rt = get_runtime()?;
|
||||
|
||||
let result = rt.block_on(async { client::list_selected_routes(api_url).await });
|
||||
|
||||
let routes = result.map_err(|e| {
|
||||
Box::new(EvalAltResult::ErrorRuntime(
|
||||
format!("Mycelium error: {}", e).into(),
|
||||
Position::NONE,
|
||||
))
|
||||
})?;
|
||||
|
||||
Ok(value_to_dynamic(routes))
|
||||
}
|
||||
|
||||
/// Wrapper for mycelium::list_fallback_routes
|
||||
///
|
||||
/// Lists all fallback routes in the Mycelium node.
|
||||
pub fn mycelium_list_fallback_routes(api_url: &str) -> Result<Dynamic, Box<EvalAltResult>> {
|
||||
let rt = get_runtime()?;
|
||||
|
||||
let result = rt.block_on(async { client::list_fallback_routes(api_url).await });
|
||||
|
||||
let routes = result.map_err(|e| {
|
||||
Box::new(EvalAltResult::ErrorRuntime(
|
||||
format!("Mycelium error: {}", e).into(),
|
||||
Position::NONE,
|
||||
))
|
||||
})?;
|
||||
|
||||
Ok(value_to_dynamic(routes))
|
||||
}
|
||||
|
||||
/// Wrapper for mycelium::send_message
|
||||
///
|
||||
/// Sends a message to a destination via the Mycelium node.
|
||||
pub fn mycelium_send_message(
|
||||
api_url: &str,
|
||||
destination: &str,
|
||||
topic: &str,
|
||||
message: &str,
|
||||
reply_deadline_secs: i64,
|
||||
) -> Result<Dynamic, Box<EvalAltResult>> {
|
||||
let rt = get_runtime()?;
|
||||
|
||||
let deadline = if reply_deadline_secs < 0 {
|
||||
None
|
||||
} else {
|
||||
Some(Duration::from_secs(reply_deadline_secs as u64))
|
||||
};
|
||||
|
||||
let result = rt.block_on(async {
|
||||
client::send_message(api_url, destination, topic, message, deadline).await
|
||||
});
|
||||
|
||||
let response = result.map_err(|e| {
|
||||
Box::new(EvalAltResult::ErrorRuntime(
|
||||
format!("Mycelium error: {}", e).into(),
|
||||
Position::NONE,
|
||||
))
|
||||
})?;
|
||||
|
||||
Ok(value_to_dynamic(response))
|
||||
}
|
||||
|
||||
/// Wrapper for mycelium::receive_messages
|
||||
///
|
||||
/// Receives messages from a topic via the Mycelium node.
|
||||
pub fn mycelium_receive_messages(
|
||||
api_url: &str,
|
||||
topic: &str,
|
||||
wait_deadline_secs: i64,
|
||||
) -> Result<Dynamic, Box<EvalAltResult>> {
|
||||
let rt = get_runtime()?;
|
||||
|
||||
let deadline = if wait_deadline_secs < 0 {
|
||||
None
|
||||
} else {
|
||||
Some(Duration::from_secs(wait_deadline_secs as u64))
|
||||
};
|
||||
|
||||
let result = rt.block_on(async { client::receive_messages(api_url, topic, deadline).await });
|
||||
|
||||
let messages = result.map_err(|e| {
|
||||
Box::new(EvalAltResult::ErrorRuntime(
|
||||
format!("Mycelium error: {}", e).into(),
|
||||
Position::NONE,
|
||||
))
|
||||
})?;
|
||||
|
||||
Ok(value_to_dynamic(messages))
|
||||
}
|
279
mycelium/tests/mycelium_client_tests.rs
Normal file
279
mycelium/tests/mycelium_client_tests.rs
Normal file
@@ -0,0 +1,279 @@
|
||||
//! Unit tests for Mycelium client functionality
|
||||
//!
|
||||
//! These tests validate the core Mycelium client operations including:
|
||||
//! - Node information retrieval
|
||||
//! - Peer management (listing, adding, removing)
|
||||
//! - Route inspection (selected and fallback routes)
|
||||
//! - Message operations (sending and receiving)
|
||||
//!
|
||||
//! Tests are designed to work with a real Mycelium node when available,
|
||||
//! but gracefully handle cases where the node is not accessible.
|
||||
|
||||
use sal_mycelium::*;
|
||||
use std::time::Duration;
|
||||
|
||||
/// Test configuration for Mycelium API
|
||||
const TEST_API_URL: &str = "http://localhost:8989";
|
||||
const FALLBACK_API_URL: &str = "http://localhost:7777";
|
||||
|
||||
/// Helper function to check if a Mycelium node is available
|
||||
async fn is_mycelium_available(api_url: &str) -> bool {
|
||||
match get_node_info(api_url).await {
|
||||
Ok(_) => true,
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper function to get an available Mycelium API URL
|
||||
async fn get_available_api_url() -> Option<String> {
|
||||
if is_mycelium_available(TEST_API_URL).await {
|
||||
Some(TEST_API_URL.to_string())
|
||||
} else if is_mycelium_available(FALLBACK_API_URL).await {
|
||||
Some(FALLBACK_API_URL.to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_get_node_info_success() {
|
||||
if let Some(api_url) = get_available_api_url().await {
|
||||
let result = get_node_info(&api_url).await;
|
||||
|
||||
match result {
|
||||
Ok(node_info) => {
|
||||
// Validate that we got a JSON response with expected fields
|
||||
assert!(node_info.is_object(), "Node info should be a JSON object");
|
||||
|
||||
// Check for common Mycelium node info fields
|
||||
let obj = node_info.as_object().unwrap();
|
||||
|
||||
// These fields are typically present in Mycelium node info
|
||||
// We check if at least one of them exists to validate the response
|
||||
let has_expected_fields = obj.contains_key("nodeSubnet")
|
||||
|| obj.contains_key("nodePubkey")
|
||||
|| obj.contains_key("peers")
|
||||
|| obj.contains_key("routes");
|
||||
|
||||
assert!(
|
||||
has_expected_fields,
|
||||
"Node info should contain expected Mycelium fields"
|
||||
);
|
||||
println!("✓ Node info retrieved successfully: {:?}", node_info);
|
||||
}
|
||||
Err(e) => {
|
||||
// If we can connect but get an error, it might be a version mismatch
|
||||
// or API change - log it but don't fail the test
|
||||
println!("⚠ Node info request failed (API might have changed): {}", e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("⚠ Skipping test_get_node_info_success: No Mycelium node available");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_get_node_info_invalid_url() {
|
||||
let invalid_url = "http://localhost:99999";
|
||||
let result = get_node_info(invalid_url).await;
|
||||
|
||||
assert!(result.is_err(), "Should fail with invalid URL");
|
||||
let error = result.unwrap_err();
|
||||
assert!(
|
||||
error.contains("Failed to send request") || error.contains("Request failed"),
|
||||
"Error should indicate connection failure: {}",
|
||||
error
|
||||
);
|
||||
println!("✓ Correctly handled invalid URL: {}", error);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_list_peers() {
|
||||
if let Some(api_url) = get_available_api_url().await {
|
||||
let result = list_peers(&api_url).await;
|
||||
|
||||
match result {
|
||||
Ok(peers) => {
|
||||
// Peers should be an array (even if empty)
|
||||
assert!(peers.is_array(), "Peers should be a JSON array");
|
||||
println!(
|
||||
"✓ Peers listed successfully: {} peers found",
|
||||
peers.as_array().unwrap().len()
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
println!(
|
||||
"⚠ List peers request failed (API might have changed): {}",
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("⚠ Skipping test_list_peers: No Mycelium node available");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_add_peer_validation() {
|
||||
if let Some(api_url) = get_available_api_url().await {
|
||||
// Test with an invalid peer address format
|
||||
let invalid_peer = "invalid-peer-address";
|
||||
let result = add_peer(&api_url, invalid_peer).await;
|
||||
|
||||
// This should either succeed (if the node accepts it) or fail with a validation error
|
||||
match result {
|
||||
Ok(response) => {
|
||||
println!("✓ Add peer response: {:?}", response);
|
||||
}
|
||||
Err(e) => {
|
||||
// Expected for invalid peer addresses
|
||||
println!("✓ Correctly rejected invalid peer address: {}", e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("⚠ Skipping test_add_peer_validation: No Mycelium node available");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_list_selected_routes() {
|
||||
if let Some(api_url) = get_available_api_url().await {
|
||||
let result = list_selected_routes(&api_url).await;
|
||||
|
||||
match result {
|
||||
Ok(routes) => {
|
||||
// Routes should be an array or object
|
||||
assert!(
|
||||
routes.is_array() || routes.is_object(),
|
||||
"Routes should be a JSON array or object"
|
||||
);
|
||||
println!("✓ Selected routes retrieved successfully");
|
||||
}
|
||||
Err(e) => {
|
||||
println!("⚠ List selected routes request failed: {}", e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("⚠ Skipping test_list_selected_routes: No Mycelium node available");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_list_fallback_routes() {
|
||||
if let Some(api_url) = get_available_api_url().await {
|
||||
let result = list_fallback_routes(&api_url).await;
|
||||
|
||||
match result {
|
||||
Ok(routes) => {
|
||||
// Routes should be an array or object
|
||||
assert!(
|
||||
routes.is_array() || routes.is_object(),
|
||||
"Routes should be a JSON array or object"
|
||||
);
|
||||
println!("✓ Fallback routes retrieved successfully");
|
||||
}
|
||||
Err(e) => {
|
||||
println!("⚠ List fallback routes request failed: {}", e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("⚠ Skipping test_list_fallback_routes: No Mycelium node available");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_send_message_validation() {
|
||||
if let Some(api_url) = get_available_api_url().await {
|
||||
// Test message sending with invalid destination
|
||||
let invalid_destination = "invalid-destination";
|
||||
let topic = "test_topic";
|
||||
let message = "test message";
|
||||
let deadline = Some(Duration::from_secs(1));
|
||||
|
||||
let result = send_message(&api_url, invalid_destination, topic, message, deadline).await;
|
||||
|
||||
// This should fail with invalid destination
|
||||
match result {
|
||||
Ok(response) => {
|
||||
// Some implementations might accept any destination format
|
||||
println!("✓ Send message response: {:?}", response);
|
||||
}
|
||||
Err(e) => {
|
||||
// Expected for invalid destinations
|
||||
println!("✓ Correctly rejected invalid destination: {}", e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("⚠ Skipping test_send_message_validation: No Mycelium node available");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_receive_messages_timeout() {
|
||||
if let Some(api_url) = get_available_api_url().await {
|
||||
let topic = "non_existent_topic";
|
||||
let deadline = Some(Duration::from_secs(1)); // Short timeout
|
||||
|
||||
let result = receive_messages(&api_url, topic, deadline).await;
|
||||
|
||||
match result {
|
||||
Ok(messages) => {
|
||||
// Should return empty or no messages for non-existent topic
|
||||
println!("✓ Receive messages completed: {:?}", messages);
|
||||
}
|
||||
Err(e) => {
|
||||
// Timeout or no messages is acceptable
|
||||
println!("✓ Receive messages handled correctly: {}", e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("⚠ Skipping test_receive_messages_timeout: No Mycelium node available");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_error_handling_malformed_url() {
|
||||
let malformed_url = "not-a-url";
|
||||
let result = get_node_info(malformed_url).await;
|
||||
|
||||
assert!(result.is_err(), "Should fail with malformed URL");
|
||||
let error = result.unwrap_err();
|
||||
assert!(
|
||||
error.contains("Failed to send request"),
|
||||
"Error should indicate request failure: {}",
|
||||
error
|
||||
);
|
||||
println!("✓ Correctly handled malformed URL: {}", error);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_base64_encoding_in_messages() {
|
||||
// Test that our message functions properly handle base64 encoding
|
||||
// This is a unit test that doesn't require a running Mycelium node
|
||||
|
||||
let topic = "test/topic";
|
||||
let message = "Hello, Mycelium!";
|
||||
|
||||
// Test base64 encoding directly
|
||||
use base64::{engine::general_purpose, Engine as _};
|
||||
let encoded_topic = general_purpose::STANDARD.encode(topic);
|
||||
let encoded_message = general_purpose::STANDARD.encode(message);
|
||||
|
||||
assert!(
|
||||
!encoded_topic.is_empty(),
|
||||
"Encoded topic should not be empty"
|
||||
);
|
||||
assert!(
|
||||
!encoded_message.is_empty(),
|
||||
"Encoded message should not be empty"
|
||||
);
|
||||
|
||||
// Verify we can decode back
|
||||
let decoded_topic = general_purpose::STANDARD.decode(&encoded_topic).unwrap();
|
||||
let decoded_message = general_purpose::STANDARD.decode(&encoded_message).unwrap();
|
||||
|
||||
assert_eq!(String::from_utf8(decoded_topic).unwrap(), topic);
|
||||
assert_eq!(String::from_utf8(decoded_message).unwrap(), message);
|
||||
|
||||
println!("✓ Base64 encoding/decoding works correctly");
|
||||
}
|
242
mycelium/tests/rhai/01_mycelium_basic.rhai
Normal file
242
mycelium/tests/rhai/01_mycelium_basic.rhai
Normal file
@@ -0,0 +1,242 @@
|
||||
// Basic Mycelium functionality tests in Rhai
|
||||
//
|
||||
// This script tests the core Mycelium operations available through Rhai.
|
||||
// It's designed to work with or without a running Mycelium node.
|
||||
|
||||
print("=== Mycelium Basic Functionality Tests ===");
|
||||
|
||||
// Test configuration
|
||||
let test_api_url = "http://localhost:8989";
|
||||
let fallback_api_url = "http://localhost:7777";
|
||||
|
||||
// Helper function to check if Mycelium is available
|
||||
fn is_mycelium_available(api_url) {
|
||||
try {
|
||||
mycelium_get_node_info(api_url);
|
||||
return true;
|
||||
} catch(err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Find an available API URL
|
||||
let api_url = "";
|
||||
if is_mycelium_available(test_api_url) {
|
||||
api_url = test_api_url;
|
||||
print(`✓ Using primary API URL: ${api_url}`);
|
||||
} else if is_mycelium_available(fallback_api_url) {
|
||||
api_url = fallback_api_url;
|
||||
print(`✓ Using fallback API URL: ${api_url}`);
|
||||
} else {
|
||||
print("⚠ No Mycelium node available - testing error handling only");
|
||||
api_url = "http://localhost:99999"; // Intentionally invalid for error testing
|
||||
}
|
||||
|
||||
// Test 1: Get Node Information
|
||||
print("\n--- Test 1: Get Node Information ---");
|
||||
try {
|
||||
let node_info = mycelium_get_node_info(api_url);
|
||||
|
||||
if api_url.contains("99999") {
|
||||
print("✗ Expected error but got success");
|
||||
assert_true(false, "Should have failed with invalid URL");
|
||||
} else {
|
||||
print("✓ Node info retrieved successfully");
|
||||
print(` Node info type: ${type_of(node_info)}`);
|
||||
|
||||
// Validate response structure
|
||||
if type_of(node_info) == "map" {
|
||||
print("✓ Node info is a proper object");
|
||||
|
||||
// Check for common fields (at least one should exist)
|
||||
let has_fields = node_info.contains("nodeSubnet") ||
|
||||
node_info.contains("nodePubkey") ||
|
||||
node_info.contains("peers") ||
|
||||
node_info.contains("routes");
|
||||
|
||||
if has_fields {
|
||||
print("✓ Node info contains expected fields");
|
||||
} else {
|
||||
print("⚠ Node info structure might have changed");
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch(err) {
|
||||
if api_url.contains("99999") {
|
||||
print("✓ Correctly handled connection error");
|
||||
assert_true(err.to_string().contains("Mycelium error"), "Error should be properly formatted");
|
||||
} else {
|
||||
print(`⚠ Unexpected error with available node: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Test 2: List Peers
|
||||
print("\n--- Test 2: List Peers ---");
|
||||
try {
|
||||
let peers = mycelium_list_peers(api_url);
|
||||
|
||||
if api_url.contains("99999") {
|
||||
print("✗ Expected error but got success");
|
||||
assert_true(false, "Should have failed with invalid URL");
|
||||
} else {
|
||||
print("✓ Peers listed successfully");
|
||||
print(` Peers type: ${type_of(peers)}`);
|
||||
|
||||
if type_of(peers) == "array" {
|
||||
print(`✓ Found ${peers.len()} peers`);
|
||||
|
||||
// If we have peers, check their structure
|
||||
if peers.len() > 0 {
|
||||
let first_peer = peers[0];
|
||||
print(` First peer type: ${type_of(first_peer)}`);
|
||||
|
||||
if type_of(first_peer) == "map" {
|
||||
print("✓ Peer has proper object structure");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
print("⚠ Peers response is not an array");
|
||||
}
|
||||
}
|
||||
} catch(err) {
|
||||
if api_url.contains("99999") {
|
||||
print("✓ Correctly handled connection error");
|
||||
} else {
|
||||
print(`⚠ Unexpected error listing peers: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Test 3: Add Peer (with validation)
|
||||
print("\n--- Test 3: Add Peer Validation ---");
|
||||
try {
|
||||
// Test with invalid peer address
|
||||
let result = mycelium_add_peer(api_url, "invalid-peer-format");
|
||||
|
||||
if api_url.contains("99999") {
|
||||
print("✗ Expected connection error but got success");
|
||||
} else {
|
||||
print("✓ Add peer completed (validation depends on node implementation)");
|
||||
print(` Result type: ${type_of(result)}`);
|
||||
}
|
||||
} catch(err) {
|
||||
if api_url.contains("99999") {
|
||||
print("✓ Correctly handled connection error");
|
||||
} else {
|
||||
print(`✓ Peer validation error (expected): ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Test 4: List Selected Routes
|
||||
print("\n--- Test 4: List Selected Routes ---");
|
||||
try {
|
||||
let routes = mycelium_list_selected_routes(api_url);
|
||||
|
||||
if api_url.contains("99999") {
|
||||
print("✗ Expected error but got success");
|
||||
} else {
|
||||
print("✓ Selected routes retrieved successfully");
|
||||
print(` Routes type: ${type_of(routes)}`);
|
||||
|
||||
if type_of(routes) == "array" {
|
||||
print(`✓ Found ${routes.len()} selected routes`);
|
||||
} else if type_of(routes) == "map" {
|
||||
print("✓ Routes returned as object");
|
||||
}
|
||||
}
|
||||
} catch(err) {
|
||||
if api_url.contains("99999") {
|
||||
print("✓ Correctly handled connection error");
|
||||
} else {
|
||||
print(`⚠ Error retrieving selected routes: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Test 5: List Fallback Routes
|
||||
print("\n--- Test 5: List Fallback Routes ---");
|
||||
try {
|
||||
let routes = mycelium_list_fallback_routes(api_url);
|
||||
|
||||
if api_url.contains("99999") {
|
||||
print("✗ Expected error but got success");
|
||||
} else {
|
||||
print("✓ Fallback routes retrieved successfully");
|
||||
print(` Routes type: ${type_of(routes)}`);
|
||||
}
|
||||
} catch(err) {
|
||||
if api_url.contains("99999") {
|
||||
print("✓ Correctly handled connection error");
|
||||
} else {
|
||||
print(`⚠ Error retrieving fallback routes: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Test 6: Send Message (validation)
|
||||
print("\n--- Test 6: Send Message Validation ---");
|
||||
try {
|
||||
let result = mycelium_send_message(api_url, "invalid-destination", "test_topic", "test message", -1);
|
||||
|
||||
if api_url.contains("99999") {
|
||||
print("✗ Expected connection error but got success");
|
||||
} else {
|
||||
print("✓ Send message completed (validation depends on node implementation)");
|
||||
print(` Result type: ${type_of(result)}`);
|
||||
}
|
||||
} catch(err) {
|
||||
if api_url.contains("99999") {
|
||||
print("✓ Correctly handled connection error");
|
||||
} else {
|
||||
print(`✓ Message validation error (expected): ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Test 7: Receive Messages (timeout test)
|
||||
print("\n--- Test 7: Receive Messages Timeout ---");
|
||||
try {
|
||||
// Use short timeout to avoid long waits
|
||||
let messages = mycelium_receive_messages(api_url, "non_existent_topic", 1);
|
||||
|
||||
if api_url.contains("99999") {
|
||||
print("✗ Expected connection error but got success");
|
||||
} else {
|
||||
print("✓ Receive messages completed");
|
||||
print(` Messages type: ${type_of(messages)}`);
|
||||
|
||||
if type_of(messages) == "array" {
|
||||
print(`✓ Received ${messages.len()} messages`);
|
||||
} else {
|
||||
print("✓ Messages returned as object");
|
||||
}
|
||||
}
|
||||
} catch(err) {
|
||||
if api_url.contains("99999") {
|
||||
print("✓ Correctly handled connection error");
|
||||
} else {
|
||||
print(`✓ Receive timeout handled correctly: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Test 8: Parameter Validation
|
||||
print("\n--- Test 8: Parameter Validation ---");
|
||||
|
||||
// Test empty API URL
|
||||
try {
|
||||
mycelium_get_node_info("");
|
||||
print("✗ Should have failed with empty API URL");
|
||||
} catch(err) {
|
||||
print("✓ Correctly rejected empty API URL");
|
||||
}
|
||||
|
||||
// Test negative timeout handling
|
||||
try {
|
||||
mycelium_receive_messages(api_url, "test_topic", -1);
|
||||
if api_url.contains("99999") {
|
||||
print("✗ Expected connection error");
|
||||
} else {
|
||||
print("✓ Negative timeout handled (treated as no timeout)");
|
||||
}
|
||||
} catch(err) {
|
||||
print("✓ Timeout parameter handled correctly");
|
||||
}
|
||||
|
||||
print("\n=== Mycelium Basic Tests Completed ===");
|
||||
print("All core Mycelium functions are properly registered and handle errors correctly.");
|
174
mycelium/tests/rhai/run_all_tests.rhai
Normal file
174
mycelium/tests/rhai/run_all_tests.rhai
Normal file
@@ -0,0 +1,174 @@
|
||||
// Mycelium Rhai Test Runner
|
||||
//
|
||||
// This script runs all Mycelium-related Rhai tests and reports results.
|
||||
// It includes simplified versions of the individual tests to avoid dependency issues.
|
||||
|
||||
print("=== Mycelium Rhai Test Suite ===");
|
||||
print("Running comprehensive tests for Mycelium Rhai integration...\n");
|
||||
|
||||
let total_tests = 0;
|
||||
let passed_tests = 0;
|
||||
let failed_tests = 0;
|
||||
let skipped_tests = 0;
|
||||
|
||||
// Test 1: Function Registration
|
||||
print("Test 1: Function Registration");
|
||||
total_tests += 1;
|
||||
try {
|
||||
// Test that all mycelium functions are registered
|
||||
let invalid_url = "http://localhost:99999";
|
||||
let all_functions_exist = true;
|
||||
|
||||
try { mycelium_get_node_info(invalid_url); } catch(err) {
|
||||
if !err.to_string().contains("Mycelium error") { all_functions_exist = false; }
|
||||
}
|
||||
|
||||
try { mycelium_list_peers(invalid_url); } catch(err) {
|
||||
if !err.to_string().contains("Mycelium error") { all_functions_exist = false; }
|
||||
}
|
||||
|
||||
try { mycelium_send_message(invalid_url, "dest", "topic", "msg", -1); } catch(err) {
|
||||
if !err.to_string().contains("Mycelium error") { all_functions_exist = false; }
|
||||
}
|
||||
|
||||
if all_functions_exist {
|
||||
passed_tests += 1;
|
||||
print("✓ PASSED: All mycelium functions are registered");
|
||||
} else {
|
||||
failed_tests += 1;
|
||||
print("✗ FAILED: Some mycelium functions are missing");
|
||||
}
|
||||
} catch(err) {
|
||||
failed_tests += 1;
|
||||
print(`✗ ERROR: Function registration test failed - ${err}`);
|
||||
}
|
||||
|
||||
// Test 2: Error Handling
|
||||
print("\nTest 2: Error Handling");
|
||||
total_tests += 1;
|
||||
try {
|
||||
mycelium_get_node_info("http://localhost:99999");
|
||||
failed_tests += 1;
|
||||
print("✗ FAILED: Should have failed with connection error");
|
||||
} catch(err) {
|
||||
if err.to_string().contains("Mycelium error") {
|
||||
passed_tests += 1;
|
||||
print("✓ PASSED: Error handling works correctly");
|
||||
} else {
|
||||
failed_tests += 1;
|
||||
print(`✗ FAILED: Unexpected error format - ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Test 3: Parameter Validation
|
||||
print("\nTest 3: Parameter Validation");
|
||||
total_tests += 1;
|
||||
try {
|
||||
mycelium_get_node_info("");
|
||||
failed_tests += 1;
|
||||
print("✗ FAILED: Should have failed with empty API URL");
|
||||
} catch(err) {
|
||||
passed_tests += 1;
|
||||
print("✓ PASSED: Parameter validation works correctly");
|
||||
}
|
||||
|
||||
// Test 4: Timeout Parameter Handling
|
||||
print("\nTest 4: Timeout Parameter Handling");
|
||||
total_tests += 1;
|
||||
try {
|
||||
let invalid_url = "http://localhost:99999";
|
||||
|
||||
// Test negative timeout (should be treated as no timeout)
|
||||
try {
|
||||
mycelium_receive_messages(invalid_url, "topic", -1);
|
||||
failed_tests += 1;
|
||||
print("✗ FAILED: Should have failed with connection error");
|
||||
} catch(err) {
|
||||
if err.to_string().contains("Mycelium error") {
|
||||
passed_tests += 1;
|
||||
print("✓ PASSED: Timeout parameter handling works correctly");
|
||||
} else {
|
||||
failed_tests += 1;
|
||||
print(`✗ FAILED: Unexpected error - ${err}`);
|
||||
}
|
||||
}
|
||||
} catch(err) {
|
||||
failed_tests += 1;
|
||||
print(`✗ ERROR: Timeout test failed - ${err}`);
|
||||
}
|
||||
|
||||
// Check if Mycelium is available for integration tests
|
||||
let test_api_url = "http://localhost:8989";
|
||||
let fallback_api_url = "http://localhost:7777";
|
||||
let available_api_url = "";
|
||||
|
||||
try {
|
||||
mycelium_get_node_info(test_api_url);
|
||||
available_api_url = test_api_url;
|
||||
} catch(err) {
|
||||
try {
|
||||
mycelium_get_node_info(fallback_api_url);
|
||||
available_api_url = fallback_api_url;
|
||||
} catch(err2) {
|
||||
// No Mycelium node available
|
||||
}
|
||||
}
|
||||
|
||||
if available_api_url != "" {
|
||||
print(`\n✓ Mycelium node available at: ${available_api_url}`);
|
||||
|
||||
// Test 5: Get Node Info
|
||||
print("\nTest 5: Get Node Info");
|
||||
total_tests += 1;
|
||||
try {
|
||||
let node_info = mycelium_get_node_info(available_api_url);
|
||||
|
||||
if type_of(node_info) == "map" {
|
||||
passed_tests += 1;
|
||||
print("✓ PASSED: Node info retrieved successfully");
|
||||
} else {
|
||||
failed_tests += 1;
|
||||
print("✗ FAILED: Node info should be an object");
|
||||
}
|
||||
} catch(err) {
|
||||
failed_tests += 1;
|
||||
print(`✗ ERROR: Node info test failed - ${err}`);
|
||||
}
|
||||
|
||||
// Test 6: List Peers
|
||||
print("\nTest 6: List Peers");
|
||||
total_tests += 1;
|
||||
try {
|
||||
let peers = mycelium_list_peers(available_api_url);
|
||||
|
||||
if type_of(peers) == "array" {
|
||||
passed_tests += 1;
|
||||
print("✓ PASSED: Peers listed successfully");
|
||||
} else {
|
||||
failed_tests += 1;
|
||||
print("✗ FAILED: Peers should be an array");
|
||||
}
|
||||
} catch(err) {
|
||||
failed_tests += 1;
|
||||
print(`✗ ERROR: List peers test failed - ${err}`);
|
||||
}
|
||||
} else {
|
||||
print("\n⚠ No Mycelium node available - skipping integration tests");
|
||||
skipped_tests += 2; // Skip node info and list peers tests
|
||||
total_tests += 2;
|
||||
}
|
||||
|
||||
// Print final results
|
||||
print("\n=== Test Results ===");
|
||||
print(`Total Tests: ${total_tests}`);
|
||||
print(`Passed: ${passed_tests}`);
|
||||
print(`Failed: ${failed_tests}`);
|
||||
print(`Skipped: ${skipped_tests}`);
|
||||
|
||||
if failed_tests == 0 {
|
||||
print("\n✓ All tests passed!");
|
||||
} else {
|
||||
print(`\n✗ ${failed_tests} test(s) failed.`);
|
||||
}
|
||||
|
||||
print("\n=== Mycelium Rhai Test Suite Completed ===");
|
313
mycelium/tests/rhai_integration_tests.rs
Normal file
313
mycelium/tests/rhai_integration_tests.rs
Normal file
@@ -0,0 +1,313 @@
|
||||
//! Rhai integration tests for Mycelium module
|
||||
//!
|
||||
//! These tests validate the Rhai wrapper functions and ensure proper
|
||||
//! integration between Rust and Rhai for Mycelium operations.
|
||||
|
||||
use rhai::{Engine, EvalAltResult};
|
||||
use sal_mycelium::rhai::*;
|
||||
|
||||
#[cfg(test)]
|
||||
mod rhai_integration_tests {
|
||||
use super::*;
|
||||
|
||||
fn create_test_engine() -> Engine {
|
||||
let mut engine = Engine::new();
|
||||
register_mycelium_module(&mut engine).expect("Failed to register mycelium module");
|
||||
engine
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rhai_module_registration() {
|
||||
let engine = create_test_engine();
|
||||
|
||||
// Test that the functions are registered by checking if they exist
|
||||
let script = r#"
|
||||
// Test that all mycelium functions are available
|
||||
let functions_exist = true;
|
||||
|
||||
// We can't actually call these without a server, but we can verify they're registered
|
||||
// by checking that the engine doesn't throw "function not found" errors
|
||||
functions_exist
|
||||
"#;
|
||||
|
||||
let result: Result<bool, Box<EvalAltResult>> = engine.eval(script);
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mycelium_get_node_info_function_exists() {
|
||||
let engine = create_test_engine();
|
||||
|
||||
// Test that mycelium_get_node_info function is registered
|
||||
let script = r#"
|
||||
// This will fail with connection error, but proves the function exists
|
||||
try {
|
||||
mycelium_get_node_info("http://localhost:99999");
|
||||
false; // Should not reach here
|
||||
} catch(err) {
|
||||
// Function exists but failed due to connection - this is expected
|
||||
return err.to_string().contains("Mycelium error");
|
||||
}
|
||||
"#;
|
||||
|
||||
let result: Result<bool, Box<EvalAltResult>> = engine.eval(script);
|
||||
if let Err(ref e) = result {
|
||||
println!("Script evaluation error: {}", e);
|
||||
}
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mycelium_list_peers_function_exists() {
|
||||
let engine = create_test_engine();
|
||||
|
||||
let script = r#"
|
||||
try {
|
||||
mycelium_list_peers("http://localhost:99999");
|
||||
return false;
|
||||
} catch(err) {
|
||||
return err.to_string().contains("Mycelium error");
|
||||
}
|
||||
"#;
|
||||
|
||||
let result: Result<bool, Box<EvalAltResult>> = engine.eval(script);
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mycelium_add_peer_function_exists() {
|
||||
let engine = create_test_engine();
|
||||
|
||||
let script = r#"
|
||||
try {
|
||||
mycelium_add_peer("http://localhost:99999", "tcp://example.com:9651");
|
||||
return false;
|
||||
} catch(err) {
|
||||
return err.to_string().contains("Mycelium error");
|
||||
}
|
||||
"#;
|
||||
|
||||
let result: Result<bool, Box<EvalAltResult>> = engine.eval(script);
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mycelium_remove_peer_function_exists() {
|
||||
let engine = create_test_engine();
|
||||
|
||||
let script = r#"
|
||||
try {
|
||||
mycelium_remove_peer("http://localhost:99999", "peer_id");
|
||||
return false;
|
||||
} catch(err) {
|
||||
return err.to_string().contains("Mycelium error");
|
||||
}
|
||||
"#;
|
||||
|
||||
let result: Result<bool, Box<EvalAltResult>> = engine.eval(script);
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mycelium_list_selected_routes_function_exists() {
|
||||
let engine = create_test_engine();
|
||||
|
||||
let script = r#"
|
||||
try {
|
||||
mycelium_list_selected_routes("http://localhost:99999");
|
||||
return false;
|
||||
} catch(err) {
|
||||
return err.to_string().contains("Mycelium error");
|
||||
}
|
||||
"#;
|
||||
|
||||
let result: Result<bool, Box<EvalAltResult>> = engine.eval(script);
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mycelium_list_fallback_routes_function_exists() {
|
||||
let engine = create_test_engine();
|
||||
|
||||
let script = r#"
|
||||
try {
|
||||
mycelium_list_fallback_routes("http://localhost:99999");
|
||||
return false;
|
||||
} catch(err) {
|
||||
return err.to_string().contains("Mycelium error");
|
||||
}
|
||||
"#;
|
||||
|
||||
let result: Result<bool, Box<EvalAltResult>> = engine.eval(script);
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mycelium_send_message_function_exists() {
|
||||
let engine = create_test_engine();
|
||||
|
||||
let script = r#"
|
||||
try {
|
||||
mycelium_send_message("http://localhost:99999", "destination", "topic", "message", -1);
|
||||
return false;
|
||||
} catch(err) {
|
||||
return err.to_string().contains("Mycelium error");
|
||||
}
|
||||
"#;
|
||||
|
||||
let result: Result<bool, Box<EvalAltResult>> = engine.eval(script);
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mycelium_receive_messages_function_exists() {
|
||||
let engine = create_test_engine();
|
||||
|
||||
let script = r#"
|
||||
try {
|
||||
mycelium_receive_messages("http://localhost:99999", "topic", 1);
|
||||
return false;
|
||||
} catch(err) {
|
||||
return err.to_string().contains("Mycelium error");
|
||||
}
|
||||
"#;
|
||||
|
||||
let result: Result<bool, Box<EvalAltResult>> = engine.eval(script);
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parameter_validation() {
|
||||
let engine = create_test_engine();
|
||||
|
||||
// Test that functions handle parameter validation correctly
|
||||
let script = r#"
|
||||
let test_results = [];
|
||||
|
||||
// Test empty API URL
|
||||
try {
|
||||
mycelium_get_node_info("");
|
||||
test_results.push(false);
|
||||
} catch(err) {
|
||||
test_results.push(true); // Expected to fail
|
||||
}
|
||||
|
||||
// Test empty peer address
|
||||
try {
|
||||
mycelium_add_peer("http://localhost:8989", "");
|
||||
test_results.push(false);
|
||||
} catch(err) {
|
||||
test_results.push(true); // Expected to fail
|
||||
}
|
||||
|
||||
// Test negative timeout handling
|
||||
try {
|
||||
mycelium_receive_messages("http://localhost:99999", "topic", -1);
|
||||
test_results.push(false);
|
||||
} catch(err) {
|
||||
// Should handle negative timeout gracefully
|
||||
test_results.push(err.to_string().contains("Mycelium error"));
|
||||
}
|
||||
|
||||
test_results
|
||||
"#;
|
||||
|
||||
let result: Result<rhai::Array, Box<EvalAltResult>> = engine.eval(script);
|
||||
assert!(result.is_ok());
|
||||
let results = result.unwrap();
|
||||
|
||||
// All parameter validation tests should pass
|
||||
for (i, result) in results.iter().enumerate() {
|
||||
assert_eq!(
|
||||
result.as_bool().unwrap_or(false),
|
||||
true,
|
||||
"Parameter validation test {} failed",
|
||||
i
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_message_format() {
|
||||
let engine = create_test_engine();
|
||||
|
||||
// Test that error messages are properly formatted
|
||||
let script = r#"
|
||||
try {
|
||||
mycelium_get_node_info("http://localhost:99999");
|
||||
return "";
|
||||
} catch(err) {
|
||||
let error_str = err.to_string();
|
||||
// Should contain "Mycelium error:" prefix
|
||||
if error_str.contains("Mycelium error:") {
|
||||
return "correct_format";
|
||||
} else {
|
||||
return error_str;
|
||||
}
|
||||
}
|
||||
"#;
|
||||
|
||||
let result: Result<String, Box<EvalAltResult>> = engine.eval(script);
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), "correct_format");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_timeout_parameter_handling() {
|
||||
let engine = create_test_engine();
|
||||
|
||||
// Test different timeout parameter values
|
||||
let script = r#"
|
||||
let timeout_tests = [];
|
||||
|
||||
// Test positive timeout
|
||||
try {
|
||||
mycelium_receive_messages("http://localhost:99999", "topic", 5);
|
||||
timeout_tests.push(false);
|
||||
} catch(err) {
|
||||
timeout_tests.push(err.to_string().contains("Mycelium error"));
|
||||
}
|
||||
|
||||
// Test zero timeout
|
||||
try {
|
||||
mycelium_receive_messages("http://localhost:99999", "topic", 0);
|
||||
timeout_tests.push(false);
|
||||
} catch(err) {
|
||||
timeout_tests.push(err.to_string().contains("Mycelium error"));
|
||||
}
|
||||
|
||||
// Test negative timeout (should be treated as no timeout)
|
||||
try {
|
||||
mycelium_receive_messages("http://localhost:99999", "topic", -1);
|
||||
timeout_tests.push(false);
|
||||
} catch(err) {
|
||||
timeout_tests.push(err.to_string().contains("Mycelium error"));
|
||||
}
|
||||
|
||||
timeout_tests
|
||||
"#;
|
||||
|
||||
let result: Result<rhai::Array, Box<EvalAltResult>> = engine.eval(script);
|
||||
assert!(result.is_ok());
|
||||
let results = result.unwrap();
|
||||
|
||||
// All timeout tests should handle the connection error properly
|
||||
for (i, result) in results.iter().enumerate() {
|
||||
assert_eq!(
|
||||
result.as_bool().unwrap_or(false),
|
||||
true,
|
||||
"Timeout test {} failed",
|
||||
i
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
16
net/Cargo.toml
Normal file
16
net/Cargo.toml
Normal file
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "sal-net"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
authors = ["PlanetFirst <info@incubaid.com>"]
|
||||
description = "SAL Network - Network connectivity utilities for TCP, HTTP, and SSH"
|
||||
repository = "https://git.threefold.info/herocode/sal"
|
||||
license = "Apache-2.0"
|
||||
keywords = ["network", "tcp", "http", "ssh", "connectivity"]
|
||||
categories = ["network-programming", "api-bindings"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.98"
|
||||
tokio = { version = "1.0", features = ["full"] }
|
||||
reqwest = { version = "0.12", features = ["json", "blocking"] }
|
||||
rhai = "1.19.0"
|
226
net/README.md
Normal file
226
net/README.md
Normal file
@@ -0,0 +1,226 @@
|
||||
# SAL Network Package
|
||||
|
||||
Network connectivity utilities for TCP, HTTP, and SSH operations.
|
||||
|
||||
## Overview
|
||||
|
||||
The `sal-net` package provides a comprehensive set of network connectivity tools for the SAL (System Abstraction Layer) ecosystem. It includes utilities for TCP port checking, HTTP/HTTPS connectivity testing, and SSH command execution.
|
||||
|
||||
## Features
|
||||
|
||||
### TCP Connectivity
|
||||
- **Port checking**: Test if specific TCP ports are open
|
||||
- **Multi-port checking**: Test multiple ports simultaneously
|
||||
- **ICMP ping**: Test host reachability using ping
|
||||
- **Configurable timeouts**: Customize connection timeout values
|
||||
|
||||
### HTTP/HTTPS Connectivity
|
||||
- **URL reachability**: Test if URLs are accessible
|
||||
- **Status code checking**: Get HTTP status codes from URLs
|
||||
- **Content fetching**: Download content from URLs
|
||||
- **Status verification**: Verify URLs return expected status codes
|
||||
|
||||
### SSH Operations
|
||||
- **Command execution**: Run commands on remote hosts via SSH
|
||||
- **Connection testing**: Test SSH connectivity to hosts
|
||||
- **Builder pattern**: Flexible SSH connection configuration
|
||||
- **Custom authentication**: Support for identity files and custom ports
|
||||
|
||||
## Rust API
|
||||
|
||||
### TCP Operations
|
||||
|
||||
```rust
|
||||
use sal_net::TcpConnector;
|
||||
use std::time::Duration;
|
||||
|
||||
// Create a TCP connector
|
||||
let connector = TcpConnector::new();
|
||||
|
||||
// Check if a port is open
|
||||
let is_open = connector.check_port("127.0.0.1".parse().unwrap(), 80).await?;
|
||||
|
||||
// Check multiple ports
|
||||
let ports = vec![22, 80, 443];
|
||||
let results = connector.check_ports("example.com".parse().unwrap(), &ports).await?;
|
||||
|
||||
// Ping a host
|
||||
let is_reachable = connector.ping("google.com").await?;
|
||||
```
|
||||
|
||||
### HTTP Operations
|
||||
|
||||
```rust
|
||||
use sal_net::HttpConnector;
|
||||
|
||||
// Create an HTTP connector
|
||||
let connector = HttpConnector::new()?;
|
||||
|
||||
// Check if a URL is reachable
|
||||
let is_reachable = connector.check_url("https://example.com").await?;
|
||||
|
||||
// Get status code
|
||||
let status = connector.check_status("https://example.com").await?;
|
||||
|
||||
// Fetch content
|
||||
let content = connector.get_content("https://api.example.com/data").await?;
|
||||
|
||||
// Verify specific status
|
||||
let matches = connector.verify_status("https://example.com", reqwest::StatusCode::OK).await?;
|
||||
```
|
||||
|
||||
### SSH Operations
|
||||
|
||||
```rust
|
||||
use sal_net::SshConnectionBuilder;
|
||||
use std::time::Duration;
|
||||
|
||||
// Build an SSH connection
|
||||
let connection = SshConnectionBuilder::new()
|
||||
.host("example.com")
|
||||
.port(22)
|
||||
.user("username")
|
||||
.timeout(Duration::from_secs(30))
|
||||
.build();
|
||||
|
||||
// Execute a command
|
||||
let (exit_code, output) = connection.execute("ls -la").await?;
|
||||
|
||||
// Test connectivity
|
||||
let is_connected = connection.ping().await?;
|
||||
```
|
||||
|
||||
## Rhai Integration
|
||||
|
||||
The package provides Rhai scripting integration for network operations:
|
||||
|
||||
### TCP Functions
|
||||
|
||||
```rhai
|
||||
// Check if a TCP port is open
|
||||
let is_open = tcp_check("127.0.0.1", 80);
|
||||
print(`Port 80 is ${is_open ? "open" : "closed"}`);
|
||||
|
||||
// Ping a host (cross-platform)
|
||||
let can_ping = tcp_ping("google.com");
|
||||
print(`Can ping Google: ${can_ping}`);
|
||||
```
|
||||
|
||||
### HTTP Functions
|
||||
|
||||
```rhai
|
||||
// Check if an HTTP URL is reachable
|
||||
let is_reachable = http_check("https://example.com");
|
||||
print(`URL is ${is_reachable ? "reachable" : "unreachable"}`);
|
||||
|
||||
// Get HTTP status code
|
||||
let status = http_status("https://example.com");
|
||||
print(`HTTP status: ${status}`);
|
||||
```
|
||||
|
||||
### SSH Functions
|
||||
|
||||
```rhai
|
||||
// Execute SSH command and get exit code
|
||||
let exit_code = ssh_execute("example.com", "user", "ls -la");
|
||||
print(`SSH command exit code: ${exit_code}`);
|
||||
|
||||
// Execute SSH command and get output
|
||||
let output = ssh_execute_output("example.com", "user", "whoami");
|
||||
print(`SSH output: ${output}`);
|
||||
|
||||
// Test SSH connectivity
|
||||
let can_connect = ssh_ping("example.com", "user");
|
||||
print(`SSH connection: ${can_connect ? "success" : "failed"}`);
|
||||
```
|
||||
|
||||
### Example Rhai Script
|
||||
|
||||
```rhai
|
||||
// Network connectivity test script
|
||||
print("=== Network Connectivity Test ===");
|
||||
|
||||
// Test TCP connectivity
|
||||
let ports = [22, 80, 443];
|
||||
for port in ports {
|
||||
let is_open = tcp_check("example.com", port);
|
||||
print(`Port ${port}: ${is_open ? "OPEN" : "CLOSED"}`);
|
||||
}
|
||||
|
||||
// Test ping connectivity
|
||||
let hosts = ["google.com", "github.com", "stackoverflow.com"];
|
||||
for host in hosts {
|
||||
let can_ping = tcp_ping(host);
|
||||
print(`${host}: ${can_ping ? "REACHABLE" : "UNREACHABLE"}`);
|
||||
}
|
||||
|
||||
// Test HTTP connectivity
|
||||
let urls = ["https://google.com", "https://github.com", "https://httpbin.org/status/200"];
|
||||
for url in urls {
|
||||
let is_reachable = http_check(url);
|
||||
let status = http_status(url);
|
||||
print(`${url}: ${is_reachable ? "REACHABLE" : "UNREACHABLE"} (Status: ${status})`);
|
||||
}
|
||||
|
||||
// Test SSH connectivity (requires SSH access)
|
||||
let ssh_hosts = ["example.com"];
|
||||
for host in ssh_hosts {
|
||||
let can_connect = ssh_ping(host, "user");
|
||||
print(`SSH ${host}: ${can_connect ? "CONNECTED" : "FAILED"}`);
|
||||
}
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
The package includes comprehensive tests:
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
cargo test
|
||||
|
||||
# Run specific test suites
|
||||
cargo test --test tcp_tests
|
||||
cargo test --test http_tests
|
||||
cargo test --test ssh_tests
|
||||
cargo test --test rhai_integration_tests
|
||||
|
||||
# Run Rhai script tests
|
||||
cargo test --test rhai_integration_tests
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `tokio`: Async runtime for network operations
|
||||
- `reqwest`: HTTP client functionality
|
||||
- `anyhow`: Error handling
|
||||
- `rhai`: Scripting integration
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- SSH operations use the system's SSH client for security
|
||||
- HTTP operations respect standard timeout and security settings
|
||||
- No credentials are logged or exposed in error messages
|
||||
- Network timeouts prevent hanging operations
|
||||
|
||||
## Platform Support
|
||||
|
||||
- **Linux**: Full support for all features
|
||||
- **macOS**: Full support for all features
|
||||
- **Windows**: TCP and HTTP support (SSH requires SSH client installation)
|
||||
|
||||
## Error Handling
|
||||
|
||||
All network operations return `Result` types with meaningful error messages. Operations gracefully handle:
|
||||
|
||||
- Network timeouts
|
||||
- Connection failures
|
||||
- Invalid hostnames/URLs
|
||||
- Authentication failures (SSH)
|
||||
- System command failures
|
||||
|
||||
## Performance
|
||||
|
||||
- Async operations for non-blocking network calls
|
||||
- Configurable timeouts for responsive applications
|
||||
- Efficient connection reuse where possible
|
||||
- Minimal memory footprint for network operations
|
84
net/src/http.rs
Normal file
84
net/src/http.rs
Normal file
@@ -0,0 +1,84 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
use reqwest::{Client, StatusCode, Url};
|
||||
|
||||
/// HTTP Connectivity module for checking HTTP/HTTPS connections
|
||||
pub struct HttpConnector {
|
||||
client: Client,
|
||||
}
|
||||
|
||||
impl HttpConnector {
|
||||
/// Create a new HTTP connector with the default configuration
|
||||
pub fn new() -> Result<Self> {
|
||||
let client = Client::builder().timeout(Duration::from_secs(30)).build()?;
|
||||
|
||||
Ok(Self { client })
|
||||
}
|
||||
|
||||
/// Create a new HTTP connector with a custom timeout
|
||||
pub fn with_timeout(timeout: Duration) -> Result<Self> {
|
||||
let client = Client::builder().timeout(timeout).build()?;
|
||||
|
||||
Ok(Self { client })
|
||||
}
|
||||
|
||||
/// Check if a URL is reachable
|
||||
pub async fn check_url<U: AsRef<str>>(&self, url: U) -> Result<bool> {
|
||||
let url_str = url.as_ref();
|
||||
let url = Url::parse(url_str)?;
|
||||
|
||||
let result = self.client.head(url).send().await;
|
||||
|
||||
Ok(result.is_ok())
|
||||
}
|
||||
|
||||
/// Check a URL and return the status code if reachable
|
||||
pub async fn check_status<U: AsRef<str>>(&self, url: U) -> Result<Option<StatusCode>> {
|
||||
let url_str = url.as_ref();
|
||||
let url = Url::parse(url_str)?;
|
||||
|
||||
let result = self.client.head(url).send().await;
|
||||
|
||||
match result {
|
||||
Ok(response) => Ok(Some(response.status())),
|
||||
Err(_) => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the content of a URL
|
||||
pub async fn get_content<U: AsRef<str>>(&self, url: U) -> Result<String> {
|
||||
let url_str = url.as_ref();
|
||||
let url = Url::parse(url_str)?;
|
||||
|
||||
let response = self.client.get(url).send().await?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"HTTP request failed with status: {}",
|
||||
response.status()
|
||||
));
|
||||
}
|
||||
|
||||
let content = response.text().await?;
|
||||
Ok(content)
|
||||
}
|
||||
|
||||
/// Verify that a URL responds with a specific status code
|
||||
pub async fn verify_status<U: AsRef<str>>(
|
||||
&self,
|
||||
url: U,
|
||||
expected_status: StatusCode,
|
||||
) -> Result<bool> {
|
||||
match self.check_status(url).await? {
|
||||
Some(status) => Ok(status == expected_status),
|
||||
None => Ok(false),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for HttpConnector {
|
||||
fn default() -> Self {
|
||||
Self::new().expect("Failed to create default HttpConnector")
|
||||
}
|
||||
}
|
9
net/src/lib.rs
Normal file
9
net/src/lib.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
pub mod http;
|
||||
pub mod rhai;
|
||||
pub mod ssh;
|
||||
pub mod tcp;
|
||||
|
||||
// Re-export main types for a cleaner API
|
||||
pub use http::HttpConnector;
|
||||
pub use ssh::{SshConnection, SshConnectionBuilder};
|
||||
pub use tcp::TcpConnector;
|
180
net/src/rhai.rs
Normal file
180
net/src/rhai.rs
Normal file
@@ -0,0 +1,180 @@
|
||||
//! Rhai wrappers for network module functions
|
||||
//!
|
||||
//! This module provides Rhai wrappers for network connectivity functions.
|
||||
|
||||
use rhai::{Engine, EvalAltResult, Module};
|
||||
|
||||
/// Create a Rhai module with network functions
|
||||
pub fn create_module() -> Module {
|
||||
// For now, we'll use a simpler approach and register functions via engine
|
||||
// This ensures compatibility with Rhai's type system
|
||||
// The module is created but functions are registered through register_net_module
|
||||
|
||||
Module::new()
|
||||
}
|
||||
|
||||
/// Register network module functions with the Rhai engine
|
||||
pub fn register_net_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
|
||||
// TCP functions
|
||||
engine.register_fn("tcp_check", tcp_check);
|
||||
engine.register_fn("tcp_ping", tcp_ping);
|
||||
|
||||
// HTTP functions
|
||||
engine.register_fn("http_check", http_check);
|
||||
engine.register_fn("http_status", http_status);
|
||||
|
||||
// SSH functions
|
||||
engine.register_fn("ssh_execute", ssh_execute);
|
||||
engine.register_fn("ssh_execute_output", ssh_execute_output);
|
||||
engine.register_fn("ssh_ping", ssh_ping_host);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check if a TCP port is open
|
||||
pub fn tcp_check(host: &str, port: i64) -> bool {
|
||||
// Use std::net::TcpStream for synchronous connection test
|
||||
use std::net::{SocketAddr, TcpStream};
|
||||
use std::time::Duration;
|
||||
|
||||
// Parse the address
|
||||
let addr_str = format!("{}:{}", host, port);
|
||||
if let Ok(socket_addr) = addr_str.parse::<SocketAddr>() {
|
||||
// Try to connect with a timeout
|
||||
TcpStream::connect_timeout(&socket_addr, Duration::from_secs(5)).is_ok()
|
||||
} else {
|
||||
// Try to resolve hostname first
|
||||
match std::net::ToSocketAddrs::to_socket_addrs(&addr_str) {
|
||||
Ok(mut addrs) => {
|
||||
if let Some(addr) = addrs.next() {
|
||||
TcpStream::connect_timeout(&addr, Duration::from_secs(5)).is_ok()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Ping a host using ICMP (cross-platform)
|
||||
pub fn tcp_ping(host: &str) -> bool {
|
||||
// Use system ping command for synchronous operation
|
||||
use std::process::Command;
|
||||
|
||||
// Cross-platform ping implementation
|
||||
let mut cmd = Command::new("ping");
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
cmd.arg("-n").arg("1").arg("-w").arg("5000"); // Windows: -n count, -w timeout in ms
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
cmd.arg("-c").arg("1").arg("-W").arg("5"); // Unix: -c count, -W timeout in seconds
|
||||
}
|
||||
|
||||
cmd.arg(host);
|
||||
|
||||
match cmd.output() {
|
||||
Ok(output) => output.status.success(),
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if an HTTP URL is reachable
|
||||
pub fn http_check(url: &str) -> bool {
|
||||
use std::time::Duration;
|
||||
|
||||
// Create a blocking HTTP client with timeout
|
||||
let client = match reqwest::blocking::Client::builder()
|
||||
.timeout(Duration::from_secs(10))
|
||||
.build()
|
||||
{
|
||||
Ok(client) => client,
|
||||
Err(_) => return false,
|
||||
};
|
||||
|
||||
// Try to make a HEAD request
|
||||
match client.head(url).send() {
|
||||
Ok(response) => response.status().is_success(),
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get HTTP status code from a URL
|
||||
pub fn http_status(url: &str) -> i64 {
|
||||
use std::time::Duration;
|
||||
|
||||
// Create a blocking HTTP client with timeout
|
||||
let client = match reqwest::blocking::Client::builder()
|
||||
.timeout(Duration::from_secs(10))
|
||||
.build()
|
||||
{
|
||||
Ok(client) => client,
|
||||
Err(_) => return -1,
|
||||
};
|
||||
|
||||
// Try to make a HEAD request
|
||||
match client.head(url).send() {
|
||||
Ok(response) => response.status().as_u16() as i64,
|
||||
Err(_) => -1,
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute a command via SSH - returns exit code as i64
|
||||
pub fn ssh_execute(host: &str, user: &str, command: &str) -> i64 {
|
||||
use std::process::Command;
|
||||
|
||||
let mut cmd = Command::new("ssh");
|
||||
cmd.arg("-o")
|
||||
.arg("ConnectTimeout=5")
|
||||
.arg("-o")
|
||||
.arg("StrictHostKeyChecking=no")
|
||||
.arg(format!("{}@{}", user, host))
|
||||
.arg(command);
|
||||
|
||||
match cmd.output() {
|
||||
Ok(output) => output.status.code().unwrap_or(-1) as i64,
|
||||
Err(_) => -1,
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute a command via SSH and get output - returns output as string
|
||||
pub fn ssh_execute_output(host: &str, user: &str, command: &str) -> String {
|
||||
use std::process::Command;
|
||||
|
||||
let mut cmd = Command::new("ssh");
|
||||
cmd.arg("-o")
|
||||
.arg("ConnectTimeout=5")
|
||||
.arg("-o")
|
||||
.arg("StrictHostKeyChecking=no")
|
||||
.arg(format!("{}@{}", user, host))
|
||||
.arg(command);
|
||||
|
||||
match cmd.output() {
|
||||
Ok(output) => String::from_utf8_lossy(&output.stdout).to_string(),
|
||||
Err(_) => "SSH command failed".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Test SSH connectivity to a host
|
||||
pub fn ssh_ping_host(host: &str, user: &str) -> bool {
|
||||
use std::process::Command;
|
||||
|
||||
let mut cmd = Command::new("ssh");
|
||||
cmd.arg("-o")
|
||||
.arg("ConnectTimeout=5")
|
||||
.arg("-o")
|
||||
.arg("StrictHostKeyChecking=no")
|
||||
.arg("-o")
|
||||
.arg("BatchMode=yes") // Non-interactive
|
||||
.arg(format!("{}@{}", user, host))
|
||||
.arg("echo 'Connection successful'");
|
||||
|
||||
match cmd.output() {
|
||||
Ok(output) => output.status.success(),
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
151
net/src/ssh.rs
Normal file
151
net/src/ssh.rs
Normal file
@@ -0,0 +1,151 @@
|
||||
use std::path::PathBuf;
|
||||
use std::process::Stdio;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
use tokio::io::{AsyncReadExt, BufReader};
|
||||
use tokio::process::Command;
|
||||
|
||||
/// SSH Connection that uses the system's SSH client
|
||||
pub struct SshConnection {
|
||||
host: String,
|
||||
port: u16,
|
||||
user: String,
|
||||
identity_file: Option<PathBuf>,
|
||||
timeout: Duration,
|
||||
}
|
||||
|
||||
impl SshConnection {
|
||||
/// Execute a command over SSH and return its output
|
||||
pub async fn execute(&self, command: &str) -> Result<(i32, String)> {
|
||||
let mut args = Vec::new();
|
||||
|
||||
// Add SSH options
|
||||
args.push("-o".to_string());
|
||||
args.push(format!("ConnectTimeout={}", self.timeout.as_secs()));
|
||||
|
||||
// Don't check host key to avoid prompts
|
||||
args.push("-o".to_string());
|
||||
args.push("StrictHostKeyChecking=no".to_string());
|
||||
|
||||
// Specify port if not default
|
||||
if self.port != 22 {
|
||||
args.push("-p".to_string());
|
||||
args.push(self.port.to_string());
|
||||
}
|
||||
|
||||
// Add identity file if provided
|
||||
if let Some(identity) = &self.identity_file {
|
||||
args.push("-i".to_string());
|
||||
args.push(identity.to_string_lossy().to_string());
|
||||
}
|
||||
|
||||
// Add user and host
|
||||
args.push(format!("{}@{}", self.user, self.host));
|
||||
|
||||
// Add the command to execute
|
||||
args.push(command.to_string());
|
||||
|
||||
// Run the SSH command
|
||||
let mut child = Command::new("ssh")
|
||||
.args(&args)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()?;
|
||||
|
||||
// Collect stdout and stderr
|
||||
let stdout = child.stdout.take().unwrap();
|
||||
let stderr = child.stderr.take().unwrap();
|
||||
|
||||
let mut stdout_reader = BufReader::new(stdout);
|
||||
let mut stderr_reader = BufReader::new(stderr);
|
||||
|
||||
let mut output = String::new();
|
||||
stdout_reader.read_to_string(&mut output).await?;
|
||||
|
||||
let mut error_output = String::new();
|
||||
stderr_reader.read_to_string(&mut error_output).await?;
|
||||
|
||||
// If there's error output, append it to the regular output
|
||||
if !error_output.is_empty() {
|
||||
if !output.is_empty() {
|
||||
output.push('\n');
|
||||
}
|
||||
output.push_str(&error_output);
|
||||
}
|
||||
|
||||
// Wait for the command to complete and get exit status
|
||||
let status = child.wait().await?;
|
||||
let code = status.code().unwrap_or(-1);
|
||||
|
||||
Ok((code, output))
|
||||
}
|
||||
|
||||
/// Check if the host is reachable via SSH
|
||||
pub async fn ping(&self) -> Result<bool> {
|
||||
let result = self.execute("echo 'Connection successful'").await?;
|
||||
Ok(result.0 == 0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder for SSH connections
|
||||
pub struct SshConnectionBuilder {
|
||||
host: String,
|
||||
port: u16,
|
||||
user: String,
|
||||
identity_file: Option<PathBuf>,
|
||||
timeout: Duration,
|
||||
}
|
||||
|
||||
impl Default for SshConnectionBuilder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl SshConnectionBuilder {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
host: "localhost".to_string(),
|
||||
port: 22,
|
||||
user: "root".to_string(),
|
||||
identity_file: None,
|
||||
timeout: Duration::from_secs(10),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn host<S: Into<String>>(mut self, host: S) -> Self {
|
||||
self.host = host.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn port(mut self, port: u16) -> Self {
|
||||
self.port = port;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn user<S: Into<String>>(mut self, user: S) -> Self {
|
||||
self.user = user.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn identity_file(mut self, path: PathBuf) -> Self {
|
||||
self.identity_file = Some(path);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn timeout(mut self, timeout: Duration) -> Self {
|
||||
self.timeout = timeout;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> SshConnection {
|
||||
SshConnection {
|
||||
host: self.host,
|
||||
port: self.port,
|
||||
user: self.user,
|
||||
identity_file: self.identity_file,
|
||||
timeout: self.timeout,
|
||||
}
|
||||
}
|
||||
}
|
78
net/src/tcp.rs
Normal file
78
net/src/tcp.rs
Normal file
@@ -0,0 +1,78 @@
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::time::timeout;
|
||||
|
||||
/// TCP Connectivity module for checking TCP connections
|
||||
pub struct TcpConnector {
|
||||
timeout: Duration,
|
||||
}
|
||||
|
||||
impl TcpConnector {
|
||||
/// Create a new TCP connector with the default timeout (5 seconds)
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
timeout: Duration::from_secs(5),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new TCP connector with a custom timeout
|
||||
pub fn with_timeout(timeout: Duration) -> Self {
|
||||
Self { timeout }
|
||||
}
|
||||
|
||||
/// Check if a TCP port is open on a host
|
||||
pub async fn check_port<A: Into<IpAddr>>(&self, host: A, port: u16) -> Result<bool> {
|
||||
let addr = SocketAddr::new(host.into(), port);
|
||||
let connect_future = TcpStream::connect(addr);
|
||||
|
||||
match timeout(self.timeout, connect_future).await {
|
||||
Ok(Ok(_)) => Ok(true),
|
||||
Ok(Err(_)) => Ok(false),
|
||||
Err(_) => Ok(false), // Timeout occurred
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if multiple TCP ports are open on a host
|
||||
pub async fn check_ports<A: Into<IpAddr> + Clone>(
|
||||
&self,
|
||||
host: A,
|
||||
ports: &[u16],
|
||||
) -> Result<Vec<(u16, bool)>> {
|
||||
let mut results = Vec::with_capacity(ports.len());
|
||||
|
||||
for &port in ports {
|
||||
let is_open = self.check_port(host.clone(), port).await?;
|
||||
results.push((port, is_open));
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
/// Check if a host is reachable on the network using ICMP ping
|
||||
pub async fn ping<S: AsRef<str>>(&self, host: S) -> Result<bool> {
|
||||
// Convert to owned strings to avoid borrowing issues
|
||||
let host_str = host.as_ref().to_string();
|
||||
let timeout_secs = self.timeout.as_secs().to_string();
|
||||
|
||||
// Run the ping command with explicit arguments
|
||||
let status = tokio::process::Command::new("ping")
|
||||
.arg("-c")
|
||||
.arg("1") // Just one ping
|
||||
.arg("-W")
|
||||
.arg(timeout_secs) // Timeout in seconds
|
||||
.arg(host_str) // Host to ping
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
Ok(status.status.success())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for TcpConnector {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user