Skip to main content

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