Skip to content

Commit 22e66d9

Browse files
authored
harness: introduce account store interface and wrapper (#114)
1 parent 19b6594 commit 22e66d9

File tree

9 files changed

+565
-16
lines changed

9 files changed

+565
-16
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ solana-hash = "2.2"
6464
solana-instruction = "2.2"
6565
solana-keccak-hasher = "2.2"
6666
solana-loader-v3-interface = "3.0"
67+
solana-loader-v4-interface = "2.2"
6768
solana-log-collector = "2.2"
6869
solana-logger = "2.2"
6970
solana-native-token = "2.2"

README.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,78 @@ constraints on instruction chains, such as loaded account keys or size.
272272
Developers should recognize that instruction chains are primarily used for
273273
testing program execution.
274274

275+
## Stateful Testing with MolluskContext
276+
277+
For complex testing scenarios involving multiple instructions or persistent
278+
state between calls, `MolluskContext` provides a stateful wrapper around
279+
`Mollusk`. It automatically manages an account store and provides the same
280+
API methods without requiring explicit account management.
281+
282+
`MolluskContext` is ideal for:
283+
* Testing instruction chains where account state persists between calls
284+
* Complex program interactions that require maintaining account state
285+
* Scenarios where manually managing accounts becomes cumbersome
286+
287+
To use `MolluskContext`, you need to provide an implementation of the
288+
`AccountStore` trait:
289+
290+
```rust
291+
use {
292+
mollusk_svm::{Mollusk, account_store::AccountStore},
293+
solana_account::Account,
294+
solana_instruction::Instruction,
295+
solana_pubkey::Pubkey,
296+
solana_system_interface::instruction as system_instruction,
297+
std::collections::HashMap,
298+
};
299+
300+
// Simple in-memory account store implementation
301+
#[derive(Default)]
302+
struct InMemoryAccountStore {
303+
accounts: HashMap<Pubkey, Account>,
304+
}
305+
306+
impl AccountStore for InMemoryAccountStore {
307+
fn get_account(&self, pubkey: &Pubkey) -> Option<Account> {
308+
self.accounts.get(pubkey).cloned()
309+
}
310+
311+
fn store_account(&mut self, pubkey: Pubkey, account: Account) {
312+
self.accounts.insert(pubkey, account);
313+
}
314+
}
315+
316+
let mollusk = Mollusk::default();
317+
let context = mollusk.with_context(InMemoryAccountStore::default());
318+
319+
let alice = Pubkey::new_unique();
320+
let bob = Pubkey::new_unique();
321+
322+
// Execute instructions without managing accounts manually
323+
let instruction1 = system_instruction::transfer(&alice, &bob, 1_000_000);
324+
let result1 = context.process_instruction(&instruction1);
325+
326+
let instruction2 = system_instruction::transfer(&bob, &alice, 500_000);
327+
let result2 = context.process_instruction(&instruction2);
328+
329+
// Account state is automatically preserved between calls
330+
```
331+
332+
The `MolluskContext` API provides the same core methods as `Mollusk`:
333+
334+
* `process_instruction`: Process an instruction with automatic account management
335+
* `process_instruction_chain`: Process a chain of instructions
336+
* `process_and_validate_instruction`: Process and validate an instruction
337+
* `process_and_validate_instruction_chain`: Process and validate an instruction chain
338+
339+
All methods return `ContextResult` instead of `InstructionResult`, which omits
340+
the `resulting_accounts` field since accounts are managed by the context's
341+
account store.
342+
343+
Note that `HashMap<Pubkey, Account>` implements `AccountStore` directly,
344+
so you can use it as a simple in-memory account store without needing
345+
to implement your own.
346+
275347
## Benchmarking Compute Units
276348
The Mollusk Compute Unit Bencher can be used to benchmark the compute unit
277349
usage of Solana programs. It provides a simple API for developers to write

harness/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ solana-fee-structure = { workspace = true }
4444
solana-hash = { workspace = true }
4545
solana-instruction = { workspace = true }
4646
solana-loader-v3-interface = { workspace = true }
47+
solana-loader-v4-interface = { workspace = true }
4748
solana-log-collector = { workspace = true }
4849
solana-logger = { workspace = true }
4950
solana-program-error = { workspace = true }

harness/src/account_store.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
//! A trait for implementing an account store, to be used with the
2+
/// `MolluskContext`.
3+
use {solana_account::Account, solana_pubkey::Pubkey, std::collections::HashMap};
4+
5+
/// A trait for implementing an account store, to be used with the
6+
/// `MolluskContext`.
7+
pub trait AccountStore {
8+
/// Returns the default account to be used when an account is not found.
9+
fn default_account(&self, _pubkey: &Pubkey) -> Account {
10+
Account::default()
11+
}
12+
13+
/// Get an account at the given public key.
14+
fn get_account(&self, pubkey: &Pubkey) -> Option<Account>;
15+
16+
/// Store an account at the given public key.
17+
fn store_account(&mut self, pubkey: Pubkey, account: Account);
18+
}
19+
20+
impl AccountStore for HashMap<Pubkey, Account> {
21+
fn get_account(&self, pubkey: &Pubkey) -> Option<Account> {
22+
self.get(pubkey).cloned()
23+
}
24+
25+
fn store_account(&mut self, pubkey: Pubkey, account: Account) {
26+
self.insert(pubkey, account);
27+
}
28+
}

harness/src/lib.rs

Lines changed: 214 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,72 @@
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
@@ -373,6 +439,7 @@
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;
376443
mod compile_accounts;
377444
pub mod file;
378445
#[cfg(any(feature = "fuzz", feature = "fuzz-fd"))]
@@ -385,9 +452,10 @@ pub mod sysvar;
385452
use result::Compare;
386453
use {
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

408476
pub(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

Comments
 (0)