From b6dc48da75f54981d1e8196f893581f43e7492b3 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sun, 1 Nov 2020 05:43:43 +0000 Subject: [PATCH] Add solana-program-test crate (bp #13324) (#13329) * MockInvokeContext::get_programs() implementation (cherry picked from commit 8acc47ee1b687a0a572effa0af2fd9c96705a909) * start_local_server() now works with Banks > 0 (cherry picked from commit fa4bab4608f6dffdfa0c5d1f3055a23ec2c7bb1e) * Add solana-program-test crate (cherry picked from commit 52a292a75b8df10022c53b26658b8aaeb2427dfe) * rebase Co-authored-by: Michael Vines --- Cargo.lock | 32 +- Cargo.toml | 1 + banks-server/src/banks_server.rs | 15 +- program-test/Cargo.toml | 21 ++ program-test/src/lib.rs | 612 +++++++++++++++++++++++++++++++ sdk/src/process_instruction.rs | 5 +- 6 files changed, 671 insertions(+), 15 deletions(-) create mode 100644 program-test/Cargo.toml create mode 100644 program-test/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index f35cce3adb..830dfa1960 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -499,14 +499,25 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.11" +version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80094f509cf8b5ae86a4966a39b3ff66cd7e2a3e594accec3743ff3fabeab5b2" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" dependencies = [ + "libc", "num-integer", "num-traits", "serde", "time 0.1.43", + "winapi 0.3.8", +] + +[[package]] +name = "chrono-humanize" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0a4c32145b4db85fe1c4f2b125a4f9493769df424f5f84baf6b04ea8eaf33c9" +dependencies = [ + "chrono", ] [[package]] @@ -4516,6 +4527,23 @@ dependencies = [ "thiserror", ] +[[package]] +name = "solana-program-test" +version = "1.4.4" +dependencies = [ + "base64 0.12.3", + "chrono", + "chrono-humanize", + "log 0.4.8", + "solana-banks-client", + "solana-banks-server", + "solana-bpf-loader-program", + "solana-logger 1.4.4", + "solana-program", + "solana-runtime", + "solana-sdk 1.4.4", +] + [[package]] name = "solana-ramp-tps" version = "1.4.4" diff --git a/Cargo.toml b/Cargo.toml index 310c4c50b2..03ed4b4d93 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,7 @@ members = [ "net-shaper", "notifier", "poh-bench", + "program-test", "programs/secp256k1", "programs/bpf_loader", "programs/budget", diff --git a/banks-server/src/banks_server.rs b/banks-server/src/banks_server.rs index 9d08308fc8..4c8ebf052b 100644 --- a/banks-server/src/banks_server.rs +++ b/banks-server/src/banks_server.rs @@ -5,11 +5,7 @@ use futures::{ prelude::stream::{self, StreamExt}, }; use solana_banks_interface::{Banks, BanksRequest, BanksResponse, TransactionStatus}; -use solana_runtime::{ - bank::Bank, - bank_forks::BankForks, - commitment::{BlockCommitmentCache, CommitmentSlots}, -}; +use solana_runtime::{bank::Bank, bank_forks::BankForks, commitment::BlockCommitmentCache}; use solana_sdk::{ account::Account, clock::Slot, @@ -21,7 +17,6 @@ use solana_sdk::{ transaction::{self, Transaction}, }; use std::{ - collections::HashMap, io, net::{Ipv4Addr, SocketAddr}, sync::{ @@ -84,11 +79,9 @@ impl BanksServer { let (transaction_sender, transaction_receiver) = channel(); let bank = bank_forks.read().unwrap().working_bank(); let slot = bank.slot(); - let block_commitment_cache = Arc::new(RwLock::new(BlockCommitmentCache::new( - HashMap::default(), - 0, - CommitmentSlots::new_from_slot(slot), - ))); + let block_commitment_cache = Arc::new(RwLock::new( + BlockCommitmentCache::new_for_tests_with_slots(slot, slot), + )); Builder::new() .name("solana-bank-forks-client".to_string()) .spawn(move || Self::run(&bank, transaction_receiver)) diff --git a/program-test/Cargo.toml b/program-test/Cargo.toml new file mode 100644 index 0000000000..c1da145a34 --- /dev/null +++ b/program-test/Cargo.toml @@ -0,0 +1,21 @@ +[package] +authors = ["Solana Maintainers "] +description = "Solana Program Test Framework" +edition = "2018" +license = "Apache-2.0" +name = "solana-program-test" +repository = "https://github.com/solana-labs/solana" +version = "1.4.4" + +[dependencies] +base64 = "0.12.3" +chrono = "0.4.19" +chrono-humanize = "0.1.1" +log = "0.4.8" +solana-banks-client = { path = "../banks-client", version = "1.4.4" } +solana-banks-server = { path = "../banks-server", version = "1.4.4" } +solana-bpf-loader-program = { path = "../programs/bpf_loader", version = "1.4.4" } +solana-logger = { path = "../logger", version = "1.4.4" } +solana-program = { path = "../sdk/program", version = "1.4.4" } +solana-runtime = { path = "../runtime", version = "1.4.4" } +solana-sdk = { path = "../sdk", version = "1.4.4" } diff --git a/program-test/src/lib.rs b/program-test/src/lib.rs new file mode 100644 index 0000000000..73434a9436 --- /dev/null +++ b/program-test/src/lib.rs @@ -0,0 +1,612 @@ +//! The solana-program-test provides a BanksClient-based test framework BPF programs + +use chrono_humanize::{Accuracy, HumanTime, Tense}; +use log::*; +use solana_banks_client::start_client; +use solana_banks_server::banks_server::start_local_server; +use solana_program::{ + account_info::AccountInfo, entrypoint::ProgramResult, hash::Hash, instruction::Instruction, + instruction::InstructionError, message::Message, native_token::sol_to_lamports, + program_error::ProgramError, program_stubs, pubkey::Pubkey, rent::Rent, +}; +use solana_runtime::{ + bank::{Bank, Builtin}, + bank_forks::BankForks, + genesis_utils::create_genesis_config_with_leader, +}; +use solana_sdk::{ + account::Account, + keyed_account::KeyedAccount, + process_instruction::BpfComputeBudget, + process_instruction::{InvokeContext, MockInvokeContext, ProcessInstructionWithContext}, + signature::{Keypair, Signer}, +}; +use std::{ + cell::RefCell, + collections::HashMap, + convert::TryFrom, + fs::File, + io::Read, + path::{Path, PathBuf}, + rc::Rc, + sync::{Arc, RwLock}, +}; + +// Export types so test clients can limit their solana crate dependencies +pub use solana_banks_client::{BanksClient, BanksClientExt}; + +#[macro_use] +extern crate solana_bpf_loader_program; + +pub fn to_instruction_error(error: ProgramError) -> InstructionError { + match error { + ProgramError::Custom(err) => InstructionError::Custom(err), + ProgramError::InvalidArgument => InstructionError::InvalidArgument, + ProgramError::InvalidInstructionData => InstructionError::InvalidInstructionData, + ProgramError::InvalidAccountData => InstructionError::InvalidAccountData, + ProgramError::AccountDataTooSmall => InstructionError::AccountDataTooSmall, + ProgramError::InsufficientFunds => InstructionError::InsufficientFunds, + ProgramError::IncorrectProgramId => InstructionError::IncorrectProgramId, + ProgramError::MissingRequiredSignature => InstructionError::MissingRequiredSignature, + ProgramError::AccountAlreadyInitialized => InstructionError::AccountAlreadyInitialized, + ProgramError::UninitializedAccount => InstructionError::UninitializedAccount, + ProgramError::NotEnoughAccountKeys => InstructionError::NotEnoughAccountKeys, + ProgramError::AccountBorrowFailed => InstructionError::AccountBorrowFailed, + ProgramError::MaxSeedLengthExceeded => InstructionError::MaxSeedLengthExceeded, + ProgramError::InvalidSeeds => InstructionError::InvalidSeeds, + } +} + +thread_local! { + static INVOKE_CONTEXT:RefCell> = RefCell::new(Rc::new(MockInvokeContext::default())); +} + +pub fn builtin_process_instruction( + process_instruction: solana_program::entrypoint::ProcessInstruction, + program_id: &Pubkey, + keyed_accounts: &[KeyedAccount], + input: &[u8], + invoke_context: &mut dyn InvokeContext, +) -> Result<(), InstructionError> { + let mut mock_invoke_context = MockInvokeContext::default(); + mock_invoke_context.programs = invoke_context.get_programs().to_vec(); + mock_invoke_context.key = *program_id; + // TODO: Populate MockInvokeContext more, or rework to avoid MockInvokeContext entirely. + // The context being passed into the program is incomplete... + let local_invoke_context = RefCell::new(Rc::new(mock_invoke_context)); + swap_invoke_context(&local_invoke_context); + + // Copy all the accounts into a HashMap to ensure there are no duplicates + let mut accounts: HashMap = keyed_accounts + .iter() + .map(|ka| (*ka.unsigned_key(), ka.account.borrow().clone())) + .collect(); + + // Create shared references to each account's lamports/data/owner + let account_refs: HashMap<_, _> = accounts + .iter_mut() + .map(|(key, account)| { + ( + *key, + ( + Rc::new(RefCell::new(&mut account.lamports)), + Rc::new(RefCell::new(&mut account.data[..])), + &account.owner, + ), + ) + }) + .collect(); + + // Create AccountInfos + let account_infos: Vec = keyed_accounts + .iter() + .map(|keyed_account| { + let key = keyed_account.unsigned_key(); + let (lamports, data, owner) = &account_refs[key]; + AccountInfo { + key, + is_signer: keyed_account.signer_key().is_some(), + is_writable: keyed_account.is_writable(), + lamports: lamports.clone(), + data: data.clone(), + owner, + executable: keyed_account.executable().unwrap(), + rent_epoch: keyed_account.rent_epoch().unwrap(), + } + }) + .collect(); + + // Execute the BPF entrypoint + let result = + process_instruction(program_id, &account_infos, input).map_err(to_instruction_error); + + if result.is_ok() { + // Commit changes to the KeyedAccounts + for keyed_account in keyed_accounts { + let mut account = keyed_account.account.borrow_mut(); + let key = keyed_account.unsigned_key(); + let (lamports, data, _owner) = &account_refs[key]; + account.lamports = **lamports.borrow(); + account.data = data.borrow().to_vec(); + } + } + + swap_invoke_context(&local_invoke_context); + + // Propagate logs back to caller's invoke context + // (TODO: This goes away if MockInvokeContext usage can be removed) + let logger = invoke_context.get_logger(); + let logger = logger.borrow_mut(); + for message in local_invoke_context.borrow().logger.log.borrow_mut().iter() { + if logger.log_enabled() { + logger.log(message); + } + } + + result +} + +/// Converts a `solana-program`-style entrypoint into the runtime's entrypoint style, for +/// use with `ProgramTest::add_program` +#[macro_export] +macro_rules! processor { + ($process_instruction:expr) => { + Some( + |program_id: &Pubkey, + keyed_accounts: &[solana_sdk::keyed_account::KeyedAccount], + input: &[u8], + invoke_context: &mut dyn solana_sdk::process_instruction::InvokeContext| { + $crate::builtin_process_instruction( + $process_instruction, + program_id, + keyed_accounts, + input, + invoke_context, + ) + }, + ) + }; +} + +pub fn swap_invoke_context(other_invoke_context: &RefCell>) { + INVOKE_CONTEXT.with(|invoke_context| { + invoke_context.swap(&other_invoke_context); + }); +} + +struct SyscallStubs {} +impl program_stubs::SyscallStubs for SyscallStubs { + fn sol_log(&self, message: &str) { + INVOKE_CONTEXT.with(|invoke_context| { + let invoke_context = invoke_context.borrow_mut(); + let logger = invoke_context.get_logger(); + let logger = logger.borrow_mut(); + + if logger.log_enabled() { + logger.log(&format!("Program log: {}", message)); + } + }); + } + + fn sol_invoke_signed( + &self, + instruction: &Instruction, + account_infos: &[AccountInfo], + signers_seeds: &[&[&[u8]]], + ) -> ProgramResult { + // + // TODO: Merge the business logic between here and the BPF invoke path in + // programs/bpf_loader/src/syscalls.rs + // + info!("SyscallStubs::sol_invoke_signed()"); + + let mut caller = Pubkey::default(); + let mut mock_invoke_context = MockInvokeContext::default(); + + INVOKE_CONTEXT.with(|invoke_context| { + let invoke_context = invoke_context.borrow_mut(); + caller = *invoke_context.get_caller().expect("get_caller"); + invoke_context.record_instruction(&instruction); + + mock_invoke_context.programs = invoke_context.get_programs().to_vec(); + // TODO: Populate MockInvokeContext more, or rework to avoid MockInvokeContext entirely. + // The context being passed into the program is incomplete... + }); + + if instruction.accounts.len() + 1 != account_infos.len() { + panic!( + "Instruction accounts mismatch. Instruction contains {} accounts, with {} + AccountInfos provided", + instruction.accounts.len(), + account_infos.len() + ); + } + let message = Message::new(&[instruction.clone()], None); + + let program_id_index = message.instructions[0].program_id_index as usize; + let program_id = message.account_keys[program_id_index]; + + let program_account_info = &account_infos[program_id_index]; + if !program_account_info.executable { + panic!("Program account is not executable"); + } + if program_account_info.is_writable { + panic!("Program account is writable"); + } + + fn ai_to_a(ai: &AccountInfo) -> Account { + Account { + lamports: ai.lamports(), + data: ai.try_borrow_data().unwrap().to_vec(), + owner: *ai.owner, + executable: ai.executable, + rent_epoch: ai.rent_epoch, + } + } + let executable_accounts = vec![(program_id, RefCell::new(ai_to_a(program_account_info)))]; + + let mut accounts = vec![]; + for instruction_account in &instruction.accounts { + for account_info in account_infos { + if *account_info.unsigned_key() == instruction_account.pubkey { + if instruction_account.is_writable && !account_info.is_writable { + panic!("Writeable mismatch for {}", instruction_account.pubkey); + } + if instruction_account.is_signer && !account_info.is_signer { + let mut program_signer = false; + for seeds in signers_seeds.iter() { + let signer = Pubkey::create_program_address(&seeds, &caller).unwrap(); + if instruction_account.pubkey == signer { + program_signer = true; + break; + } + } + if !program_signer { + panic!("Signer mismatch for {}", instruction_account.pubkey); + } + } + accounts.push(Rc::new(RefCell::new(ai_to_a(account_info)))); + break; + } + } + } + assert_eq!(accounts.len(), instruction.accounts.len()); + + solana_runtime::message_processor::MessageProcessor::process_cross_program_instruction( + &message, + &executable_accounts, + &accounts, + &mut mock_invoke_context, + ) + .map_err(|err| ProgramError::try_from(err).unwrap_or_else(|err| panic!("{}", err)))?; + + // Propagate logs back to caller's invoke context + // (TODO: This goes away if MockInvokeContext usage can be removed) + INVOKE_CONTEXT.with(|invoke_context| { + let logger = invoke_context.borrow().get_logger(); + let logger = logger.borrow_mut(); + for message in mock_invoke_context.logger.log.borrow_mut().iter() { + if logger.log_enabled() { + logger.log(message); + } + } + }); + + // Copy writeable account modifications back into the caller's AccountInfos + for (i, instruction_account) in instruction.accounts.iter().enumerate() { + if !instruction_account.is_writable { + continue; + } + + for account_info in account_infos { + if *account_info.unsigned_key() == instruction_account.pubkey { + let account = &accounts[i]; + **account_info.try_borrow_mut_lamports().unwrap() = account.borrow().lamports; + + let mut data = account_info.try_borrow_mut_data()?; + let new_data = &account.borrow().data; + if data.len() != new_data.len() { + // TODO: Figure out how to change the callers account data size + panic!( + "Account resizing ({} -> {}) not supported yet", + data.len(), + new_data.len() + ); + } + data.clone_from_slice(new_data); + } + } + } + + Ok(()) + } +} + +fn find_file(filename: &str) -> Option { + for path in &["", "tests/fixtures"] { + let candidate = Path::new(path).join(&filename); + if candidate.exists() { + return Some(candidate); + } + } + None +} + +fn read_file>(path: P) -> Vec { + let path = path.as_ref(); + let mut file = File::open(path) + .unwrap_or_else(|err| panic!("Failed to open \"{}\": {}", path.display(), err)); + + let mut file_data = Vec::new(); + file.read_to_end(&mut file_data) + .unwrap_or_else(|err| panic!("Failed to read \"{}\": {}", path.display(), err)); + file_data +} + +pub struct ProgramTest { + accounts: Vec<(Pubkey, Account)>, + builtins: Vec, + bpf_compute_max_units: Option, + prefer_bpf: bool, +} + +impl Default for ProgramTest { + /// Initialize a new ProgramTest + /// + /// The `bpf` environment variable controls how BPF programs are selected during operation: + /// `export bpf=1` -- use BPF programs if present, otherwise fall back to the + /// native instruction processors provided with the test + /// `export bpf=0` -- use native instruction processor if present, otherwise fall back to + /// the BPF program + /// (default) + /// and the `ProgramTest::prefer_bpf()` method may be used to override the selection at runtime + /// + /// BPF program shared objects and account data files are searched for in + /// * the current working directory (the default output location for `cargo build-bpf), + /// * the `tests/fixtures` sub-directory + /// + fn default() -> Self { + solana_logger::setup_with_default( + "solana_bpf_loader=debug,\ + solana_rbpf::vm=debug,\ + solana_runtime::message_processor=info,\ + solana_runtime::system_instruction_processor=trace,\ + solana_program_test=info", + ); + let prefer_bpf = match std::env::var("bpf") { + Ok(val) => !matches!(val.as_str(), "0" | ""), + Err(_err) => false, + }; + + Self { + accounts: vec![], + builtins: vec![], + bpf_compute_max_units: None, + prefer_bpf, + } + } +} + +impl ProgramTest { + pub fn new( + program_name: &str, + program_id: Pubkey, + process_instruction: Option, + ) -> Self { + let mut me = Self::default(); + me.add_program(program_name, program_id, process_instruction); + me + } + + /// Override default BPF program selection + pub fn prefer_bpf(&mut self, prefer_bpf: bool) { + self.prefer_bpf = prefer_bpf; + } + + /// Override the BPF compute budget + pub fn set_bpf_compute_max_units(&mut self, bpf_compute_max_units: u64) { + self.bpf_compute_max_units = Some(bpf_compute_max_units); + } + + /// Add an account to the test environment + pub fn add_account(&mut self, address: Pubkey, account: Account) { + self.accounts.push((address, account)); + } + + /// Add an account to the test environment with the account data in the provided `filename` + pub fn add_account_with_file_data( + &mut self, + address: Pubkey, + lamports: u64, + owner: Pubkey, + filename: &str, + ) { + self.add_account( + address, + Account { + lamports, + data: read_file(find_file(filename).unwrap_or_else(|| { + panic!("Unable to locate {}", filename); + })), + owner, + executable: false, + rent_epoch: 0, + }, + ); + } + + /// Add an account to the test environment with the account data in the provided as a base 64 + /// string + pub fn add_account_with_base64_data( + &mut self, + address: Pubkey, + lamports: u64, + owner: Pubkey, + data_base64: &str, + ) { + self.add_account( + address, + Account { + lamports, + data: base64::decode(data_base64) + .unwrap_or_else(|err| panic!("Failed to base64 decode: {}", err)), + owner, + executable: false, + rent_epoch: 0, + }, + ); + } + + /// Add a BPF program to the test environment. + /// + /// `program_name` will also used to locate the BPF shared object in the current or fixtures + /// directory. + /// + /// If `process_instruction` is provided, the natively built-program may be used instead of the + /// BPF shared object depending on the `bpf` environment variable. + pub fn add_program( + &mut self, + program_name: &str, + program_id: Pubkey, + process_instruction: Option, + ) { + let loader = solana_program::bpf_loader::id(); + let program_file = find_file(&format!("{}.so", program_name)); + + if process_instruction.is_none() && program_file.is_none() { + panic!("Unable to add program {} ({})", program_name, program_id); + } + + if (program_file.is_some() && self.prefer_bpf) || process_instruction.is_none() { + let program_file = program_file.unwrap_or_else(|| { + panic!( + "Program file data not available for {} ({})", + program_name, program_id + ); + }); + let data = read_file(&program_file); + info!( + "\"{}\" BPF program from {}{}", + program_name, + program_file.display(), + std::fs::metadata(&program_file) + .map(|metadata| { + metadata + .modified() + .map(|time| { + format!( + ", modified {}", + HumanTime::from(time) + .to_text_en(Accuracy::Precise, Tense::Past) + ) + }) + .ok() + }) + .ok() + .flatten() + .unwrap_or_else(|| "".to_string()) + ); + + self.add_account( + program_id, + Account { + lamports: Rent::default().minimum_balance(data.len()).min(1), + data, + owner: loader, + executable: true, + rent_epoch: 0, + }, + ); + } else { + info!("\"{}\" program loaded as native code", program_name); + self.builtins.push(Builtin::new( + program_name, + program_id, + process_instruction.unwrap_or_else(|| { + panic!( + "Program processor not available for {} ({})", + program_name, program_id + ); + }), + )); + } + } + + /// Start the test client + /// + /// Returns a `BanksClient` interface into the test environment as well as a payer `Keypair` + /// with SOL for sending transactions + pub async fn start(self) -> (BanksClient, Keypair, Hash) { + { + use std::sync::Once; + static ONCE: Once = Once::new(); + + ONCE.call_once(|| { + program_stubs::set_syscall_stubs(Box::new(SyscallStubs {})); + }); + } + + let bootstrap_validator_pubkey = Pubkey::new_unique(); + let bootstrap_validator_stake_lamports = 42; + + let gci = create_genesis_config_with_leader( + sol_to_lamports(1_000_000.0), + &bootstrap_validator_pubkey, + bootstrap_validator_stake_lamports, + ); + let mut genesis_config = gci.genesis_config; + genesis_config.rent = Rent::default(); + genesis_config.fee_rate_governor = + solana_program::fee_calculator::FeeRateGovernor::default(); + let payer = gci.mint_keypair; + debug!("Payer address: {}", payer.pubkey()); + debug!("Genesis config: {}", genesis_config); + + let mut bank = Bank::new(&genesis_config); + + for loader in &[ + solana_bpf_loader_deprecated_program!(), + solana_bpf_loader_program!(), + ] { + bank.add_builtin(&loader.0, loader.1, loader.2); + } + + // User-supplied additional builtins + for builtin in self.builtins { + bank.add_builtin( + &builtin.name, + builtin.id, + builtin.process_instruction_with_context, + ); + } + + for (address, account) in self.accounts { + if bank.get_account(&address).is_some() { + panic!("An account at {} already exists", address); + } + bank.store_account(&address, &account); + } + bank.set_capitalization(); + if let Some(max_units) = self.bpf_compute_max_units { + bank.set_bpf_compute_budget(Some(BpfComputeBudget { + max_units, + ..BpfComputeBudget::default() + })); + } + + // Advance beyond slot 0 for a slightly more realistic test environment + let bank = Arc::new(bank); + let bank = Bank::new_from_parent(&bank, bank.collector_id(), bank.slot() + 1); + debug!("Bank slot: {}", bank.slot()); + + let bank_forks = Arc::new(RwLock::new(BankForks::new(bank))); + + let transport = start_local_server(&bank_forks).await; + let mut banks_client = start_client(transport) + .await + .unwrap_or_else(|err| panic!("Failed to start banks client: {}", err)); + + let recent_blockhash = banks_client.get_recent_blockhash().await.unwrap(); + (banks_client, payer, recent_blockhash) + } +} diff --git a/sdk/src/process_instruction.rs b/sdk/src/process_instruction.rs index 6b15f021f1..0785d10168 100644 --- a/sdk/src/process_instruction.rs +++ b/sdk/src/process_instruction.rs @@ -202,12 +202,12 @@ impl Logger for MockLogger { } } -#[derive(Debug)] pub struct MockInvokeContext { pub key: Pubkey, pub logger: MockLogger, pub bpf_compute_budget: BpfComputeBudget, pub compute_meter: MockComputeMeter, + pub programs: Vec<(Pubkey, ProcessInstructionWithContext)>, } impl Default for MockInvokeContext { fn default() -> Self { @@ -218,6 +218,7 @@ impl Default for MockInvokeContext { compute_meter: MockComputeMeter { remaining: std::i64::MAX as u64, }, + programs: vec![], } } } @@ -238,7 +239,7 @@ impl InvokeContext for MockInvokeContext { Ok(&self.key) } fn get_programs(&self) -> &[(Pubkey, ProcessInstructionWithContext)] { - &[] + &self.programs } fn get_logger(&self) -> Rc> { Rc::new(RefCell::new(self.logger.clone()))