Mollusk
Mollusk adalah alat pengujian ringan untuk menguji program Solana. Mollusk menyediakan antarmuka sederhana untuk menguji instruksi program Solana dalam lingkungan Solana Virtual Machine (SVM) yang diminimalkan. Semua akun pengujian harus didefinisikan secara eksplisit, memastikan tes yang deterministik dan dapat diulang.
Instalasi
Tambahkan mollusk-svm
sebagai dependensi di Cargo.toml
:
$cargo add mollusk-svm --dev
[dev-dependencies]mollusk-svm = "0.5"
Untuk melakukan benchmark penggunaan unit komputasi, tambahkan
mollusk-svm-bencher
sebagai dependensi di Cargo.toml
:
$cargo add mollusk-svm-bencher --dev
[dev-dependencies]mollusk-svm-bencher = "0.5"
Untuk menggunakan Token Program, Token2022 Program (Token Extensions), dan
Associated Token Program untuk pengujian dengan Mollusk, tambahkan
mollusk-svm-programs-token
sebagai dependensi di Cargo.toml
:
$cargo add mollusk-svm-programs-token --dev
[dev-dependencies]mollusk-svm-programs-token = "0.5"
Mollusk SVM
Contoh berikut menunjukkan pengaturan minimal untuk menguji program Solana dasar menggunakan Mollusk.
Program Hello World
Contoh ini mendemonstrasikan cara menguji program Solana dasar menggunakan Mollusk. Program ini hanya mencetak "Hello, world!" ke log program saat dipanggil.
Menjalankan cargo build-sbf
menghasilkan program terkompilasi di
/target/deploy/<program_name>.so
.
use solana_program::{account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, msg, pubkey::Pubkey,};entrypoint!(process_instruction);pub fn process_instruction(_program_id: &Pubkey,_accounts: &[AccountInfo],_instruction_data: &[u8],) -> ProgramResult {msg!("Hello, world!");Ok(())}#[cfg(test)]mod tests {use mollusk_svm::{result::Check, Mollusk};use solana_sdk::{instruction::Instruction, pubkey::Pubkey};#[test]fn test_hello_world() {let program_id = Pubkey::new_unique();let mollusk = Mollusk::new(&program_id, "target/deploy/hello_world");let instruction = Instruction::new_with_bytes(program_id, &[], vec![]);mollusk.process_and_validate_instruction(&instruction, &[], &[Check::success()]);}}
Untuk menguji program Solana dengan Mollusk:
- Buat instance
Mollusk
- Inisialisasi Mollusk dengan program ID dan jalur ke program terkompilasi (file.so
) - Bangun instruksi - Buat instruksi untuk memanggil program
- Proses dan validasi - Proses instruksi menggunakan Mollusk dan validasi hasilnya
#[cfg(test)]mod tests {use mollusk_svm::{result::Check, Mollusk};use solana_sdk::{instruction::Instruction, pubkey::Pubkey};#[test]fn test_hello_world() {let program_id = Pubkey::new_unique();let mollusk = Mollusk::new(&program_id, "target/deploy/hello_world");let instruction = Instruction::new_with_bytes(program_id, &[], vec![]);mollusk.process_and_validate_instruction(&instruction, &[], &[Check::success()]);}}
Untuk menjalankan pengujian, jalankan cargo test
.
Ketika pengujian berjalan dengan sukses, Anda akan melihat output yang mirip dengan berikut ini:
running 1 test[2025-09-22T19:25:50.427685000Z DEBUG solana_runtime::message_processor::stable_log] Program 11157t3sqMV725NVRLrVQbAu98Jjfk1uCKehJnXXQs invoke [1][2025-09-22T19:25:50.429669000Z DEBUG solana_runtime::message_processor::stable_log] Program log: Hello, world![2025-09-22T19:25:50.429690000Z DEBUG solana_runtime::message_processor::stable_log] Program 11157t3sqMV725NVRLrVQbAu98Jjfk1uCKehJnXXQs consumed 211 of 1400000 compute units[2025-09-22T19:25:50.429726000Z DEBUG solana_runtime::message_processor::stable_log] Program 11157t3sqMV725NVRLrVQbAu98Jjfk1uCKehJnXXQs successtest tests::test_hello_world ... oktest result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02sDoc-tests hello_world
Struct
Mollusk
menyediakan antarmuka sederhana untuk menguji program Solana. Semua bidang dapat
dimanipulasi melalui beberapa metode pembantu, tetapi pengguna juga dapat
langsung mengakses dan memodifikasinya jika mereka menginginkan kontrol yang
lebih besar.
Untuk menginisialisasi Mollusk dengan instance default, gunakan metode
Mollusk::default
.
// Default instance with no custom programslet mollusk = Mollusk::default();
Untuk menginisialisasi Mollusk dengan program tertentu, gunakan metode
Mollusk::new
.
// Initialize Mollusk with a specific program from a file pathlet program_id = Pubkey::new_unique();let mollusk = Mollusk::new(&program_id, "target/deploy/my_program");
Untuk menambahkan program ke Mollusk, gunakan metode Mollusk::add_program
.
let mollusk = Mollusk::default();let program_id = Pubkey::new_unique();// Add a program to Molluskmollusk.add_program(&program_id,"target/deploy/my_program",&bpf_loader_upgradeable::id(),);
Saat menyediakan jalur file, jangan sertakan ekstensi .so
. Misalnya,
"path/to/my_program"
adalah benar, tetapi "path/to/my_program.so"
tidak
benar.
Memproses instruksi
Mollusk menyediakan empat metode utama untuk memproses instruksi:
Metode | Deskripsi |
---|---|
process_instruction | Memproses instruksi dan mengembalikan hasilnya. |
process_and_validate_instruction | Memproses instruksi dan melakukan serangkaian pemeriksaan pada hasilnya, panik jika ada pemeriksaan yang gagal. |
process_instruction_chain | Memproses beberapa instruksi dan mengembalikan hasilnya. |
process_and_validate_instruction_chain | Memproses beberapa instruksi dan melakukan serangkaian pemeriksaan pada setiap hasil, panik jika ada pemeriksaan yang gagal. |
InstructionResult
berisi detail dari instruksi yang diproses.
Instruksi tunggal
Gunakan metode process_instruction
untuk memproses instruksi tunggal tanpa
pemeriksaan pada hasilnya. Anda dapat memvalidasi hasil secara manual setelah
pemrosesan.
pub fn process_instruction(&self,instruction: &Instruction,accounts: &[(Pubkey, Account)],) -> InstructionResult
Contoh berikut memproses instruksi transfer SOL tanpa pemeriksaan validasi.
Contoh di bawah ini menjalankan Mollusk dalam fungsi main
untuk tujuan
demonstrasi. Dalam praktiknya, Anda biasanya akan menggunakan Mollusk dalam
modul pengujian yang diberi anotasi dengan atribut #[test]
.
use {mollusk_svm::Mollusk,solana_sdk::{account::Account, pubkey::Pubkey, system_instruction, system_program},};fn main() {// Initialize Mollusklet mollusk = Mollusk::default();// Set up accountslet sender = Pubkey::new_unique();let recipient = Pubkey::new_unique();let initial_lamports = 1_000_000;let transfer_amount = 250_000;// Create transfer instructionlet instruction = system_instruction::transfer(&sender, &recipient, transfer_amount);// Define initial account stateslet accounts = vec![(sender,Account {lamports: initial_lamports,data: vec![],owner: system_program::id(),executable: false,rent_epoch: 0,},),(recipient,Account {lamports: 0,data: vec![],owner: system_program::id(),executable: false,rent_epoch: 0,},),];// Process the instructionlet result = mollusk.process_instruction(&instruction, &accounts);println!("{:#?}", result);// Check the resultassert!(result.program_result.is_ok());assert_eq!(result.get_account(&sender).unwrap().lamports, 750_000);assert_eq!(result.get_account(&recipient).unwrap().lamports, 250_000);}
Instruksi Tunggal dengan Pemeriksaan
Gunakan metode process_and_validate_instruction
untuk memproses instruksi
tunggal dengan pemeriksaan validasi. Metode ini akan menimbulkan panic jika ada
pemeriksaan yang gagal.
pub fn process_and_validate_instruction(&self,instruction: &Instruction,accounts: &[(Pubkey, Account)],checks: &[Check],) -> InstructionResult
Contoh berikut memproses instruksi transfer SOL dengan pemeriksaan validasi.
use {mollusk_svm::{Mollusk, result::Check},solana_sdk::{account::Account, pubkey::Pubkey, system_instruction, system_program},};fn main() {let mollusk = Mollusk::default();let sender = Pubkey::new_unique();let recipient = Pubkey::new_unique();let initial_lamports = 1_000_000;let transfer_amount = 250_000;let instruction = system_instruction::transfer(&sender, &recipient, transfer_amount);let accounts = vec![(sender,Account {lamports: initial_lamports,data: vec![],owner: system_program::id(),executable: false,rent_epoch: 0,},),(recipient,Account {lamports: 0,data: vec![],owner: system_program::id(),executable: false,rent_epoch: 0,},),];// Define validation checkslet checks = vec![Check::success(),Check::account(&sender).lamports(750_000).build(),Check::account(&recipient).lamports(250_000).build(),];// Process and validate (will panic if any check fails)let result = mollusk.process_and_validate_instruction(&instruction, &accounts, &checks);println!("{:#?}", result);}
Beberapa Instruksi
Gunakan metode process_instruction_chain
untuk memproses beberapa instruksi
secara berurutan tanpa pemeriksaan validasi.
pub fn process_instruction_chain(&self,instructions: &[Instruction],accounts: &[(Pubkey, Account)],) -> InstructionResult
Contoh berikut memproses dua instruksi transfer SOL tanpa pemeriksaan validasi.
use {mollusk_svm::Mollusk,solana_sdk::{account::Account, pubkey::Pubkey, system_instruction, system_program},};fn main() {let mollusk = Mollusk::default();// Set up accountslet alice = Pubkey::new_unique();let bob = Pubkey::new_unique();let charlie = Pubkey::new_unique();let initial_lamports = 1_000_000;// Create chain of transferslet instructions = vec![system_instruction::transfer(&alice, &bob, 300_000), // Alice -> Bobsystem_instruction::transfer(&bob, &charlie, 100_000), // Bob -> Charlie];let accounts = vec![(alice,Account {lamports: initial_lamports,data: vec![],owner: system_program::id(),executable: false,rent_epoch: 0,},),(bob,Account {lamports: 0,data: vec![],owner: system_program::id(),executable: false,rent_epoch: 0,},),(charlie,Account {lamports: 0,data: vec![],owner: system_program::id(),executable: false,rent_epoch: 0,},),];// Process the instruction chainlet result = mollusk.process_instruction_chain(&instructions, &accounts);println!("{:#?}", result);// Final balances: Alice=700K, Bob=200K, Charlie=100Kassert_eq!(result.get_account(&alice).unwrap().lamports, 700_000);assert_eq!(result.get_account(&bob).unwrap().lamports, 200_000);assert_eq!(result.get_account(&charlie).unwrap().lamports, 100_000);}
Beberapa Instruksi dengan Pemeriksaan
Gunakan metode process_and_validate_instruction_chain
untuk memproses beberapa
instruksi dengan pemeriksaan validasi setelah setiap instruksi. Setiap instruksi
memiliki serangkaian pemeriksaan sendiri yang harus lulus.
pub fn process_and_validate_instruction_chain(&self,instructions: &[(&Instruction, &[Check])],accounts: &[(Pubkey, Account)],) -> InstructionResult
Contoh berikut memproses rangkaian dua instruksi transfer SOL dengan pemeriksaan validasi setelah setiap instruksi.
use {mollusk_svm::{result::Check, Mollusk},solana_sdk::{account::Account, pubkey::Pubkey, system_instruction, system_program},};fn main() {let mollusk = Mollusk::default();// Create accountslet alice = Pubkey::new_unique();let bob = Pubkey::new_unique();let charlie = Pubkey::new_unique();let initial_lamports = 1_000_000;// Create transfer instructionslet transfer1 = system_instruction::transfer(&alice, &bob, 300_000);let transfer2 = system_instruction::transfer(&bob, &charlie, 100_000);// Initial accountslet accounts = vec![(alice,Account {lamports: initial_lamports,data: vec![],owner: system_program::id(),executable: false,rent_epoch: 0,},),(bob,Account {lamports: 0,data: vec![],owner: system_program::id(),executable: false,rent_epoch: 0,},),(charlie,Account {lamports: 0,data: vec![],owner: system_program::id(),executable: false,rent_epoch: 0,},),];// Define checks for each instructionlet checks_after_transfer1 = vec![Check::success(),Check::account(&alice).lamports(700_000) // 1M - 300K.build(),Check::account(&bob).lamports(300_000) // 0 + 300K.build(),Check::account(&charlie).lamports(0) // Unchanged.build(),];let checks_after_transfer2 = vec![Check::success(),Check::account(&alice).lamports(700_000) // Unchanged from previous.build(),Check::account(&bob).lamports(200_000) // 300K - 100K.build(),Check::account(&charlie).lamports(100_000) // 0 + 100K.build(),];// Process with validation at each steplet instruction_and_checks = [(&transfer1, checks_after_transfer1.as_slice()),(&transfer2, checks_after_transfer2.as_slice()),];// Execute chain (panics if any check fails)let result = mollusk.process_and_validate_instruction_chain(&instruction_and_checks, &accounts);println!("{:#?}", result);}
Pemeriksaan Validasi
Mollusk menyediakan serangkaian metode pembantu untuk memeriksa hasil dari instruksi yang diproses.
use mollusk_svm::result::Check;
Gunakan metode berikut untuk memvalidasi hasil instruksi:
// Program execution succeededCheck::success()// Program returned specific errorCheck::err(ProgramError::InvalidArgument)// Instruction level errorCheck::instruction_err(InstructionError::InsufficientFunds)// Check with specific program resultCheck::program_result(ProgramResult::Success)// Compute units consumedCheck::compute_units(1000)// Execution timeCheck::time(100)// Return data from instruction executionCheck::return_data(&[1, 2, 3, 4])
Gunakan berikut ini untuk memvalidasi status Akun:
// Single account validationCheck::account(&pubkey).lamports(1_000_000) // Exact lamports.owner(&program_id) // Account owner.data(&expected_data) // Exact data match.data_slice(8, &[1, 2, 3]) // Partial data match at offset.executable(false) // Executable flag.space(100) // Account data size.closed() // Account is closed (0 lamports).rent_exempt() // Account is rent-exempt.build()// Check all accounts are rent exemptCheck::all_rent_exempt()
Status Akun Persisten
MolluskContext
adalah pembungkus untuk Mollusk
yang mempertahankan status akun di beberapa
panggilan instruksi melalui account_store
. Metode untuk memproses instruksi
identik dengan Mollusk
.
Tidak seperti Mollusk
, yang memerlukan pengiriman accounts
ke setiap metode
(misalnya process_instruction
), MolluskContext
mengelola akun secara
internal melalui account_store
. Ini menghilangkan kebutuhan parameter
accounts
saat memproses instruksi.
Buat account_store
menggunakan metode with_context
:
use std::collections::HashMap;use solana_sdk::{account::Account, pubkey::Pubkey, system_program};use mollusk_svm::Mollusk;let mollusk = Mollusk::default();let account_address = Pubkey::new_unique();let mut account_store = HashMap::new();account_store.insert(account_address,Account {lamports: 1_000_000,data: vec![],owner: system_program::id(),executable: false,rent_epoch: 0,},);let context = mollusk.with_context(account_store);
Contoh berikut memproses dua instruksi transfer SOL terpisah dengan status akun
persisten antara instruksi melalui account_store
.
use {mollusk_svm::Mollusk,solana_sdk::{account::Account, pubkey::Pubkey, system_instruction, system_program},std::collections::HashMap,};fn main() {// Initialize Mollusklet mollusk = Mollusk::default();// Create accountslet sender = Pubkey::new_unique();let recipient = Pubkey::new_unique();// Create account store with initial balanceslet mut account_store = HashMap::new();account_store.insert(sender,Account {lamports: 1_000_000,data: vec![],owner: system_program::id(),executable: false,rent_epoch: 0,},);account_store.insert(recipient,Account {lamports: 0,data: vec![],owner: system_program::id(),executable: false,rent_epoch: 0,},);// Create a stateful contextlet context = mollusk.with_context(account_store);// First transfer: 200,000 lamportslet instruction1 = system_instruction::transfer(&sender, &recipient, 200_000);context.process_instruction(&instruction1);// Second transfer: 100,000 lamports (state persists from first transfer)let instruction2 = system_instruction::transfer(&sender, &recipient, 100_000);context.process_instruction(&instruction2);// Check final balanceslet store = context.account_store.borrow();let sender_account = store.get(&sender).unwrap();let recipient_account = store.get(&recipient).unwrap();println!("Sender: {:#?}", sender_account);println!("Recipient: {:#?}", recipient_account);}
Sysvars Mollusk
Mollusk menyediakan struktur
Sysvars
kustom untuk memodifikasi nilai-nilainya untuk pengujian.
Gunakan metode warp_to_slot
untuk memperbarui jam sysvar untuk mensimulasikan
pergerakan maju atau mundur dalam waktu ke slot tertentu.
use mollusk_svm::Mollusk;fn main() {// Initialize Mollusklet mut mollusk = Mollusk::default();// Show initial slotprintln!("Initial slot: {}", mollusk.sysvars.clock.slot);// Warp to slot 1000mollusk.warp_to_slot(100);println!("After warp: {}", mollusk.sysvars.clock.slot);// Warp to slot 10mollusk.warp_to_slot(10);println!("After second warp: {}", mollusk.sysvars.clock.slot);}
Contoh berikut menunjukkan cara memodifikasi sysvar Mollusk secara langsung
dengan mengakses bidang sysvars
untuk mengubah parameter rent. Anda dapat
memodifikasi nilai sysvar lainnya dengan cara yang sama.
use {mollusk_svm::Mollusk, solana_sdk::rent::Rent};fn main() {let mut mollusk = Mollusk::default();// Show default rentprintln!("Default rent exemption for 1000 bytes: {} lamports",mollusk.sysvars.rent.minimum_balance(1000));// Customize rent parametersmollusk.sysvars.rent = Rent {lamports_per_byte_year: 1,exemption_threshold: 1.0,burn_percent: 0,};// Show custom rentprintln!("Custom rent exemption for 1000 bytes: {} lamports",mollusk.sysvars.rent.minimum_balance(1000));}
Benchmarking Unit Komputasi
MolluskComputeUnitBencher
melacak penggunaan unit komputasi dari instruksi program. Hasil ditulis ke file
markdown.
Memerlukan
mollusk-svm-bencher
sebagai dependensi.
Contoh berikut melakukan benchmark penggunaan unit komputasi dari instruksi transfer SOL.
use {mollusk_svm::Mollusk,mollusk_svm_bencher::MolluskComputeUnitBencher,solana_sdk::{account::Account, pubkey::Pubkey, system_instruction, system_program},};fn main() {// Initialize Mollusklet mollusk = Mollusk::default();// Create test accountslet sender = Pubkey::new_unique();let receiver = Pubkey::new_unique();// Transfer instructionlet transfer = system_instruction::transfer(&sender, &receiver, 100_000);let accounts = vec![(sender,Account {lamports: 1_000_000,data: vec![],owner: system_program::id(),executable: false,rent_epoch: 0,},),(receiver,Account {lamports: 0,data: vec![],owner: system_program::id(),executable: false,rent_epoch: 0,},),];// Run benchmarkMolluskComputeUnitBencher::new(mollusk).bench(("transfer", &transfer, &accounts)).must_pass(true).out_dir("./target/benches").execute();}
Hasil benchmark ditulis ke out_dir
yang ditentukan sebagai file markdown
bernama compute_units.md
.
#### 2025-09-19 22:28:53.691839 UTCSolana CLI Version: solana-cli 2.2.20 (src:dabc99a5; feat:3073396398,client:Agave)| Name | CUs | Delta || -------- | --- | ------- || transfer | 150 | - new - |
Pengujian Token Program
Gunakan
mollusk-svm-programs-token
crate untuk menambahkan token program, token2022 program (token extensions), dan
associated token program ke Mollusk untuk pengujian.
use {mollusk_svm::Mollusk,mollusk_svm_programs_token::{associated_token, token, token2022},};let mut mollusk = Mollusk::default();// Add SPL Token Programtoken::add_program(&mut mollusk);// Add SPL Token-2022 Programtoken2022::add_program(&mut mollusk);// Add Associated Token Account Programassociated_token::add_program(&mut mollusk);
Contoh berikut mendemonstrasikan pengujian transfer token menggunakan Mollusk.
Contoh di bawah ini mendefinisikan akun pengujian secara manual untuk tujuan
demonstrasi. mollusk-svm-programs-token
juga menyertakan fungsi pembantu
untuk membuat akun mint dan token.
use {mollusk_svm::{result::Check, Mollusk},mollusk_svm_programs_token::token,solana_sdk::{account::Account, program_pack::Pack, pubkey::Pubkey},spl_token::{instruction::transfer_checked,state::{Account as TokenAccount, Mint},},};fn main() {// Initialize Mollusk with Token programlet mut mollusk = Mollusk::default();token::add_program(&mut mollusk);// Create account keyslet mint = Pubkey::new_unique();let source = Pubkey::new_unique();let destination = Pubkey::new_unique();let authority = Pubkey::new_unique();// Token configurationlet decimals = 6;let transfer_amount = 1_000_000; // 1 token with 6 decimalslet initial_balance = 10_000_000; // 10 tokens// Calculate rent-exempt minimumslet mint_rent = mollusk.sysvars.rent.minimum_balance(Mint::LEN);let account_rent = mollusk.sysvars.rent.minimum_balance(TokenAccount::LEN);// Create mint accountlet mut mint_data = vec![0u8; Mint::LEN];Mint::pack(Mint {mint_authority: Some(authority).into(),supply: initial_balance,decimals,is_initialized: true,freeze_authority: None.into(),},&mut mint_data,).unwrap();// Create source token accountlet mut source_data = vec![0u8; TokenAccount::LEN];TokenAccount::pack(TokenAccount {mint,owner: authority,amount: initial_balance,delegate: None.into(),state: spl_token::state::AccountState::Initialized,is_native: None.into(),delegated_amount: 0,close_authority: None.into(),},&mut source_data,).unwrap();// Create destination token accountlet mut destination_data = vec![0u8; TokenAccount::LEN];TokenAccount::pack(TokenAccount {mint,owner: Pubkey::new_unique(),amount: 0,delegate: None.into(),state: spl_token::state::AccountState::Initialized,is_native: None.into(),delegated_amount: 0,close_authority: None.into(),},&mut destination_data,).unwrap();// Setup accounts for transfer_checkedlet accounts = vec![(source,Account {lamports: account_rent,data: source_data,owner: token::ID,executable: false,rent_epoch: 0,},),(mint,Account {lamports: mint_rent,data: mint_data,owner: token::ID,executable: false,rent_epoch: 0,},),(destination,Account {lamports: account_rent,data: destination_data,owner: token::ID,executable: false,rent_epoch: 0,},),(authority,Account {lamports: 1_000_000,data: vec![],owner: Pubkey::default(),executable: false,rent_epoch: 0,},),];// Create transfer_checked instructionlet instruction = transfer_checked(&token::ID,&source,&mint,&destination,&authority,&[],transfer_amount,decimals,).unwrap();// Expected balances after transferlet expected_source_balance = (initial_balance - transfer_amount).to_le_bytes();let expected_dest_balance = transfer_amount.to_le_bytes();// Define validation checkslet checks = vec![Check::success(),Check::account(&source).data_slice(64, &expected_source_balance) // Token amount is at offset 64.build(),Check::account(&destination).data_slice(64, &expected_dest_balance).build(),];// Process and validate the instructionlet result = mollusk.process_and_validate_instruction(&instruction, &accounts, &checks);println!("{:#?}", result);// Deserialize token account datalet source_account = result.get_account(&source).unwrap();let source_token = TokenAccount::unpack(&source_account.data).unwrap();println!("Source Token Account: {:#?}", source_token);let destination_account = result.get_account(&destination).unwrap();let dest_token = TokenAccount::unpack(&destination_account.data).unwrap();println!("Destination Token Account: {:#?}", dest_token);}
Is this page helpful?