forc_test/
execute.rs

1use crate::ecal::EcalSyscallHandler;
2use crate::maxed_consensus_params;
3use crate::setup::TestSetup;
4use crate::TestResult;
5use crate::TEST_METADATA_SEED;
6use forc_pkg::PkgTestEntry;
7use fuel_tx::GasCostsValues;
8use fuel_tx::{self as tx, output::contract::Contract, Chargeable, Finalizable};
9use fuel_vm::error::InterpreterError;
10use fuel_vm::fuel_asm;
11use fuel_vm::prelude::Instruction;
12use fuel_vm::prelude::RegId;
13use fuel_vm::{
14    self as vm, checked_transaction::builder::TransactionBuilderExt, interpreter::Interpreter,
15    prelude::SecretKey, storage::MemoryStorage,
16};
17use rand::{Rng, SeedableRng};
18
19use tx::Receipt;
20
21use vm::interpreter::{InterpreterParams, MemoryInstance};
22use vm::state::DebugEval;
23use vm::state::ProgramState;
24
25/// An interface for executing a test within a VM [Interpreter] instance.
26#[derive(Debug, Clone)]
27pub struct TestExecutor {
28    pub interpreter: Interpreter<MemoryInstance, MemoryStorage, tx::Script, EcalSyscallHandler>,
29    pub tx: vm::checked_transaction::Ready<tx::Script>,
30    pub test_entry: PkgTestEntry,
31    pub name: String,
32    pub jump_instruction_index: usize,
33    pub relative_jump_in_bytes: u32,
34}
35
36/// The result of executing a test with breakpoints enabled.
37#[derive(Debug)]
38pub enum DebugResult {
39    // Holds the test result.
40    TestComplete(TestResult),
41    // Holds the program counter of where the program stopped due to a breakpoint.
42    Breakpoint(u64),
43}
44
45impl TestExecutor {
46    pub fn build(
47        bytecode: &[u8],
48        test_instruction_index: u32,
49        test_setup: TestSetup,
50        test_entry: &PkgTestEntry,
51        name: String,
52        gas_costs_values: GasCostsValues,
53    ) -> anyhow::Result<Self> {
54        let storage = test_setup.storage().clone();
55
56        // Find the instruction which we will jump into the
57        // specified test
58        let jump_instruction_index = find_jump_instruction_index(bytecode);
59
60        // Create a transaction to execute the test function.
61        let script_input_data = vec![];
62        let rng = &mut rand::rngs::StdRng::seed_from_u64(TEST_METADATA_SEED);
63
64        // Prepare the transaction metadata.
65        let secret_key = SecretKey::random(rng);
66        let utxo_id = rng.r#gen();
67        let amount = 1;
68        let maturity = 1.into();
69        // NOTE: fuel-core is using dynamic asset id and interacting with the fuel-core, using static
70        // asset id is not correct. But since forc-test maintains its own interpreter instance, correct
71        // base asset id is indeed the static `tx::AssetId::BASE`.
72        let asset_id = tx::AssetId::BASE;
73        let tx_pointer = rng.r#gen();
74        let block_height = (u32::MAX >> 1).into();
75        let gas_price = 0;
76
77        let mut tx_builder = tx::TransactionBuilder::script(bytecode.to_vec(), script_input_data);
78
79        let params = maxed_consensus_params(gas_costs_values);
80
81        tx_builder
82            .with_params(params)
83            .add_unsigned_coin_input(secret_key, utxo_id, amount, asset_id, tx_pointer)
84            .maturity(maturity);
85
86        let mut output_index = 1;
87        // Insert contract ids into tx input
88        for contract_id in test_setup.contract_ids() {
89            tx_builder
90                .add_input(tx::Input::contract(
91                    tx::UtxoId::new(tx::Bytes32::zeroed(), 0),
92                    tx::Bytes32::zeroed(),
93                    tx::Bytes32::zeroed(),
94                    tx::TxPointer::new(0u32.into(), 0),
95                    contract_id,
96                ))
97                .add_output(tx::Output::Contract(Contract {
98                    input_index: output_index,
99                    balance_root: fuel_tx::Bytes32::zeroed(),
100                    state_root: tx::Bytes32::zeroed(),
101                }));
102            output_index += 1;
103        }
104
105        let consensus_params = tx_builder.get_params().clone();
106        // Temporarily finalize to calculate `script_gas_limit`
107        let tmp_tx = tx_builder.clone().finalize();
108        // Get `max_gas` used by everything except the script execution. Add `1` because of rounding.
109        let max_gas =
110            tmp_tx.max_gas(consensus_params.gas_costs(), consensus_params.fee_params()) + 1;
111        // Increase `script_gas_limit` to the maximum allowed value.
112        tx_builder.script_gas_limit(consensus_params.tx_params().max_gas_per_tx() - max_gas);
113
114        // We need to increase the tx size limit as the default is 110 * 1024 and for big tests
115        // such as std this is not enough.
116
117        let tx = tx_builder
118            .finalize_checked(block_height)
119            .into_ready(
120                gas_price,
121                consensus_params.gas_costs(),
122                consensus_params.fee_params(),
123                None,
124            )
125            .map_err(|e| anyhow::anyhow!("{e:?}"))?;
126
127        let interpreter_params = InterpreterParams::new(gas_price, &consensus_params);
128        let memory_instance = MemoryInstance::new();
129        let interpreter = Interpreter::with_storage(memory_instance, storage, interpreter_params);
130
131        Ok(TestExecutor {
132            interpreter,
133            tx,
134            test_entry: test_entry.clone(),
135            name,
136            jump_instruction_index,
137            relative_jump_in_bytes: (test_instruction_index - jump_instruction_index as u32)
138                * Instruction::SIZE as u32,
139        })
140    }
141
142    // single-step until the jump-to-test instruction, then
143    // jump into the first instruction of the test
144    fn single_step_until_test(&mut self) -> ProgramState {
145        let jump_pc = (self.jump_instruction_index * Instruction::SIZE) as u64;
146
147        let old_single_stepping = self.interpreter.single_stepping();
148        self.interpreter.set_single_stepping(true);
149        let mut state = {
150            let transition = self.interpreter.transact(self.tx.clone());
151            Ok(*transition.unwrap().state())
152        };
153
154        loop {
155            match state {
156                // if the VM fails, we interpret as a revert
157                Err(_) => {
158                    break ProgramState::Revert(0);
159                }
160                Ok(
161                    state @ ProgramState::Return(_)
162                    | state @ ProgramState::ReturnData(_)
163                    | state @ ProgramState::Revert(_),
164                ) => break state,
165                Ok(
166                    s @ ProgramState::RunProgram(eval) | s @ ProgramState::VerifyPredicate(eval),
167                ) => {
168                    // time to jump into the specified test
169                    if let Some(b) = eval.breakpoint() {
170                        if b.pc() == jump_pc {
171                            self.interpreter.registers_mut()[RegId::PC] +=
172                                self.relative_jump_in_bytes as u64;
173                            self.interpreter.set_single_stepping(old_single_stepping);
174                            break s;
175                        }
176                    }
177
178                    state = self.interpreter.resume();
179                }
180            }
181        }
182    }
183
184    /// Execute the test with breakpoints enabled.
185    pub fn start_debugging(&mut self) -> anyhow::Result<DebugResult> {
186        let start = std::time::Instant::now();
187
188        let _ = self.single_step_until_test();
189        let state = self
190            .interpreter
191            .resume()
192            .map_err(|err: InterpreterError<_>| {
193                anyhow::anyhow!("VM failed to resume. {:?}", err)
194            })?;
195        if let ProgramState::RunProgram(DebugEval::Breakpoint(breakpoint)) = state {
196            // A breakpoint was hit, so we tell the client to stop.
197            return Ok(DebugResult::Breakpoint(breakpoint.pc()));
198        }
199
200        let duration = start.elapsed();
201        let (gas_used, logs) = Self::get_gas_and_receipts(self.interpreter.receipts().to_vec())?;
202        let span = self.test_entry.span.clone();
203        let file_path = self.test_entry.file_path.clone();
204        let condition = self.test_entry.pass_condition.clone();
205        let name = self.name.clone();
206        Ok(DebugResult::TestComplete(TestResult {
207            name,
208            file_path,
209            duration,
210            span,
211            state,
212            condition,
213            logs,
214            gas_used,
215            ecal: Box::new(self.interpreter.ecal_state().clone()),
216        }))
217    }
218
219    /// Continue executing the test with breakpoints enabled.
220    pub fn continue_debugging(&mut self) -> anyhow::Result<DebugResult> {
221        let start = std::time::Instant::now();
222        let state = self
223            .interpreter
224            .resume()
225            .map_err(|err: InterpreterError<_>| {
226                anyhow::anyhow!("VM failed to resume. {:?}", err)
227            })?;
228        if let ProgramState::RunProgram(DebugEval::Breakpoint(breakpoint)) = state {
229            // A breakpoint was hit, so we tell the client to stop.
230            return Ok(DebugResult::Breakpoint(breakpoint.pc()));
231        }
232        let duration = start.elapsed();
233        let (gas_used, logs) = Self::get_gas_and_receipts(self.interpreter.receipts().to_vec())?; // TODO: calculate culumlative
234        let span = self.test_entry.span.clone();
235        let file_path = self.test_entry.file_path.clone();
236        let condition = self.test_entry.pass_condition.clone();
237        let name = self.name.clone();
238        Ok(DebugResult::TestComplete(TestResult {
239            name,
240            file_path,
241            duration,
242            span,
243            state,
244            condition,
245            logs,
246            gas_used,
247            ecal: Box::new(self.interpreter.ecal_state().clone()),
248        }))
249    }
250
251    pub fn execute(&mut self) -> anyhow::Result<TestResult> {
252        self.interpreter.ecal_state_mut().clear();
253
254        let start = std::time::Instant::now();
255
256        let mut state = Ok(self.single_step_until_test());
257
258        // Run test until its end
259        loop {
260            match state {
261                Err(_) => {
262                    state = Ok(ProgramState::Revert(0));
263                    break;
264                }
265                Ok(
266                    ProgramState::Return(_) | ProgramState::ReturnData(_) | ProgramState::Revert(_),
267                ) => break,
268                Ok(ProgramState::RunProgram(_) | ProgramState::VerifyPredicate(_)) => {
269                    state = self.interpreter.resume();
270                }
271            }
272        }
273
274        let duration = start.elapsed();
275        let (gas_used, logs) = Self::get_gas_and_receipts(self.interpreter.receipts().to_vec())?;
276        let span = self.test_entry.span.clone();
277        let file_path = self.test_entry.file_path.clone();
278        let condition = self.test_entry.pass_condition.clone();
279        let name = self.name.clone();
280        Ok(TestResult {
281            name,
282            file_path,
283            duration,
284            span,
285            state: state.unwrap(),
286            condition,
287            logs,
288            gas_used,
289            ecal: Box::new(self.interpreter.ecal_state().clone()),
290        })
291    }
292
293    fn get_gas_and_receipts(receipts: Vec<Receipt>) -> anyhow::Result<(u64, Vec<Receipt>)> {
294        let gas_used = *receipts
295            .iter()
296            .find_map(|receipt| match receipt {
297                tx::Receipt::ScriptResult { gas_used, .. } => Some(gas_used),
298                _ => None,
299            })
300            .ok_or_else(|| anyhow::anyhow!("missing used gas information from test execution"))?;
301
302        // Only retain `Log` and `LogData` receipts.
303        let logs = receipts
304            .into_iter()
305            .filter(|receipt| {
306                matches!(receipt, tx::Receipt::Log { .. })
307                    || matches!(receipt, tx::Receipt::LogData { .. })
308            })
309            .collect();
310        Ok((gas_used, logs))
311    }
312}
313
314fn find_jump_instruction_index(bytecode: &[u8]) -> usize {
315    // Search first `move $$locbase $sp`
316    // This will be `__entry` for script/predicate/contract using encoding v1;
317    // `main` for script/predicate using encoding v0;
318    // or the first function for libraries
319    // MOVE R59 $sp                                    ;; [26, 236, 80, 0]
320    let a = vm::fuel_asm::op::move_(59, fuel_asm::RegId::SP).to_bytes();
321
322    // for contracts using encoding v0
323    // search the first `lw $r0 $fp i73`
324    // which is the start of the fn selector
325    // LW $writable $fp 0x49                           ;; [93, 64, 96, 73]
326    let b = vm::fuel_asm::op::lw(fuel_asm::RegId::WRITABLE, fuel_asm::RegId::FP, 73).to_bytes();
327
328    bytecode
329        .chunks(Instruction::SIZE)
330        .position(|instruction| {
331            let instruction: [u8; 4] = instruction.try_into().unwrap();
332            instruction == a || instruction == b
333        })
334        .unwrap()
335}