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