forc_test/
execute.rs

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