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#[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#[derive(Debug)]
39pub enum DebugResult {
40 TestComplete(TestResult),
42 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 let jump_instruction_index = find_jump_instruction_index(bytecode);
61
62 let script_input_data = vec![];
64 let rng = &mut rand::rngs::StdRng::seed_from_u64(TEST_METADATA_SEED);
65
66 let secret_key = SecretKey::random(rng);
68 let utxo_id = rng.r#gen();
69 let amount = 1;
70 let maturity = 1.into();
71 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 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 let tmp_tx = tx_builder.clone().finalize();
110 let max_gas =
112 tmp_tx.max_gas(consensus_params.gas_costs(), consensus_params.fee_params()) + 1;
113 tx_builder.script_gas_limit(consensus_params.tx_params().max_gas_per_tx() - max_gas);
115
116 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 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 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 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 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 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 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 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())?; 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 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 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 let a = vm::fuel_asm::op::move_(59, fuel_asm::RegId::SP).to_bytes();
323
324 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}