forc_test/
execute.rs

1use crate::maxed_consensus_params;
2use crate::setup::TestSetup;
3use crate::TestResult;
4use crate::TEST_METADATA_SEED;
5use forc_pkg::PkgTestEntry;
6use fuel_tx::{self as tx, output::contract::Contract, Chargeable, Finalizable};
7use fuel_vm::error::InterpreterError;
8use fuel_vm::fuel_asm;
9use fuel_vm::prelude::Instruction;
10use fuel_vm::prelude::RegId;
11use fuel_vm::{
12    self as vm,
13    checked_transaction::builder::TransactionBuilderExt,
14    interpreter::{Interpreter, NotSupportedEcal},
15    prelude::SecretKey,
16    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, NotSupportedEcal>,
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    ) -> 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.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.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();
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 and core 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        }))
216    }
217
218    /// Continue executing the test with breakpoints enabled.
219    pub fn continue_debugging(&mut self) -> anyhow::Result<DebugResult> {
220        let start = std::time::Instant::now();
221        let state = self
222            .interpreter
223            .resume()
224            .map_err(|err: InterpreterError<_>| {
225                anyhow::anyhow!("VM failed to resume. {:?}", err)
226            })?;
227        if let ProgramState::RunProgram(DebugEval::Breakpoint(breakpoint)) = state {
228            // A breakpoint was hit, so we tell the client to stop.
229            return Ok(DebugResult::Breakpoint(breakpoint.pc()));
230        }
231        let duration = start.elapsed();
232        let (gas_used, logs) = Self::get_gas_and_receipts(self.interpreter.receipts().to_vec())?; // TODO: calculate culumlative
233        let span = self.test_entry.span.clone();
234        let file_path = self.test_entry.file_path.clone();
235        let condition = self.test_entry.pass_condition.clone();
236        let name = self.name.clone();
237        Ok(DebugResult::TestComplete(TestResult {
238            name,
239            file_path,
240            duration,
241            span,
242            state,
243            condition,
244            logs,
245            gas_used,
246        }))
247    }
248
249    pub fn execute(&mut self) -> anyhow::Result<TestResult> {
250        let start = std::time::Instant::now();
251
252        let mut state = Ok(self.single_step_until_test());
253
254        // Run test until its end
255        loop {
256            match state {
257                Err(_) => {
258                    state = Ok(ProgramState::Revert(0));
259                    break;
260                }
261                Ok(
262                    ProgramState::Return(_) | ProgramState::ReturnData(_) | ProgramState::Revert(_),
263                ) => break,
264                Ok(ProgramState::RunProgram(_) | ProgramState::VerifyPredicate(_)) => {
265                    state = self.interpreter.resume();
266                }
267            }
268        }
269
270        let duration = start.elapsed();
271        let (gas_used, logs) = Self::get_gas_and_receipts(self.interpreter.receipts().to_vec())?;
272        let span = self.test_entry.span.clone();
273        let file_path = self.test_entry.file_path.clone();
274        let condition = self.test_entry.pass_condition.clone();
275        let name = self.name.clone();
276        Ok(TestResult {
277            name,
278            file_path,
279            duration,
280            span,
281            state: state.unwrap(),
282            condition,
283            logs,
284            gas_used,
285        })
286    }
287
288    fn get_gas_and_receipts(receipts: Vec<Receipt>) -> anyhow::Result<(u64, Vec<Receipt>)> {
289        let gas_used = *receipts
290            .iter()
291            .find_map(|receipt| match receipt {
292                tx::Receipt::ScriptResult { gas_used, .. } => Some(gas_used),
293                _ => None,
294            })
295            .ok_or_else(|| anyhow::anyhow!("missing used gas information from test execution"))?;
296
297        // Only retain `Log` and `LogData` receipts.
298        let logs = receipts
299            .into_iter()
300            .filter(|receipt| {
301                matches!(receipt, tx::Receipt::Log { .. })
302                    || matches!(receipt, tx::Receipt::LogData { .. })
303            })
304            .collect();
305        Ok((gas_used, logs))
306    }
307}
308
309fn find_jump_instruction_index(bytecode: &[u8]) -> usize {
310    // Search first `move $$locbase $sp`
311    // This will be `__entry` for script/predicate/contract using encoding v1;
312    // `main` for script/predicate using encoding v0;
313    // or the first function for libraries
314    // MOVE R59 $sp                                    ;; [26, 236, 80, 0]
315    let a = vm::fuel_asm::op::move_(59, fuel_asm::RegId::SP).to_bytes();
316
317    // for contracts using encoding v0
318    // search the first `lw $r0 $fp i73`
319    // which is the start of the fn selector
320    // LW $writable $fp 0x49                           ;; [93, 64, 96, 73]
321    let b = vm::fuel_asm::op::lw(fuel_asm::RegId::WRITABLE, fuel_asm::RegId::FP, 73).to_bytes();
322
323    bytecode
324        .chunks(Instruction::SIZE)
325        .position(|instruction| {
326            let instruction: [u8; 4] = instruction.try_into().unwrap();
327            instruction == a || instruction == b
328        })
329        .unwrap()
330}