QuasarSVM (Rust)
Testing Quasar programs with the Rust-based QuasarSVM test harness.
QuasarSVM is a Solana VM harness for testing Quasar programs from Rust, without running a validator.
Setup
Add these dev-dependencies to your Cargo.toml:
[dev-dependencies]
quasar-svm = { git = "https://github.com/blueshift-gg/quasar-svm" }
solana-account = { version = "3.4.0" }
solana-address = { version = "2.2.0", features = ["decode"] }
solana-instruction = { version = "3.2.0", features = ["bincode"] }
solana-pubkey = { version = "4.1.0" }Or run quasar init and select Rust when prompted for a testing framework.
Your lib.rs should include the test module:
#[cfg(test)]
mod tests;Loading programs
Create a QuasarSvm instance and load your compiled .so binary:
use quasar_svm::{Account, ExecutionStatus, Instruction, Pubkey, QuasarSvm};
use solana_address::Address;
fn setup() -> QuasarSvm {
let elf = include_bytes!("../target/deploy/my_program.so");
QuasarSvm::new()
.with_program(&Pubkey::from(crate::ID), elf)
}include_bytes! embeds the program binary at compile time. Run quasar build before running tests so the .so file exists.
Sending transactions
Build instructions using the generated client types, then call process_transaction:
use my_program_client::InitializeInstruction;
#[test]
fn test_initialize() {
let mut svm = setup();
let payer = Pubkey::new_unique();
let system_program = quasar_svm::system_program::ID;
let instruction: Instruction = InitializeInstruction {
payer: Address::from(payer.to_bytes()),
system_program: Address::from(system_program.to_bytes()),
}
.into();
let result = svm.process_transaction(
&[instruction],
&[(payer, Account::new(10_000_000_000, 0, &system_program))],
);
match result.status() {
ExecutionStatus::Success => {},
ExecutionStatus::Err(e) => panic!("initialize failed: {e}\n{:?}", result.logs),
}
}The second argument seeds the VM with initial account state as (Pubkey, Account) tuples. Unlisted accounts default to empty system-owned accounts.
Inspecting account state
The result contains updated account state:
let user_after = result.resulting_accounts[0].1.lamports;
let vault_after = result.resulting_accounts[1].1.lamports;
assert_eq!(user_after, 10_000_000_000 - deposit_amount);
assert_eq!(vault_after, deposit_amount);Accounts in resulting_accounts follow the same order as the instruction's account list.
Multi-step transactions
Feed resulting accounts from one transaction into the next:
// Step 1: Deposit
let deposit_result = svm.process_transaction(
&[deposit_ix],
&[(user, user_account), (vault, vault_account)],
);
assert!(matches!(deposit_result.status(), ExecutionStatus::Success));
let user_after_deposit = deposit_result.resulting_accounts[0].1.clone();
let vault_after_deposit = deposit_result.resulting_accounts[1].1.clone();
// Step 2: Withdraw using updated state
let withdraw_result = svm.process_transaction(
&[withdraw_ix],
&[(user, user_after_deposit), (vault, vault_after_deposit)],
);
assert!(matches!(withdraw_result.status(), ExecutionStatus::Success));Compute budget testing
Assert CU limits or track regressions:
let result = svm.process_transaction(&[instruction], &accounts);
println!("CU consumed: {}", result.compute_units_consumed);
assert!(result.compute_units_consumed < 5_000, "CU budget exceeded");Full example: Vault program
A complete test file for the vault example program:
extern crate std;
use quasar_svm::{Account, ExecutionStatus, Instruction, Pubkey, QuasarSvm};
use solana_address::Address;
use my_vault_client::{DepositInstruction, WithdrawInstruction};
fn setup() -> QuasarSvm {
let elf = include_bytes!("../target/deploy/my_vault.so");
QuasarSvm::new()
.with_program(&Pubkey::from(crate::ID), elf)
}
#[test]
fn test_deposit() {
let mut svm = setup();
let user = Pubkey::new_unique();
let system_program = quasar_svm::system_program::ID;
let (vault, _bump) = Address::find_program_address(
&[b"vault", Address::from(user.to_bytes()).as_ref()],
&crate::ID,
);
let instruction: Instruction = DepositInstruction {
user: Address::from(user.to_bytes()),
vault,
system_program: Address::from(system_program.to_bytes()),
amount: 1_000_000_000,
}
.into();
let result = svm.process_transaction(
&[instruction],
&[(user, Account::new(10_000_000_000, 0, &system_program))],
);
match result.status() {
ExecutionStatus::Success => {},
ExecutionStatus::Err(e) => panic!("deposit failed: {e}"),
}
}
#[test]
fn test_withdraw() {
let mut svm = setup();
let user = Pubkey::new_unique();
let system_program = quasar_svm::system_program::ID;
let (vault, _bump) = Address::find_program_address(
&[b"vault", Address::from(user.to_bytes()).as_ref()],
&crate::ID,
);
// Deposit first
let deposit_ix: Instruction = DepositInstruction {
user: Address::from(user.to_bytes()),
vault,
system_program: Address::from(system_program.to_bytes()),
amount: 1_000_000_000,
}
.into();
let deposit_result = svm.process_transaction(
&[deposit_ix],
&[(user, Account::new(10_000_000_000, 0, &system_program))],
);
let user_after = deposit_result.resulting_accounts[0].1.clone();
let vault_after = deposit_result.resulting_accounts[1].1.clone();
// Now withdraw half
let withdraw_ix: Instruction = WithdrawInstruction {
user: Address::from(user.to_bytes()),
vault,
amount: 500_000_000,
}
.into();
let result = svm.process_transaction(
&[withdraw_ix],
&[(user, user_after), (vault, vault_after)],
);
match result.status() {
ExecutionStatus::Success => {},
ExecutionStatus::Err(e) => panic!("withdraw failed: {e}"),
}
}Running tests
quasar testBuilds the program, generates the client, and runs cargo test. Filter by test name:
quasar test --filter depositOr run tests directly with cargo:
cargo test -- --nocapture--nocapture shows println! output (e.g., CU consumption numbers).
