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