forc_test/
execute.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
use crate::maxed_consensus_params;
use crate::setup::TestSetup;
use crate::TestResult;
use crate::TEST_METADATA_SEED;
use forc_pkg::PkgTestEntry;
use fuel_tx::{self as tx, output::contract::Contract, Chargeable, Finalizable};
use fuel_vm::error::InterpreterError;
use fuel_vm::{
    self as vm,
    checked_transaction::builder::TransactionBuilderExt,
    interpreter::{Interpreter, NotSupportedEcal},
    prelude::SecretKey,
    storage::MemoryStorage,
};
use rand::{Rng, SeedableRng};

use tx::Receipt;

use vm::interpreter::{InterpreterParams, MemoryInstance};
use vm::state::DebugEval;
use vm::state::ProgramState;

/// An interface for executing a test within a VM [Interpreter] instance.
#[derive(Debug, Clone)]
pub struct TestExecutor {
    pub interpreter: Interpreter<MemoryInstance, MemoryStorage, tx::Script, NotSupportedEcal>,
    pub tx: vm::checked_transaction::Ready<tx::Script>,
    pub test_entry: PkgTestEntry,
    pub name: String,
}

/// The result of executing a test with breakpoints enabled.
#[derive(Debug)]
pub enum DebugResult {
    // Holds the test result.
    TestComplete(TestResult),
    // Holds the program counter of where the program stopped due to a breakpoint.
    Breakpoint(u64),
}

impl TestExecutor {
    pub fn build(
        bytecode: &[u8],
        test_offset: u32,
        test_setup: TestSetup,
        test_entry: &PkgTestEntry,
        name: String,
    ) -> anyhow::Result<Self> {
        let storage = test_setup.storage().clone();

        // Patch the bytecode to jump to the relevant test.
        let bytecode = patch_test_bytecode(bytecode, test_offset).into_owned();

        // Create a transaction to execute the test function.
        let script_input_data = vec![];
        let rng = &mut rand::rngs::StdRng::seed_from_u64(TEST_METADATA_SEED);

        // Prepare the transaction metadata.
        let secret_key = SecretKey::random(rng);
        let utxo_id = rng.gen();
        let amount = 1;
        let maturity = 1.into();
        // NOTE: fuel-core is using dynamic asset id and interacting with the fuel-core, using static
        // asset id is not correct. But since forc-test maintains its own interpreter instance, correct
        // base asset id is indeed the static `tx::AssetId::BASE`.
        let asset_id = tx::AssetId::BASE;
        let tx_pointer = rng.gen();
        let block_height = (u32::MAX >> 1).into();
        let gas_price = 0;

        let mut tx_builder = tx::TransactionBuilder::script(bytecode, script_input_data);

        let params = maxed_consensus_params();

        tx_builder
            .with_params(params)
            .add_unsigned_coin_input(secret_key, utxo_id, amount, asset_id, tx_pointer)
            .maturity(maturity);

        let mut output_index = 1;
        // Insert contract ids into tx input
        for contract_id in test_setup.contract_ids() {
            tx_builder
                .add_input(tx::Input::contract(
                    tx::UtxoId::new(tx::Bytes32::zeroed(), 0),
                    tx::Bytes32::zeroed(),
                    tx::Bytes32::zeroed(),
                    tx::TxPointer::new(0u32.into(), 0),
                    contract_id,
                ))
                .add_output(tx::Output::Contract(Contract {
                    input_index: output_index,
                    balance_root: fuel_tx::Bytes32::zeroed(),
                    state_root: tx::Bytes32::zeroed(),
                }));
            output_index += 1;
        }

        let consensus_params = tx_builder.get_params().clone();
        // Temporarily finalize to calculate `script_gas_limit`
        let tmp_tx = tx_builder.clone().finalize();
        // Get `max_gas` used by everything except the script execution. Add `1` because of rounding.
        let max_gas =
            tmp_tx.max_gas(consensus_params.gas_costs(), consensus_params.fee_params()) + 1;
        // Increase `script_gas_limit` to the maximum allowed value.
        tx_builder.script_gas_limit(consensus_params.tx_params().max_gas_per_tx() - max_gas);

        // We need to increase the tx size limit as the default is 110 * 1024 and for big tests
        // such as std and core this is not enough.

        let tx = tx_builder
            .finalize_checked(block_height)
            .into_ready(
                gas_price,
                consensus_params.gas_costs(),
                consensus_params.fee_params(),
            )
            .map_err(|e| anyhow::anyhow!("{e:?}"))?;

        let interpreter_params = InterpreterParams::new(gas_price, &consensus_params);
        let memory_instance = MemoryInstance::new();
        let interpreter = Interpreter::with_storage(memory_instance, storage, interpreter_params);

        Ok(TestExecutor {
            interpreter,
            tx,
            test_entry: test_entry.clone(),
            name,
        })
    }

    /// Execute the test with breakpoints enabled.
    pub fn start_debugging(&mut self) -> anyhow::Result<DebugResult> {
        let start = std::time::Instant::now();
        let transition = self
            .interpreter
            .transact(self.tx.clone())
            .map_err(|err: InterpreterError<_>| anyhow::anyhow!(err))?;
        let state = *transition.state();
        if let ProgramState::RunProgram(DebugEval::Breakpoint(breakpoint)) = state {
            // A breakpoint was hit, so we tell the client to stop.
            return Ok(DebugResult::Breakpoint(breakpoint.pc()));
        }
        let duration = start.elapsed();
        let (gas_used, logs) = Self::get_gas_and_receipts(transition.receipts().to_vec())?;
        let span = self.test_entry.span.clone();
        let file_path = self.test_entry.file_path.clone();
        let condition = self.test_entry.pass_condition.clone();
        let name = self.name.clone();
        Ok(DebugResult::TestComplete(TestResult {
            name,
            file_path,
            duration,
            span,
            state,
            condition,
            logs,
            gas_used,
        }))
    }

    /// Continue executing the test with breakpoints enabled.
    pub fn continue_debugging(&mut self) -> anyhow::Result<DebugResult> {
        let start = std::time::Instant::now();
        let state = self
            .interpreter
            .resume()
            .map_err(|err: InterpreterError<_>| {
                anyhow::anyhow!("VM failed to resume. {:?}", err)
            })?;
        if let ProgramState::RunProgram(DebugEval::Breakpoint(breakpoint)) = state {
            // A breakpoint was hit, so we tell the client to stop.
            return Ok(DebugResult::Breakpoint(breakpoint.pc()));
        }
        let duration = start.elapsed();
        let (gas_used, logs) = Self::get_gas_and_receipts(self.interpreter.receipts().to_vec())?; // TODO: calculate culumlative
        let span = self.test_entry.span.clone();
        let file_path = self.test_entry.file_path.clone();
        let condition = self.test_entry.pass_condition.clone();
        let name = self.name.clone();
        Ok(DebugResult::TestComplete(TestResult {
            name,
            file_path,
            duration,
            span,
            state,
            condition,
            logs,
            gas_used,
        }))
    }

    pub fn execute(&mut self) -> anyhow::Result<TestResult> {
        let start = std::time::Instant::now();
        let transition = self
            .interpreter
            .transact(self.tx.clone())
            .map_err(|err: InterpreterError<_>| anyhow::anyhow!(err))?;
        let state = *transition.state();

        let duration = start.elapsed();
        let (gas_used, logs) = Self::get_gas_and_receipts(transition.receipts().to_vec())?;
        let span = self.test_entry.span.clone();
        let file_path = self.test_entry.file_path.clone();
        let condition = self.test_entry.pass_condition.clone();
        let name = self.name.clone();
        Ok(TestResult {
            name,
            file_path,
            duration,
            span,
            state,
            condition,
            logs,
            gas_used,
        })
    }

    fn get_gas_and_receipts(receipts: Vec<Receipt>) -> anyhow::Result<(u64, Vec<Receipt>)> {
        let gas_used = *receipts
            .iter()
            .find_map(|receipt| match receipt {
                tx::Receipt::ScriptResult { gas_used, .. } => Some(gas_used),
                _ => None,
            })
            .ok_or_else(|| anyhow::anyhow!("missing used gas information from test execution"))?;

        // Only retain `Log` and `LogData` receipts.
        let logs = receipts
            .into_iter()
            .filter(|receipt| {
                matches!(receipt, tx::Receipt::Log { .. })
                    || matches!(receipt, tx::Receipt::LogData { .. })
            })
            .collect();
        Ok((gas_used, logs))
    }
}

/// Given some bytecode and an instruction offset for some test's desired entry point, patch the
/// bytecode with a `JI` (jump) instruction to jump to the desired test.
///
/// We want to splice in the `JI` only after the initial data section setup is complete, and only
/// if the entry point doesn't begin exactly after the data section setup.
///
/// The following is how the beginning of the bytecode is laid out:
///
/// ```ignore
/// [ 0] ji   i(4 + 2)                 ; Jumps to the data section setup.
/// [ 1] noop
/// [ 2] DATA_SECTION_OFFSET[0..32]
/// [ 3] DATA_SECTION_OFFSET[32..64]
/// [ 4] CONFIGURABLES_OFFSET[0..32]
/// [ 5] CONFIGURABLES_OFFSET[32..64]
/// [ 6] lw   $ds $is 1                ; The data section setup, i.e. where the first ji lands.
/// [ 7] add  $$ds $$ds $is
/// [ 8] <first-entry-point>           ; This is where we want to jump from to our test code!
/// ```
fn patch_test_bytecode(bytecode: &[u8], test_offset: u32) -> std::borrow::Cow<[u8]> {
    // Each instruction is 4 bytes,
    // so we divide the total byte-size by 4 to get the instruction offset.
    const PROGRAM_START_INST_OFFSET: u32 = (sway_core::PRELUDE_SIZE_IN_BYTES / 4) as u32;
    const PROGRAM_START_BYTE_OFFSET: usize = sway_core::PRELUDE_SIZE_IN_BYTES;

    // If our desired entry point is the program start, no need to jump.
    if test_offset == PROGRAM_START_INST_OFFSET {
        return std::borrow::Cow::Borrowed(bytecode);
    }

    // Create the jump instruction and splice it into the bytecode.
    let ji = vm::fuel_asm::op::ji(test_offset);
    let ji_bytes = ji.to_bytes();
    let start = PROGRAM_START_BYTE_OFFSET;
    let end = start + ji_bytes.len();
    let mut patched = bytecode.to_vec();
    patched.splice(start..end, ji_bytes);
    std::borrow::Cow::Owned(patched)
}