sui_move/
unit_test.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use clap::Parser;
5use move_cli::base::{
6    self,
7    test::{self, UnitTestResult},
8};
9use move_package_alt_compilation::build_config::BuildConfig;
10use move_unit_test::{UnitTestingConfig, vm_test_setup::VMTestSetup};
11use move_vm_config::runtime::VMConfig;
12use move_vm_runtime::natives::extensions::NativeContextExtensions;
13use std::{
14    cell::RefCell,
15    collections::BTreeMap,
16    ops::{Deref, DerefMut},
17    path::Path,
18    rc::Rc,
19    sync::{Arc, LazyLock},
20};
21use sui_adapter::gas_meter::SuiGasMeter;
22use sui_move_build::decorate_warnings;
23use sui_move_natives::{
24    NativesCostTable, object_runtime::ObjectRuntime, test_scenario::InMemoryTestStore,
25    transaction_context::TransactionContext,
26};
27use sui_package_alt::{SuiFlavor, find_environment};
28use sui_protocol_config::ProtocolConfig;
29use sui_sdk::wallet_context::WalletContext;
30use sui_types::{
31    base_types::{SuiAddress, TxContext},
32    digests::TransactionDigest,
33    gas::{SuiGasStatus, SuiGasStatusAPI},
34    gas_model::{tables::GasStatus, units_types::Gas},
35    in_memory_storage::InMemoryStorage,
36    metrics::ExecutionMetrics,
37};
38
39// Move unit tests will halt after executing this many steps. This is a protection to avoid divergence
40pub static MAX_UNIT_TEST_INSTRUCTIONS: LazyLock<u64> =
41    LazyLock::new(|| ProtocolConfig::get_for_max_version_UNSAFE().max_tx_gas());
42
43/// Gas price used for the meter during Move unit tests.
44const TEST_GAS_PRICE: u64 = 500;
45
46#[derive(Parser)]
47#[group(id = "sui-move-test")]
48pub struct Test {
49    #[clap(flatten)]
50    pub test: test::Test,
51}
52
53impl Test {
54    pub async fn execute(
55        self,
56        path: Option<&Path>,
57        mut build_config: BuildConfig,
58        wallet: &WalletContext,
59        flavor: SuiFlavor,
60    ) -> anyhow::Result<UnitTestResult> {
61        let compute_coverage = self.test.compute_coverage;
62        if !cfg!(feature = "tracing") && compute_coverage {
63            return Err(anyhow::anyhow!(
64                "The --coverage flag is currently supported only in builds built with the `tracing` feature enabled. \
65                Please build the Sui CLI from source with `--features tracing` to use this flag."
66            ));
67        }
68        // save disassembly if trace execution is enabled
69        let save_disassembly = self.test.trace.is_some();
70        // set the default flavor to Sui if not already set by the user
71        if build_config.default_flavor.is_none() {
72            build_config.default_flavor = Some(move_compiler::editions::Flavor::Sui);
73        }
74
75        // find manifest file directory from a given path or (if missing) from current dir
76        let rerooted_path = base::reroot_path(path)?;
77
78        // If no gas limit is set, set it to the default max. This allows
79        // users to provide custom configs but not have to worry about setting a gas limit unless that
80        // is what they care about.
81        let unit_test_config = self
82            .test
83            .unit_test_config(Some(*MAX_UNIT_TEST_INSTRUCTIONS));
84
85        // set the environment (this is a little janky: we get it from the manifest here, then pass
86        // it as the optional argument in the build-config, which then looks it up again, but it
87        // should be ok.
88        let environment =
89            find_environment(&rerooted_path, build_config.environment, wallet, false).await?;
90        build_config.environment = Some(environment.name);
91
92        run_move_unit_tests(
93            &rerooted_path,
94            build_config,
95            Some(unit_test_config),
96            compute_coverage,
97            save_disassembly,
98            flavor,
99        )
100        .await
101    }
102}
103
104/// This function returns a result of UnitTestResult. The outer result indicates whether it
105/// successfully started running the test, and the inner result indicatests whether all tests pass.
106pub async fn run_move_unit_tests(
107    path: &Path,
108    build_config: BuildConfig,
109    config: Option<UnitTestingConfig>,
110    compute_coverage: bool,
111    save_disassembly: bool,
112    flavor: SuiFlavor,
113) -> anyhow::Result<UnitTestResult> {
114    let config = config.unwrap_or_else(|| {
115        UnitTestingConfig::default_with_bound(Some(*MAX_UNIT_TEST_INSTRUCTIONS))
116    });
117
118    let result = move_cli::base::test::run_move_unit_tests(
119        path,
120        build_config,
121        UnitTestingConfig {
122            report_stacktrace_on_abort: true,
123            ..config
124        },
125        flavor,
126        SuiVMTestSetup::new(),
127        compute_coverage,
128        save_disassembly,
129        &mut std::io::stdout(),
130    )
131    .await;
132
133    result.map(|(test_result, warning_diags)| {
134        if test_result == UnitTestResult::Success
135            && let Some(diags) = warning_diags
136        {
137            decorate_warnings(diags, None);
138        }
139        test_result
140    })
141}
142
143pub struct SuiVMTestSetup {
144    gas_price: u64,
145    reference_gas_price: u64,
146    protocol_config: ProtocolConfig,
147    native_function_table: move_vm_runtime::natives::functions::NativeFunctionTable,
148}
149
150impl Default for SuiVMTestSetup {
151    fn default() -> Self {
152        Self::new()
153    }
154}
155
156impl SuiVMTestSetup {
157    pub fn new() -> Self {
158        let protocol_config = ProtocolConfig::get_for_max_version_UNSAFE();
159        let native_function_table =
160            sui_move_natives::all_natives(/* silent */ false, &protocol_config);
161        Self {
162            gas_price: TEST_GAS_PRICE,
163            reference_gas_price: TEST_GAS_PRICE,
164            protocol_config,
165            native_function_table,
166        }
167    }
168
169    pub fn max_gas_budget(&self) -> u64 {
170        self.protocol_config.max_tx_gas()
171    }
172}
173
174impl VMTestSetup for SuiVMTestSetup {
175    type Meter<'a> = SuiGasMeter<SuiGasStatusTestWrapper>;
176    type ExtensionsBuilder<'a> = InMemoryTestStore;
177
178    fn new_meter<'a>(&'a self, execution_bound: Option<u64>) -> Self::Meter<'a> {
179        SuiGasMeter(SuiGasStatusTestWrapper(
180            SuiGasStatus::new(
181                execution_bound.unwrap_or(*MAX_UNIT_TEST_INSTRUCTIONS),
182                self.gas_price,
183                self.reference_gas_price,
184                &self.protocol_config,
185            )
186            .unwrap(),
187        ))
188    }
189
190    fn used_gas<'a>(&'a self, execution_bound: u64, meter: Self::Meter<'a>) -> u64 {
191        let gas_status = &meter.0;
192        Gas::new(execution_bound)
193            .checked_sub(gas_status.remaining_gas())
194            .unwrap()
195            .into()
196    }
197
198    fn vm_config(&self) -> VMConfig {
199        sui_adapter::adapter::vm_config(&self.protocol_config)
200    }
201
202    fn native_function_table(&self) -> move_vm_runtime::natives::functions::NativeFunctionTable {
203        self.native_function_table.clone()
204    }
205
206    fn new_extensions_builder(&self) -> InMemoryTestStore {
207        InMemoryTestStore(RefCell::new(InMemoryStorage::default()))
208    }
209
210    fn new_native_context_extensions<'ext>(
211        &self,
212        store: &'ext InMemoryTestStore,
213    ) -> NativeContextExtensions<'ext> {
214        let mut ext = NativeContextExtensions::default();
215        // Use a throwaway metrics registry for testing.
216        let registry = prometheus::Registry::new();
217        let metrics = Arc::new(ExecutionMetrics::new(&registry));
218
219        ext.add(ObjectRuntime::new(
220            store,
221            BTreeMap::new(),
222            false,
223            Box::leak(Box::new(ProtocolConfig::get_for_max_version_UNSAFE())), // leak for testing
224            metrics,
225            0, // epoch id
226        ));
227        ext.add(NativesCostTable::from_protocol_config(
228            &self.protocol_config,
229        ));
230        let tx_context = TxContext::new_from_components(
231            &SuiAddress::ZERO,
232            &TransactionDigest::default(),
233            &0,
234            0,
235            0,
236            0,
237            0,
238            None,
239            &self.protocol_config,
240        );
241        ext.add(TransactionContext::new_for_testing(Rc::new(RefCell::new(
242            tx_context,
243        ))));
244        ext.add(store);
245        ext
246    }
247}
248
249// Massaging to get traits to line up.
250pub struct SuiGasStatusTestWrapper(SuiGasStatus);
251
252impl Deref for SuiGasStatusTestWrapper {
253    type Target = GasStatus;
254
255    fn deref(&self) -> &Self::Target {
256        self.0.move_gas_status()
257    }
258}
259
260impl DerefMut for SuiGasStatusTestWrapper {
261    fn deref_mut(&mut self) -> &mut Self::Target {
262        self.0.move_gas_status_mut()
263    }
264}