diff --git a/.github-proposal/README.md b/.github-proposal/README.md
new file mode 100644
index 00000000..8c440c45
--- /dev/null
+++ b/.github-proposal/README.md
@@ -0,0 +1,87 @@
+# GitHub Actions Workflows for OSVM CLI
+
+This directory contains GitHub Actions workflow proposals for the OSVM CLI project. These workflows automate testing, benchmarking, and deployment processes.
+
+## Workflows
+
+### CI (Continuous Integration)
+
+The CI workflow (`workflows/ci.yml`) runs on every push to the main branch and on pull requests. It performs:
+
+1. **Sanity Checks**: Runs `rustfmt` and `clippy` to ensure code quality and style.
+2. **Unit Tests**: Runs unit tests for the library and binary components.
+3. **End-to-End Tests**: Runs the end-to-end tests that verify CLI functionality.
+4. **Code Coverage**: Generates a code coverage report using `cargo-tarpaulin` and uploads it to Codecov.
+
+### Cross-Platform Tests
+
+The Cross-Platform Tests workflow (`workflows/cross-platform.yml`) runs on every push to the main branch and on pull requests. It:
+
+1. Builds and tests the application on multiple operating systems:
+ - Ubuntu Linux
+ - macOS
+ - Windows
+2. Ensures the CLI works consistently across different platforms.
+
+### Benchmarks
+
+The Benchmarks workflow (`workflows/benchmarks.yml`) runs on every push to the main branch, on pull requests, and weekly on Sundays. It:
+
+1. Runs performance benchmarks using `cargo-criterion`.
+2. Uploads benchmark results as artifacts.
+3. Generates a benchmark report.
+4. For pull requests, compares benchmarks with the main branch to detect performance regressions.
+
+### Security Scan
+
+The Security Scan workflow (`workflows/security.yml`) runs on every push to the main branch, on pull requests, and weekly on Mondays. It:
+
+1. **Security Audit**: Runs `cargo-audit` to check for known vulnerabilities in dependencies.
+2. **Dependency Review**: Reviews dependencies for security issues in pull requests.
+3. **Code Scanning**: Uses GitHub's CodeQL to scan for security vulnerabilities in the code.
+
+### Release
+
+The Release workflow (`workflows/release.yml`) runs when a tag starting with 'v' is pushed. It:
+
+1. **Builds** the release binary.
+2. **Creates a GitHub Release** with the binary attached.
+3. **Deploys to APT Repository**: Creates a Debian package and deploys it to an APT repository.
+4. **Deploys to Homebrew**: Creates a Homebrew formula and submits it to Homebrew.
+5. **Deploys Documentation**: Generates documentation using `cargo doc` and deploys it to GitHub Pages.
+
+## Usage
+
+To use these workflows:
+
+1. Move the `.github-proposal` directory to `.github` in your repository.
+2. Customize the workflows as needed for your specific requirements.
+3. For the Release workflow, you'll need to set up:
+ - An APT repository for Debian package deployment
+ - A Homebrew tap for formula submission
+ - GitHub Pages for documentation hosting
+
+## Requirements
+
+These workflows require:
+
+- GitHub Actions enabled on your repository
+- Appropriate permissions for the GitHub token
+- For the Release workflow, additional secrets may be needed for deployment
+
+## Customization
+
+You can customize these workflows by:
+
+- Adjusting the triggers (e.g., which branches to run on)
+- Adding or removing steps
+- Changing the deployment targets
+- Modifying the build parameters
+
+## Troubleshooting
+
+If you encounter issues with these workflows:
+
+- Check the GitHub Actions logs for detailed error messages
+- Ensure all required secrets are properly set
+- Verify that the repository has the necessary permissions
\ No newline at end of file
diff --git a/.github-proposal/workflows/benchmarks.yml b/.github-proposal/workflows/benchmarks.yml
new file mode 100644
index 00000000..70462e15
--- /dev/null
+++ b/.github-proposal/workflows/benchmarks.yml
@@ -0,0 +1,78 @@
+name: Benchmarks
+
+on:
+ push:
+ branches: [ main ]
+ pull_request:
+ branches: [ main ]
+ schedule:
+ - cron: '0 0 * * 0' # Run weekly on Sundays
+
+env:
+ CARGO_TERM_COLOR: always
+ RUST_BACKTRACE: 1
+
+jobs:
+ benchmarks:
+ name: Run Benchmarks
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Install Rust
+ uses: actions-rs/toolchain@v1
+ with:
+ profile: minimal
+ toolchain: stable
+ override: true
+
+ - name: Cache dependencies
+ uses: actions/cache@v3
+ with:
+ path: |
+ ~/.cargo/registry
+ ~/.cargo/git
+ target
+ key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
+
+ - name: Install cargo-criterion
+ uses: actions-rs/install@v0.1
+ with:
+ crate: cargo-criterion
+ version: latest
+ use-tool-cache: true
+
+ - name: Run benchmarks
+ uses: actions-rs/cargo@v1
+ with:
+ command: criterion
+
+ - name: Upload benchmark results
+ uses: actions/upload-artifact@v3
+ with:
+ name: benchmark-results
+ path: target/criterion
+
+ - name: Generate benchmark report
+ run: |
+ mkdir -p benchmark-report
+ cp -r target/criterion/* benchmark-report/
+ echo "# Benchmark Results" > benchmark-report/README.md
+ echo "Generated on $(date)" >> benchmark-report/README.md
+ echo "## Summary" >> benchmark-report/README.md
+ find target/criterion -name "*/new/estimates.json" -exec cat {} \; | jq -r '.mean | { command: .point_estimate, lower_bound: .confidence_interval.lower_bound, upper_bound: .confidence_interval.upper_bound }' >> benchmark-report/README.md
+
+ - name: Upload benchmark report
+ uses: actions/upload-artifact@v3
+ with:
+ name: benchmark-report
+ path: benchmark-report
+
+ - name: Compare with previous benchmarks
+ if: github.event_name == 'pull_request'
+ run: |
+ git fetch origin ${{ github.base_ref }}
+ git checkout FETCH_HEAD
+ cargo criterion --baseline main
+ git checkout ${{ github.sha }}
+ cargo criterion --baseline main
\ No newline at end of file
diff --git a/.github-proposal/workflows/ci.yml b/.github-proposal/workflows/ci.yml
new file mode 100644
index 00000000..b2e4e310
--- /dev/null
+++ b/.github-proposal/workflows/ci.yml
@@ -0,0 +1,144 @@
+name: CI
+
+on:
+ push:
+ branches: [ main ]
+ pull_request:
+ branches: [ main ]
+
+env:
+ CARGO_TERM_COLOR: always
+ RUST_BACKTRACE: 1
+
+jobs:
+ sanity-check:
+ name: Sanity Checks
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Install Rust
+ uses: actions-rs/toolchain@v1
+ with:
+ profile: minimal
+ toolchain: stable
+ override: true
+ components: rustfmt, clippy
+
+ - name: Cache dependencies
+ uses: actions/cache@v3
+ with:
+ path: |
+ ~/.cargo/registry
+ ~/.cargo/git
+ target
+ key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
+
+ - name: Check formatting
+ uses: actions-rs/cargo@v1
+ with:
+ command: fmt
+ args: --all -- --check
+
+ - name: Run clippy
+ uses: actions-rs/cargo@v1
+ with:
+ command: clippy
+ args: -- -D warnings
+
+ unit-tests:
+ name: Unit Tests
+ needs: sanity-check
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Install Rust
+ uses: actions-rs/toolchain@v1
+ with:
+ profile: minimal
+ toolchain: stable
+ override: true
+
+ - name: Cache dependencies
+ uses: actions/cache@v3
+ with:
+ path: |
+ ~/.cargo/registry
+ ~/.cargo/git
+ target
+ key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
+
+ - name: Run unit tests
+ uses: actions-rs/cargo@v1
+ with:
+ command: test
+ args: --lib --bins
+
+ e2e-tests:
+ name: End-to-End Tests
+ needs: unit-tests
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Install Rust
+ uses: actions-rs/toolchain@v1
+ with:
+ profile: minimal
+ toolchain: stable
+ override: true
+
+ - name: Cache dependencies
+ uses: actions/cache@v3
+ with:
+ path: |
+ ~/.cargo/registry
+ ~/.cargo/git
+ target
+ key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
+
+ - name: Build binary
+ uses: actions-rs/cargo@v1
+ with:
+ command: build
+ args: --release
+
+ - name: Run e2e tests
+ uses: actions-rs/cargo@v1
+ with:
+ command: test
+ args: --test main
+
+ code-coverage:
+ name: Code Coverage
+ needs: [unit-tests, e2e-tests]
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Install Rust
+ uses: actions-rs/toolchain@v1
+ with:
+ profile: minimal
+ toolchain: stable
+ override: true
+
+ - name: Install cargo-tarpaulin
+ uses: actions-rs/install@v0.1
+ with:
+ crate: cargo-tarpaulin
+ version: latest
+ use-tool-cache: true
+
+ - name: Generate coverage report
+ uses: actions-rs/cargo@v1
+ with:
+ command: tarpaulin
+ args: --out Xml --output-dir coverage
+
+ - name: Upload coverage to Codecov
+ uses: codecov/codecov-action@v3
+ with:
+ directory: ./coverage/
+ fail_ci_if_error: true
\ No newline at end of file
diff --git a/.github-proposal/workflows/cross-platform.yml b/.github-proposal/workflows/cross-platform.yml
new file mode 100644
index 00000000..3b01bd1d
--- /dev/null
+++ b/.github-proposal/workflows/cross-platform.yml
@@ -0,0 +1,54 @@
+name: Cross-Platform Tests
+
+on:
+ push:
+ branches: [ main ]
+ pull_request:
+ branches: [ main ]
+
+env:
+ CARGO_TERM_COLOR: always
+ RUST_BACKTRACE: 1
+
+jobs:
+ build-and-test:
+ name: Build and Test
+ strategy:
+ matrix:
+ os: [ubuntu-latest, macos-latest, windows-latest]
+ runs-on: ${{ matrix.os }}
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Install Rust
+ uses: actions-rs/toolchain@v1
+ with:
+ profile: minimal
+ toolchain: stable
+ override: true
+
+ - name: Cache dependencies
+ uses: actions/cache@v3
+ with:
+ path: |
+ ~/.cargo/registry
+ ~/.cargo/git
+ target
+ key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
+
+ - name: Build
+ uses: actions-rs/cargo@v1
+ with:
+ command: build
+
+ - name: Run unit tests
+ uses: actions-rs/cargo@v1
+ with:
+ command: test
+ args: --lib --bins
+
+ - name: Run e2e tests
+ uses: actions-rs/cargo@v1
+ with:
+ command: test
+ args: --test main
\ No newline at end of file
diff --git a/.github-proposal/workflows/release.yml b/.github-proposal/workflows/release.yml
new file mode 100644
index 00000000..42808083
--- /dev/null
+++ b/.github-proposal/workflows/release.yml
@@ -0,0 +1,188 @@
+name: Release
+
+on:
+ push:
+ tags:
+ - 'v*'
+
+env:
+ CARGO_TERM_COLOR: always
+ RUST_BACKTRACE: 1
+
+jobs:
+ build:
+ name: Build Release Binaries
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Install Rust
+ uses: actions-rs/toolchain@v1
+ with:
+ profile: minimal
+ toolchain: stable
+ override: true
+
+ - name: Build release binary
+ uses: actions-rs/cargo@v1
+ with:
+ command: build
+ args: --release
+
+ - name: Upload binary
+ uses: actions/upload-artifact@v3
+ with:
+ name: osvm-binary
+ path: target/release/osvm
+
+ create-github-release:
+ name: Create GitHub Release
+ needs: build
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Download binary
+ uses: actions/download-artifact@v3
+ with:
+ name: osvm-binary
+ path: ./
+
+ - name: Make binary executable
+ run: chmod +x ./osvm
+
+ - name: Create release
+ id: create_release
+ uses: softprops/action-gh-release@v1
+ with:
+ files: ./osvm
+ draft: false
+ prerelease: false
+ generate_release_notes: true
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ deploy-apt:
+ name: Deploy to APT Repository
+ needs: create-github-release
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Download binary
+ uses: actions/download-artifact@v3
+ with:
+ name: osvm-binary
+ path: ./
+
+ - name: Make binary executable
+ run: chmod +x ./osvm
+
+ - name: Set up Debian packaging environment
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y debhelper dh-make devscripts
+
+ - name: Create Debian package
+ run: |
+ VERSION=$(echo ${{ github.ref_name }} | sed 's/^v//')
+ mkdir -p osvm-$VERSION
+ cp ./osvm osvm-$VERSION/
+ cd osvm-$VERSION
+ dh_make -y -s -c mit -e maintainer@example.com -f ../osvm
+ cd ..
+ dpkg-buildpackage -us -uc
+
+ - name: Upload Debian package
+ uses: actions/upload-artifact@v3
+ with:
+ name: osvm-deb-package
+ path: ../osvm_*.deb
+
+ - name: Deploy to APT repository
+ run: |
+ # This is a placeholder for the actual APT repository deployment
+ # In a real scenario, you would use a service like Launchpad or a custom APT repository
+ echo "Deploying to APT repository..."
+ # Example: scp ../osvm_*.deb user@apt-repo:/path/to/repo/
+ # Then update the repository index
+
+ deploy-homebrew:
+ name: Deploy to Homebrew
+ needs: create-github-release
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Set up Homebrew
+ uses: Homebrew/actions/setup-homebrew@master
+
+ - name: Create Homebrew formula
+ run: |
+ VERSION=$(echo ${{ github.ref_name }} | sed 's/^v//')
+ SHA=$(curl -sL https://github.com/${{ github.repository }}/archive/${{ github.ref_name }}.tar.gz | shasum -a 256 | cut -d ' ' -f 1)
+
+ cat > osvm.rb << EOF
+ class Osvm < Formula
+ desc "OpenSVM CLI tool for managing SVM nodes"
+ homepage "https://github.com/${{ github.repository }}"
+ url "https://github.com/${{ github.repository }}/archive/${{ github.ref_name }}.tar.gz"
+ sha256 "$SHA"
+ version "$VERSION"
+
+ depends_on "rust" => :build
+
+ def install
+ system "cargo", "build", "--release"
+ bin.install "target/release/osvm"
+ end
+
+ test do
+ system "#{bin}/osvm", "--version"
+ end
+ end
+ EOF
+
+ - name: Upload Homebrew formula
+ uses: actions/upload-artifact@v3
+ with:
+ name: osvm-homebrew-formula
+ path: ./osvm.rb
+
+ - name: Submit to Homebrew
+ run: |
+ # This is a placeholder for the actual Homebrew submission
+ # In a real scenario, you would create a PR to homebrew-core or a custom tap
+ echo "Submitting to Homebrew..."
+ # Example: Create a PR to homebrew-core with the formula
+
+ deploy-cargo-doc:
+ name: Deploy to cargo doc
+ needs: create-github-release
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Install Rust
+ uses: actions-rs/toolchain@v1
+ with:
+ profile: minimal
+ toolchain: stable
+ override: true
+
+ - name: Generate documentation
+ uses: actions-rs/cargo@v1
+ with:
+ command: doc
+ args: --no-deps --document-private-items
+
+ - name: Create index.html
+ run: |
+ echo '' > target/doc/index.html
+
+ - name: Deploy to GitHub Pages
+ uses: peaceiris/actions-gh-pages@v3
+ with:
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ publish_dir: ./target/doc
+ force_orphan: true
\ No newline at end of file
diff --git a/.github-proposal/workflows/security.yml b/.github-proposal/workflows/security.yml
new file mode 100644
index 00000000..8c0ad0ae
--- /dev/null
+++ b/.github-proposal/workflows/security.yml
@@ -0,0 +1,77 @@
+name: Security Scan
+
+on:
+ push:
+ branches: [ main ]
+ pull_request:
+ branches: [ main ]
+ schedule:
+ - cron: '0 0 * * 1' # Run weekly on Mondays
+
+jobs:
+ security-audit:
+ name: Security Audit
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Install Rust
+ uses: actions-rs/toolchain@v1
+ with:
+ profile: minimal
+ toolchain: stable
+ override: true
+
+ - name: Install cargo-audit
+ uses: actions-rs/install@v0.1
+ with:
+ crate: cargo-audit
+ version: latest
+ use-tool-cache: true
+
+ - name: Run cargo-audit
+ uses: actions-rs/cargo@v1
+ with:
+ command: audit
+
+ - name: Run cargo-deny
+ uses: EmbarkStudios/cargo-deny-action@v1
+ with:
+ command: check advisories
+
+ dependency-review:
+ name: Dependency Review
+ runs-on: ubuntu-latest
+ if: github.event_name == 'pull_request'
+ steps:
+ - name: Checkout Repository
+ uses: actions/checkout@v3
+
+ - name: Dependency Review
+ uses: actions/dependency-review-action@v3
+ with:
+ fail-on-severity: high
+
+ code-scanning:
+ name: Code Scanning
+ runs-on: ubuntu-latest
+ permissions:
+ security-events: write
+ actions: read
+ contents: read
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v3
+
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@v2
+ with:
+ languages: rust
+
+ - name: Build
+ uses: actions-rs/cargo@v1
+ with:
+ command: build
+
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@v2
\ No newline at end of file
diff --git a/Cargo.toml b/Cargo.toml
index fa6c3942..d9f3bced 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -6,7 +6,6 @@ license = "WTFPL"
publish = false
[dependencies]
-borsh = "0.9.3"
clap = "2.33.3"
lazy_static = "1.4.0"
serde = { version = "1.0.125", features = ["derive"] }
@@ -35,6 +34,8 @@ serde_json = "1.0"
dirs = "5.0"
[dev-dependencies]
-lazy_static = "1.4.0"
-solana-test-validator = "1.14.*"
-solana-streamer = "1.14.*"
\ No newline at end of file
+assert_cmd = "2.0"
+predicates = "3.0"
+tempfile = "3.8"
+serial_test = "2.0"
+mockito = "1.2"
\ No newline at end of file
diff --git a/src/clparse.rs b/src/clparse.rs
index 43c61956..56566153 100644
--- a/src/clparse.rs
+++ b/src/clparse.rs
@@ -14,6 +14,7 @@ pub fn parse_command_line() -> ArgMatches<'static> {
.version(crate_version!())
.setting(AppSettings::SubcommandRequiredElseHelp)
.setting(AppSettings::AllowExternalSubcommands)
+ // Global arguments
.arg({
let arg = Arg::with_name("config_file")
.short("C")
@@ -87,106 +88,7 @@ pub fn parse_command_line() -> ArgMatches<'static> {
.possible_values(&["mainnet", "testnet", "devnet"])
.default_value("mainnet")
.help("Network to deploy on"),
- )
- .subcommand(
- SubCommand::with_name("balance").about("Get balance").arg(
- Arg::with_name("address")
- .validator(is_valid_pubkey)
- .value_name("ADDRESS")
- .takes_value(true)
- .index(1)
- .help("Address to get the balance of"),
- ),
- )
- .subcommand(
- SubCommand::with_name("mint")
- .about("Mint a new key/value pair to an account")
- .arg(
- Arg::with_name("to-owner")
- .display_order(1)
- .long("to-owner")
- .short("t")
- .required(true)
- .takes_value(true)
- .help("Owner of accounts")
- .possible_values(&["User1", "User2"]),
- )
- .arg(
- Arg::with_name("key")
- .display_order(2)
- .long("key")
- .short("k")
- .required(true)
- .takes_value(true)
- .help("The key of key/value pair"),
- )
- .arg(
- Arg::with_name("value")
- .display_order(3)
- .long("value")
- .required(true)
- .min_values(1)
- .help("The value string of key/value pair"),
- ),
- )
- .subcommand(
- SubCommand::with_name("transfer")
- .about("Transfer a key/value pair from one account to another")
- .arg(
- Arg::with_name("from-owner")
- .display_order(1)
- .long("from-owner")
- .short("f")
- .required(true)
- .takes_value(true)
- .help("Owner to transfer from")
- .possible_values(&["User1", "User2"]),
- )
- .arg(
- Arg::with_name("to-owner")
- .display_order(2)
- .long("to-owner")
- .short("t")
- .required(true)
- .takes_value(true)
- .help("Owner to transfer to")
- .possible_values(&["User1", "User2"]),
- )
- .arg(
- Arg::with_name("key")
- .display_order(3)
- .long("key")
- .short("k")
- .required(true)
- .takes_value(true)
- .help("The key of key/value pair to transfer"),
- ),
- )
- .subcommand(
- SubCommand::with_name("burn")
- .about("Burn (delete) a key/value pair from an account")
- .arg(
- Arg::with_name("from-owner")
- .display_order(1)
- .long("from-owner")
- .short("f")
- .required(true)
- .takes_value(true)
- .help("Owner to burn key/value from")
- .possible_values(&["User1", "User2"]),
- )
- .arg(
- Arg::with_name("key")
- .display_order(2)
- .long("key")
- .short("k")
- .required(true)
- .takes_value(true)
- .help("The key of key/value pair to burn"),
- ),
- )
- .subcommand(SubCommand::with_name("ping").about("Send a ping transaction"))
- .subcommand(
+ ) .subcommand(
SubCommand::with_name("examples")
.about("Show usage examples for OSVM CLI commands")
.arg(
@@ -246,6 +148,7 @@ pub fn parse_command_line() -> ArgMatches<'static> {
)
)
)
+ // Node management commands
.subcommand(
SubCommand::with_name("nodes")
.about("Manage validator and RPC nodes")
@@ -413,8 +316,16 @@ pub fn parse_command_line() -> ArgMatches<'static> {
Arg::with_name("host")
.long("host")
.value_name("HOST")
+ .required(true)
+ .takes_value(true)
+ .help("Remote host to deploy on (format: user@host[:port])")
+ )
+ .arg(
+ Arg::with_name("name")
+ .long("name")
+ .value_name("NAME")
.takes_value(true)
- .help("Remote host to deploy on (format: user@host)")
+ .help("Custom name for the node (default: auto-generated)")
)
)
)
diff --git a/src/lib.rs b/src/lib.rs
index 59ff22e3..fd90ad6d 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,10 +1,23 @@
-pub mod utils;
+//! OSVM CLI Library
+//!
+//! This library provides the core functionality for the OSVM CLI.
+//! It includes utilities for managing SVMs, nodes, and SSH deployments.
+//!
+//! # Features
+//!
+//! - SVM Management: List, get details, and install SVMs
+//! - Node Management: Deploy, monitor, and control validator and RPC nodes
+//! - SSH Deployment: Deploy SVMs and nodes to remote hosts
+//! - Interactive Dashboards: Monitor SVMs and nodes in real-time
+//!
+//! # Architecture
+//!
+//! The OSVM CLI is organized into several modules:
+//!
+//! - `utils`: Core utilities for SVM and node management
+//! - `clparse`: Command-line parsing and argument definitions
+//! - `main`: Main entry point and command handlers
-/// Exports key capabilities
-pub mod prelude {
- pub use crate::utils::{
- account_state::*,
- keys_db::{KEYS_DB, PROG_KEY},
- txn_utils::*,
- };
-}
+pub mod utils;
+pub mod clparse;
+pub mod prelude;
\ No newline at end of file
diff --git a/src/main.rs b/src/main.rs
index d121bee2..2b0eeeb7 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -560,14 +560,11 @@ async fn main() -> Result<(), Box> {
additional_params: std::collections::HashMap::new(),
};
- if let Err(e) = ssh_deploy::deploy_svm_node(connection, deploy_config, None).await {
- eprintln!("Deployment error: {}", e);
- exit(1);
- }
+ ssh_deploy::deploy_svm_node(connection, deploy_config, None).await
+ .context("Deployment failed")?;
}
(cmd, _) => {
- eprintln!("Unknown command: {}", cmd);
- exit(1);
+ return Err(anyhow::anyhow!("Unknown command: {}", cmd));
}
};
diff --git a/src/prelude.rs b/src/prelude.rs
index 294e935e..24415876 100644
--- a/src/prelude.rs
+++ b/src/prelude.rs
@@ -1,10 +1,19 @@
//! Exports key capabilities in a single module for convenient use
+//!
+//! This module re-exports the key modules from the utils directory
+//! to make them easily accessible throughout the codebase.
pub use crate::utils::{
- account_state::unpack_account_data,
- keys_db::{KEYS_DB, PROG_KEY},
- txn_utils::{
- burn_instruction, load_account, load_wallet, mint_transaction, ping_instruction,
- transfer_instruction,
- },
+ /// SVM information and management utilities
+ svm_info,
+ /// SSH deployment utilities
+ ssh_deploy,
+ /// Node management utilities
+ nodes,
+ /// Dashboard utilities
+ dashboard,
+ /// Example command utilities
+ examples,
+ /// Color formatting utilities
+ color,
};
\ No newline at end of file
diff --git a/src/utils/account_state.rs b/src/utils/account_state.rs
deleted file mode 100644
index d2451a54..00000000
--- a/src/utils/account_state.rs
+++ /dev/null
@@ -1,26 +0,0 @@
-//! @brief Account state access
-
-use {
- crate::utils::txn_utils::get_account_for,
- solana_client::rpc_client::RpcClient,
- solana_sdk::{commitment_config::CommitmentConfig, signature::Keypair, signer::Signer},
- std::{collections::BTreeMap, error::Error},
-};
-
-/// Unpacks token state for the accumulator
-pub fn unpack_account_data(
- rpc_client: &RpcClient,
- account: &Keypair,
- commitment_config: CommitmentConfig,
-) -> Result<(bool, BTreeMap), Box> {
- match get_account_for(rpc_client, &account.pubkey(), commitment_config) {
- Some(_account) => {
- // Implement custom unpacking logic here instead of using sol_template_shared
- Ok((false, BTreeMap::new()))
- },
- None => Err(Box::::from(format!(
- "account not found for \"{:?}\". ",
- account
- ))),
- }
-}
diff --git a/src/utils/examples.rs b/src/utils/examples.rs
index 43505764..b4148b00 100644
--- a/src/utils/examples.rs
+++ b/src/utils/examples.rs
@@ -62,18 +62,6 @@ pub struct Example {
pub fn get_all_examples() -> Vec {
vec![
// Basic Commands
- Example {
- title: "Check your balance",
- command: "osvm balance",
- explanation: "Displays the SOL balance of your default keypair",
- category: ExampleCategory::Basic,
- },
- Example {
- title: "Check a specific address balance",
- command: "osvm balance 5vXNUCLCvfBxLrZcgJnSury7KxZ6niwg3gdMrys4a6Uh",
- explanation: "Displays the SOL balance of the specified wallet address",
- category: ExampleCategory::Basic,
- },
Example {
title: "Run with increased verbosity",
command: "osvm -v svm list",
@@ -82,10 +70,16 @@ pub fn get_all_examples() -> Vec {
},
Example {
title: "Specify a different RPC URL",
- command: "osvm --url https://api.mainnet-beta.solana.com balance",
+ command: "osvm --url https://api.mainnet-beta.solana.com svm list",
explanation: "Run a command using a specific Solana RPC endpoint",
category: ExampleCategory::Basic,
},
+ Example {
+ title: "Disable colored output",
+ command: "osvm --no-color svm list",
+ explanation: "Run a command with colored output disabled",
+ category: ExampleCategory::Basic,
+ },
// SVM Management
Example {
diff --git a/src/utils/keys_db.rs b/src/utils/keys_db.rs
deleted file mode 100644
index 69e31299..00000000
--- a/src/utils/keys_db.rs
+++ /dev/null
@@ -1,241 +0,0 @@
-//! keys encapsulates key management
-//!
-//! Processes the keys in the `keys/accounts` folder by
-//! 1. Read the keys_db.yml file
-//! 2. Faults in keys from file system as needed
-
-use {
- super::load_keys_config_file,
- lazy_static::lazy_static,
- serde::{Deserialize, Serialize},
- solana_sdk::signature::{read_keypair_file, Keypair},
- std::{
- collections::HashMap,
- error, fs,
- path::{Path, PathBuf},
- },
-};
-
-/// The base folder for the keys DB
-const KEYS_DB_CONFIG_PATH: &str = "keys";
-/// The configuration file name for the keys in the DB
-const KEYS_DB_CONFIG_FILE_NAME: &str = "keys_db.yml";
-/// Standardized wallet key name
-const WALLET: &str = "wallet";
-/// Standardized account key name
-const ACCOUNT: &str = "account";
-/// The base folder for the program
-const KEY_PROGRAM_PATH: &str = "program";
-/// Our fee receiving account owner
-const SERVICE_OWNER: &str = "Service";
-
-// Initialize the pathbuf for the path/filename of keys db configuration
-lazy_static! {
- static ref KEYS_CONFIG_PATH: PathBuf = {
- let path = Path::new(KEYS_DB_CONFIG_PATH);
- path.join(KEYS_DB_CONFIG_FILE_NAME)
- };
-}
-
-// Initialize the yaml keys hashmap
-lazy_static! {
- static ref KEYS_YAML_DB: KeysYamlDB = KeysYamlDB::load();
-}
-
-// Initialize the pathbuf for the path/subpath for the program key
-lazy_static! {
- static ref PROGRAM_KEY_PATH: PathBuf = {
- let path = Path::new(KEYS_DB_CONFIG_PATH);
- path.join(KEY_PROGRAM_PATH)
- };
-}
-
-// Initialize the Program Key
-lazy_static! {
- pub static ref PROG_KEY: Keypair = load_program_key();
-}
-
-/// Load the programs key for deployment or usage in transactions
-fn load_program_key() -> Keypair {
- if PROGRAM_KEY_PATH.is_dir() {
- let mut fpath = fs::read_dir(PROGRAM_KEY_PATH.as_path()).unwrap();
- let entry = fpath.next().unwrap().unwrap();
- match read_keypair_file(entry.path()) {
- Ok(f) => f,
- Err(_) => panic!(),
- }
- } else {
- panic!()
- }
-}
-
-#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
-/// Encapsulates the users and keypair keypaths, internal only
-struct KeysYamlDB {
- version: String,
- registry: HashMap>, // Maps from friendly "User" down to keypair paths
-}
-
-impl KeysYamlDB {
- fn load() -> Self {
- match load_keys_config_file(KEYS_CONFIG_PATH.as_path()) {
- Ok(t) => t,
- Err(_) => {
- eprintln!(
- "{:?} not found during keys_db configuration load",
- *KEYS_CONFIG_PATH
- );
- panic!()
- }
- }
- }
-
- /// Returns the configuration file map
- pub fn registry(&self) -> &HashMap> {
- &self.registry
- }
-
- /// Returns the configuration file version
- #[allow(dead_code)]
- pub fn version(&self) -> &String {
- &self.version
- }
-}
-
-// Initialize the keypairs hashmap
-lazy_static! {
- pub static ref KEYS_DB: KeysDB = KeysDB::load();
-}
-
-#[derive(Debug)]
-/// Encapsulates the users and keypairs
-pub struct KeysDB {
- keys_registry: HashMap>,
-}
-
-impl KeysDB {
- /// Load a account file into a Keypair
- #[allow(dead_code)]
- fn load_keypair(path: &Path) -> Result> {
- match read_keypair_file(&path) {
- Err(e) => Err(std::io::Error::new(
- std::io::ErrorKind::Other,
- format!("could not read keypair file \"{}\". Run \"solana-keygen new\" to create a keypair file: {}",
- path.display(), e),
- )
- .into()),
- Ok(kp) => Ok(kp),
- }
- }
-
- /// Loads the KEYS_DB keypairs from the configuration
- #[allow(dead_code)]
- fn load() -> Self {
- let mut keys_reg = HashMap::>::new();
- for accounts in KEYS_YAML_DB.registry().iter() {
- let mut keys_hm = HashMap::::new();
- keys_hm.insert(
- WALLET.to_string().to_string(),
- Self::load_keypair(accounts.1.get(WALLET).unwrap()).unwrap(),
- );
- keys_hm.insert(
- ACCOUNT.to_string().to_string(),
- Self::load_keypair(accounts.1.get(ACCOUNT).unwrap()).unwrap(),
- );
- keys_reg.insert(accounts.0.clone(), keys_hm);
- }
- KeysDB {
- keys_registry: keys_reg,
- }
- }
- /// Fetch a reference to the registry of keypairs
- pub fn keys_registry(&self) -> &HashMap> {
- &self.keys_registry
- }
- /// Returns a vector of key owners
- pub fn key_owners(&self) -> Vec {
- let mut result = Vec::::new();
- for x in self.keys_registry.keys() {
- result.push(x.to_string());
- }
- result
- }
- /// Returns non service account owner names
- pub fn non_service_key_owners(&self) -> Vec {
- let mut all_owners = self.key_owners();
- all_owners.remove(
- all_owners
- .iter()
- .position(|x| *x == SERVICE_OWNER)
- .expect("needle not found"),
- );
- all_owners
- }
- /// Get a wallet and account keypair for name
- pub fn wallet_and_account(
- &self,
- name: String,
- ) -> Result<(&Keypair, &Keypair), Box> {
- match self.keys_registry.contains_key(&name) {
- true => {
- let owner = self.keys_registry.get(&name).unwrap();
- Ok((owner.get(WALLET).unwrap(), owner.get(ACCOUNT).unwrap()))
- }
- false => Err(Box::::from(format!(
- "could not find owner \"{}\". key in DB",
- name
- ))),
- }
- }
-}
-
-#[cfg(test)]
-mod tests {
- use solana_sdk::signer::Signer;
-
- use super::*;
-
- #[test]
- fn test_program_key() {
- println!("{}", PROG_KEY.pubkey());
- }
- #[test]
- fn test_keys_config_db_load() {
- assert_eq!("1.5.0", KEYS_YAML_DB.version());
- }
-
- #[test]
- fn test_keys_keypair_load() {
- assert!(KEYS_DB.keys_registry().contains_key(SERVICE_OWNER));
- assert!(KEYS_DB.keys_registry().contains_key("User1"));
- assert!(KEYS_DB.keys_registry().contains_key("User2"));
- if let Some(user) = KEYS_DB.keys_registry().get(SERVICE_OWNER) {
- assert!(user.contains_key(WALLET));
- assert!(user.contains_key(ACCOUNT));
- }
- if let Some(user) = KEYS_DB.keys_registry().get("User1") {
- assert!(user.contains_key(WALLET));
- assert!(user.contains_key(ACCOUNT));
- }
- if let Some(user) = KEYS_DB.keys_registry().get("User2") {
- assert!(user.contains_key(WALLET));
- assert!(user.contains_key(ACCOUNT));
- }
- }
-
- #[test]
- fn test_list_key_holders() {
- let key_owners = KEYS_DB.key_owners();
- assert!(key_owners.contains(&"Service".to_string()));
- assert!(key_owners.contains(&"User1".to_string()));
- assert!(key_owners.contains(&"User2".to_string()));
- }
-
- #[test]
- fn test_non_service_key_holders() {
- let key_owners = KEYS_DB.non_service_key_owners();
- assert!(!key_owners.contains(&"Service".to_string()));
- assert!(key_owners.contains(&"User1".to_string()));
- assert!(key_owners.contains(&"User2".to_string()));
- }
-}
diff --git a/src/utils/mod.rs b/src/utils/mod.rs
index f5f85e3f..32031a1d 100644
--- a/src/utils/mod.rs
+++ b/src/utils/mod.rs
@@ -1,22 +1,55 @@
-//! Module exports for utility modules
+//! Utility modules for the OSVM CLI
+//!
+//! This directory contains various utility modules that provide the core functionality
+//! for the OSVM CLI, including SVM and node management, SSH deployment, and UI components.
use std::{fs::File, io, path::Path};
+use serde::de::DeserializeOwned;
-pub mod account_state;
+// UI and display utilities
+/// Color formatting utilities for terminal output
pub mod color;
-pub mod keys_db;
+/// Example command utilities for displaying usage examples
pub mod examples;
+/// Dashboard utilities for interactive SVM monitoring
+pub mod dashboard;
+/// Node dashboard utilities for interactive node monitoring
+pub mod nodes_dashboard;
+
+// Core functionality
+/// SVM information and management utilities
pub mod svm_info;
+/// SSH deployment utilities for remote node deployment
pub mod ssh_deploy;
-pub mod txn_utils;
-pub mod dashboard;
+/// Node management utilities for monitoring and controlling nodes
pub mod nodes;
-pub mod nodes_dashboard;
-/// Loads a yaml file
+/// Loads a YAML configuration file and deserializes it into the specified type
+///
+/// # Arguments
+///
+/// * `config_file` - Path to the YAML configuration file
+///
+/// # Returns
+///
+/// * `Result` - The deserialized configuration or an error
+///
+/// # Examples
+///
+/// ```
+/// use osvm::utils::load_keys_config_file;
+/// use serde::Deserialize;
+///
+/// #[derive(Deserialize)]
+/// struct Config {
+/// key: String,
+/// }
+///
+/// let config: Config = load_keys_config_file("config.yml").unwrap();
+/// ```
pub fn load_keys_config_file(config_file: P) -> Result
where
- T: serde::de::DeserializeOwned,
+ T: DeserializeOwned,
P: AsRef,
{
let file = File::open(config_file)?;
diff --git a/src/utils/nodes.rs b/src/utils/nodes.rs
index aea8c556..4696f545 100644
--- a/src/utils/nodes.rs
+++ b/src/utils/nodes.rs
@@ -968,7 +968,6 @@ pub fn get_node_info(
/// Get node logs
///
/// # Arguments
-/// * `client` - RPC client
/// * `node_id` - Node ID
/// * `lines` - Number of lines to show
/// * `follow` - Whether to follow the logs
@@ -980,8 +979,8 @@ pub fn get_node_logs(
lines: usize,
follow: bool
) -> Result<(), Box> {
- let db = NodeDatabase::load()?;
- let node = db.get_node(node_id)?;
+ let db = NodeDatabase::load().map_err(|e| anyhow::anyhow!("Failed to load node database: {}", e))?;
+ let node = db.get_node(node_id).map_err(|e| anyhow::anyhow!("Failed to get node {}: {}", node_id, e))?;
// Create SSH client
let server_config = ServerConfig {
@@ -998,12 +997,15 @@ pub fn get_node_logs(
let service_name = format!("{}-{}-{}", node.svm_type, node.node_type, node.network);
// Create runtime for executing SSH client
- let rt = Runtime::new()?;
+ let rt = Runtime::new().map_err(|e| anyhow::anyhow!("Failed to create async runtime: {}", e))?;
rt.block_on(async {
use crate::utils::ssh_deploy::SshClient;
- let mut client = SshClient::new(server_config.clone())?;
- client.connect()?;
+ let mut client = SshClient::new(server_config.clone())
+ .map_err(|e| anyhow::anyhow!("Failed to create SSH client: {}", e))?;
+
+ client.connect()
+ .map_err(|e| anyhow::anyhow!("Failed to connect to {}: {}", node.host, e))?;
let command = if follow {
println!("Note: Log streaming is not fully supported. Displaying current logs...");
@@ -1017,14 +1019,16 @@ pub fn get_node_logs(
client.stream_command(&command, |line| {
println!("{}", line);
true // Continue streaming
- })?;
+ })
+ .map_err(|e| anyhow::anyhow!("Failed to stream logs: {}", e))?;
} else {
// For non-follow mode, just execute and print
- let logs = client.execute_command(&command)?;
+ let logs = client.execute_command(&command)
+ .map_err(|e| anyhow::anyhow!("Failed to execute log command: {}", e))?;
println!("{}", logs);
}
- Ok::<_, Box>(())
+ Ok::<_, anyhow::Error>(())
})?;
Ok(())
@@ -1114,13 +1118,22 @@ trait SshClientExt {
}
impl SshClientExt for crate::utils::ssh_deploy::SshClient {
- fn stream_command(&mut self, command: &str, callback: F) -> Result<(), Box>
+ fn stream_command(&mut self, command: &str, mut callback: F) -> Result<(), Box>
where
F: FnMut(&str) -> bool,
{
- // Execute the command and process all output at once as a fallback
+ // First try to execute the command
let output = self.execute_command(command)?;
- output.lines().all(callback);
+
+ // Process each line of the output
+ let mut continue_processing = true;
+ for line in output.lines() {
+ continue_processing = callback(line);
+ if !continue_processing {
+ break;
+ }
+ }
+
Ok(())
}
}
\ No newline at end of file
diff --git a/src/utils/ssh_deploy.rs b/src/utils/ssh_deploy.rs
index 6a437798..79c9e865 100644
--- a/src/utils/ssh_deploy.rs
+++ b/src/utils/ssh_deploy.rs
@@ -648,46 +648,52 @@ fn validate_system_requirements(
// Check CPU cores
let cpu_cores = system_info.get("cpu_cores")
- .and_then(|s| s.parse::().ok())
- .unwrap_or(0);
-
+ .ok_or_else(|| DeploymentError::ValidationError("CPU cores information not available".to_string()))?
+ .parse::()
+ .map_err(|e| DeploymentError::ValidationError(format!("Invalid CPU cores value: {}", e)))?;
+
if cpu_cores < required_cpu {
return Err(DeploymentError::ValidationError(format!(
"Insufficient CPU cores: {} (required: {})",
cpu_cores, required_cpu
)));
}
-
+
// Check memory
let memory_gb = system_info.get("memory_gb")
- .and_then(|s| s.parse::().ok())
- .unwrap_or(0);
-
+ .ok_or_else(|| DeploymentError::ValidationError("Memory information not available".to_string()))?
+ .parse::()
+ .map_err(|e| DeploymentError::ValidationError(format!("Invalid memory value: {}", e)))?;
+
if memory_gb < required_memory {
return Err(DeploymentError::ValidationError(format!(
"Insufficient memory: {} GB (required: {} GB)",
memory_gb, required_memory
)));
}
-
+
// Check available disk space
let available_disk = system_info.get("disk_available")
- .and_then(|s| {
- // Parse disk space - handle different units (G, T)
- if s.ends_with('G') {
- s[..s.len()-1].parse::().ok().map(|v| v as u16)
- } else if s.ends_with('T') {
- s[..s.len()-1].parse::().ok().map(|v| (v * 1024.0) as u16)
- } else {
- None
- }
- })
- .unwrap_or(0);
-
- if available_disk < required_disk {
+ .ok_or_else(|| DeploymentError::ValidationError("Disk space information not available".to_string()))?;
+
+ // Parse disk space - handle different units (G, T)
+ let available_disk_gb = if available_disk.ends_with('G') {
+ available_disk[..available_disk.len()-1].parse::()
+ .map_err(|e| DeploymentError::ValidationError(format!("Invalid disk space value: {}", e)))?
+ } else if available_disk.ends_with('T') {
+ available_disk[..available_disk.len()-1].parse::()
+ .map_err(|e| DeploymentError::ValidationError(format!("Invalid disk space value: {}", e)))?
+ * 1024.0
+ } else {
+ return Err(DeploymentError::ValidationError(format!(
+ "Unrecognized disk space unit: {}", available_disk
+ )));
+ };
+
+ if available_disk_gb as u16 < required_disk {
return Err(DeploymentError::ValidationError(format!(
"Insufficient disk space: {} GB (required: {} GB)",
- available_disk, required_disk
+ available_disk_gb, required_disk
)));
}
diff --git a/src/utils/txn_utils.rs b/src/utils/txn_utils.rs
deleted file mode 100644
index 98f723ad..00000000
--- a/src/utils/txn_utils.rs
+++ /dev/null
@@ -1,252 +0,0 @@
-//! @brief Transaction utilities
-
-use {
- crate::utils::keys_db::PROG_KEY,
- solana_client::rpc_client::RpcClient,
- solana_sdk::{
- account::Account,
- commitment_config::CommitmentConfig,
- instruction::{AccountMeta, Instruction},
- message::Message,
- pubkey::Pubkey,
- signature::{Keypair, Signature},
- signer::Signer,
- system_instruction,
- transaction::Transaction,
- },
-};
-
-/// Checks for existence of account
-fn account_for_key(
- rpc_client: &RpcClient,
- key: &Pubkey,
- commitment_config: CommitmentConfig,
-) -> Option {
- rpc_client
- .get_account_with_commitment(key, commitment_config)
- .unwrap()
- .value
-}
-
-/// Gets the account from the ledger
-pub fn get_account_for(
- rpc_client: &RpcClient,
- account: &Pubkey,
- commitment_config: CommitmentConfig,
-) -> Option {
- account_for_key(rpc_client, account, commitment_config)
-}
-
-/// Fund a wallet by transferring rent-free amount from core account
-fn fund_wallet(
- rpc_client: &RpcClient,
- wallet_signer: &dyn Signer,
- signer: &dyn Signer,
- commitment_config: CommitmentConfig,
-) -> Result<(), Box> {
- let recent_blockhash = rpc_client
- .get_latest_blockhash()
- .map_err(|err| format!("error: unable to get recent blockhash: {}", err))
- .unwrap();
-
- let mut transaction = Transaction::new_unsigned(Message::new(
- &[system_instruction::transfer(
- &signer.pubkey(),
- &wallet_signer.pubkey(),
- 50_000_000,
- )],
- Some(&signer.pubkey()),
- ));
-
- transaction
- .try_sign(&vec![signer], recent_blockhash)
- .map_err(|err| format!("error: failed to sign transaction: {}", err))
- .unwrap();
- let _signature = rpc_client
- .send_and_confirm_transaction_with_spinner_and_commitment(&transaction, commitment_config)
- .map_err(|err| format!("error: send transaction: {}", err))
- .unwrap();
- let _account = rpc_client
- .get_account_with_commitment(&wallet_signer.pubkey(), commitment_config)
- .unwrap()
- .value
- .unwrap();
- Ok(())
-}
-
-/// Load wallet and, if needed, fund it
-pub fn load_wallet(
- rpc_client: &RpcClient,
- wallet_keypair: &Keypair,
- signer: &dyn Signer,
- commitment_config: CommitmentConfig,
-) -> Result<(), Box> {
- if account_for_key(rpc_client, &wallet_keypair.pubkey(), commitment_config).is_some() {
- } else {
- fund_wallet(rpc_client, wallet_keypair, signer, commitment_config)?;
- };
- Ok(())
-}
-
-/// Create a new program account with account state data allocation
-fn new_account(
- rpc_client: &RpcClient,
- wallet_signer: &dyn Signer,
- account_pair: &dyn Signer,
- program_owner: &Pubkey,
- state_space: u64,
- commitment_config: CommitmentConfig,
-) -> Result<(), Box> {
- let account_lamports = rpc_client
- .get_minimum_balance_for_rent_exemption(state_space as usize)
- .unwrap();
-
- let mut transaction = Transaction::new_with_payer(
- &[
- system_instruction::create_account(
- &wallet_signer.pubkey(),
- &account_pair.pubkey(),
- account_lamports,
- state_space,
- program_owner,
- ),
- Instruction::new_with_borsh(
- *program_owner,
- &"initialize_account", // Replace ProgramInstruction with string command
- vec![
- AccountMeta::new(account_pair.pubkey(), false),
- AccountMeta::new(wallet_signer.pubkey(), false),
- ],
- ),
- ],
- Some(&wallet_signer.pubkey()),
- );
-
- let recent_blockhash = rpc_client
- .get_latest_blockhash()
- .map_err(|err| format!("error: unable to get recent blockhash: {}", err))
- .unwrap();
- transaction
- .try_sign(&vec![wallet_signer, account_pair], recent_blockhash)
- .map_err(|err| format!("error: failed to sign transaction: {}", err))
- .unwrap();
- let _signature = rpc_client
- .send_and_confirm_transaction_with_spinner_and_commitment(&transaction, commitment_config)
- .map_err(|err| format!("error: send transaction: {}", err))
- .unwrap();
- let _account = rpc_client
- .get_account_with_commitment(&account_pair.pubkey(), commitment_config)
- .map_err(|err| format!("error: getting account after initialization: {}", err))
- .unwrap();
-
- Ok(())
-}
-
-/// Load account with size
-pub fn load_account(
- rpc_client: &RpcClient,
- account_pair: &Keypair,
- wallet_signer: &dyn Signer,
- program_owner: &Pubkey,
- space: u64,
- commitment_config: CommitmentConfig,
-) -> Result<(), Box> {
- match get_account_for(rpc_client, &account_pair.pubkey(), commitment_config) {
- Some(_) => {}
- None => new_account(
- rpc_client,
- wallet_signer,
- account_pair,
- program_owner,
- space,
- commitment_config,
- )
- .unwrap(),
- };
- Ok(())
-}
-
-/// Submits the program instruction as per the
-/// instruction definition
-pub fn submit_transaction(
- rpc_client: &RpcClient,
- wallet_signer: &dyn Signer,
- instruction: Instruction,
- commitment_config: CommitmentConfig,
-) -> Result> {
- let mut transaction =
- Transaction::new_unsigned(Message::new(&[instruction], Some(&wallet_signer.pubkey())));
- let recent_blockhash = rpc_client
- .get_latest_blockhash()
- .map_err(|err| format!("error: unable to get recent blockhash: {}", err))?;
- transaction
- .try_sign(&vec![wallet_signer], recent_blockhash)
- .map_err(|err| format!("error: failed to sign transaction: {}", err))?;
- let signature = rpc_client
- .send_and_confirm_transaction_with_spinner_and_commitment(&transaction, commitment_config)
- .map_err(|err| format!("error: send transaction: {}", err))?;
- Ok(signature)
-}
-
-/// Perform a mint transaction consisting of a key/value pair
-pub fn mint_transaction(
- rpc_client: &RpcClient,
- accounts: &[AccountMeta],
- wallet_signer: &dyn Signer,
- mint_key: &str,
- mint_value: &str,
- commitment_config: CommitmentConfig,
-) -> Result> {
- let instruction = Instruction::new_with_borsh(
- PROG_KEY.pubkey(),
- &format!("mint_to_account:{}:{}", mint_key, mint_value), // Replace ProgramInstruction with string command
- accounts.to_vec(),
- );
- submit_transaction(rpc_client, wallet_signer, instruction, commitment_config)
-}
-
-/// Transfer a minted key/value from one account to another account
-pub fn transfer_instruction(
- rpc_client: &RpcClient,
- accounts: &[AccountMeta],
- wallet_signer: &dyn Signer,
- transfer_key: &str,
- commitment_config: CommitmentConfig,
-) -> Result> {
- let instruction = Instruction::new_with_borsh(
- PROG_KEY.pubkey(),
- &format!("transfer_between_accounts:{}", transfer_key), // Replace ProgramInstruction with string command
- accounts.to_vec(),
- );
- submit_transaction(rpc_client, wallet_signer, instruction, commitment_config)
-}
-
-/// Burn, delete, the key/value from the owning account
-pub fn burn_instruction(
- rpc_client: &RpcClient,
- accounts: &[AccountMeta],
- wallet_signer: &dyn Signer,
- burn_key: &str,
- commitment_config: CommitmentConfig,
-) -> Result> {
- let instruction = Instruction::new_with_borsh(
- PROG_KEY.pubkey(),
- &format!("burn_from_account:{}", burn_key), // Replace ProgramInstruction with string command
- accounts.to_vec(),
- );
- submit_transaction(rpc_client, wallet_signer, instruction, commitment_config)
-}
-
-pub fn ping_instruction(
- rpc_client: &RpcClient,
- signer: &dyn Signer,
- commitment_config: CommitmentConfig,
-) -> Result> {
- let amount = 0;
- submit_transaction(
- rpc_client,
- signer,
- system_instruction::transfer(&signer.pubkey(), &signer.pubkey(), amount),
- commitment_config,
- )
-}
diff --git a/tests/README.md b/tests/README.md
new file mode 100644
index 00000000..5869c37f
--- /dev/null
+++ b/tests/README.md
@@ -0,0 +1,67 @@
+# OSVM CLI Tests
+
+This directory contains tests for the OSVM CLI application.
+
+## End-to-End (E2E) Tests
+
+The `e2e` directory contains end-to-end tests that verify the CLI functionality by running commands and checking their output. These tests simulate real user interactions with the CLI.
+
+### Test Structure
+
+- `common.rs`: Common utilities for e2e tests, including functions to run commands and check output
+- `svm_tests.rs`: Tests for SVM-related commands
+- `node_tests.rs`: Tests for node-related commands
+
+### Running the Tests
+
+To run all tests:
+
+```bash
+cargo test
+```
+
+To run only e2e tests:
+
+```bash
+cargo test --test main
+```
+
+To run a specific test:
+
+```bash
+cargo test --test main test_svm_list
+```
+
+### Test Coverage
+
+The e2e tests cover the following functionality:
+
+#### SVM Commands
+- `svm list`: List all available SVMs
+- `svm get`: Get detailed information about a specific SVM
+- `svm dashboard`: Launch the SVM dashboard
+
+#### Node Commands
+- `nodes list`: List all managed nodes
+- `nodes list` with filters: Test filtering by network, type, and status
+- `nodes dashboard`: Launch the node dashboard
+- `nodes get`: Get detailed information about a specific node
+
+#### General CLI Features
+- Custom configuration file
+- Custom RPC URL
+- Verbose output
+- No-color mode
+- Help command
+
+### Mock Server
+
+The tests use a mock server to simulate API responses for testing without requiring a real network connection. This allows the tests to run in any environment without external dependencies.
+
+### Test Dependencies
+
+- `assert_cmd`: For running commands and making assertions about their output
+- `predicates`: For making assertions about command output
+- `tempfile`: For creating temporary directories and files
+- `serial_test`: For running tests serially to avoid conflicts
+- `mockito`: For creating a mock server to simulate API responses
\ No newline at end of file
diff --git a/tests/e2e/README.md b/tests/e2e/README.md
new file mode 100644
index 00000000..f9dee268
--- /dev/null
+++ b/tests/e2e/README.md
@@ -0,0 +1,156 @@
+# End-to-End (E2E) Tests for OSVM CLI
+
+This directory contains end-to-end tests for the OSVM CLI application. These tests verify the CLI functionality by running commands and checking their output, simulating real user interactions.
+
+## Test Structure
+
+- `mod.rs`: Main module file that includes all test modules
+- `common.rs`: Common utilities for e2e tests, including functions to run commands and check output
+- `svm_tests.rs`: Tests for SVM-related commands
+- `node_tests.rs`: Tests for node-related commands
+- `examples.rs`: Example tests demonstrating how to write e2e tests
+
+## Test Utilities
+
+The `common.rs` file provides several utilities for writing e2e tests:
+
+- `run_osvm_command()`: Returns a Command object for running OSVM commands
+- `run_osvm_command_string()`: Runs an OSVM command and returns the output as a string
+- `output_contains()`: Checks if the output contains the expected text
+- `create_temp_dir()`: Creates a temporary directory for test files
+- `create_mock_config()`: Creates a mock config file in the given directory
+- `MockServer`: A mock server for testing SSH deployment and API responses
+
+## Writing Tests
+
+### Basic Test Structure
+
+```rust
+#[test]
+#[serial] // Run tests serially to avoid conflicts
+fn test_name() {
+ // Run a command and get the output
+ let output = run_osvm_command_string(&["command", "arg1", "arg2"]);
+
+ // Check if the output contains expected text
+ assert!(output_contains(&output, "Expected text"));
+}
+```
+
+### Using assert_cmd for More Complex Assertions
+
+```rust
+#[test]
+#[serial]
+fn test_with_assert_cmd() {
+ // Use assert_cmd to run a command and make assertions about the output
+ let assert = run_osvm_command()
+ .args(&["command", "arg1", "arg2"])
+ .assert();
+
+ // Make assertions about the command output
+ assert.success()
+ .stdout(predicate::str::contains("Expected text"))
+ .stderr(predicate::str::contains("Error").not());
+}
+```
+
+### Using a Mock Server
+
+```rust
+#[test]
+#[serial]
+fn test_with_mock_server() {
+ // Create a mock server
+ let mock_server = MockServer::new();
+
+ // Set up a mock endpoint
+ let _mock = mock_server.mock_svm_list();
+
+ // Run a command that uses the mock server
+ let output = run_osvm_command_string(&["--url", &format!("http://{}", mock_server.server.host_with_port()), "svm", "list"]);
+
+ // Check if the output contains expected text
+ assert!(output_contains(&output, "Expected text"));
+}
+```
+
+### Using a Custom Config File
+
+```rust
+#[test]
+#[serial]
+fn test_with_custom_config() {
+ // Create a temporary directory and config file
+ let temp_dir = create_temp_dir();
+ let config_path = create_mock_config(&temp_dir);
+
+ // Run a command with the custom config file
+ let output = run_osvm_command_string(&["-C", config_path.to_str().unwrap(), "svm", "list"]);
+
+ // Check if the output contains expected text
+ assert!(output_contains(&output, "Expected text"));
+}
+```
+
+## Running Tests
+
+To run all e2e tests:
+
+```bash
+cargo test --test main
+```
+
+To run a specific test:
+
+```bash
+cargo test --test main test_svm_list
+```
+
+To run tests with verbose output:
+
+```bash
+cargo test --test main -- --nocapture
+```
+
+## Test Coverage
+
+The e2e tests cover the following functionality:
+
+### SVM Commands
+- `svm list`: List all available SVMs
+- `svm get`: Get detailed information about a specific SVM
+- `svm dashboard`: Launch the SVM dashboard
+
+### Node Commands
+- `nodes list`: List all managed nodes
+- `nodes list` with filters: Test filtering by network, type, and status
+- `nodes dashboard`: Launch the node dashboard
+- `nodes get`: Get detailed information about a specific node
+
+### General CLI Features
+- Custom configuration file
+- Custom RPC URL
+- Verbose output
+- No-color mode
+- Help command
+
+## Adding New Tests
+
+To add a new test:
+
+1. Decide which module the test belongs to (svm_tests, node_tests, or create a new one)
+2. Add a new test function with the `#[test]` and `#[serial]` attributes
+3. Use the utilities from `common.rs` to run commands and check output
+4. Add assertions to verify the expected behavior
+
+Example:
+
+```rust
+#[test]
+#[serial]
+fn test_new_command() {
+ let output = run_osvm_command_string(&["new", "command"]);
+ assert!(output_contains(&output, "Expected output"));
+}
+```
\ No newline at end of file
diff --git a/tests/e2e/common.rs b/tests/e2e/common.rs
new file mode 100644
index 00000000..c98ad57b
--- /dev/null
+++ b/tests/e2e/common.rs
@@ -0,0 +1,98 @@
+//! Common utilities for e2e tests
+
+use std::process::Command;
+use std::path::PathBuf;
+use std::env;
+use assert_cmd::prelude::*;
+use predicates::prelude::*;
+use tempfile::TempDir;
+use mockito::Server;
+
+/// Path to the osvm binary
+pub fn osvm_bin_path() -> PathBuf {
+ // In a real environment, this would be the path to the installed binary
+ // For testing, we'll use the debug build in the target directory
+ let mut path = env::current_dir().unwrap();
+ path.push("target");
+ path.push("debug");
+ path.push("osvm");
+ path
+}
+
+/// Run an OSVM command and return the command for further assertions
+pub fn run_osvm_command() -> Command {
+ Command::cargo_bin("osvm").expect("Failed to find osvm binary")
+}
+
+/// Run an OSVM command with arguments and return the output as a string
+pub fn run_osvm_command_string(args: &[&str]) -> String {
+ let output = run_osvm_command()
+ .args(args)
+ .output()
+ .expect("Failed to execute osvm command");
+
+ String::from_utf8_lossy(&output.stdout).to_string()
+}
+
+/// Check if the output contains the expected text
+pub fn output_contains(output: &str, expected: &str) -> bool {
+ output.contains(expected)
+}
+
+/// Create a temporary directory for test files
+pub fn create_temp_dir() -> TempDir {
+ TempDir::new().expect("Failed to create temporary directory")
+}
+
+/// Create a mock config file in the given directory
+pub fn create_mock_config(dir: &TempDir) -> PathBuf {
+ let config_path = dir.path().join("config.yml");
+ std::fs::write(&config_path, "json_rpc_url: http://localhost:8899\nkeypair_path: ~/.config/osvm/id.json\n").expect("Failed to write config file");
+ config_path
+}
+
+/// Mock server for testing SSH deployment
+pub struct MockServer {
+ pub server: Server,
+}
+
+impl MockServer {
+ /// Create a new mock server
+ pub fn new() -> Self {
+ MockServer {
+ server: mockito::Server::new(),
+ }
+ }
+
+ /// Get the connection string for the mock server
+ pub fn connection_string(&self) -> String {
+ format!("test@{}", self.server.host_with_port())
+ }
+
+ /// Mock an SVM list endpoint
+ pub fn mock_svm_list(&self) -> mockito::Mock {
+ self.server.mock("GET", "/api/svms")
+ .with_status(200)
+ .with_header("content-type", "application/json")
+ .with_body(r#"{"solana":{"name":"solana","display_name":"Solana"},"sonic":{"name":"sonic","display_name":"Sonic"}}"#)
+ .create()
+ }
+
+ /// Mock an SVM get endpoint
+ pub fn mock_svm_get(&self, svm_name: &str) -> mockito::Mock {
+ self.server.mock("GET", &format!("/api/svms/{}", svm_name))
+ .with_status(200)
+ .with_header("content-type", "application/json")
+ .with_body(format!(r#"{{"name":"{}","display_name":"{}","token_symbol":"TEST","token_price_usd":1.0}}"#, svm_name, svm_name.to_uppercase()))
+ .create()
+ }
+
+ /// Mock an SVM get endpoint with 404 response
+ pub fn mock_svm_get_not_found(&self, svm_name: &str) -> mockito::Mock {
+ self.server.mock("GET", &format!("/api/svms/{}", svm_name))
+ .with_status(404)
+ .with_header("content-type", "application/json")
+ .with_body(format!(r#"{{"error":"SVM not found: {}"}}"#, svm_name))
+ .create()
+ }
+}
\ No newline at end of file
diff --git a/tests/e2e/examples.rs b/tests/e2e/examples.rs
new file mode 100644
index 00000000..3ef02516
--- /dev/null
+++ b/tests/e2e/examples.rs
@@ -0,0 +1,67 @@
+//! Example tests to demonstrate how to write e2e tests
+
+use crate::tests::e2e::common::{run_osvm_command, run_osvm_command_string, output_contains, create_temp_dir, create_mock_config, MockServer};
+use predicates::prelude::*;
+use serial_test::serial;
+
+/// Example test that demonstrates how to test a simple command
+#[test]
+#[serial]
+fn example_test_simple_command() {
+ // Run a command and get the output as a string
+ let output = run_osvm_command_string(&["--help"]);
+
+ // Check if the output contains expected text
+ assert!(output_contains(&output, "USAGE:"));
+ assert!(output_contains(&output, "FLAGS:"));
+ assert!(output_contains(&output, "SUBCOMMANDS:"));
+}
+
+/// Example test that demonstrates how to use assert_cmd for more complex assertions
+#[test]
+#[serial]
+fn example_test_with_assert_cmd() {
+ // Use assert_cmd to run a command and make assertions about the output
+ let assert = run_osvm_command()
+ .arg("--help")
+ .assert();
+
+ // Make assertions about the command output
+ assert.success()
+ .stdout(predicate::str::contains("USAGE:"))
+ .stdout(predicate::str::contains("FLAGS:"))
+ .stdout(predicate::str::contains("SUBCOMMANDS:"));
+}
+
+/// Example test that demonstrates how to use a mock server
+#[test]
+#[serial]
+fn example_test_with_mock_server() {
+ // Create a mock server
+ let mock_server = MockServer::new();
+
+ // Set up a mock endpoint
+ let _mock = mock_server.mock_svm_list();
+
+ // Run a command that uses the mock server
+ let output = run_osvm_command_string(&["--url", &format!("http://{}", mock_server.server.host_with_port()), "svm", "list"]);
+
+ // Check if the output contains expected text
+ assert!(output_contains(&output, "Available SVMs in the chain:") ||
+ output_contains(&output, "NAME"));
+}
+
+/// Example test that demonstrates how to use a custom config file
+#[test]
+#[serial]
+fn example_test_with_custom_config() {
+ // Create a temporary directory and config file
+ let temp_dir = create_temp_dir();
+ let config_path = create_mock_config(&temp_dir);
+
+ // Run a command with the custom config file
+ let output = run_osvm_command_string(&["-C", config_path.to_str().unwrap(), "svm", "list"]);
+
+ // Check if the output contains expected text
+ assert!(output_contains(&output, "Available SVMs in the chain:"));
+}
\ No newline at end of file
diff --git a/tests/e2e/mod.rs b/tests/e2e/mod.rs
new file mode 100644
index 00000000..00a040fb
--- /dev/null
+++ b/tests/e2e/mod.rs
@@ -0,0 +1,8 @@
+//! End-to-end tests for the OSVM CLI
+//!
+//! These tests verify the CLI functionality by running commands and checking their output.
+
+mod svm_tests;
+mod node_tests;
+mod common;
+mod examples;
\ No newline at end of file
diff --git a/tests/e2e/node_tests.rs b/tests/e2e/node_tests.rs
new file mode 100644
index 00000000..3ccc32a7
--- /dev/null
+++ b/tests/e2e/node_tests.rs
@@ -0,0 +1,146 @@
+//! End-to-end tests for node-related commands
+
+use crate::tests::e2e::common::{run_osvm_command, run_osvm_command_string, output_contains, create_temp_dir, create_mock_config, MockServer};
+use predicates::prelude::*;
+use serial_test::serial;
+
+#[test]
+#[serial]
+fn test_nodes_list() {
+ let output = run_osvm_command_string(&["nodes", "list"]);
+
+ // Verify the output contains expected headers
+ assert!(output_contains(&output, "OSVM - Node Management"));
+ assert!(output_contains(&output, "Managed SVM Nodes:"));
+
+ // The output might show "No nodes are currently managed" if no nodes are configured
+ // or it might show a list of nodes if some are configured
+ assert!(
+ output_contains(&output, "No nodes are currently managed") ||
+ output_contains(&output, "ID") && output_contains(&output, "SVM") && output_contains(&output, "TYPE")
+ );
+}
+
+#[test]
+#[serial]
+fn test_nodes_list_with_filters() {
+ // Test with network filter
+ let output = run_osvm_command_string(&["nodes", "list", "--network", "mainnet"]);
+ assert!(output_contains(&output, "OSVM - Node Management"));
+
+ // Test with type filter
+ let output = run_osvm_command_string(&["nodes", "list", "--type", "validator"]);
+ assert!(output_contains(&output, "OSVM - Node Management"));
+
+ // Test with status filter
+ let output = run_osvm_command_string(&["nodes", "list", "--status", "running"]);
+ assert!(output_contains(&output, "OSVM - Node Management"));
+
+ // Test with JSON output
+ let output = run_osvm_command_string(&["nodes", "list", "--json"]);
+ // JSON output should start with a curly brace or square bracket
+ assert!(output.trim().starts_with('{') || output.trim().starts_with('[') || output.trim().is_empty());
+}
+
+#[test]
+#[serial]
+fn test_nodes_dashboard() {
+ // This test is more complex as it involves an interactive dashboard
+ // For now, we'll just verify the command doesn't immediately fail with "unknown command"
+ let assert = run_osvm_command()
+ .args(&["nodes", "dashboard"])
+ .assert();
+
+ // The dashboard might not work in a test environment, so we're just checking
+ // that the command is recognized
+ assert.stderr(predicate::str::contains("Unknown command").not());
+}
+
+#[test]
+#[serial]
+fn test_nodes_get_invalid() {
+ let assert = run_osvm_command()
+ .args(&["nodes", "get", "invalid_node_id"])
+ .assert();
+
+ // Verify the command fails with a non-zero exit code
+ assert.failure()
+ .stderr(predicate::str::contains("Node not found").or(predicate::str::contains("Error:")));
+}
+
+#[test]
+#[serial]
+fn test_examples_command() {
+ // Test the examples command
+ let output = run_osvm_command_string(&["examples"]);
+
+ // Verify the output contains examples
+ assert!(output_contains(&output, "OSVM CLI Examples") ||
+ output_contains(&output, "Available SVMs in the chain:") ||
+ output_contains(&output, "Basic Commands"));
+
+ // Test examples with category filter
+ let output = run_osvm_command_string(&["examples", "--category", "basic"]);
+ assert!(output_contains(&output, "Basic Commands"));
+
+ // Test listing categories
+ let output = run_osvm_command_string(&["examples", "--list-categories"]);
+ assert!(output_contains(&output, "Available example categories:"));
+}
+
+#[test]
+#[serial]
+fn test_verbose_output() {
+ // Test with verbose flag
+ let output = run_osvm_command_string(&["--verbose", "svm", "list"]);
+
+ // Verbose output should include JSON RPC URL or other verbose information
+ assert!(output_contains(&output, "JSON RPC URL:") ||
+ output_contains(&output, "Available SVMs in the chain:"));
+
+ // Test with very verbose flag
+ let output = run_osvm_command_string(&["-vv", "svm", "list"]);
+
+ // Very verbose output should include keypair info or other very verbose information
+ assert!(output_contains(&output, "Using keypair:") ||
+ output_contains(&output, "Available SVMs in the chain:"));
+}
+
+#[test]
+#[serial]
+fn test_no_color_flag() {
+ // Test with no-color flag
+ let output = run_osvm_command_string(&["--no-color", "svm", "list"]);
+
+ // Output should still contain the expected text, but without color codes
+ assert!(output_contains(&output, "Available SVMs in the chain:"));
+}
+
+#[test]
+#[serial]
+fn test_with_custom_config() {
+ // Create a temporary directory and config file
+ let temp_dir = create_temp_dir();
+ let config_path = create_mock_config(&temp_dir);
+
+ // Run the command with the config file
+ let output = run_osvm_command_string(&["-C", config_path.to_str().unwrap(), "nodes", "list"]);
+
+ // Verify the output contains expected headers
+ assert!(output_contains(&output, "OSVM - Node Management"));
+}
+
+#[test]
+#[serial]
+fn test_help_command() {
+ // Test the help command
+ let assert = run_osvm_command()
+ .arg("--help")
+ .assert();
+
+ // Verify the output contains help information
+ assert.success()
+ .stdout(predicate::str::contains("USAGE:"))
+ .stdout(predicate::str::contains("FLAGS:"))
+ .stdout(predicate::str::contains("SUBCOMMANDS:"));
+}
\ No newline at end of file
diff --git a/tests/e2e/svm_tests.rs b/tests/e2e/svm_tests.rs
new file mode 100644
index 00000000..01bdef64
--- /dev/null
+++ b/tests/e2e/svm_tests.rs
@@ -0,0 +1,92 @@
+//! End-to-end tests for SVM-related commands
+
+use crate::tests::e2e::common::{run_osvm_command, run_osvm_command_string, output_contains};
+
+#[test]
+fn test_svm_list() {
+ let output = run_osvm_command_string(&["svm", "list"]);
+
+ // Verify the output contains expected headers
+ assert!(output_contains(&output, "Available SVMs in the chain:"));
+ assert!(output_contains(&output, "NAME"));
+ assert!(output_contains(&output, "DISPLAY NAME"));
+ assert!(output_contains(&output, "TOKEN"));
+
+ // Verify the output contains some expected SVMs
+ assert!(output_contains(&output, "solana"));
+ assert!(output_contains(&output, "sonic"));
+ assert!(output_contains(&output, "opensvm"));
+}
+
+#[test]
+fn test_svm_get_solana() {
+ let output = run_osvm_command_string(&["svm", "get", "solana"]);
+
+ // Verify the output contains expected Solana information
+ assert!(output_contains(&output, "SVM Information: Solana"));
+ assert!(output_contains(&output, "Token: SOL"));
+ assert!(output_contains(&output, "Website: https://solana.com"));
+
+ // Verify network information is present
+ assert!(output_contains(&output, "MAINNET Network:"));
+ assert!(output_contains(&output, "TESTNET Network:"));
+ assert!(output_contains(&output, "DEVNET Network:"));
+
+ // Verify system requirements are present
+ assert!(output_contains(&output, "Validator Requirements:"));
+ assert!(output_contains(&output, "RPC Node Requirements:"));
+}
+
+#[test]
+fn test_svm_get_invalid() {
+ let output = run_osvm_command(&["svm", "get", "invalid_svm"]);
+
+ // Verify the command fails with a non-zero exit code
+ assert!(!output.status.success());
+
+ // Verify the error message
+ let error = String::from_utf8_lossy(&output.stderr);
+ assert!(error.contains("SVM not found") || error.contains("Error:"));
+}
+
+#[test]
+#[serial]
+fn test_svm_dashboard() {
+ // This test is more complex as it involves an interactive dashboard
+ // For now, we'll just verify the command doesn't immediately fail with "unknown command"
+ let assert = run_osvm_command()
+ .args(&["svm", "dashboard"])
+ .assert();
+
+ // The dashboard might not work in a test environment, so we're just checking
+ // that the command is recognized
+ assert.stderr(predicate::str::contains("Unknown command").not());
+}
+
+#[test]
+#[serial]
+fn test_svm_with_config_file() {
+ // Create a temporary directory and config file
+ let temp_dir = create_temp_dir();
+ let config_path = create_mock_config(&temp_dir);
+
+ // Run the command with the config file
+ let output = run_osvm_command_string(&["-C", config_path.to_str().unwrap(), "svm", "list"]);
+
+ // Verify the output contains expected headers
+ assert!(output_contains(&output, "Available SVMs in the chain:"));
+}
+
+#[test]
+#[serial]
+fn test_svm_with_url() {
+ // Create a mock server
+ let mock_server = MockServer::new();
+ let _mock = mock_server.mock_svm_list();
+
+ // Run the command with a custom URL
+ let output = run_osvm_command_string(&["--url", &format!("http://{}", mock_server.server.host_with_port()), "svm", "list"]);
+
+ // Verify the output contains expected headers
+ assert!(output_contains(&output, "Available SVMs in the chain:"));
+}
\ No newline at end of file
diff --git a/tests/main.rs b/tests/main.rs
new file mode 100644
index 00000000..e68f9e06
--- /dev/null
+++ b/tests/main.rs
@@ -0,0 +1,4 @@
+//! Main test file for OSVM CLI
+
+// Include the test modules
+mod e2e;
\ No newline at end of file
diff --git a/tests/mod.rs b/tests/mod.rs
new file mode 100644
index 00000000..9e61aab6
--- /dev/null
+++ b/tests/mod.rs
@@ -0,0 +1,3 @@
+//! Test modules for OSVM CLI
+
+pub mod e2e;
\ No newline at end of file