use crate::maxed_consensus_params;
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::fuel_asm;
use fuel_vm::prelude::Instruction;
use fuel_vm::prelude::RegId;
use fuel_vm::{
self as vm,
checked_transaction::builder::TransactionBuilderExt,
interpreter::{Interpreter, NotSupportedEcal},
prelude::SecretKey,
storage::MemoryStorage,
};
use rand::{Rng, SeedableRng};
use tx::Receipt;
use vm::interpreter::{InterpreterParams, MemoryInstance};
use vm::state::DebugEval;
use vm::state::ProgramState;
#[derive(Debug, Clone)]
pub struct TestExecutor {
pub interpreter: Interpreter<MemoryInstance, MemoryStorage, tx::Script, NotSupportedEcal>,
pub tx: vm::checked_transaction::Ready<tx::Script>,
pub test_entry: PkgTestEntry,
pub name: String,
pub jump_instruction_index: usize,
pub relative_jump_in_bytes: u32,
}
#[derive(Debug)]
pub enum DebugResult {
TestComplete(TestResult),
Breakpoint(u64),
}
impl TestExecutor {
pub fn build(
bytecode: &[u8],
test_instruction_index: u32,
test_setup: TestSetup,
test_entry: &PkgTestEntry,
name: String,
) -> anyhow::Result<Self> {
let storage = test_setup.storage().clone();
let jump_instruction_index = find_jump_instruction_index(bytecode);
let script_input_data = vec![];
let rng = &mut rand::rngs::StdRng::seed_from_u64(TEST_METADATA_SEED);
let secret_key = SecretKey::random(rng);
let utxo_id = rng.gen();
let amount = 1;
let maturity = 1.into();
let asset_id = tx::AssetId::BASE;
let tx_pointer = rng.gen();
let block_height = (u32::MAX >> 1).into();
let gas_price = 0;
let mut tx_builder = tx::TransactionBuilder::script(bytecode.to_vec(), script_input_data);
let params = maxed_consensus_params();
tx_builder
.with_params(params)
.add_unsigned_coin_input(secret_key, utxo_id, amount, asset_id, tx_pointer)
.maturity(maturity);
let mut output_index = 1;
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();
let tmp_tx = tx_builder.clone().finalize();
let max_gas =
tmp_tx.max_gas(consensus_params.gas_costs(), consensus_params.fee_params()) + 1;
tx_builder.script_gas_limit(consensus_params.tx_params().max_gas_per_tx() - max_gas);
let tx = tx_builder
.finalize_checked(block_height)
.into_ready(
gas_price,
consensus_params.gas_costs(),
consensus_params.fee_params(),
)
.map_err(|e| anyhow::anyhow!("{e:?}"))?;
let interpreter_params = InterpreterParams::new(gas_price, &consensus_params);
let memory_instance = MemoryInstance::new();
let interpreter = Interpreter::with_storage(memory_instance, storage, interpreter_params);
Ok(TestExecutor {
interpreter,
tx,
test_entry: test_entry.clone(),
name,
jump_instruction_index,
relative_jump_in_bytes: (test_instruction_index - jump_instruction_index as u32)
* Instruction::SIZE as u32,
})
}
fn single_step_until_test(&mut self) -> ProgramState {
let jump_pc = (self.jump_instruction_index * Instruction::SIZE) as u64;
let old_single_stepping = self.interpreter.single_stepping();
self.interpreter.set_single_stepping(true);
let mut state = {
let transition = self.interpreter.transact(self.tx.clone());
Ok(*transition.unwrap().state())
};
loop {
match state {
Err(_) => {
break ProgramState::Revert(0);
}
Ok(
state @ ProgramState::Return(_)
| state @ ProgramState::ReturnData(_)
| state @ ProgramState::Revert(_),
) => break state,
Ok(
s @ ProgramState::RunProgram(eval) | s @ ProgramState::VerifyPredicate(eval),
) => {
if let Some(b) = eval.breakpoint() {
if b.pc() == jump_pc {
self.interpreter.registers_mut()[RegId::PC] +=
self.relative_jump_in_bytes as u64;
self.interpreter.set_single_stepping(old_single_stepping);
break s;
}
}
state = self.interpreter.resume();
}
}
}
}
pub fn start_debugging(&mut self) -> anyhow::Result<DebugResult> {
let start = std::time::Instant::now();
let _ = self.single_step_until_test();
let state = self
.interpreter
.resume()
.map_err(|err: InterpreterError<_>| {
anyhow::anyhow!("VM failed to resume. {:?}", err)
})?;
if let ProgramState::RunProgram(DebugEval::Breakpoint(breakpoint)) = state {
return Ok(DebugResult::Breakpoint(breakpoint.pc()));
}
let duration = start.elapsed();
let (gas_used, logs) = Self::get_gas_and_receipts(self.interpreter.receipts().to_vec())?;
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(DebugResult::TestComplete(TestResult {
name,
file_path,
duration,
span,
state,
condition,
logs,
gas_used,
}))
}
pub fn continue_debugging(&mut self) -> anyhow::Result<DebugResult> {
let start = std::time::Instant::now();
let state = self
.interpreter
.resume()
.map_err(|err: InterpreterError<_>| {
anyhow::anyhow!("VM failed to resume. {:?}", err)
})?;
if let ProgramState::RunProgram(DebugEval::Breakpoint(breakpoint)) = state {
return Ok(DebugResult::Breakpoint(breakpoint.pc()));
}
let duration = start.elapsed();
let (gas_used, logs) = Self::get_gas_and_receipts(self.interpreter.receipts().to_vec())?; 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(DebugResult::TestComplete(TestResult {
name,
file_path,
duration,
span,
state,
condition,
logs,
gas_used,
}))
}
pub fn execute(&mut self) -> anyhow::Result<TestResult> {
let start = std::time::Instant::now();
let mut state = Ok(self.single_step_until_test());
loop {
match state {
Err(_) => {
state = Ok(ProgramState::Revert(0));
break;
}
Ok(
ProgramState::Return(_) | ProgramState::ReturnData(_) | ProgramState::Revert(_),
) => break,
Ok(ProgramState::RunProgram(_) | ProgramState::VerifyPredicate(_)) => {
state = self.interpreter.resume();
}
}
}
let duration = start.elapsed();
let (gas_used, logs) = Self::get_gas_and_receipts(self.interpreter.receipts().to_vec())?;
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: state.unwrap(),
condition,
logs,
gas_used,
})
}
fn get_gas_and_receipts(receipts: Vec<Receipt>) -> anyhow::Result<(u64, Vec<Receipt>)> {
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"))?;
let logs = receipts
.into_iter()
.filter(|receipt| {
matches!(receipt, tx::Receipt::Log { .. })
|| matches!(receipt, tx::Receipt::LogData { .. })
})
.collect();
Ok((gas_used, logs))
}
}
fn find_jump_instruction_index(bytecode: &[u8]) -> usize {
let a = vm::fuel_asm::op::move_(59, fuel_asm::RegId::SP).to_bytes();
let b = vm::fuel_asm::op::lw(fuel_asm::RegId::WRITABLE, fuel_asm::RegId::FP, 73).to_bytes();
bytecode
.chunks(Instruction::SIZE)
.position(|instruction| {
let instruction: [u8; 4] = instruction.try_into().unwrap();
instruction == a || instruction == b
})
.unwrap()
}