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