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, extensions::set_extension_hook};
11use move_vm_runtime::native_extensions::NativeContextExtensions;
12use once_cell::sync::Lazy;
13use std::{cell::RefCell, collections::BTreeMap, path::Path, rc::Rc, sync::Arc};
14use sui_move_build::decorate_warnings;
15use sui_move_natives::{
16    NativesCostTable, object_runtime::ObjectRuntime, test_scenario::InMemoryTestStore,
17    transaction_context::TransactionContext,
18};
19use sui_package_alt::find_environment;
20use sui_protocol_config::ProtocolConfig;
21use sui_sdk::wallet_context::WalletContext;
22use sui_types::{
23    base_types::{SuiAddress, TxContext},
24    digests::TransactionDigest,
25    gas_model::tables::initial_cost_schedule_for_unit_tests,
26    in_memory_storage::InMemoryStorage,
27    metrics::LimitsMetrics,
28};
29
30// Move unit tests will halt after executing this many steps. This is a protection to avoid divergence
31const MAX_UNIT_TEST_INSTRUCTIONS: u64 = 1_000_000;
32
33#[derive(Parser)]
34#[group(id = "sui-move-test")]
35pub struct Test {
36    #[clap(flatten)]
37    pub test: test::Test,
38}
39
40impl Test {
41    pub async fn execute(
42        self,
43        path: Option<&Path>,
44        mut build_config: BuildConfig,
45        wallet: &WalletContext,
46    ) -> anyhow::Result<UnitTestResult> {
47        let compute_coverage = self.test.compute_coverage;
48        if !cfg!(feature = "tracing") && compute_coverage {
49            return Err(anyhow::anyhow!(
50                "The --coverage flag is currently supported only in builds built with the `tracing` feature enabled. \
51                Please build the Sui CLI from source with `--features tracing` to use this flag."
52            ));
53        }
54        // save disassembly if trace execution is enabled
55        let save_disassembly = self.test.trace;
56        // set the default flavor to Sui if not already set by the user
57        if build_config.default_flavor.is_none() {
58            build_config.default_flavor = Some(move_compiler::editions::Flavor::Sui);
59        }
60
61        // find manifest file directory from a given path or (if missing) from current dir
62        let rerooted_path = base::reroot_path(path)?;
63        let unit_test_config = self.test.unit_test_config();
64
65        // set the environment (this is a little janky: we get it from the manifest here, then pass
66        // it as the optional argument in the build-config, which then looks it up again, but it
67        // should be ok.
68        let environment =
69            find_environment(&rerooted_path, build_config.environment, wallet).await?;
70        build_config.environment = Some(environment.name);
71
72        run_move_unit_tests(
73            &rerooted_path,
74            build_config,
75            Some(unit_test_config),
76            compute_coverage,
77            save_disassembly,
78        )
79        .await
80    }
81}
82
83// Create a separate test store per-thread.
84thread_local! {
85    static TEST_STORE_INNER: RefCell<InMemoryStorage> = RefCell::new(InMemoryStorage::default());
86}
87
88static TEST_STORE: Lazy<InMemoryTestStore> = Lazy::new(|| InMemoryTestStore(&TEST_STORE_INNER));
89
90static SET_EXTENSION_HOOK: Lazy<()> =
91    Lazy::new(|| set_extension_hook(Box::new(new_testing_object_and_natives_cost_runtime)));
92
93/// This function returns a result of UnitTestResult. The outer result indicates whether it
94/// successfully started running the test, and the inner result indicatests whether all tests pass.
95pub async fn run_move_unit_tests(
96    path: &Path,
97    build_config: BuildConfig,
98    config: Option<UnitTestingConfig>,
99    compute_coverage: bool,
100    save_disassembly: bool,
101) -> anyhow::Result<UnitTestResult> {
102    // bind the extension hook if it has not yet been done
103    Lazy::force(&SET_EXTENSION_HOOK);
104
105    let config = config
106        .unwrap_or_else(|| UnitTestingConfig::default_with_bound(Some(MAX_UNIT_TEST_INSTRUCTIONS)));
107
108    let result = move_cli::base::test::run_move_unit_tests::<sui_package_alt::SuiFlavor, _>(
109        path,
110        build_config,
111        UnitTestingConfig {
112            report_stacktrace_on_abort: true,
113            ..config
114        },
115        sui_move_natives::all_natives(
116            /* silent */ false,
117            &ProtocolConfig::get_for_max_version_UNSAFE(),
118        ),
119        Some(initial_cost_schedule_for_unit_tests()),
120        compute_coverage,
121        save_disassembly,
122        &mut std::io::stdout(),
123    )
124    .await;
125
126    result.map(|(test_result, warning_diags)| {
127        if test_result == UnitTestResult::Success
128            && let Some(diags) = warning_diags
129        {
130            decorate_warnings(diags, None);
131        }
132        test_result
133    })
134}
135
136fn new_testing_object_and_natives_cost_runtime(ext: &mut NativeContextExtensions) {
137    // Use a throwaway metrics registry for testing.
138    let registry = prometheus::Registry::new();
139    let metrics = Arc::new(LimitsMetrics::new(&registry));
140    let store = Lazy::force(&TEST_STORE);
141    let protocol_config = ProtocolConfig::get_for_max_version_UNSAFE();
142
143    ext.add(ObjectRuntime::new(
144        store,
145        BTreeMap::new(),
146        false,
147        Box::leak(Box::new(ProtocolConfig::get_for_max_version_UNSAFE())), // leak for testing
148        metrics,
149        0, // epoch id
150    ));
151    ext.add(NativesCostTable::from_protocol_config(&protocol_config));
152    let tx_context = TxContext::new_from_components(
153        &SuiAddress::ZERO,
154        &TransactionDigest::default(),
155        &0,
156        0,
157        0,
158        0,
159        0,
160        None,
161        &protocol_config,
162    );
163    ext.add(TransactionContext::new_for_testing(Rc::new(RefCell::new(
164        tx_context,
165    ))));
166    ext.add(store);
167}