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