-
Notifications
You must be signed in to change notification settings - Fork 32
[REPLACED] Mollusk tests, part 1: Initialize, InitializeChecked, harness #150
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,112 @@ | ||
| use { | ||
| super::{ | ||
| instruction_builders::{InstructionConfig, InstructionExecution}, | ||
| lifecycle::StakeLifecycle, | ||
| utils::{add_sysvars, STAKE_RENT_EXEMPTION}, | ||
| }, | ||
| mollusk_svm::{result::Check, Mollusk}, | ||
| solana_account::AccountSharedData, | ||
| solana_instruction::Instruction, | ||
| solana_pubkey::Pubkey, | ||
| solana_stake_program::id, | ||
| }; | ||
|
|
||
| /// Builder for creating stake accounts with customizable parameters | ||
| pub struct StakeAccountBuilder { | ||
| lifecycle: StakeLifecycle, | ||
| } | ||
|
|
||
| impl StakeAccountBuilder { | ||
| pub fn build(self) -> (Pubkey, AccountSharedData) { | ||
| let stake_pubkey = Pubkey::new_unique(); | ||
| let account = self.lifecycle.create_uninitialized_account(); | ||
| (stake_pubkey, account) | ||
| } | ||
| } | ||
|
|
||
| /// Consolidated test context for stake account tests | ||
| pub struct StakeTestContext { | ||
| pub mollusk: Mollusk, | ||
| pub rent_exempt_reserve: u64, | ||
| pub staker: Pubkey, | ||
| pub withdrawer: Pubkey, | ||
| } | ||
|
|
||
| impl StakeTestContext { | ||
| /// Create a new test context with all standard setup | ||
| pub fn new() -> Self { | ||
| let mollusk = Mollusk::new(&id(), "solana_stake_program"); | ||
|
|
||
| Self { | ||
| mollusk, | ||
| rent_exempt_reserve: STAKE_RENT_EXEMPTION, | ||
| staker: Pubkey::new_unique(), | ||
| withdrawer: Pubkey::new_unique(), | ||
| } | ||
| } | ||
|
|
||
| /// Create a stake account builder for the specified lifecycle stage | ||
| /// | ||
| /// Example: | ||
| /// ``` | ||
| /// let (stake, account) = ctx | ||
| /// .stake_account(StakeLifecycle::Uninitialized) | ||
| /// .build(); | ||
| /// ``` | ||
| pub fn stake_account(&mut self, lifecycle: StakeLifecycle) -> StakeAccountBuilder { | ||
| StakeAccountBuilder { lifecycle } | ||
| } | ||
|
|
||
| /// Process an instruction | ||
| pub fn process_with<'b, C: InstructionConfig>( | ||
| &self, | ||
| config: C, | ||
| ) -> InstructionExecution<'_, 'b> { | ||
| InstructionExecution::new( | ||
| config.build_instruction(self), | ||
| config.build_accounts(), | ||
| self, | ||
| ) | ||
| } | ||
|
|
||
| /// Process an instruction with optional missing signer testing | ||
| pub(crate) fn process_instruction_maybe_test_signers( | ||
| &self, | ||
| instruction: &Instruction, | ||
| accounts: Vec<(Pubkey, AccountSharedData)>, | ||
| checks: &[Check], | ||
| test_missing_signers: bool, | ||
| ) -> mollusk_svm::result::InstructionResult { | ||
| if test_missing_signers { | ||
| use solana_program_error::ProgramError; | ||
|
|
||
| // Test that removing each signer causes failure | ||
| for i in 0..instruction.accounts.len() { | ||
| if instruction.accounts[i].is_signer { | ||
| let mut modified_instruction = instruction.clone(); | ||
| modified_instruction.accounts[i].is_signer = false; | ||
|
|
||
| let accounts_with_sysvars = | ||
| add_sysvars(&self.mollusk, &modified_instruction, accounts.clone()); | ||
|
|
||
| self.mollusk.process_and_validate_instruction( | ||
| &modified_instruction, | ||
| &accounts_with_sysvars, | ||
| &[Check::err(ProgramError::MissingRequiredSignature)], | ||
| ); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Process with all signers present | ||
| let accounts_with_sysvars = add_sysvars(&self.mollusk, instruction, accounts); | ||
| self.mollusk | ||
| .process_and_validate_instruction(instruction, &accounts_with_sysvars, checks) | ||
| } | ||
| } | ||
|
|
||
| impl Default for StakeTestContext { | ||
| fn default() -> Self { | ||
| Self::new() | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,105 @@ | ||
| use { | ||
| super::context::StakeTestContext, | ||
| mollusk_svm::result::Check, | ||
| solana_account::AccountSharedData, | ||
| solana_instruction::Instruction, | ||
| solana_pubkey::Pubkey, | ||
| solana_stake_interface::{ | ||
| instruction as ixn, | ||
| state::{Authorized, Lockup}, | ||
| }, | ||
| }; | ||
|
|
||
| // Trait for instruction configuration that builds instruction and accounts | ||
| pub trait InstructionConfig { | ||
| fn build_instruction(&self, ctx: &StakeTestContext) -> Instruction; | ||
| fn build_accounts(&self) -> Vec<(Pubkey, AccountSharedData)>; | ||
| } | ||
|
|
||
| /// Execution builder with validation and signer testing | ||
| pub struct InstructionExecution<'a, 'b> { | ||
| instruction: Instruction, | ||
| accounts: Vec<(Pubkey, AccountSharedData)>, | ||
| ctx: &'a StakeTestContext, | ||
| checks: Option<&'b [Check<'b>]>, | ||
| test_missing_signers: Option<bool>, // `None` runs if `Check::success` | ||
| } | ||
|
|
||
| impl<'b> InstructionExecution<'_, 'b> { | ||
| pub fn checks(mut self, checks: &'b [Check<'b>]) -> Self { | ||
| self.checks = Some(checks); | ||
| self | ||
| } | ||
|
|
||
| pub fn test_missing_signers(mut self, test: bool) -> Self { | ||
| self.test_missing_signers = Some(test); | ||
| self | ||
| } | ||
|
|
||
| /// Executes the instruction. If `checks` is `None` or empty, uses `Check::success()`. | ||
| /// Fail-safe default: when `test_missing_signers` is `None`, runs the missing-signers | ||
| /// test (`true`). Callers must explicitly opt out with `.test_missing_signers(false)`. | ||
| pub fn execute(self) -> mollusk_svm::result::InstructionResult { | ||
| let default_checks = [Check::success()]; | ||
| let checks = match self.checks { | ||
| Some(c) if !c.is_empty() => c, | ||
| _ => &default_checks, | ||
| }; | ||
|
|
||
| let test_missing_signers = self.test_missing_signers.unwrap_or(true); | ||
|
|
||
| self.ctx.process_instruction_maybe_test_signers( | ||
| &self.instruction, | ||
| self.accounts, | ||
| checks, | ||
| test_missing_signers, | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| impl<'a> InstructionExecution<'a, '_> { | ||
| pub(crate) fn new( | ||
| instruction: Instruction, | ||
| accounts: Vec<(Pubkey, AccountSharedData)>, | ||
| ctx: &'a StakeTestContext, | ||
| ) -> Self { | ||
| Self { | ||
| instruction, | ||
| accounts, | ||
| ctx, | ||
| checks: None, | ||
| test_missing_signers: None, | ||
| } | ||
| } | ||
| } | ||
|
|
||
| pub struct InitializeConfig<'a> { | ||
| pub stake: (&'a Pubkey, &'a AccountSharedData), | ||
| pub authorized: &'a Authorized, | ||
| pub lockup: &'a Lockup, | ||
| } | ||
|
|
||
| impl InstructionConfig for InitializeConfig<'_> { | ||
| fn build_instruction(&self, _ctx: &StakeTestContext) -> Instruction { | ||
| ixn::initialize(self.stake.0, self.authorized, self.lockup) | ||
| } | ||
|
|
||
| fn build_accounts(&self) -> Vec<(Pubkey, AccountSharedData)> { | ||
| vec![(*self.stake.0, self.stake.1.clone())] | ||
| } | ||
| } | ||
|
|
||
| pub struct InitializeCheckedConfig<'a> { | ||
| pub stake: (&'a Pubkey, &'a AccountSharedData), | ||
| pub authorized: &'a Authorized, | ||
| } | ||
|
|
||
| impl InstructionConfig for InitializeCheckedConfig<'_> { | ||
| fn build_instruction(&self, _ctx: &StakeTestContext) -> Instruction { | ||
| ixn::initialize_checked(self.stake.0, self.authorized) | ||
| } | ||
|
|
||
| fn build_accounts(&self) -> Vec<(Pubkey, AccountSharedData)> { | ||
| vec![(*self.stake.0, self.stake.1.clone())] | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| use { | ||
| super::utils::STAKE_RENT_EXEMPTION, solana_account::AccountSharedData, | ||
| solana_stake_interface::state::StakeStateV2, solana_stake_program::id, | ||
| }; | ||
|
|
||
| /// Lifecycle states for stake accounts in tests | ||
| #[allow(dead_code)] // TODO: remove once tests include all lifecycles | ||
| #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] | ||
| pub enum StakeLifecycle { | ||
| Uninitialized = 0, | ||
| Initialized, | ||
| Activating, | ||
| Active, | ||
| Deactivating, | ||
| Deactive, | ||
| Closed, | ||
| } | ||
|
|
||
| impl StakeLifecycle { | ||
| /// Create an uninitialized stake account | ||
| pub fn create_uninitialized_account(self) -> AccountSharedData { | ||
| AccountSharedData::new_data_with_space( | ||
| STAKE_RENT_EXEMPTION, | ||
| &StakeStateV2::Uninitialized, | ||
| StakeStateV2::size_of(), | ||
| &id(), | ||
| ) | ||
| .unwrap() | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| #![allow(clippy::arithmetic_side_effects)] | ||
|
|
||
| pub mod context; | ||
| pub mod instruction_builders; | ||
| pub mod lifecycle; | ||
| pub mod utils; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| use { | ||
| mollusk_svm::Mollusk, | ||
| solana_account::{Account, AccountSharedData}, | ||
| solana_instruction::Instruction, | ||
| solana_pubkey::Pubkey, | ||
| solana_rent::Rent, | ||
| solana_stake_interface::{stake_history::StakeHistory, state::StakeStateV2}, | ||
| solana_sysvar_id::SysvarId, | ||
| std::collections::HashMap, | ||
| }; | ||
|
|
||
| // hardcoded for convenience | ||
| pub const STAKE_RENT_EXEMPTION: u64 = 2_282_880; | ||
|
|
||
| #[test] | ||
| fn assert_stake_rent_exemption() { | ||
| assert_eq!( | ||
| Rent::default().minimum_balance(StakeStateV2::size_of()), | ||
| STAKE_RENT_EXEMPTION | ||
| ); | ||
| } | ||
|
|
||
| /// Resolve all accounts for an instruction, including sysvars and instruction accounts | ||
| /// | ||
| /// This function re-serializes the stake history sysvar from mollusk.sysvars.stake_history | ||
| /// every time it's called, ensuring that any updates to the stake history are reflected in the accounts. | ||
| pub fn add_sysvars( | ||
| mollusk: &Mollusk, | ||
| instruction: &Instruction, | ||
| accounts: Vec<(Pubkey, AccountSharedData)>, | ||
| ) -> Vec<(Pubkey, Account)> { | ||
| // Build a map of provided accounts | ||
| let mut account_map: HashMap<Pubkey, Account> = accounts | ||
| .into_iter() | ||
| .map(|(pk, acc)| (pk, acc.into())) | ||
| .collect(); | ||
|
|
||
| // Now resolve all accounts from the instruction | ||
| let mut result = Vec::new(); | ||
| for account_meta in &instruction.accounts { | ||
| let key = account_meta.pubkey; | ||
| let account = if let Some(acc) = account_map.remove(&key) { | ||
| // Use the provided account | ||
| acc | ||
| } else if Rent::check_id(&key) { | ||
| mollusk.sysvars.keyed_account_for_rent_sysvar().1 | ||
| } else if solana_clock::Clock::check_id(&key) { | ||
| mollusk.sysvars.keyed_account_for_clock_sysvar().1 | ||
| } else if solana_epoch_schedule::EpochSchedule::check_id(&key) { | ||
| mollusk.sysvars.keyed_account_for_epoch_schedule_sysvar().1 | ||
| } else if solana_epoch_rewards::EpochRewards::check_id(&key) { | ||
| mollusk.sysvars.keyed_account_for_epoch_rewards_sysvar().1 | ||
| } else if StakeHistory::check_id(&key) { | ||
| // Re-serialize stake history from mollusk.sysvars.stake_history | ||
| mollusk.sysvars.keyed_account_for_stake_history_sysvar().1 | ||
| } else { | ||
| // Default empty account | ||
| Account::default() | ||
| }; | ||
|
|
||
| result.push((key, account)); | ||
| } | ||
|
|
||
| result | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The idea here is to make test-writing safer.
test_missing_signers, where the instruction is run with every signer missing to ensureMissingRequiredSignature, is now the default, and must be explicitly disabled with.test_missing_signers(false)This prevents a test writer from simply forgetting to test all missing signers.
.test_missing_signers(true)is still used to keep test intentions transparent.