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