mirror of
https://github.com/FuelLabs/sway.git
synced 2025-08-10 21:58:27 +00:00
Refactor forc-test to expose VM interpreter (#5409)
## Description Related https://github.com/FuelLabs/sway/issues/2350 This refactor is not intended to change any behavior. It adds `TestExecutor` which has a handle to `vm::Interpreter`. This will be used to access the debugger within the VM. Also moved the "setup" related code into a new file to make the code more organized. ## Checklist - [ ] I have linked to any relevant issues. - [ ] I have commented my code, particularly in hard-to-understand areas. - [ ] I have updated the documentation where relevant (API docs, the reference, and the Sway book). - [ ] I have added tests that prove my fix is effective or that my feature works. - [ ] I have added (or requested a maintainer to add) the necessary `Breaking*` or `New Feature` labels where relevant. - [ ] I have done my best to ensure that my PR adheres to [the Fuel Labs Code Review Standards](https://github.com/FuelLabs/rfcs/blob/master/text/code-standards/external-contributors.md). - [ ] I have requested a review from the relevant team or maintainers. --------- Co-authored-by: Joshua Batty <joshpbatty@gmail.com>
This commit is contained in:
parent
fe65ca46dd
commit
d3da28d37c
3 changed files with 304 additions and 258 deletions
178
forc-test/src/execute.rs
Normal file
178
forc-test/src/execute.rs
Normal file
|
@ -0,0 +1,178 @@
|
||||||
|
use crate::setup::TestSetup;
|
||||||
|
use crate::TestResult;
|
||||||
|
use crate::TEST_METADATA_SEED;
|
||||||
|
use forc_pkg::PkgTestEntry;
|
||||||
|
use fuel_tx::{self as tx, output::contract::Contract, Chargeable, Finalizable};
|
||||||
|
use fuel_vm::error::InterpreterError;
|
||||||
|
use fuel_vm::{
|
||||||
|
self as vm,
|
||||||
|
checked_transaction::builder::TransactionBuilderExt,
|
||||||
|
interpreter::{Interpreter, NotSupportedEcal},
|
||||||
|
prelude::{Instruction, SecretKey},
|
||||||
|
storage::MemoryStorage,
|
||||||
|
};
|
||||||
|
use rand::{Rng, SeedableRng};
|
||||||
|
|
||||||
|
/// An interface for executing a test within a VM [Interpreter] instance.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct TestExecutor {
|
||||||
|
pub interpreter: Interpreter<MemoryStorage, tx::Script, NotSupportedEcal>,
|
||||||
|
tx_builder: tx::TransactionBuilder<tx::Script>,
|
||||||
|
test_entry: PkgTestEntry,
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TestExecutor {
|
||||||
|
pub fn new(
|
||||||
|
bytecode: &[u8],
|
||||||
|
test_offset: u32,
|
||||||
|
test_setup: TestSetup,
|
||||||
|
test_entry: &PkgTestEntry,
|
||||||
|
name: String,
|
||||||
|
) -> Self {
|
||||||
|
let storage = test_setup.storage().clone();
|
||||||
|
|
||||||
|
// Patch the bytecode to jump to the relevant test.
|
||||||
|
let bytecode = patch_test_bytecode(bytecode, test_offset).into_owned();
|
||||||
|
|
||||||
|
// Create a transaction to execute the test function.
|
||||||
|
let script_input_data = vec![];
|
||||||
|
let rng = &mut rand::rngs::StdRng::seed_from_u64(TEST_METADATA_SEED);
|
||||||
|
|
||||||
|
// Prepare the transaction metadata.
|
||||||
|
let secret_key = SecretKey::random(rng);
|
||||||
|
let utxo_id = rng.gen();
|
||||||
|
let amount = 1;
|
||||||
|
let maturity = 1.into();
|
||||||
|
let asset_id = rng.gen();
|
||||||
|
let tx_pointer = rng.gen();
|
||||||
|
|
||||||
|
let mut tx_builder = tx::TransactionBuilder::script(bytecode, script_input_data)
|
||||||
|
.add_unsigned_coin_input(
|
||||||
|
secret_key,
|
||||||
|
utxo_id,
|
||||||
|
amount,
|
||||||
|
asset_id,
|
||||||
|
tx_pointer,
|
||||||
|
0u32.into(),
|
||||||
|
)
|
||||||
|
.maturity(maturity)
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
let mut output_index = 1;
|
||||||
|
// Insert contract ids into tx input
|
||||||
|
for contract_id in test_setup.contract_ids() {
|
||||||
|
tx_builder
|
||||||
|
.add_input(tx::Input::contract(
|
||||||
|
tx::UtxoId::new(tx::Bytes32::zeroed(), 0),
|
||||||
|
tx::Bytes32::zeroed(),
|
||||||
|
tx::Bytes32::zeroed(),
|
||||||
|
tx::TxPointer::new(0u32.into(), 0),
|
||||||
|
contract_id,
|
||||||
|
))
|
||||||
|
.add_output(tx::Output::Contract(Contract {
|
||||||
|
input_index: output_index,
|
||||||
|
balance_root: fuel_tx::Bytes32::zeroed(),
|
||||||
|
state_root: tx::Bytes32::zeroed(),
|
||||||
|
}));
|
||||||
|
output_index += 1;
|
||||||
|
}
|
||||||
|
let consensus_params = tx_builder.get_params().clone();
|
||||||
|
|
||||||
|
// Temporarily finalize to calculate `script_gas_limit`
|
||||||
|
let tmp_tx = tx_builder.clone().finalize();
|
||||||
|
// Get `max_gas` used by everything except the script execution. Add `1` because of rounding.
|
||||||
|
let max_gas =
|
||||||
|
tmp_tx.max_gas(consensus_params.gas_costs(), consensus_params.fee_params()) + 1;
|
||||||
|
// Increase `script_gas_limit` to the maximum allowed value.
|
||||||
|
tx_builder.script_gas_limit(consensus_params.tx_params().max_gas_per_tx - max_gas);
|
||||||
|
|
||||||
|
TestExecutor {
|
||||||
|
interpreter: Interpreter::with_storage(storage, consensus_params.into()),
|
||||||
|
tx_builder,
|
||||||
|
test_entry: test_entry.clone(),
|
||||||
|
name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn execute(&mut self) -> anyhow::Result<TestResult> {
|
||||||
|
let block_height = (u32::MAX >> 1).into();
|
||||||
|
let start = std::time::Instant::now();
|
||||||
|
let transition = self
|
||||||
|
.interpreter
|
||||||
|
.transact(self.tx_builder.finalize_checked(block_height))
|
||||||
|
.map_err(|err: InterpreterError<_>| anyhow::anyhow!(err))?;
|
||||||
|
let duration = start.elapsed();
|
||||||
|
let state = *transition.state();
|
||||||
|
let receipts = transition.receipts().to_vec();
|
||||||
|
|
||||||
|
let gas_used = *receipts
|
||||||
|
.iter()
|
||||||
|
.find_map(|receipt| match receipt {
|
||||||
|
tx::Receipt::ScriptResult { gas_used, .. } => Some(gas_used),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("missing used gas information from test execution"))?;
|
||||||
|
|
||||||
|
// Only retain `Log` and `LogData` receipts.
|
||||||
|
let logs = receipts
|
||||||
|
.into_iter()
|
||||||
|
.filter(|receipt| {
|
||||||
|
matches!(receipt, tx::Receipt::Log { .. })
|
||||||
|
|| matches!(receipt, tx::Receipt::LogData { .. })
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let span = self.test_entry.span.clone();
|
||||||
|
let file_path = self.test_entry.file_path.clone();
|
||||||
|
let condition = self.test_entry.pass_condition.clone();
|
||||||
|
let name = self.name.clone();
|
||||||
|
Ok(TestResult {
|
||||||
|
name,
|
||||||
|
file_path,
|
||||||
|
duration,
|
||||||
|
span,
|
||||||
|
state,
|
||||||
|
condition,
|
||||||
|
logs,
|
||||||
|
gas_used,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Given some bytecode and an instruction offset for some test's desired entry point, patch the
|
||||||
|
/// bytecode with a `JI` (jump) instruction to jump to the desired test.
|
||||||
|
///
|
||||||
|
/// We want to splice in the `JI` only after the initial data section setup is complete, and only
|
||||||
|
/// if the entry point doesn't begin exactly after the data section setup.
|
||||||
|
///
|
||||||
|
/// The following is how the beginning of the bytecode is laid out:
|
||||||
|
///
|
||||||
|
/// ```ignore
|
||||||
|
/// [0] ji i4 ; Jumps to the data section setup.
|
||||||
|
/// [1] noop
|
||||||
|
/// [2] DATA_SECTION_OFFSET[0..32]
|
||||||
|
/// [3] DATA_SECTION_OFFSET[32..64]
|
||||||
|
/// [4] lw $ds $is 1 ; The data section setup, i.e. where the first ji lands.
|
||||||
|
/// [5] add $$ds $$ds $is
|
||||||
|
/// [6] <first-entry-point> ; This is where we want to jump from to our test code!
|
||||||
|
/// ```
|
||||||
|
fn patch_test_bytecode(bytecode: &[u8], test_offset: u32) -> std::borrow::Cow<[u8]> {
|
||||||
|
// TODO: Standardize this or add metadata to bytecode.
|
||||||
|
const PROGRAM_START_INST_OFFSET: u32 = 6;
|
||||||
|
const PROGRAM_START_BYTE_OFFSET: usize = PROGRAM_START_INST_OFFSET as usize * Instruction::SIZE;
|
||||||
|
|
||||||
|
// If our desired entry point is the program start, no need to jump.
|
||||||
|
if test_offset == PROGRAM_START_INST_OFFSET {
|
||||||
|
return std::borrow::Cow::Borrowed(bytecode);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the jump instruction and splice it into the bytecode.
|
||||||
|
let ji = vm::fuel_asm::op::ji(test_offset);
|
||||||
|
let ji_bytes = ji.to_bytes();
|
||||||
|
let start = PROGRAM_START_BYTE_OFFSET;
|
||||||
|
let end = start + ji_bytes.len();
|
||||||
|
let mut patched = bytecode.to_vec();
|
||||||
|
patched.splice(start..end, ji_bytes);
|
||||||
|
std::borrow::Cow::Owned(patched)
|
||||||
|
}
|
|
@ -1,8 +1,15 @@
|
||||||
|
pub mod execute;
|
||||||
|
pub mod setup;
|
||||||
|
|
||||||
|
use crate::execute::TestExecutor;
|
||||||
|
use crate::setup::{
|
||||||
|
ContractDeploymentSetup, ContractTestSetup, DeploymentSetup, ScriptTestSetup, TestSetup,
|
||||||
|
};
|
||||||
use forc_pkg as pkg;
|
use forc_pkg as pkg;
|
||||||
use fuel_abi_types::error_codes::ErrorSignal;
|
use fuel_abi_types::error_codes::ErrorSignal;
|
||||||
use fuel_tx as tx;
|
use fuel_tx as tx;
|
||||||
use fuel_vm::checked_transaction::builder::TransactionBuilderExt;
|
use fuel_vm::checked_transaction::builder::TransactionBuilderExt;
|
||||||
use fuel_vm::{self as vm, fuel_asm, prelude::Instruction};
|
use fuel_vm::{self as vm};
|
||||||
use pkg::TestPassCondition;
|
use pkg::TestPassCondition;
|
||||||
use pkg::{Built, BuiltPackage};
|
use pkg::{Built, BuiltPackage};
|
||||||
use rand::{Rng, SeedableRng};
|
use rand::{Rng, SeedableRng};
|
||||||
|
@ -10,8 +17,6 @@ use rayon::prelude::*;
|
||||||
use std::{collections::HashMap, fs, path::PathBuf, sync::Arc};
|
use std::{collections::HashMap, fs, path::PathBuf, sync::Arc};
|
||||||
use sway_core::BuildTarget;
|
use sway_core::BuildTarget;
|
||||||
use sway_types::Span;
|
use sway_types::Span;
|
||||||
use tx::output::contract::Contract;
|
|
||||||
use tx::{Chargeable, Finalizable};
|
|
||||||
use vm::prelude::SecretKey;
|
use vm::prelude::SecretKey;
|
||||||
|
|
||||||
/// The result of a `forc test` invocation.
|
/// The result of a `forc test` invocation.
|
||||||
|
@ -117,31 +122,6 @@ pub enum PackageWithDeploymentToTest {
|
||||||
Contract(ContractToTest),
|
Contract(ContractToTest),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Required test setup for package types that requires a deployment.
|
|
||||||
#[derive(Debug)]
|
|
||||||
enum DeploymentSetup {
|
|
||||||
Script(ScriptTestSetup),
|
|
||||||
Contract(ContractTestSetup),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DeploymentSetup {
|
|
||||||
/// Returns the storage for this test setup
|
|
||||||
fn storage(&self) -> &vm::storage::MemoryStorage {
|
|
||||||
match self {
|
|
||||||
DeploymentSetup::Script(script_setup) => &script_setup.storage,
|
|
||||||
DeploymentSetup::Contract(contract_setup) => &contract_setup.storage,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return the root contract id if this is a contract setup.
|
|
||||||
fn root_contract_id(&self) -> Option<tx::ContractId> {
|
|
||||||
match self {
|
|
||||||
DeploymentSetup::Script(_) => None,
|
|
||||||
DeploymentSetup::Contract(contract_setup) => Some(contract_setup.root_contract_id),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The set of options provided to the `test` function.
|
/// The set of options provided to the `test` function.
|
||||||
#[derive(Default, Clone)]
|
#[derive(Default, Clone)]
|
||||||
pub struct Opts {
|
pub struct Opts {
|
||||||
|
@ -176,69 +156,6 @@ pub struct TestPrintOpts {
|
||||||
pub print_logs: bool,
|
pub print_logs: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The storage and the contract id (if a contract is being tested) for a test.
|
|
||||||
#[derive(Debug)]
|
|
||||||
enum TestSetup {
|
|
||||||
WithDeployment(DeploymentSetup),
|
|
||||||
WithoutDeployment(vm::storage::MemoryStorage),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TestSetup {
|
|
||||||
/// Returns the storage for this test setup
|
|
||||||
fn storage(&self) -> &vm::storage::MemoryStorage {
|
|
||||||
match self {
|
|
||||||
TestSetup::WithDeployment(deployment_setup) => deployment_setup.storage(),
|
|
||||||
TestSetup::WithoutDeployment(storage) => storage,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Produces an iterator yielding contract ids of contract dependencies for this test setup.
|
|
||||||
fn contract_dependency_ids(&self) -> impl Iterator<Item = &tx::ContractId> + '_ {
|
|
||||||
match self {
|
|
||||||
TestSetup::WithDeployment(deployment_setup) => match deployment_setup {
|
|
||||||
DeploymentSetup::Script(script_setup) => {
|
|
||||||
script_setup.contract_dependency_ids.iter()
|
|
||||||
}
|
|
||||||
DeploymentSetup::Contract(contract_setup) => {
|
|
||||||
contract_setup.contract_dependency_ids.iter()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
TestSetup::WithoutDeployment(_) => [].iter(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return the root contract id if this is a contract setup.
|
|
||||||
fn root_contract_id(&self) -> Option<tx::ContractId> {
|
|
||||||
match self {
|
|
||||||
TestSetup::WithDeployment(deployment_setup) => deployment_setup.root_contract_id(),
|
|
||||||
TestSetup::WithoutDeployment(_) => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Produces an iterator yielding all contract ids required to be included in the transaction
|
|
||||||
/// for this test setup.
|
|
||||||
fn contract_ids(&self) -> impl Iterator<Item = tx::ContractId> + '_ {
|
|
||||||
self.contract_dependency_ids()
|
|
||||||
.cloned()
|
|
||||||
.chain(self.root_contract_id())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The data collected to test a contract.
|
|
||||||
#[derive(Debug)]
|
|
||||||
struct ContractTestSetup {
|
|
||||||
storage: vm::storage::MemoryStorage,
|
|
||||||
contract_dependency_ids: Vec<tx::ContractId>,
|
|
||||||
root_contract_id: tx::ContractId,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The data collected to test a script.
|
|
||||||
#[derive(Debug)]
|
|
||||||
struct ScriptTestSetup {
|
|
||||||
storage: vm::storage::MemoryStorage,
|
|
||||||
contract_dependency_ids: Vec<tx::ContractId>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TestedPackage {
|
impl TestedPackage {
|
||||||
pub fn tests_passed(&self) -> bool {
|
pub fn tests_passed(&self) -> bool {
|
||||||
self.tests.iter().all(|test| test.passed())
|
self.tests.iter().all(|test| test.passed())
|
||||||
|
@ -412,64 +329,42 @@ impl<'a> PackageTests {
|
||||||
.bytecode
|
.bytecode
|
||||||
.entries
|
.entries
|
||||||
.par_iter()
|
.par_iter()
|
||||||
.filter_map(|entry| entry.kind.test().map(|test| (entry, test)))
|
.filter_map(|entry| {
|
||||||
.filter(|(entry, _)| {
|
if let Some(test_entry) = entry.kind.test() {
|
||||||
// If a test filter is specified, only the tests containing the filter phrase in
|
// If a test filter is specified, only the tests containing the filter phrase in
|
||||||
// their name are going to be executed.
|
// their name are going to be executed.
|
||||||
match &test_filter {
|
let name = entry.finalized.fn_name.clone();
|
||||||
Some(filter) => filter.filter(&entry.finalized.fn_name),
|
if let Some(filter) = test_filter {
|
||||||
None => true,
|
if !filter.filter(&name) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Some((entry, test_entry));
|
||||||
}
|
}
|
||||||
|
None
|
||||||
})
|
})
|
||||||
.map(|(entry, test_entry)| {
|
.map(|(entry, test_entry)| {
|
||||||
|
// Execute the test and return the result.
|
||||||
let offset = u32::try_from(entry.finalized.imm)
|
let offset = u32::try_from(entry.finalized.imm)
|
||||||
.expect("test instruction offset out of range");
|
.expect("test instruction offset out of range");
|
||||||
let name = entry.finalized.fn_name.clone();
|
let name = entry.finalized.fn_name.clone();
|
||||||
let test_setup = self.setup()?;
|
let test_setup = self.setup()?;
|
||||||
let (state, duration, receipts) =
|
TestExecutor::new(
|
||||||
exec_test(&pkg_with_tests.bytecode.bytes, offset, test_setup);
|
&pkg_with_tests.bytecode.bytes,
|
||||||
|
offset,
|
||||||
let gas_used = *receipts
|
test_setup,
|
||||||
.iter()
|
test_entry,
|
||||||
.find_map(|receipt| match receipt {
|
|
||||||
tx::Receipt::ScriptResult { gas_used, .. } => Some(gas_used),
|
|
||||||
_ => None,
|
|
||||||
})
|
|
||||||
.ok_or_else(|| {
|
|
||||||
anyhow::anyhow!("missing used gas information from test execution")
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// Only retain `Log` and `LogData` receipts.
|
|
||||||
let logs = receipts
|
|
||||||
.into_iter()
|
|
||||||
.filter(|receipt| {
|
|
||||||
matches!(receipt, fuel_tx::Receipt::Log { .. })
|
|
||||||
|| matches!(receipt, fuel_tx::Receipt::LogData { .. })
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let span = test_entry.span.clone();
|
|
||||||
let file_path = test_entry.file_path.clone();
|
|
||||||
let condition = test_entry.pass_condition.clone();
|
|
||||||
Ok(TestResult {
|
|
||||||
name,
|
name,
|
||||||
file_path,
|
)
|
||||||
duration,
|
.execute()
|
||||||
span,
|
|
||||||
state,
|
|
||||||
condition,
|
|
||||||
logs,
|
|
||||||
gas_used,
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
.collect::<anyhow::Result<_>>()
|
.collect::<anyhow::Result<_>>()
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let tested_pkg = TestedPackage {
|
Ok(TestedPackage {
|
||||||
built: Box::new(pkg_with_tests.clone()),
|
built: Box::new(pkg_with_tests.clone()),
|
||||||
tests,
|
tests,
|
||||||
};
|
})
|
||||||
Ok(tested_pkg)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Setup the storage for a test and returns a contract id for testing contracts.
|
/// Setup the storage for a test and returns a contract id for testing contracts.
|
||||||
|
@ -528,7 +423,7 @@ impl TestResult {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the revert code for this `TestResult` if the test is reverted.
|
/// Return the revert code for this [TestResult] if the test is reverted.
|
||||||
pub fn revert_code(&self) -> Option<u64> {
|
pub fn revert_code(&self) -> Option<u64> {
|
||||||
match self.state {
|
match self.state {
|
||||||
vm::state::ProgramState::Revert(revert_code) => Some(revert_code),
|
vm::state::ProgramState::Revert(revert_code) => Some(revert_code),
|
||||||
|
@ -536,7 +431,7 @@ impl TestResult {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return a `ErrorSignal` for this `TestResult` if the test is failed to pass.
|
/// Return an [ErrorSignal] for this [TestResult] if the test is failed to pass.
|
||||||
pub fn error_signal(&self) -> anyhow::Result<ErrorSignal> {
|
pub fn error_signal(&self) -> anyhow::Result<ErrorSignal> {
|
||||||
let revert_code = self.revert_code().ok_or_else(|| {
|
let revert_code = self.revert_code().ok_or_else(|| {
|
||||||
anyhow::anyhow!("there is no revert code to convert to `ErrorSignal`")
|
anyhow::anyhow!("there is no revert code to convert to `ErrorSignal`")
|
||||||
|
@ -544,7 +439,7 @@ impl TestResult {
|
||||||
ErrorSignal::try_from_revert_code(revert_code).map_err(|e| anyhow::anyhow!(e))
|
ErrorSignal::try_from_revert_code(revert_code).map_err(|e| anyhow::anyhow!(e))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return `TestDetails` from the span of the function declaring this test.
|
/// Return [TestDetails] from the span of the function declaring this test.
|
||||||
pub fn details(&self) -> anyhow::Result<TestDetails> {
|
pub fn details(&self) -> anyhow::Result<TestDetails> {
|
||||||
let span_start = self.span.start();
|
let span_start = self.span.start();
|
||||||
let file_str = fs::read_to_string(&*self.file_path)?;
|
let file_str = fs::read_to_string(&*self.file_path)?;
|
||||||
|
@ -658,9 +553,6 @@ pub fn build(opts: Opts) -> anyhow::Result<BuiltTests> {
|
||||||
BuiltTests::from_built(built, &member_contract_dependencies)
|
BuiltTests::from_built(built, &member_contract_dependencies)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Result of preparing a deployment transaction setup for a contract.
|
|
||||||
type ContractDeploymentSetup = (tx::ContractId, vm::checked_transaction::Checked<tx::Create>);
|
|
||||||
|
|
||||||
/// Deploys the provided contract and returns an interpreter instance ready to be used in test
|
/// Deploys the provided contract and returns an interpreter instance ready to be used in test
|
||||||
/// executions with deployed contract.
|
/// executions with deployed contract.
|
||||||
fn deployment_transaction(
|
fn deployment_transaction(
|
||||||
|
@ -722,123 +614,6 @@ fn run_tests(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Given some bytecode and an instruction offset for some test's desired entry point, patch the
|
|
||||||
/// bytecode with a `JI` (jump) instruction to jump to the desired test.
|
|
||||||
///
|
|
||||||
/// We want to splice in the `JI` only after the initial data section setup is complete, and only
|
|
||||||
/// if the entry point doesn't begin exactly after the data section setup.
|
|
||||||
///
|
|
||||||
/// The following is how the beginning of the bytecode is laid out:
|
|
||||||
///
|
|
||||||
/// ```ignore
|
|
||||||
/// [0] ji i4 ; Jumps to the data section setup.
|
|
||||||
/// [1] noop
|
|
||||||
/// [2] DATA_SECTION_OFFSET[0..32]
|
|
||||||
/// [3] DATA_SECTION_OFFSET[32..64]
|
|
||||||
/// [4] lw $ds $is 1 ; The data section setup, i.e. where the first ji lands.
|
|
||||||
/// [5] add $$ds $$ds $is
|
|
||||||
/// [6] <first-entry-point> ; This is where we want to jump from to our test code!
|
|
||||||
/// ```
|
|
||||||
fn patch_test_bytecode(bytecode: &[u8], test_offset: u32) -> std::borrow::Cow<[u8]> {
|
|
||||||
// TODO: Standardize this or add metadata to bytecode.
|
|
||||||
const PROGRAM_START_INST_OFFSET: u32 = 6;
|
|
||||||
const PROGRAM_START_BYTE_OFFSET: usize = PROGRAM_START_INST_OFFSET as usize * Instruction::SIZE;
|
|
||||||
|
|
||||||
// If our desired entry point is the program start, no need to jump.
|
|
||||||
if test_offset == PROGRAM_START_INST_OFFSET {
|
|
||||||
return std::borrow::Cow::Borrowed(bytecode);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the jump instruction and splice it into the bytecode.
|
|
||||||
let ji = fuel_asm::op::ji(test_offset);
|
|
||||||
let ji_bytes = ji.to_bytes();
|
|
||||||
let start = PROGRAM_START_BYTE_OFFSET;
|
|
||||||
let end = start + ji_bytes.len();
|
|
||||||
let mut patched = bytecode.to_vec();
|
|
||||||
patched.splice(start..end, ji_bytes);
|
|
||||||
std::borrow::Cow::Owned(patched)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Execute the test whose entry point is at the given instruction offset as if it were a script.
|
|
||||||
fn exec_test(
|
|
||||||
bytecode: &[u8],
|
|
||||||
test_offset: u32,
|
|
||||||
test_setup: TestSetup,
|
|
||||||
) -> (
|
|
||||||
vm::state::ProgramState,
|
|
||||||
std::time::Duration,
|
|
||||||
Vec<fuel_tx::Receipt>,
|
|
||||||
) {
|
|
||||||
let storage = test_setup.storage().clone();
|
|
||||||
|
|
||||||
// Patch the bytecode to jump to the relevant test.
|
|
||||||
let bytecode = patch_test_bytecode(bytecode, test_offset).into_owned();
|
|
||||||
|
|
||||||
// Create a transaction to execute the test function.
|
|
||||||
let script_input_data = vec![];
|
|
||||||
let rng = &mut rand::rngs::StdRng::seed_from_u64(TEST_METADATA_SEED);
|
|
||||||
|
|
||||||
// Prepare the transaction metadata.
|
|
||||||
let secret_key = SecretKey::random(rng);
|
|
||||||
let utxo_id = rng.gen();
|
|
||||||
let amount = 1;
|
|
||||||
let maturity = 1.into();
|
|
||||||
let asset_id = rng.gen();
|
|
||||||
let tx_pointer = rng.gen();
|
|
||||||
let block_height = (u32::MAX >> 1).into();
|
|
||||||
|
|
||||||
let mut tb = tx::TransactionBuilder::script(bytecode, script_input_data)
|
|
||||||
.add_unsigned_coin_input(
|
|
||||||
secret_key,
|
|
||||||
utxo_id,
|
|
||||||
amount,
|
|
||||||
asset_id,
|
|
||||||
tx_pointer,
|
|
||||||
0u32.into(),
|
|
||||||
)
|
|
||||||
.maturity(maturity)
|
|
||||||
.clone();
|
|
||||||
let mut output_index = 1;
|
|
||||||
// Insert contract ids into tx input
|
|
||||||
for contract_id in test_setup.contract_ids() {
|
|
||||||
tb.add_input(tx::Input::contract(
|
|
||||||
tx::UtxoId::new(tx::Bytes32::zeroed(), 0),
|
|
||||||
tx::Bytes32::zeroed(),
|
|
||||||
tx::Bytes32::zeroed(),
|
|
||||||
tx::TxPointer::new(0u32.into(), 0),
|
|
||||||
contract_id,
|
|
||||||
))
|
|
||||||
.add_output(tx::Output::Contract(Contract {
|
|
||||||
input_index: output_index,
|
|
||||||
balance_root: fuel_tx::Bytes32::zeroed(),
|
|
||||||
state_root: tx::Bytes32::zeroed(),
|
|
||||||
}));
|
|
||||||
output_index += 1;
|
|
||||||
}
|
|
||||||
let consensus_params = tb.get_params().clone();
|
|
||||||
|
|
||||||
// Temporarily finalize to calculate `script_gas_limit`
|
|
||||||
let tmp_tx = tb.clone().finalize();
|
|
||||||
// Get `max_gas` used by everything except the script execution. Add `1` because of rounding.
|
|
||||||
let max_gas = tmp_tx.max_gas(consensus_params.gas_costs(), consensus_params.fee_params()) + 1;
|
|
||||||
// Increase `script_gas_limit` to the maximum allowed value.
|
|
||||||
tb.script_gas_limit(consensus_params.tx_params().max_gas_per_tx - max_gas);
|
|
||||||
|
|
||||||
let tx = tb.finalize_checked(block_height);
|
|
||||||
|
|
||||||
let mut interpreter: vm::prelude::Interpreter<_, _, vm::interpreter::NotSupportedEcal> =
|
|
||||||
vm::interpreter::Interpreter::with_storage(storage, consensus_params.into());
|
|
||||||
|
|
||||||
// Execute and return the result.
|
|
||||||
let start = std::time::Instant::now();
|
|
||||||
let transition = interpreter.transact(tx).unwrap();
|
|
||||||
let duration = start.elapsed();
|
|
||||||
let state = *transition.state();
|
|
||||||
let receipts = transition.receipts().to_vec();
|
|
||||||
|
|
||||||
(state, duration, receipts)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
93
forc-test/src/setup.rs
Normal file
93
forc-test/src/setup.rs
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
use fuel_tx as tx;
|
||||||
|
use fuel_vm::{self as vm};
|
||||||
|
|
||||||
|
/// Result of preparing a deployment transaction setup for a contract.
|
||||||
|
pub type ContractDeploymentSetup = (tx::ContractId, vm::checked_transaction::Checked<tx::Create>);
|
||||||
|
|
||||||
|
/// Required test setup for package types that requires a deployment.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum DeploymentSetup {
|
||||||
|
Script(ScriptTestSetup),
|
||||||
|
Contract(ContractTestSetup),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DeploymentSetup {
|
||||||
|
/// Returns the storage for this test setup
|
||||||
|
fn storage(&self) -> &vm::storage::MemoryStorage {
|
||||||
|
match self {
|
||||||
|
DeploymentSetup::Script(script_setup) => &script_setup.storage,
|
||||||
|
DeploymentSetup::Contract(contract_setup) => &contract_setup.storage,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the root contract id if this is a contract setup.
|
||||||
|
fn root_contract_id(&self) -> Option<tx::ContractId> {
|
||||||
|
match self {
|
||||||
|
DeploymentSetup::Script(_) => None,
|
||||||
|
DeploymentSetup::Contract(contract_setup) => Some(contract_setup.root_contract_id),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The storage and the contract id (if a contract is being tested) for a test.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum TestSetup {
|
||||||
|
WithDeployment(DeploymentSetup),
|
||||||
|
WithoutDeployment(vm::storage::MemoryStorage),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TestSetup {
|
||||||
|
/// Returns the storage for this test setup
|
||||||
|
pub fn storage(&self) -> &vm::storage::MemoryStorage {
|
||||||
|
match self {
|
||||||
|
TestSetup::WithDeployment(deployment_setup) => deployment_setup.storage(),
|
||||||
|
TestSetup::WithoutDeployment(storage) => storage,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Produces an iterator yielding contract ids of contract dependencies for this test setup.
|
||||||
|
pub fn contract_dependency_ids(&self) -> impl Iterator<Item = &tx::ContractId> + '_ {
|
||||||
|
match self {
|
||||||
|
TestSetup::WithDeployment(deployment_setup) => match deployment_setup {
|
||||||
|
DeploymentSetup::Script(script_setup) => {
|
||||||
|
script_setup.contract_dependency_ids.iter()
|
||||||
|
}
|
||||||
|
DeploymentSetup::Contract(contract_setup) => {
|
||||||
|
contract_setup.contract_dependency_ids.iter()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
TestSetup::WithoutDeployment(_) => [].iter(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the root contract id if this is a contract setup.
|
||||||
|
pub fn root_contract_id(&self) -> Option<tx::ContractId> {
|
||||||
|
match self {
|
||||||
|
TestSetup::WithDeployment(deployment_setup) => deployment_setup.root_contract_id(),
|
||||||
|
TestSetup::WithoutDeployment(_) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Produces an iterator yielding all contract ids required to be included in the transaction
|
||||||
|
/// for this test setup.
|
||||||
|
pub fn contract_ids(&self) -> impl Iterator<Item = tx::ContractId> + '_ {
|
||||||
|
self.contract_dependency_ids()
|
||||||
|
.cloned()
|
||||||
|
.chain(self.root_contract_id())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The data collected to test a contract.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ContractTestSetup {
|
||||||
|
pub storage: vm::storage::MemoryStorage,
|
||||||
|
pub contract_dependency_ids: Vec<tx::ContractId>,
|
||||||
|
pub root_contract_id: tx::ContractId,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The data collected to test a script.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ScriptTestSetup {
|
||||||
|
pub storage: vm::storage::MemoryStorage,
|
||||||
|
pub contract_dependency_ids: Vec<tx::ContractId>,
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue