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#[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#[derive(Debug)]
37pub enum DebugResult {
38 TestComplete(TestResult),
40 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 let jump_instruction_index = find_jump_instruction_index(bytecode);
57
58 let script_input_data = vec![];
60 let rng = &mut rand::rngs::StdRng::seed_from_u64(TEST_METADATA_SEED);
61
62 let secret_key = SecretKey::random(rng);
64 let utxo_id = rng.r#gen();
65 let amount = 1;
66 let maturity = 1.into();
67 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 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 let tmp_tx = tx_builder.clone().finalize();
106 let max_gas =
108 tmp_tx.max_gas(consensus_params.gas_costs(), consensus_params.fee_params()) + 1;
109 tx_builder.script_gas_limit(consensus_params.tx_params().max_gas_per_tx() - max_gas);
111
112 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 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 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 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 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 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 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 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())?; 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 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 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 let a = vm::fuel_asm::op::move_(59, fuel_asm::RegId::SP).to_bytes();
319
320 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}