1use 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::{
11 UnitTestingConfig, extensions::set_extension_hook, vm_test_setup::VMTestSetup,
12};
13use move_vm_config::runtime::VMConfig;
14use move_vm_runtime::native_extensions::NativeContextExtensions;
15use once_cell::sync::Lazy;
16use std::{
17 cell::RefCell,
18 collections::BTreeMap,
19 ops::{Deref, DerefMut},
20 path::Path,
21 rc::Rc,
22 sync::{Arc, LazyLock},
23};
24use sui_adapter::gas_meter::SuiGasMeter;
25use sui_move_build::decorate_warnings;
26use sui_move_natives::{
27 NativesCostTable, object_runtime::ObjectRuntime, test_scenario::InMemoryTestStore,
28 transaction_context::TransactionContext,
29};
30use sui_package_alt::find_environment;
31use sui_protocol_config::ProtocolConfig;
32use sui_sdk::wallet_context::WalletContext;
33use sui_types::{
34 base_types::{SuiAddress, TxContext},
35 digests::TransactionDigest,
36 gas::{SuiGasStatus, SuiGasStatusAPI},
37 gas_model::{tables::GasStatus, units_types::Gas},
38 in_memory_storage::InMemoryStorage,
39 metrics::LimitsMetrics,
40};
41
42pub static MAX_UNIT_TEST_INSTRUCTIONS: LazyLock<u64> =
44 LazyLock::new(|| ProtocolConfig::get_for_max_version_UNSAFE().max_tx_gas());
45
46const TEST_GAS_PRICE: u64 = 500;
48
49#[derive(Parser)]
50#[group(id = "sui-move-test")]
51pub struct Test {
52 #[clap(flatten)]
53 pub test: test::Test,
54}
55
56impl Test {
57 pub async fn execute(
58 self,
59 path: Option<&Path>,
60 mut build_config: BuildConfig,
61 wallet: &WalletContext,
62 ) -> anyhow::Result<UnitTestResult> {
63 let compute_coverage = self.test.compute_coverage;
64 if !cfg!(feature = "tracing") && compute_coverage {
65 return Err(anyhow::anyhow!(
66 "The --coverage flag is currently supported only in builds built with the `tracing` feature enabled. \
67 Please build the Sui CLI from source with `--features tracing` to use this flag."
68 ));
69 }
70 let save_disassembly = self.test.trace;
72 if build_config.default_flavor.is_none() {
74 build_config.default_flavor = Some(move_compiler::editions::Flavor::Sui);
75 }
76
77 let rerooted_path = base::reroot_path(path)?;
79 let unit_test_config = self.test.unit_test_config();
80
81 let environment =
85 find_environment(&rerooted_path, build_config.environment, wallet).await?;
86 build_config.environment = Some(environment.name);
87
88 run_move_unit_tests(
89 &rerooted_path,
90 build_config,
91 Some(unit_test_config),
92 compute_coverage,
93 save_disassembly,
94 )
95 .await
96 }
97}
98
99thread_local! {
101 static TEST_STORE_INNER: RefCell<InMemoryStorage> = RefCell::new(InMemoryStorage::default());
102}
103
104static TEST_STORE: Lazy<InMemoryTestStore> = Lazy::new(|| InMemoryTestStore(&TEST_STORE_INNER));
105
106static SET_EXTENSION_HOOK: Lazy<()> =
107 Lazy::new(|| set_extension_hook(Box::new(new_testing_object_and_natives_cost_runtime)));
108
109pub async fn run_move_unit_tests(
112 path: &Path,
113 build_config: BuildConfig,
114 config: Option<UnitTestingConfig>,
115 compute_coverage: bool,
116 save_disassembly: bool,
117) -> anyhow::Result<UnitTestResult> {
118 Lazy::force(&SET_EXTENSION_HOOK);
120
121 let config = config.unwrap_or_else(|| {
122 UnitTestingConfig::default_with_bound(Some(*MAX_UNIT_TEST_INSTRUCTIONS))
123 });
124
125 let result = move_cli::base::test::run_move_unit_tests::<sui_package_alt::SuiFlavor, _, _>(
126 path,
127 build_config,
128 UnitTestingConfig {
129 report_stacktrace_on_abort: true,
130 ..config
131 },
132 SuiVMTestSetup::new(),
133 compute_coverage,
134 save_disassembly,
135 &mut std::io::stdout(),
136 )
137 .await;
138
139 result.map(|(test_result, warning_diags)| {
140 if test_result == UnitTestResult::Success
141 && let Some(diags) = warning_diags
142 {
143 decorate_warnings(diags, None);
144 }
145 test_result
146 })
147}
148
149fn new_testing_object_and_natives_cost_runtime(ext: &mut NativeContextExtensions) {
150 let registry = prometheus::Registry::new();
152 let metrics = Arc::new(LimitsMetrics::new(®istry));
153 let store = Lazy::force(&TEST_STORE);
154 let protocol_config = ProtocolConfig::get_for_max_version_UNSAFE();
155
156 ext.add(ObjectRuntime::new(
157 store,
158 BTreeMap::new(),
159 false,
160 Box::leak(Box::new(ProtocolConfig::get_for_max_version_UNSAFE())), metrics,
162 0, ));
164 ext.add(NativesCostTable::from_protocol_config(&protocol_config));
165 let tx_context = TxContext::new_from_components(
166 &SuiAddress::ZERO,
167 &TransactionDigest::default(),
168 &0,
169 0,
170 0,
171 0,
172 0,
173 None,
174 &protocol_config,
175 );
176 ext.add(TransactionContext::new_for_testing(Rc::new(RefCell::new(
177 tx_context,
178 ))));
179 ext.add(store);
180}
181
182pub struct SuiVMTestSetup {
183 gas_price: u64,
184 reference_gas_price: u64,
185 protocol_config: ProtocolConfig,
186 native_function_table: move_vm_runtime::native_functions::NativeFunctionTable,
187}
188
189impl Default for SuiVMTestSetup {
190 fn default() -> Self {
191 Self::new()
192 }
193}
194
195impl SuiVMTestSetup {
196 pub fn new() -> Self {
197 let protocol_config = ProtocolConfig::get_for_max_version_UNSAFE();
198 let native_function_table =
199 sui_move_natives::all_natives(false, &protocol_config);
200 Self {
201 gas_price: TEST_GAS_PRICE,
202 reference_gas_price: TEST_GAS_PRICE,
203 protocol_config,
204 native_function_table,
205 }
206 }
207
208 pub fn max_gas_budget(&self) -> u64 {
209 self.protocol_config.max_tx_gas()
210 }
211}
212
213impl VMTestSetup for SuiVMTestSetup {
214 type Meter<'a> = SuiGasMeter<SuiGasStatusTestWrapper>;
215
216 fn new_meter<'a>(&'a self, execution_bound: Option<u64>) -> Self::Meter<'a> {
217 SuiGasMeter(SuiGasStatusTestWrapper(
218 SuiGasStatus::new(
219 execution_bound.unwrap_or(*MAX_UNIT_TEST_INSTRUCTIONS),
220 self.gas_price,
221 self.reference_gas_price,
222 &self.protocol_config,
223 )
224 .unwrap(),
225 ))
226 }
227
228 fn used_gas<'a>(&'a self, execution_bound: u64, meter: Self::Meter<'a>) -> u64 {
229 let gas_status = &meter.0;
230 Gas::new(execution_bound)
231 .checked_sub(gas_status.remaining_gas())
232 .unwrap()
233 .into()
234 }
235
236 fn vm_config(&self) -> VMConfig {
237 sui_adapter::adapter::vm_config(&self.protocol_config)
238 }
239
240 fn native_function_table(&self) -> move_vm_runtime::native_functions::NativeFunctionTable {
241 self.native_function_table.clone()
242 }
243}
244
245pub struct SuiGasStatusTestWrapper(SuiGasStatus);
247
248impl Deref for SuiGasStatusTestWrapper {
249 type Target = GasStatus;
250
251 fn deref(&self) -> &Self::Target {
252 self.0.move_gas_status()
253 }
254}
255
256impl DerefMut for SuiGasStatusTestWrapper {
257 fn deref_mut(&mut self) -> &mut Self::Target {
258 self.0.move_gas_status_mut()
259 }
260}