sui_cluster_test/
lib.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3use crate::faucet::{FaucetClient, FaucetClientFactory};
4use async_trait::async_trait;
5use cluster::{Cluster, ClusterFactory};
6use config::ClusterTestOpt;
7use futures::{StreamExt, stream::FuturesUnordered};
8use helper::ObjectChecker;
9use jsonrpsee::core::params::ArrayParams;
10use jsonrpsee::{core::client::ClientT, http_client::HttpClientBuilder};
11use std::sync::Arc;
12use sui_faucet::{CoinInfo, RequestStatus};
13use sui_json_rpc_types::{
14    SuiExecutionStatus, SuiTransactionBlockEffectsAPI, SuiTransactionBlockResponse,
15    SuiTransactionBlockResponseOptions, TransactionBlockBytes,
16};
17use sui_sdk::wallet_context::WalletContext;
18use sui_test_transaction_builder::batch_make_transfer_transactions;
19use sui_types::base_types::TransactionDigest;
20use sui_types::object::Owner;
21use sui_types::quorum_driver_types::ExecuteTransactionRequestType;
22use sui_types::sui_system_state::sui_system_state_summary::SuiSystemStateSummary;
23
24use sui_sdk::SuiClient;
25use sui_types::gas_coin::GasCoin;
26use sui_types::{
27    base_types::SuiAddress,
28    transaction::{Transaction, TransactionData},
29};
30use test_case::{
31    coin_index_test::CoinIndexTest, coin_merge_split_test::CoinMergeSplitTest,
32    fullnode_build_publish_transaction_test::FullNodeBuildPublishTransactionTest,
33    fullnode_execute_transaction_test::FullNodeExecuteTransactionTest,
34    native_transfer_test::NativeTransferTest, random_beacon_test::RandomBeaconTest,
35    shared_object_test::SharedCounterTest,
36};
37use tokio::time::{self, Duration};
38use tracing::{error, info};
39use wallet_client::WalletClient;
40
41pub mod cluster;
42pub mod config;
43pub mod faucet;
44pub mod helper;
45pub mod test_case;
46pub mod wallet_client;
47
48#[allow(unused)]
49pub struct TestContext {
50    /// Cluster handle that allows access to various components in a cluster
51    cluster: Box<dyn Cluster + Sync + Send>,
52    /// Client that provides wallet context and fullnode access
53    client: WalletClient,
54    /// Facuet client that provides faucet access to a test
55    faucet: Arc<dyn FaucetClient + Sync + Send>,
56}
57
58impl TestContext {
59    async fn get_sui_from_faucet(&self, minimum_coins: Option<usize>) -> Vec<GasCoin> {
60        let addr = self.get_wallet_address();
61
62        let faucet_response = self.faucet.request_sui_coins(addr).await;
63        if let RequestStatus::Failure(e) = faucet_response.status {
64            panic!("Failed to get coins from faucet: {e}");
65        }
66
67        let coin_info = faucet_response.coins_sent.unwrap_or_default();
68
69        let digests = coin_info
70            .iter()
71            .map(|coin_info| coin_info.transfer_tx_digest)
72            .collect::<Vec<_>>();
73
74        self.let_fullnode_sync(digests, 5).await;
75
76        let gas_coins = self.check_owner_and_into_gas_coin(coin_info, addr).await;
77
78        let minimum_coins = minimum_coins.unwrap_or(1);
79
80        if gas_coins.len() < minimum_coins {
81            panic!(
82                "Expect to get at least {minimum_coins} Sui Coins for address {addr}, but only got {}",
83                gas_coins.len()
84            )
85        }
86
87        gas_coins
88    }
89
90    fn get_context(&self) -> &WalletClient {
91        &self.client
92    }
93
94    fn get_fullnode_client(&self) -> &SuiClient {
95        self.client.get_fullnode_client()
96    }
97
98    fn clone_fullnode_client(&self) -> SuiClient {
99        self.client.get_fullnode_client().clone()
100    }
101
102    fn get_fullnode_rpc_url(&self) -> &str {
103        self.cluster.fullnode_url()
104    }
105
106    fn get_wallet(&self) -> &WalletContext {
107        self.client.get_wallet()
108    }
109
110    async fn get_latest_sui_system_state(&self) -> SuiSystemStateSummary {
111        self.client
112            .get_fullnode_client()
113            .governance_api()
114            .get_latest_sui_system_state()
115            .await
116            .unwrap()
117    }
118
119    async fn get_reference_gas_price(&self) -> u64 {
120        self.client
121            .get_fullnode_client()
122            .governance_api()
123            .get_reference_gas_price()
124            .await
125            .unwrap()
126    }
127
128    fn get_wallet_address(&self) -> SuiAddress {
129        self.client.get_wallet_address()
130    }
131
132    /// See `make_transactions_with_wallet_context` for potential caveats
133    /// of this helper function.
134    pub async fn make_transactions(&self, max_txn_num: usize) -> Vec<Transaction> {
135        batch_make_transfer_transactions(self.get_wallet(), max_txn_num).await
136    }
137
138    pub async fn build_transaction_remotely(
139        &self,
140        method: &str,
141        params: ArrayParams,
142    ) -> anyhow::Result<TransactionData> {
143        let fn_rpc_url = self.get_fullnode_rpc_url();
144        // TODO cache this?
145        let rpc_client = HttpClientBuilder::default().build(fn_rpc_url)?;
146
147        TransactionBlockBytes::to_data(rpc_client.request(method, params).await?)
148    }
149
150    async fn sign_and_execute(
151        &self,
152        txn_data: TransactionData,
153        desc: &str,
154    ) -> SuiTransactionBlockResponse {
155        let signature = self.get_context().sign(&txn_data, desc).await;
156        let resp = self
157            .get_fullnode_client()
158            .quorum_driver_api()
159            .execute_transaction_block(
160                Transaction::from_data(txn_data, vec![signature]),
161                SuiTransactionBlockResponseOptions::new()
162                    .with_object_changes()
163                    .with_balance_changes()
164                    .with_effects()
165                    .with_events(),
166                Some(ExecuteTransactionRequestType::WaitForLocalExecution),
167            )
168            .await
169            .unwrap_or_else(|e| panic!("Failed to execute transaction for {}. {}", desc, e));
170        assert!(
171            matches!(
172                resp.effects.as_ref().unwrap().status(),
173                SuiExecutionStatus::Success
174            ),
175            "Failed to execute transaction for {desc}: {:?}",
176            resp
177        );
178        resp
179    }
180
181    pub async fn setup(options: ClusterTestOpt) -> Result<Self, anyhow::Error> {
182        let cluster = ClusterFactory::start(&options).await?;
183        let wallet_client = WalletClient::new_from_cluster(&cluster).await;
184        let faucet = FaucetClientFactory::new_from_cluster(&cluster).await;
185        Ok(Self {
186            cluster,
187            client: wallet_client,
188            faucet,
189        })
190    }
191
192    // TODO: figure out a more efficient way to test a local cluster
193    // A potential way to do this is to subscribe to txns from fullnode
194    // when the feature is ready
195    pub async fn let_fullnode_sync(&self, digests: Vec<TransactionDigest>, timeout_sec: u64) {
196        let mut futures = FuturesUnordered::new();
197        for digest in digests.clone() {
198            let task = self.get_tx_with_retry_times(digest, 1);
199            futures.push(Box::pin(task));
200        }
201        let mut sleep = Box::pin(time::sleep(Duration::from_secs(timeout_sec)));
202
203        loop {
204            tokio::select! {
205                _ = &mut sleep => {
206                    panic!("Fullnode does not know all of {:?} after {} secs.", digests, timeout_sec);
207                }
208                res = futures.next() => {
209                    match res {
210                        Some((true, _, _)) => {},
211                        Some((false, digest, retry_times)) => {
212                            let task = self.get_tx_with_retry_times(digest, retry_times);
213                            futures.push(Box::pin(task));
214                        },
215                        None => break, // all txns appear on fullnode, mission completed
216                    }
217                }
218            }
219        }
220    }
221
222    async fn get_tx_with_retry_times(
223        &self,
224        digest: TransactionDigest,
225        retry_times: u64,
226    ) -> (bool, TransactionDigest, u64) {
227        match self
228            .client
229            .get_fullnode_client()
230            .read_api()
231            .get_transaction_with_options(digest, SuiTransactionBlockResponseOptions::new())
232            .await
233        {
234            Ok(_) => (true, digest, retry_times),
235            Err(_) => {
236                time::sleep(Duration::from_millis(300 * retry_times)).await;
237                (false, digest, retry_times + 1)
238            }
239        }
240    }
241
242    async fn check_owner_and_into_gas_coin(
243        &self,
244        coin_info: Vec<CoinInfo>,
245        owner: SuiAddress,
246    ) -> Vec<GasCoin> {
247        futures::future::join_all(
248            coin_info
249                .iter()
250                .map(|coin_info| {
251                    ObjectChecker::new(coin_info.id)
252                        .owner(Owner::AddressOwner(owner))
253                        .check_into_gas_coin(self.get_fullnode_client())
254                })
255                .collect::<Vec<_>>(),
256        )
257        .await
258        .into_iter()
259        .collect::<Vec<_>>()
260    }
261}
262
263pub struct TestCase<'a> {
264    test_case: Box<dyn TestCaseImpl + 'a>,
265}
266
267impl<'a> TestCase<'a> {
268    pub fn new(test_case: impl TestCaseImpl + 'a) -> Self {
269        TestCase {
270            test_case: (Box::new(test_case)),
271        }
272    }
273
274    pub async fn run(self, ctx: &mut TestContext) -> bool {
275        let test_name = self.test_case.name();
276        info!("Running test {}.", test_name);
277
278        // TODO: unwind panic and fail gracefully?
279
280        match self.test_case.run(ctx).await {
281            Ok(()) => {
282                info!("Test {test_name} succeeded.");
283                true
284            }
285            Err(e) => {
286                error!("Test {test_name} failed with error: {e}.");
287                false
288            }
289        }
290    }
291}
292
293#[async_trait]
294pub trait TestCaseImpl {
295    fn name(&self) -> &'static str;
296    fn description(&self) -> &'static str;
297    async fn run(&self, ctx: &mut TestContext) -> Result<(), anyhow::Error>;
298}
299
300pub struct ClusterTest;
301
302impl ClusterTest {
303    pub async fn run(options: ClusterTestOpt) {
304        let mut ctx = TestContext::setup(options)
305            .await
306            .unwrap_or_else(|e| panic!("Failed to set up TestContext, e: {e}"));
307
308        // TODO: collect tests from each test_case file instead.
309        let tests = vec![
310            TestCase::new(NativeTransferTest {}),
311            TestCase::new(CoinMergeSplitTest {}),
312            TestCase::new(SharedCounterTest {}),
313            TestCase::new(FullNodeExecuteTransactionTest {}),
314            TestCase::new(FullNodeBuildPublishTransactionTest {}),
315            TestCase::new(CoinIndexTest {}),
316            TestCase::new(RandomBeaconTest {}),
317        ];
318
319        // TODO: improve the runner parallelism for efficiency
320        // For now we run tests serially
321        let mut success_cnt = 0;
322        let total_cnt = tests.len() as i32;
323        for t in tests {
324            let is_success = t.run(&mut ctx).await as i32;
325            success_cnt += is_success;
326        }
327        if success_cnt < total_cnt {
328            // If any test failed, panic to bubble up the signal
329            panic!("{success_cnt} of {total_cnt} tests passed.");
330        }
331        info!("{success_cnt} of {total_cnt} tests passed.");
332    }
333}