Skip to content

Commit 274de12

Browse files
committed
migrated Initialize and InitializeChecked tests to mollusk
StakeTestContext to reduce boilerplate InstructionConfig builder pattern improve InitializeChecked coverage
1 parent 547c746 commit 274de12

File tree

9 files changed

+585
-110
lines changed

9 files changed

+585
-110
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.

program/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ solana-system-interface = { version = "2.0.0", features = ["bincode"] }
4545
solana-sysvar-id = "3.0.0"
4646
solana-transaction = "3.0.0"
4747
test-case = "3.3.1"
48+
tokio = { version = "1", features = ["full"] }
4849

4950
[lib]
5051
crate-type = ["cdylib", "lib"]

program/tests/helpers/context.rs

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
use {
2+
super::{
3+
instruction_builders::{InstructionConfig, InstructionExecution},
4+
lifecycle::StakeLifecycle,
5+
utils::{add_sysvars, STAKE_RENT_EXEMPTION},
6+
},
7+
mollusk_svm::{result::Check, Mollusk},
8+
solana_account::AccountSharedData,
9+
solana_instruction::Instruction,
10+
solana_pubkey::Pubkey,
11+
solana_stake_program::id,
12+
};
13+
14+
/// Builder for creating stake accounts with customizable parameters
15+
pub struct StakeAccountBuilder {
16+
lifecycle: StakeLifecycle,
17+
}
18+
19+
impl StakeAccountBuilder {
20+
pub fn build(self) -> (Pubkey, AccountSharedData) {
21+
let stake_pubkey = Pubkey::new_unique();
22+
let account = self.lifecycle.create_uninitialized_account();
23+
(stake_pubkey, account)
24+
}
25+
}
26+
27+
/// Consolidated test context for stake account tests
28+
pub struct StakeTestContext {
29+
pub mollusk: Mollusk,
30+
pub rent_exempt_reserve: u64,
31+
pub staker: Pubkey,
32+
pub withdrawer: Pubkey,
33+
}
34+
35+
impl StakeTestContext {
36+
/// Create a new test context with all standard setup
37+
pub fn new() -> Self {
38+
let mollusk = Mollusk::new(&id(), "solana_stake_program");
39+
40+
Self {
41+
mollusk,
42+
rent_exempt_reserve: STAKE_RENT_EXEMPTION,
43+
staker: Pubkey::new_unique(),
44+
withdrawer: Pubkey::new_unique(),
45+
}
46+
}
47+
48+
/// Create a stake account builder for the specified lifecycle stage
49+
///
50+
/// Example:
51+
/// ```
52+
/// let (stake, account) = ctx
53+
/// .stake_account(StakeLifecycle::Uninitialized)
54+
/// .build();
55+
/// ```
56+
pub fn stake_account(&mut self, lifecycle: StakeLifecycle) -> StakeAccountBuilder {
57+
StakeAccountBuilder { lifecycle }
58+
}
59+
60+
/// Process an instruction
61+
pub fn process_with<'b, C: InstructionConfig>(
62+
&self,
63+
config: C,
64+
) -> InstructionExecution<'_, 'b> {
65+
InstructionExecution::new(
66+
config.build_instruction(self),
67+
config.build_accounts(),
68+
self,
69+
)
70+
}
71+
72+
/// Process an instruction with optional missing signer testing
73+
pub(crate) fn process_instruction_maybe_test_signers(
74+
&self,
75+
instruction: &Instruction,
76+
accounts: Vec<(Pubkey, AccountSharedData)>,
77+
checks: &[Check],
78+
test_missing_signers: bool,
79+
) -> mollusk_svm::result::InstructionResult {
80+
if test_missing_signers {
81+
use solana_program_error::ProgramError;
82+
83+
// Test that removing each signer causes failure
84+
for i in 0..instruction.accounts.len() {
85+
if instruction.accounts[i].is_signer {
86+
let mut modified_instruction = instruction.clone();
87+
modified_instruction.accounts[i].is_signer = false;
88+
89+
let accounts_with_sysvars =
90+
add_sysvars(&self.mollusk, &modified_instruction, accounts.clone());
91+
92+
self.mollusk.process_and_validate_instruction(
93+
&modified_instruction,
94+
&accounts_with_sysvars,
95+
&[Check::err(ProgramError::MissingRequiredSignature)],
96+
);
97+
}
98+
}
99+
}
100+
101+
// Process with all signers present
102+
let accounts_with_sysvars = add_sysvars(&self.mollusk, instruction, accounts);
103+
self.mollusk
104+
.process_and_validate_instruction(instruction, &accounts_with_sysvars, checks)
105+
}
106+
}
107+
108+
impl Default for StakeTestContext {
109+
fn default() -> Self {
110+
Self::new()
111+
}
112+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
use {
2+
super::context::StakeTestContext,
3+
mollusk_svm::result::Check,
4+
solana_account::AccountSharedData,
5+
solana_instruction::Instruction,
6+
solana_pubkey::Pubkey,
7+
solana_stake_interface::{
8+
instruction as ixn,
9+
state::{Authorized, Lockup},
10+
},
11+
};
12+
13+
// Trait for instruction configuration that builds instruction and accounts
14+
pub trait InstructionConfig {
15+
fn build_instruction(&self, ctx: &StakeTestContext) -> Instruction;
16+
fn build_accounts(&self) -> Vec<(Pubkey, AccountSharedData)>;
17+
}
18+
19+
/// Execution builder with validation and signer testing
20+
pub struct InstructionExecution<'a, 'b> {
21+
instruction: Instruction,
22+
accounts: Vec<(Pubkey, AccountSharedData)>,
23+
ctx: &'a StakeTestContext,
24+
checks: Option<&'b [Check<'b>]>,
25+
test_missing_signers: bool,
26+
}
27+
28+
impl<'b> InstructionExecution<'_, 'b> {
29+
pub fn checks(mut self, checks: &'b [Check<'b>]) -> Self {
30+
self.checks = Some(checks);
31+
self
32+
}
33+
34+
pub fn test_missing_signers(mut self) -> Self {
35+
self.test_missing_signers = true;
36+
self
37+
}
38+
39+
pub fn execute(self) -> mollusk_svm::result::InstructionResult {
40+
let default_checks = [Check::success()];
41+
let checks = self.checks.unwrap_or(&default_checks);
42+
self.ctx.process_instruction_maybe_test_signers(
43+
&self.instruction,
44+
self.accounts,
45+
checks,
46+
self.test_missing_signers,
47+
)
48+
}
49+
}
50+
51+
impl<'a> InstructionExecution<'a, '_> {
52+
pub(crate) fn new(
53+
instruction: Instruction,
54+
accounts: Vec<(Pubkey, AccountSharedData)>,
55+
ctx: &'a StakeTestContext,
56+
) -> Self {
57+
Self {
58+
instruction,
59+
accounts,
60+
ctx,
61+
checks: None,
62+
test_missing_signers: false,
63+
}
64+
}
65+
}
66+
67+
pub struct InitializeConfig<'a> {
68+
pub stake: (&'a Pubkey, &'a AccountSharedData),
69+
pub authorized: &'a Authorized,
70+
pub lockup: &'a Lockup,
71+
}
72+
73+
impl InstructionConfig for InitializeConfig<'_> {
74+
fn build_instruction(&self, _ctx: &StakeTestContext) -> Instruction {
75+
ixn::initialize(self.stake.0, self.authorized, self.lockup)
76+
}
77+
78+
fn build_accounts(&self) -> Vec<(Pubkey, AccountSharedData)> {
79+
vec![(*self.stake.0, self.stake.1.clone())]
80+
}
81+
}
82+
83+
pub struct InitializeCheckedConfig<'a> {
84+
pub stake: (&'a Pubkey, &'a AccountSharedData),
85+
pub authorized: &'a Authorized,
86+
}
87+
88+
impl InstructionConfig for InitializeCheckedConfig<'_> {
89+
fn build_instruction(&self, _ctx: &StakeTestContext) -> Instruction {
90+
ixn::initialize_checked(self.stake.0, self.authorized)
91+
}
92+
93+
fn build_accounts(&self) -> Vec<(Pubkey, AccountSharedData)> {
94+
vec![(*self.stake.0, self.stake.1.clone())]
95+
}
96+
}

program/tests/helpers/lifecycle.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
use {
2+
super::utils::STAKE_RENT_EXEMPTION, solana_account::AccountSharedData,
3+
solana_stake_interface::state::StakeStateV2, solana_stake_program::id,
4+
};
5+
6+
/// Lifecycle states for stake accounts in tests
7+
#[allow(dead_code)]
8+
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
9+
pub enum StakeLifecycle {
10+
Uninitialized = 0,
11+
Initialized,
12+
Activating,
13+
Active,
14+
Deactivating,
15+
Deactive,
16+
Closed,
17+
}
18+
19+
impl StakeLifecycle {
20+
/// Create an uninitialized stake account
21+
pub fn create_uninitialized_account(self) -> AccountSharedData {
22+
AccountSharedData::new_data_with_space(
23+
STAKE_RENT_EXEMPTION,
24+
&StakeStateV2::Uninitialized,
25+
StakeStateV2::size_of(),
26+
&id(),
27+
)
28+
.unwrap()
29+
}
30+
}

program/tests/helpers/mod.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#![allow(clippy::arithmetic_side_effects)]
2+
3+
pub mod context;
4+
pub mod instruction_builders;
5+
pub mod lifecycle;
6+
pub mod utils;
7+
8+
pub use {
9+
context::StakeTestContext, instruction_builders::InitializeConfig, lifecycle::StakeLifecycle,
10+
};

program/tests/helpers/utils.rs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
use {
2+
mollusk_svm::Mollusk,
3+
solana_account::{Account, AccountSharedData},
4+
solana_instruction::Instruction,
5+
solana_pubkey::Pubkey,
6+
solana_rent::Rent,
7+
solana_stake_interface::{stake_history::StakeHistory, state::StakeStateV2},
8+
solana_sysvar_id::SysvarId,
9+
std::collections::HashMap,
10+
};
11+
12+
// hardcoded for convenience
13+
pub const STAKE_RENT_EXEMPTION: u64 = 2_282_880;
14+
15+
#[test]
16+
fn assert_stake_rent_exemption() {
17+
assert_eq!(
18+
Rent::default().minimum_balance(StakeStateV2::size_of()),
19+
STAKE_RENT_EXEMPTION
20+
);
21+
}
22+
23+
/// Resolve all accounts for an instruction, including sysvars and instruction accounts
24+
///
25+
/// This function re-serializes the stake history sysvar from mollusk.sysvars.stake_history
26+
/// every time it's called, ensuring that any updates to the stake history are reflected in the accounts.
27+
pub fn add_sysvars(
28+
mollusk: &Mollusk,
29+
instruction: &Instruction,
30+
accounts: Vec<(Pubkey, AccountSharedData)>,
31+
) -> Vec<(Pubkey, Account)> {
32+
// Build a map of provided accounts
33+
let mut account_map: HashMap<Pubkey, Account> = accounts
34+
.into_iter()
35+
.map(|(pk, acc)| (pk, acc.into()))
36+
.collect();
37+
38+
// Now resolve all accounts from the instruction
39+
let mut result = Vec::new();
40+
for account_meta in &instruction.accounts {
41+
let key = account_meta.pubkey;
42+
let account = if let Some(acc) = account_map.remove(&key) {
43+
// Use the provided account
44+
acc
45+
} else if Rent::check_id(&key) {
46+
mollusk.sysvars.keyed_account_for_rent_sysvar().1
47+
} else if solana_clock::Clock::check_id(&key) {
48+
mollusk.sysvars.keyed_account_for_clock_sysvar().1
49+
} else if solana_epoch_schedule::EpochSchedule::check_id(&key) {
50+
mollusk.sysvars.keyed_account_for_epoch_schedule_sysvar().1
51+
} else if solana_epoch_rewards::EpochRewards::check_id(&key) {
52+
mollusk.sysvars.keyed_account_for_epoch_rewards_sysvar().1
53+
} else if StakeHistory::check_id(&key) {
54+
// Re-serialize stake history from mollusk.sysvars.stake_history
55+
mollusk.sysvars.keyed_account_for_stake_history_sysvar().1
56+
} else {
57+
// Default empty account
58+
Account::default()
59+
};
60+
61+
result.push((key, account));
62+
}
63+
64+
result
65+
}

0 commit comments

Comments
 (0)