Advanced Functionalities
Mollusk provides flexible initialization options to accommodate different testing scenarios. You can create instances pre-loaded with your program or start with a minimal environment and add components as needed.
When testing a specific program, initialize Mollusk with your program pre-loaded:
use mollusk_svm::Mollusk;
use solana_sdk::pubkey::Pubkey;
const ID: Pubkey = solana_sdk::pubkey!("22222222222222222222222222222222222222222222");
#[test]
fn test() {
let mollusk = Mollusk::new(&ID, "target/deploy/program.so");
}
This approach automatically loads your compiled program and makes it available for testing, streamlining the setup process for program-specific test suites.
For broader testing scenarios or when you need to add programs dynamically, start with the default instance:
use mollusk_svm::Mollusk;
#[test]
fn test() {
// System Program, ...
let mollusk = Mollusk::default();
}
The default instance includes essential builtin programs like the System Program, providing a foundation for most Solana operations without the overhead of programs you don't need.
When your tests require the System Program, Mollusk provides a convenient helper to generate the necessary account references:
let (system_program, system_program_account) = keyed_account_for_system_program();
To replicate program loading functionality or load custom programs that are not present by default, you can use these helpers:
use mollusk_svm::Mollusk;
use mollusk_svm::program::create_program_account_loader_v3;
#[test]
fn test() {
let mut mollusk = Mollusk::default();
// Get the account that you need
let program = &ID; // ID of the program we're trying to load into mollusk
let program_account = create_program_account_loader_v3(&ID);
// Load the program into your mollusk instance
mollusk.add_program(
&ID,
"src/programs/program",
&mollusk_svm::program::loader_keys::LOADER_V3
);
}
Token Program
Mollusk's token program helpers significantly simplify testing scenarios involving SPL tokens. The mollusk-svm-programs-token
crate provides pre-configured support for Token
, Token2022
, and Associated Token
programs.
Program Accounts
After including the token helper crate, add the specific token programs your tests require:
use mollusk_svm::Mollusk;
#[test]
fn test() {
let mut mollusk = Mollusk::default();
// Add the SPL Token Program
mollusk_svm_programs_token::token::add_program(&mut mollusk);
// Add the Token2022 Program
mollusk_svm_programs_token::token2022::add_program(&mut mollusk);
// Add the Associated Token Program
mollusk_svm_programs_token::associated_token::add_program(&mut mollusk);
}
And create the account references needed for your test scenarios:
// SPL Token Program
let (token_program, token_program_account) =
mollusk_svm_programs_token::token::keyed_account();
// Token2022 Program
let (token2022_program, token2022_program_account) =
mollusk_svm_programs_token::token2022::keyed_account();
// Associated Token Program
let (associated_token_program, associated_token_program_account) =
mollusk_svm_programs_token::associated_token::keyed_account();
These helpers ensure that token-related tests have access to the correct program accounts with proper configuration, enabling comprehensive testing of token operations without manual program setup.
State Accounts
During our tests, we might need to have a Mint
, Token
or Associated Token
account that has already been initialized. Luckily for us, Mollusk has some handy helpers for us.
To create a Mint
account we can use the create_account_for_mint()
function like this:
use spl_token::state::Mint;
use mollusk_svm_programs_token::token::create_account_for_mint;
let mint_data = Mint {
mint_authority: Pubkey::new_unique(),
supply: 10_000_000_000,
decimals: 6,
is_initialized: true,
freeze_authority: None,
};
let mint_account = create_account_for_mint(mint_data)
To create a Token
account we can use the create_account_for_token_account()
function like this:
use spl_token::state::{TokenAccount, AccountState};
use mollusk_svm_programs_token::token::create_account_for_token_account;
let token_data = TokenAccount {
mint: Pubkey::new_unique(),
owner: Pubkey::new_unique(),
amount: 1_000_000,
delegate: None,
state: AccountState::Initialized,
is_native: None,
delegated_amount: 0,
close_authority: None,
};
let token_account = create_account_for_token_account(token_data)
To create an Associated Token
account we can use the create_account_for_associated_token_account()
function like this:
use spl_token::state::{TokenAccount, AccountState};
use mollusk_svm_programs_token::associated_token::pub fn create_account_for_associated_token_account;
let token_data = TokenAccount {
mint: Pubkey::new_unique(),
owner: Pubkey::new_unique(),
amount: 1_000_000,
delegate: None,
state: AccountState::Initialized,
is_native: None,
delegated_amount: 0,
close_authority: None,
};
let associated_token_account = create_account_for_associated_token_account(token_data)
Benchmarking Compute Units
Mollusk includes a dedicated compute unit benchmarking system that enables precise measurement and tracking of your program's computational efficiency. The MolluskComputeUnitBencher
provides a streamlined API for creating comprehensive benchmarks that monitor compute unit consumption across different instruction scenarios.
This benchmarking system is particularly valuable for performance optimization, as it generates detailed reports showing both current compute unit usage and deltas from previous runs.
This allows you to immediately see the impact of code changes on your program's efficiency, helping you optimize critical performance bottlenecks.
The bencher integrates seamlessly with your existing Mollusk test setup:
use {
mollusk_svm_bencher::MolluskComputeUnitBencher,
mollusk_svm::Mollusk,
/* ... */
};
// Optionally disable logging.
solana_logger::setup_with("");
/* Instruction & accounts setup ... */
let mollusk = Mollusk::new(&program_id, "my_program");
MolluskComputeUnitBencher::new(mollusk)
.bench(("bench0", &instruction0, &accounts0))
.bench(("bench1", &instruction1, &accounts1))
.bench(("bench2", &instruction2, &accounts2))
.bench(("bench3", &instruction3, &accounts3))
.must_pass(true)
.out_dir("../target/benches")
.execute();
Configuration Options
The bencher provides several configuration options:
must_pass(true)
: Triggers a panic if any benchmark fails to execute successfully, ensuring your benchmarks remain valid as code changesout_dir("../target/benches")
: Specifies where the markdown report will be generated, allowing integration with CI/CD systems and documentation workflows
Integration with Cargo
To run benchmarks using cargo bench
, add a benchmark configuration to your Cargo.toml
:
[[bench]]
name = "compute_units"
harness = false
Benchmark Reports
The bencher generates markdown reports that provide both current performance metrics and historical comparison:
| Name | CUs | Delta |
|--------|-------|--------|
| bench0 | 450 | -- |
| bench1 | 579 | -129 |
| bench2 | 1,204 | +754 |
| bench3 | 2,811 | +2,361 |
The report format includes:
- Name: The benchmark identifier you specified
- CUs: Current compute unit consumption for this scenario
- Delta: Change from the previous benchmark run (positive indicates increased usage, negative indicates optimization)
Custom Syscalls
Mollusk supports the creation and testing of custom syscalls, enabling you to extend the Solana Virtual Machine with specialized functionality for testing scenarios.
This capability is particularly valuable for testing the creation of new Syscall that can be added through SIMD by simulating specific runtime behaviors, or creating controlled environments for testing.
Defining Custom Syscalls
Custom syscalls are defined using the declare_builtin_function!
macro, which creates a syscall that can be registered with Mollusk's runtime environment:
use {
mollusk_svm::{result::Check, Mollusk},
solana_instruction::Instruction,
solana_program_runtime::{
invoke_context::InvokeContext,
solana_sbpf::{declare_builtin_function, memory_region::MemoryMapping},
},
solana_pubkey::Pubkey,
};
declare_builtin_function!(
/// A custom syscall to burn compute units for testing
SyscallBurnCus,
fn rust(
invoke_context: &mut InvokeContext,
to_burn: u64,
_arg2: u64,
_arg3: u64,
_arg4: u64,
_arg5: u64,
_memory_mapping: &mut MemoryMapping,
) -> Result<u64, Box<dyn std::error::Error>> {
// Consume the specified number of compute units
invoke_context.consume_checked(to_burn)?;
Ok(0)
}
);
The syscall function signature follows a specific pattern:
invoke_context
: Provides access to the execution context and runtime state- Arguments 1-5: Up to five 64-bit arguments can be passed from the program
- memory_mapping: Provides access to the program's memory space
- Return value: A
Result<u64, Box<dyn std::error::Error>>
indicating success or failure
Registering Custom Syscalls
Once defined, custom syscalls must be registered with Mollusk's program runtime environment before they can be used:
#[test]
fn test_custom_syscall() {
std::env::set_var("SBF_OUT_DIR", "../target/deploy");
let program_id = Pubkey::new_unique();
let mollusk = {
let mut mollusk = Mollusk::default();
// Register the custom syscall with a specific name
mollusk
.program_cache
.program_runtime_environment
.register_function("sol_burn_cus", SyscallBurnCus::vm)
.unwrap();
// Add your program that uses the custom syscall
mollusk.add_program(
&program_id,
"test_program_custom_syscall",
&mollusk_svm::program::loader_keys::LOADER_V3,
);
mollusk
};
}
The syscall is registered with a name ("sol_burn_cus" in this example) that your program can reference when making the syscall.
Testing Custom Syscall Behavior
Custom syscalls can be tested like any other program functionality, with the added benefit of precise control over their behavior:
fn instruction_burn_cus(program_id: &Pubkey, to_burn: u64) -> Instruction {
Instruction::new_with_bytes(*program_id, &to_burn.to_le_bytes(), vec![])
}
#[test]
fn test_custom_syscall() {
// ... mollusk setup ...
// Establish baseline compute unit usage
let base_cus = mollusk
.process_and_validate_instruction(
&instruction_burn_cus(&program_id, 0),
&[],
&[Check::success()],
)
.compute_units_consumed;
// Test different compute unit consumption levels
for to_burn in [100, 1_000, 10_000] {
mollusk.process_and_validate_instruction(
&instruction_burn_cus(&program_id, to_burn),
&[],
&[
Check::success(),
Check::compute_units(base_cus + to_burn), // Verify exact CU consumption
],
);
}
}
Configuration Methods
Mollusk provides comprehensive configuration options that allow you to customize the execution environment to match specific testing requirements as we can see from Mollusk Context
:
/// Instruction context fixture.
pub struct Context {
/// The compute budget to use for the simulation.
pub compute_budget: ComputeBudget,
/// The feature set to use for the simulation.
pub feature_set: FeatureSet,
/// The runtime sysvars to use for the simulation.
pub sysvars: Sysvars,
/// The program ID of the program being invoked.
pub program_id: Pubkey,
/// Accounts to pass to the instruction.
pub instruction_accounts: Vec<AccountMeta>,
/// The instruction data.
pub instruction_data: Vec<u8>,
/// Input accounts with state.
pub accounts: Vec<(Pubkey, Account)>,
}
These configuration methods enable precise control over compute budgets, feature availability, and system variables, making it possible to test programs under various runtime conditions.
Basic Configuration Setup
use mollusk_svm::Mollusk;
use solana_sdk::feature_set::FeatureSet;
#[test]
fn test() {
let mut mollusk = Mollusk::new(&program_id, "path/to/program.so");
// Configure compute budget for performance testing
mollusk.set_compute_budget(200_000);
// Configure feature set to enable/disable specific Solana features
mollusk.set_feature_set(FeatureSet::all_enabled());
// Sysvars are handled automatically but can be customized if needed
}
The compute budget determines how many compute units are available for program execution. This is crucial for testing programs that approach or exceed compute limits:
// Test with standard compute budget
mollusk.set_compute_budget(200_000);
Solana's feature set controls which blockchain features are active during program execution. Mollusk allows you to configure these features to test compatibility across different network states:
use solana_sdk::feature_set::FeatureSet;
// Enable all features (latest functionality)
mollusk.set_feature_set(FeatureSet::all_enabled());
// Use default feature set (production-like environment)
mollusk.set_feature_set(FeatureSet::default());
For a comprehensive list of available features, consult the agave-feature-set
crate documentation, which details all configurable blockchain features and their implications.
Mollusk provides access to all system variables (sysvars) that programs can query during execution. While these are automatically configured with reasonable defaults, you can customize them for specific testing scenarios:
/// Mollusk sysvars wrapper for easy manipulation
pub struct Sysvars {
pub clock: Clock, // Current slot, epoch, and timestamp
pub epoch_rewards: EpochRewards, // Epoch reward distribution info
pub epoch_schedule: EpochSchedule, // Epoch timing and slot configuration
pub last_restart_slot: LastRestartSlot, // Last validator restart information
pub rent: Rent, // Rent calculation parameters
pub slot_hashes: SlotHashes, // Recent slot hash history
pub stake_history: StakeHistory, // Historical stake activation data
}
You can customize specific sysvars to test time-dependent logic, rent calculations, or other system-dependent behaviors or use some of the helpers:
#[test]
fn test() {
let mut mollusk = Mollusk::new(&program_id, "path/to/program.so");
// Customize clock for time-based testing
mollusk.sysvars.clock.epoch = 10;
mollusk.sysvars.clock.unix_timestamp = 1234567890;
// Jump to Slot 1000
warp_to_slot(&mut Mollusk, 1000)
}