276276//! Developers should recognize that instruction chains are primarily used for
277277//! testing program execution.
278278//!
279+ //! ## Stateful Testing with MolluskContext
280+ //!
281+ //! For complex testing scenarios that involve multiple instructions or require
282+ //! persistent state between calls, `MolluskContext` provides a stateful wrapper
283+ //! around `Mollusk`. It automatically manages an account store and provides the
284+ //! same API methods but without requiring explicit account management.
285+ //!
286+ //! ```rust,ignore
287+ //! use {
288+ //! mollusk_svm::{Mollusk, account_store::AccountStore},
289+ //! solana_account::Account,
290+ //! solana_instruction::Instruction,
291+ //! solana_pubkey::Pubkey,
292+ //! solana_system_interface::instruction as system_instruction,
293+ //! std::collections::HashMap,
294+ //! };
295+ //!
296+ //! // Simple in-memory account store implementation
297+ //! #[derive(Default)]
298+ //! struct InMemoryAccountStore {
299+ //! accounts: HashMap<Pubkey, Account>,
300+ //! }
301+ //!
302+ //! impl AccountStore for InMemoryAccountStore {
303+ //! fn get_account(&self, pubkey: &Pubkey) -> Option<Account> {
304+ //! self.accounts.get(pubkey).cloned()
305+ //! }
306+ //!
307+ //! fn store_account(&mut self, pubkey: Pubkey, account: Account) {
308+ //! self.accounts.insert(pubkey, account);
309+ //! }
310+ //! }
311+ //!
312+ //! let mollusk = Mollusk::default();
313+ //! let context = mollusk.with_context(InMemoryAccountStore::default());
314+ //!
315+ //! let alice = Pubkey::new_unique();
316+ //! let bob = Pubkey::new_unique();
317+ //!
318+ //! // Execute instructions without managing accounts manually
319+ //! let instruction1 = system_instruction::transfer(&alice, &bob, 1_000_000);
320+ //! let result1 = context.process_instruction(&instruction1);
321+ //!
322+ //! let instruction2 = system_instruction::transfer(&bob, &alice, 500_000);
323+ //! let result2 = context.process_instruction(&instruction2);
324+ //!
325+ //! // Account state is automatically preserved between calls
326+ //! ```
327+ //!
328+ //! The `MolluskContext` API provides the same core methods as `Mollusk`:
329+ //!
330+ //! * `process_instruction`: Process an instruction with automatic account
331+ //! management
332+ //! * `process_instruction_chain`: Process a chain of instructions
333+ //! * `process_and_validate_instruction`: Process and validate an instruction
334+ //! * `process_and_validate_instruction_chain`: Process and validate an
335+ //! instruction chain
336+ //!
337+ //! All methods return `ContextResult` instead of `InstructionResult`, which
338+ //! omits the `resulting_accounts` field since accounts are managed by the
339+ //! context's account store.
340+ //!
341+ //! Note that `HashMap<Pubkey, Account>` implements `AccountStore` directly,
342+ //! so you can use it as a simple in-memory account store without needing
343+ //! to implement your own.
344+ //!
279345//! ## Fixtures
280346//!
281347//! Mollusk also supports working with multiple kinds of fixtures, which can
373439//! Fixtures can be loaded from files or decoded from raw blobs. These
374440//! capabilities are provided by the respective fixture crates.
375441
442+ pub mod account_store;
376443mod compile_accounts;
377444pub mod file;
378445#[ cfg( any( feature = "fuzz" , feature = "fuzz-fd" ) ) ]
@@ -385,9 +452,10 @@ pub mod sysvar;
385452use result:: Compare ;
386453use {
387454 crate :: {
455+ account_store:: AccountStore ,
388456 compile_accounts:: CompiledAccounts ,
389457 program:: ProgramCache ,
390- result:: { Check , CheckContext , Config , InstructionResult } ,
458+ result:: { Check , CheckContext , Config , ContextResult , InstructionResult } ,
391459 sysvar:: Sysvars ,
392460 } ,
393461 agave_feature_set:: FeatureSet ,
@@ -397,12 +465,12 @@ use {
397465 solana_compute_budget:: compute_budget:: ComputeBudget ,
398466 solana_fee_structure:: FeeStructure ,
399467 solana_hash:: Hash ,
400- solana_instruction:: Instruction ,
468+ solana_instruction:: { AccountMeta , Instruction } ,
401469 solana_program_runtime:: invoke_context:: { EnvironmentConfig , InvokeContext } ,
402470 solana_pubkey:: Pubkey ,
403471 solana_timings:: ExecuteTimings ,
404472 solana_transaction_context:: TransactionContext ,
405- std:: { cell:: RefCell , rc:: Rc , sync:: Arc } ,
473+ std:: { cell:: RefCell , collections :: HashSet , iter :: once , rc:: Rc , sync:: Arc } ,
406474} ;
407475
408476pub ( crate ) const DEFAULT_LOADER_KEY : Pubkey = solana_sdk_ids:: bpf_loader_upgradeable:: id ( ) ;
@@ -550,7 +618,7 @@ impl Mollusk {
550618 ) ;
551619
552620 let invoke_result = {
553- let mut program_cache = self . program_cache . cache ( ) . write ( ) . unwrap ( ) ;
621+ let mut program_cache = self . program_cache . cache ( ) ;
554622 let sysvar_cache = self . sysvars . setup_sysvar_cache ( accounts) ;
555623 let mut invoke_context = InvokeContext :: new (
556624 & mut transaction_context,
@@ -971,4 +1039,146 @@ impl Mollusk {
9711039 result. compare_with_config ( & expected, checks, & self . config ) ;
9721040 result
9731041 }
1042+
1043+ /// Convert this `Mollusk` instance into a `MolluskContext` for stateful
1044+ /// testing.
1045+ ///
1046+ /// Creates a context wrapper that manages persistent state between
1047+ /// instruction executions, starting with the provided account store.
1048+ ///
1049+ /// See [`MolluskContext`] for more details on how to use it.
1050+ pub fn with_context < AS : AccountStore > ( self , mut account_store : AS ) -> MolluskContext < AS > {
1051+ // For convenience, load all program accounts into the account store,
1052+ // but only if they don't exist.
1053+ self . program_cache
1054+ . get_all_keyed_program_accounts ( )
1055+ . into_iter ( )
1056+ . for_each ( |( pubkey, account) | {
1057+ if account_store. get_account ( & pubkey) . is_none ( ) {
1058+ account_store. store_account ( pubkey, account) ;
1059+ }
1060+ } ) ;
1061+ MolluskContext {
1062+ mollusk : self ,
1063+ account_store : Rc :: new ( RefCell :: new ( account_store) ) ,
1064+ }
1065+ }
1066+ }
1067+
1068+ /// A stateful wrapper around `Mollusk` that provides additional context and
1069+ /// convenience features for testing programs.
1070+ ///
1071+ /// `MolluskContext` maintains persistent state between instruction executions,
1072+ /// starting with an account store that automatically manages account
1073+ /// lifecycles. This makes it ideal for complex testing scenarios involving
1074+ /// multiple instructions, instruction chains, and stateful program
1075+ /// interactions.
1076+ ///
1077+ /// Note: Account state is only persisted if the instruction execution
1078+ /// was successful. If an instruction fails, the account state will not
1079+ /// be updated.
1080+ ///
1081+ /// The API is functionally identical to `Mollusk` but with enhanced state
1082+ /// management and a streamlined interface. Namely, the input `accounts` slice
1083+ /// is no longer required, and the returned result does not contain a
1084+ /// `resulting_accounts` field.
1085+ pub struct MolluskContext < AS : AccountStore > {
1086+ pub mollusk : Mollusk ,
1087+ pub account_store : Rc < RefCell < AS > > ,
1088+ }
1089+
1090+ impl < AS : AccountStore > MolluskContext < AS > {
1091+ fn load_accounts_for_instructions < ' a > (
1092+ & self ,
1093+ instructions : impl Iterator < Item = & ' a Instruction > ,
1094+ ) -> Vec < ( Pubkey , Account ) > {
1095+ let mut seen = HashSet :: new ( ) ;
1096+ let mut accounts = Vec :: new ( ) ;
1097+ let store = self . account_store . borrow ( ) ;
1098+ instructions. for_each ( |instruction| {
1099+ instruction
1100+ . accounts
1101+ . iter ( )
1102+ . for_each ( |AccountMeta { pubkey, .. } | {
1103+ if seen. insert ( * pubkey) {
1104+ let account = store
1105+ . get_account ( pubkey)
1106+ . unwrap_or_else ( || store. default_account ( pubkey) ) ;
1107+ accounts. push ( ( * pubkey, account) ) ;
1108+ }
1109+ } ) ;
1110+ } ) ;
1111+ accounts
1112+ }
1113+
1114+ fn consume_mollusk_result ( & self , result : InstructionResult ) -> ContextResult {
1115+ let InstructionResult {
1116+ compute_units_consumed,
1117+ execution_time,
1118+ program_result,
1119+ raw_result,
1120+ return_data,
1121+ resulting_accounts,
1122+ } = result;
1123+
1124+ let mut store = self . account_store . borrow_mut ( ) ;
1125+ for ( pubkey, account) in resulting_accounts {
1126+ store. store_account ( pubkey, account) ;
1127+ }
1128+
1129+ ContextResult {
1130+ compute_units_consumed,
1131+ execution_time,
1132+ program_result,
1133+ raw_result,
1134+ return_data,
1135+ }
1136+ }
1137+
1138+ /// Process an instruction using the minified Solana Virtual Machine (SVM)
1139+ /// environment. Simply returns the result.
1140+ pub fn process_instruction ( & self , instruction : & Instruction ) -> ContextResult {
1141+ let accounts = self . load_accounts_for_instructions ( once ( instruction) ) ;
1142+ let result = self . mollusk . process_instruction ( instruction, & accounts) ;
1143+ self . consume_mollusk_result ( result)
1144+ }
1145+
1146+ /// Process a chain of instructions using the minified Solana Virtual
1147+ /// Machine (SVM) environment.
1148+ pub fn process_instruction_chain ( & self , instructions : & [ Instruction ] ) -> ContextResult {
1149+ let accounts = self . load_accounts_for_instructions ( instructions. iter ( ) ) ;
1150+ let result = self
1151+ . mollusk
1152+ . process_instruction_chain ( instructions, & accounts) ;
1153+ self . consume_mollusk_result ( result)
1154+ }
1155+
1156+ /// Process an instruction using the minified Solana Virtual Machine (SVM)
1157+ /// environment, then perform checks on the result.
1158+ pub fn process_and_validate_instruction (
1159+ & self ,
1160+ instruction : & Instruction ,
1161+ checks : & [ Check ] ,
1162+ ) -> ContextResult {
1163+ let accounts = self . load_accounts_for_instructions ( once ( instruction) ) ;
1164+ let result = self
1165+ . mollusk
1166+ . process_and_validate_instruction ( instruction, & accounts, checks) ;
1167+ self . consume_mollusk_result ( result)
1168+ }
1169+
1170+ /// Process a chain of instructions using the minified Solana Virtual
1171+ /// Machine (SVM) environment, then perform checks on the result.
1172+ pub fn process_and_validate_instruction_chain (
1173+ & self ,
1174+ instructions : & [ ( & Instruction , & [ Check ] ) ] ,
1175+ ) -> ContextResult {
1176+ let accounts = self . load_accounts_for_instructions (
1177+ instructions. iter ( ) . map ( |( instruction, _) | * instruction) ,
1178+ ) ;
1179+ let result = self
1180+ . mollusk
1181+ . process_and_validate_instruction_chain ( instructions, & accounts) ;
1182+ self . consume_mollusk_result ( result)
1183+ }
9741184}
0 commit comments