Skip to content

Commit 5c09c76

Browse files
committed
InstructionConfig, migrate Initialize tests
1 parent 547c746 commit 5c09c76

File tree

7 files changed

+579
-121
lines changed

7 files changed

+579
-121
lines changed

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: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
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: Option<bool>, // `None` runs if `Check::success`
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, test: bool) -> Self {
35+
self.test_missing_signers = Some(test);
36+
self
37+
}
38+
39+
/// Executes the instruction. If `checks` is `None` or empty, uses `Check::success()`.
40+
/// Fail-safe default: when `test_missing_signers` is `None`, runs the missing-signers
41+
/// test (`true`). Callers must explicitly opt out with `.test_missing_signers(false)`.
42+
pub fn execute(self) -> mollusk_svm::result::InstructionResult {
43+
let default_checks = [Check::success()];
44+
let checks = match self.checks {
45+
Some(c) if !c.is_empty() => c,
46+
_ => &default_checks,
47+
};
48+
49+
let test_missing_signers = self.test_missing_signers.unwrap_or(true);
50+
51+
self.ctx.process_instruction_maybe_test_signers(
52+
&self.instruction,
53+
self.accounts,
54+
checks,
55+
test_missing_signers,
56+
)
57+
}
58+
}
59+
60+
impl<'a> InstructionExecution<'a, '_> {
61+
pub(crate) fn new(
62+
instruction: Instruction,
63+
accounts: Vec<(Pubkey, AccountSharedData)>,
64+
ctx: &'a StakeTestContext,
65+
) -> Self {
66+
Self {
67+
instruction,
68+
accounts,
69+
ctx,
70+
checks: None,
71+
test_missing_signers: None,
72+
}
73+
}
74+
}
75+
76+
pub struct InitializeConfig<'a> {
77+
pub stake: (&'a Pubkey, &'a AccountSharedData),
78+
pub authorized: &'a Authorized,
79+
pub lockup: &'a Lockup,
80+
}
81+
82+
impl InstructionConfig for InitializeConfig<'_> {
83+
fn build_instruction(&self, _ctx: &StakeTestContext) -> Instruction {
84+
ixn::initialize(self.stake.0, self.authorized, self.lockup)
85+
}
86+
87+
fn build_accounts(&self) -> Vec<(Pubkey, AccountSharedData)> {
88+
vec![(*self.stake.0, self.stake.1.clone())]
89+
}
90+
}
91+
92+
pub struct InitializeCheckedConfig<'a> {
93+
pub stake: (&'a Pubkey, &'a AccountSharedData),
94+
pub authorized: &'a Authorized,
95+
}
96+
97+
impl InstructionConfig for InitializeCheckedConfig<'_> {
98+
fn build_instruction(&self, _ctx: &StakeTestContext) -> Instruction {
99+
ixn::initialize_checked(self.stake.0, self.authorized)
100+
}
101+
102+
fn build_accounts(&self) -> Vec<(Pubkey, AccountSharedData)> {
103+
vec![(*self.stake.0, self.stake.1.clone())]
104+
}
105+
}

program/tests/helpers/lifecycle.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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+
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
8+
pub enum StakeLifecycle {
9+
Uninitialized = 0,
10+
Initialized,
11+
Activating,
12+
Active,
13+
Deactivating,
14+
Deactive,
15+
Closed,
16+
}
17+
18+
impl StakeLifecycle {
19+
/// Create an uninitialized stake account
20+
pub fn create_uninitialized_account(self) -> AccountSharedData {
21+
AccountSharedData::new_data_with_space(
22+
STAKE_RENT_EXEMPTION,
23+
&StakeStateV2::Uninitialized,
24+
StakeStateV2::size_of(),
25+
&id(),
26+
)
27+
.unwrap()
28+
}
29+
}

program/tests/helpers/mod.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#![allow(clippy::arithmetic_side_effects)]
2+
#![allow(dead_code)]
3+
4+
pub mod context;
5+
pub mod instruction_builders;
6+
pub mod lifecycle;
7+
pub mod utils;

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)