* MockInvokeContext::get_programs() implementation (cherry picked from commit8acc47ee1b
) * start_local_server() now works with Banks > 0 (cherry picked from commitfa4bab4608
) * Add solana-program-test crate (cherry picked from commit52a292a75b
) * rebase Co-authored-by: Michael Vines <mvines@gmail.com>
This commit is contained in:
32
Cargo.lock
generated
32
Cargo.lock
generated
@ -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"
|
||||
|
@ -38,6 +38,7 @@ members = [
|
||||
"net-shaper",
|
||||
"notifier",
|
||||
"poh-bench",
|
||||
"program-test",
|
||||
"programs/secp256k1",
|
||||
"programs/bpf_loader",
|
||||
"programs/budget",
|
||||
|
@ -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))
|
||||
|
21
program-test/Cargo.toml
Normal file
21
program-test/Cargo.toml
Normal file
@ -0,0 +1,21 @@
|
||||
[package]
|
||||
authors = ["Solana Maintainers <maintainers@solana.foundation>"]
|
||||
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" }
|
612
program-test/src/lib.rs
Normal file
612
program-test/src/lib.rs
Normal file
@ -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<Rc<MockInvokeContext>> = 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<Pubkey, Account> = 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<AccountInfo> = 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<Rc<MockInvokeContext>>) {
|
||||
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<PathBuf> {
|
||||
for path in &["", "tests/fixtures"] {
|
||||
let candidate = Path::new(path).join(&filename);
|
||||
if candidate.exists() {
|
||||
return Some(candidate);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn read_file<P: AsRef<Path>>(path: P) -> Vec<u8> {
|
||||
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<Builtin>,
|
||||
bpf_compute_max_units: Option<u64>,
|
||||
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<ProcessInstructionWithContext>,
|
||||
) -> 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<ProcessInstructionWithContext>,
|
||||
) {
|
||||
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)
|
||||
}
|
||||
}
|
@ -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<RefCell<dyn Logger>> {
|
||||
Rc::new(RefCell::new(self.logger.clone()))
|
||||
|
Reference in New Issue
Block a user