Skip to content

Commit 423127d

Browse files
committed
basic helpers and Initialize(Checked) tests
1 parent 8b24830 commit 423127d

9 files changed

Lines changed: 683 additions & 121 deletions

File tree

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
@@ -43,6 +43,7 @@ solana-sdk-ids = "3.0.0"
4343
solana-signature = "3.0.0"
4444
solana-signer = "3.0.0"
4545
solana-svm-log-collector = "3.0.0"
46+
solana-stake-client = { path = "../clients/rust" }
4647
solana-system-interface = { version = "2.0.0", features = ["bincode"] }
4748
solana-transaction = "3.0.0"
4849
test-case = "3.3.1"

program/tests/helpers/context.rs

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
use {
2+
super::{
3+
instruction_builders::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(
62+
&self,
63+
instruction: Instruction,
64+
accounts: Vec<(Pubkey, AccountSharedData)>,
65+
) -> InstructionExecution {
66+
InstructionExecution::new(instruction, accounts, self)
67+
}
68+
69+
/// Process an instruction with optional missing signer testing
70+
pub(crate) fn process_instruction_maybe_test_signers(
71+
&self,
72+
instruction: &Instruction,
73+
accounts: Vec<(Pubkey, AccountSharedData)>,
74+
checks: &[Check],
75+
test_missing_signers: bool,
76+
) -> mollusk_svm::result::InstructionResult {
77+
if test_missing_signers {
78+
use solana_program_error::ProgramError;
79+
80+
// Test that removing each signer causes failure
81+
for i in 0..instruction.accounts.len() {
82+
if instruction.accounts[i].is_signer {
83+
let mut modified_instruction = instruction.clone();
84+
modified_instruction.accounts[i].is_signer = false;
85+
86+
let accounts_with_sysvars =
87+
add_sysvars(&self.mollusk, &modified_instruction, accounts.clone());
88+
89+
self.mollusk.process_and_validate_instruction(
90+
&modified_instruction,
91+
&accounts_with_sysvars,
92+
&[Check::err(ProgramError::MissingRequiredSignature)],
93+
);
94+
}
95+
}
96+
}
97+
98+
// Process with all signers present
99+
let accounts_with_sysvars = add_sysvars(&self.mollusk, instruction, accounts);
100+
self.mollusk
101+
.process_and_validate_instruction(instruction, &accounts_with_sysvars, checks)
102+
}
103+
}
104+
105+
impl Default for StakeTestContext {
106+
fn default() -> Self {
107+
Self::new()
108+
}
109+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
use {
2+
super::context::StakeTestContext, mollusk_svm::result::Check,
3+
solana_account::AccountSharedData, solana_instruction::Instruction, solana_pubkey::Pubkey,
4+
};
5+
6+
/// Execution builder with validation and signer testing
7+
pub struct InstructionExecution<'a, 'b> {
8+
instruction: Instruction,
9+
accounts: Vec<(Pubkey, AccountSharedData)>,
10+
ctx: &'a StakeTestContext,
11+
checks: Option<&'b [Check<'b>]>,
12+
test_missing_signers: Option<bool>, // `None` runs if `Check::success`
13+
}
14+
15+
impl<'b> InstructionExecution<'_, 'b> {
16+
pub fn checks(mut self, checks: &'b [Check<'b>]) -> Self {
17+
self.checks = Some(checks);
18+
self
19+
}
20+
21+
pub fn test_missing_signers(mut self, test: bool) -> Self {
22+
self.test_missing_signers = Some(test);
23+
self
24+
}
25+
26+
/// Executes the instruction. If `checks` is `None` or empty, uses `Check::success()`.
27+
/// Fail-safe default: when `test_missing_signers` is `None`, runs the missing-signers
28+
/// test (`true`). Callers must explicitly opt out with `.test_missing_signers(false)`.
29+
pub fn execute(self) -> mollusk_svm::result::InstructionResult {
30+
let default_checks = [Check::success()];
31+
let checks = match self.checks {
32+
Some(c) if !c.is_empty() => c,
33+
_ => &default_checks,
34+
};
35+
36+
let test_missing_signers = self.test_missing_signers.unwrap_or(true);
37+
38+
self.ctx.process_instruction_maybe_test_signers(
39+
&self.instruction,
40+
self.accounts,
41+
checks,
42+
test_missing_signers,
43+
)
44+
}
45+
}
46+
47+
impl<'a> InstructionExecution<'a, '_> {
48+
pub(crate) fn new(
49+
instruction: Instruction,
50+
accounts: Vec<(Pubkey, AccountSharedData)>,
51+
ctx: &'a StakeTestContext,
52+
) -> Self {
53+
Self {
54+
instruction,
55+
accounts,
56+
ctx,
57+
checks: None,
58+
test_missing_signers: None,
59+
}
60+
}
61+
}

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)