Skip to content
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

forc test single-step until jump point instead of patching binary #6731

Merged
merged 11 commits into from
Nov 22, 2024
157 changes: 104 additions & 53 deletions forc-test/src/execute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ 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,
Expand All @@ -27,6 +30,8 @@ pub struct TestExecutor {
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,
}

/// The result of executing a test with breakpoints enabled.
Expand All @@ -41,15 +46,16 @@ pub enum DebugResult {
impl TestExecutor {
pub fn build(
bytecode: &[u8],
test_offset: u32,
test_instruction_index: u32,
test_setup: TestSetup,
test_entry: &PkgTestEntry,
name: String,
) -> anyhow::Result<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();
// Find the instruction which we will jump into the
// specified test
let jump_instruction_index = find_jump_instruction_index(bytecode);

// Create a transaction to execute the test function.
let script_input_data = vec![];
Expand All @@ -68,7 +74,7 @@ impl TestExecutor {
let block_height = (u32::MAX >> 1).into();
let gas_price = 0;

let mut tx_builder = tx::TransactionBuilder::script(bytecode, script_input_data);
let mut tx_builder = tx::TransactionBuilder::script(bytecode.to_vec(), script_input_data);

let params = maxed_consensus_params();

Expand Down Expand Up @@ -126,23 +132,72 @@ impl TestExecutor {
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,
})
}

// single-step until the jump-to-test instruction, then
// jump into the first instruction of the test
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 {
// if the VM fails, we interpret as a revert
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),
) => {
// time to jump into the specified test
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();
}
}
}
}

/// Execute the test with breakpoints enabled.
pub fn start_debugging(&mut self) -> anyhow::Result<DebugResult> {
let start = std::time::Instant::now();
let transition = self

let _ = self.single_step_until_test();
let state = self
.interpreter
.transact(self.tx.clone())
.map_err(|err: InterpreterError<_>| anyhow::anyhow!(err))?;
let state = *transition.state();
.resume()
.map_err(|err: InterpreterError<_>| {
anyhow::anyhow!("VM failed to resume. {:?}", err)
})?;
if let ProgramState::RunProgram(DebugEval::Breakpoint(breakpoint)) = state {
// A breakpoint was hit, so we tell the client to stop.
return Ok(DebugResult::Breakpoint(breakpoint.pc()));
}

let duration = start.elapsed();
let (gas_used, logs) = Self::get_gas_and_receipts(transition.receipts().to_vec())?;
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();
Expand Down Expand Up @@ -192,14 +247,27 @@ impl TestExecutor {

pub fn execute(&mut self) -> anyhow::Result<TestResult> {
let start = std::time::Instant::now();
let transition = self
.interpreter
.transact(self.tx.clone())
.map_err(|err: InterpreterError<_>| anyhow::anyhow!(err))?;
let state = *transition.state();

let mut state = Ok(self.single_step_until_test());

// Run test until its end
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(transition.receipts().to_vec())?;
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();
Expand All @@ -209,7 +277,7 @@ impl TestExecutor {
file_path,
duration,
span,
state,
state: state.unwrap(),
condition,
logs,
gas_used,
Expand Down Expand Up @@ -237,42 +305,25 @@ impl TestExecutor {
}
}

/// 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 i(4 + 2) ; Jumps to the data section setup.
/// [ 1] noop
/// [ 2] DATA_SECTION_OFFSET[0..32]
/// [ 3] DATA_SECTION_OFFSET[32..64]
/// [ 4] CONFIGURABLES_OFFSET[0..32]
/// [ 5] CONFIGURABLES_OFFSET[32..64]
/// [ 6] lw $ds $is 1 ; The data section setup, i.e. where the first ji lands.
/// [ 7] add $$ds $$ds $is
/// [ 8] <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]> {
// Each instruction is 4 bytes,
// so we divide the total byte-size by 4 to get the instruction offset.
const PROGRAM_START_INST_OFFSET: u32 = (sway_core::PRELUDE_SIZE_IN_BYTES / 4) as u32;
const PROGRAM_START_BYTE_OFFSET: usize = sway_core::PRELUDE_SIZE_IN_BYTES;

// 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);
}
fn find_jump_instruction_index(bytecode: &[u8]) -> usize {
// Search first `move $$locbase $sp`
// This will be `__entry` for script/predicate/contract using encoding v1;
// `main` for script/predicate using encoding v0;
// or the first function for libraries
// MOVE R59 $sp ;; [26, 236, 80, 0]
let a = vm::fuel_asm::op::move_(59, fuel_asm::RegId::SP).to_bytes();

// 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)
// for contracts using encoding v0
// search the first `lw $r0 $fp i73`
// which is the start of the fn selector
// LW $writable $fp 0x49 ;; [93, 64, 96, 73]
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()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[[package]]
name = "configurable_tests"
source = "member"
dependencies = ["std"]

[[package]]
name = "core"
source = "path+from-root-CEAD1EF3DC39BB76"

[[package]]
name = "std"
source = "path+from-root-CEAD1EF3DC39BB76"
dependencies = ["core"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[project]
authors = ["Fuel Labs <[email protected]>"]
entry = "main.sw"
license = "Apache-2.0"
name = "configurable_tests"

[dependencies]
std = { path = "../../../../../../../sway-lib-std" }
Loading
Loading