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#[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#[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 ) -> 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.gen();
67 let amount = 1;
68 let maturity = 1.into();
69 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 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 }))
216 }
217
218 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 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())?; 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 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 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 let a = vm::fuel_asm::op::move_(59, fuel_asm::RegId::SP).to_bytes();
316
317 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}