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::GasCostsValues;
8use fuel_tx::{self as tx, output::contract::Contract, Chargeable, Finalizable};
9use fuel_vm::error::InterpreterError;
10use fuel_vm::fuel_asm;
11use fuel_vm::prelude::Instruction;
12use fuel_vm::prelude::RegId;
13use fuel_vm::{
14 self as vm, checked_transaction::builder::TransactionBuilderExt, interpreter::Interpreter,
15 prelude::SecretKey, storage::MemoryStorage,
16};
17use rand::{Rng, SeedableRng};
18
19use tx::Receipt;
20
21use vm::interpreter::{InterpreterParams, MemoryInstance};
22use vm::state::DebugEval;
23use vm::state::ProgramState;
24
25#[derive(Debug, Clone)]
27pub struct TestExecutor {
28 pub interpreter: Interpreter<MemoryInstance, MemoryStorage, tx::Script, EcalSyscallHandler>,
29 pub tx: vm::checked_transaction::Ready<tx::Script>,
30 pub test_entry: PkgTestEntry,
31 pub name: String,
32 pub jump_instruction_index: usize,
33 pub relative_jump_in_bytes: u32,
34}
35
36#[derive(Debug)]
38pub enum DebugResult {
39 TestComplete(TestResult),
41 Breakpoint(u64),
43}
44
45impl TestExecutor {
46 pub fn build(
47 bytecode: &[u8],
48 test_instruction_index: u32,
49 test_setup: TestSetup,
50 test_entry: &PkgTestEntry,
51 name: String,
52 gas_costs_values: GasCostsValues,
53 ) -> anyhow::Result<Self> {
54 let storage = test_setup.storage().clone();
55
56 let jump_instruction_index = find_jump_instruction_index(bytecode);
59
60 let script_input_data = vec![];
62 let rng = &mut rand::rngs::StdRng::seed_from_u64(TEST_METADATA_SEED);
63
64 let secret_key = SecretKey::random(rng);
66 let utxo_id = rng.r#gen();
67 let amount = 1;
68 let maturity = 1.into();
69 let asset_id = tx::AssetId::BASE;
73 let tx_pointer = rng.r#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(gas_costs_values);
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 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 let tmp_tx = tx_builder.clone().finalize();
108 let max_gas =
110 tmp_tx.max_gas(consensus_params.gas_costs(), consensus_params.fee_params()) + 1;
111 tx_builder.script_gas_limit(consensus_params.tx_params().max_gas_per_tx() - max_gas);
113
114 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 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 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 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 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 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 ecal: Box::new(self.interpreter.ecal_state().clone()),
216 }))
217 }
218
219 pub fn continue_debugging(&mut self) -> anyhow::Result<DebugResult> {
221 let start = std::time::Instant::now();
222 let state = self
223 .interpreter
224 .resume()
225 .map_err(|err: InterpreterError<_>| {
226 anyhow::anyhow!("VM failed to resume. {:?}", err)
227 })?;
228 if let ProgramState::RunProgram(DebugEval::Breakpoint(breakpoint)) = state {
229 return Ok(DebugResult::Breakpoint(breakpoint.pc()));
231 }
232 let duration = start.elapsed();
233 let (gas_used, logs) = Self::get_gas_and_receipts(self.interpreter.receipts().to_vec())?; let span = self.test_entry.span.clone();
235 let file_path = self.test_entry.file_path.clone();
236 let condition = self.test_entry.pass_condition.clone();
237 let name = self.name.clone();
238 Ok(DebugResult::TestComplete(TestResult {
239 name,
240 file_path,
241 duration,
242 span,
243 state,
244 condition,
245 logs,
246 gas_used,
247 ecal: Box::new(self.interpreter.ecal_state().clone()),
248 }))
249 }
250
251 pub fn execute(&mut self) -> anyhow::Result<TestResult> {
252 self.interpreter.ecal_state_mut().clear();
253
254 let start = std::time::Instant::now();
255
256 let mut state = Ok(self.single_step_until_test());
257
258 loop {
260 match state {
261 Err(_) => {
262 state = Ok(ProgramState::Revert(0));
263 break;
264 }
265 Ok(
266 ProgramState::Return(_) | ProgramState::ReturnData(_) | ProgramState::Revert(_),
267 ) => break,
268 Ok(ProgramState::RunProgram(_) | ProgramState::VerifyPredicate(_)) => {
269 state = self.interpreter.resume();
270 }
271 }
272 }
273
274 let duration = start.elapsed();
275 let (gas_used, logs) = Self::get_gas_and_receipts(self.interpreter.receipts().to_vec())?;
276 let span = self.test_entry.span.clone();
277 let file_path = self.test_entry.file_path.clone();
278 let condition = self.test_entry.pass_condition.clone();
279 let name = self.name.clone();
280 Ok(TestResult {
281 name,
282 file_path,
283 duration,
284 span,
285 state: state.unwrap(),
286 condition,
287 logs,
288 gas_used,
289 ecal: Box::new(self.interpreter.ecal_state().clone()),
290 })
291 }
292
293 fn get_gas_and_receipts(receipts: Vec<Receipt>) -> anyhow::Result<(u64, Vec<Receipt>)> {
294 let gas_used = *receipts
295 .iter()
296 .find_map(|receipt| match receipt {
297 tx::Receipt::ScriptResult { gas_used, .. } => Some(gas_used),
298 _ => None,
299 })
300 .ok_or_else(|| anyhow::anyhow!("missing used gas information from test execution"))?;
301
302 let logs = receipts
304 .into_iter()
305 .filter(|receipt| {
306 matches!(receipt, tx::Receipt::Log { .. })
307 || matches!(receipt, tx::Receipt::LogData { .. })
308 })
309 .collect();
310 Ok((gas_used, logs))
311 }
312}
313
314fn find_jump_instruction_index(bytecode: &[u8]) -> usize {
315 let a = vm::fuel_asm::op::move_(59, fuel_asm::RegId::SP).to_bytes();
321
322 let b = vm::fuel_asm::op::lw(fuel_asm::RegId::WRITABLE, fuel_asm::RegId::FP, 73).to_bytes();
327
328 bytecode
329 .chunks(Instruction::SIZE)
330 .position(|instruction| {
331 let instruction: [u8; 4] = instruction.try_into().unwrap();
332 instruction == a || instruction == b
333 })
334 .unwrap()
335}