iota_move/
unit_test.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use std::{cell::RefCell, collections::BTreeMap, path::Path, sync::Arc};
6
7use clap::Parser;
8use iota_move_build::decorate_warnings;
9use iota_move_natives::{
10    NativesCostTable, object_runtime::ObjectRuntime, test_scenario::InMemoryTestStore,
11};
12use iota_protocol_config::ProtocolConfig;
13use iota_types::{
14    gas_model::tables::initial_cost_schedule_for_unit_tests, in_memory_storage::InMemoryStorage,
15    metrics::LimitsMetrics,
16};
17use move_cli::base::{
18    self,
19    test::{self, UnitTestResult},
20};
21use move_package::BuildConfig;
22use move_unit_test::{UnitTestingConfig, extensions::set_extension_hook};
23use move_vm_runtime::native_extensions::NativeContextExtensions;
24use once_cell::sync::Lazy;
25
26// Move unit tests will halt after executing this many steps. This is a
27// protection to avoid divergence
28const MAX_UNIT_TEST_INSTRUCTIONS: u64 = 1_000_000;
29
30#[derive(Parser)]
31#[group(id = "iota-move-test")]
32pub struct Test {
33    #[command(flatten)]
34    pub test: test::Test,
35}
36
37impl Test {
38    pub fn execute(
39        self,
40        path: Option<&Path>,
41        build_config: BuildConfig,
42    ) -> anyhow::Result<UnitTestResult> {
43        let compute_coverage = self.test.compute_coverage;
44        if !cfg!(debug_assertions) && compute_coverage {
45            return Err(anyhow::anyhow!(
46                "The --coverage flag is currently supported only in debug builds. Please build the IOTA CLI from source in debug mode."
47            ));
48        }
49        // save disassembly if trace execution is enabled
50        let save_disassembly = self.test.trace_execution.is_some();
51        // find manifest file directory from a given path or (if missing) from current
52        // dir
53        let rerooted_path = base::reroot_path(path)?;
54        let unit_test_config = self.test.unit_test_config();
55        run_move_unit_tests(
56            &rerooted_path,
57            build_config,
58            Some(unit_test_config),
59            compute_coverage,
60            save_disassembly,
61        )
62    }
63}
64
65// Create a separate test store per-thread.
66thread_local! {
67    static TEST_STORE_INNER: RefCell<InMemoryStorage> = RefCell::new(InMemoryStorage::default());
68}
69
70static TEST_STORE: Lazy<InMemoryTestStore> = Lazy::new(|| InMemoryTestStore(&TEST_STORE_INNER));
71
72static SET_EXTENSION_HOOK: Lazy<()> =
73    Lazy::new(|| set_extension_hook(Box::new(new_testing_object_and_natives_cost_runtime)));
74
75/// This function returns a result of UnitTestResult. The outer result indicates
76/// whether it successfully started running the test, and the inner result
77/// indicates whether all tests pass.
78pub fn run_move_unit_tests(
79    path: &Path,
80    build_config: BuildConfig,
81    config: Option<UnitTestingConfig>,
82    compute_coverage: bool,
83    save_disassembly: bool,
84) -> anyhow::Result<UnitTestResult> {
85    // bind the extension hook if it has not yet been done
86    Lazy::force(&SET_EXTENSION_HOOK);
87
88    let config = config
89        .unwrap_or_else(|| UnitTestingConfig::default_with_bound(Some(MAX_UNIT_TEST_INSTRUCTIONS)));
90
91    let result = move_cli::base::test::run_move_unit_tests(
92        path,
93        build_config,
94        UnitTestingConfig {
95            report_stacktrace_on_abort: true,
96            ..config
97        },
98        iota_move_natives::all_natives(
99            // silent
100            false,
101            &ProtocolConfig::get_for_max_version_UNSAFE(),
102        ),
103        Some(initial_cost_schedule_for_unit_tests()),
104        compute_coverage,
105        save_disassembly,
106        &mut std::io::stdout(),
107    );
108    result.map(|(test_result, warning_diags)| {
109        if test_result == UnitTestResult::Success {
110            if let Some(diags) = warning_diags {
111                decorate_warnings(diags, None);
112            }
113        }
114        test_result
115    })
116}
117
118fn new_testing_object_and_natives_cost_runtime(ext: &mut NativeContextExtensions) {
119    // Use a throwaway metrics registry for testing.
120    let registry = prometheus::Registry::new();
121    let metrics = Arc::new(LimitsMetrics::new(&registry));
122    let store = Lazy::force(&TEST_STORE);
123
124    ext.add(ObjectRuntime::new(
125        store,
126        BTreeMap::new(),
127        false,
128        Box::leak(Box::new(ProtocolConfig::get_for_max_version_UNSAFE())), // leak for testing
129        metrics,
130        0, // epoch id
131    ));
132    ext.add(NativesCostTable::from_protocol_config(
133        &ProtocolConfig::get_for_max_version_UNSAFE(),
134    ));
135
136    ext.add(store);
137}