Skip to main content

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::future::join_all;
8use helper::ObjectChecker;
9use std::sync::Arc;
10use sui_faucet::{CoinInfo, RequestStatus};
11use sui_rpc_api::Client as GrpcClient;
12use sui_rpc_api::client::ExecutedTransaction;
13use sui_sdk::wallet_context::WalletContext;
14use sui_test_transaction_builder::TestTransactionBuilder;
15use sui_types::base_types::{ObjectRef, TransactionDigest};
16use sui_types::object::Owner;
17use sui_types::sui_system_state::sui_system_state_summary::SuiSystemStateSummary;
18
19use sui_types::gas_coin::GasCoin;
20use sui_types::{
21    base_types::SuiAddress,
22    transaction::{Transaction, TransactionData},
23};
24use test_case::{
25    coin_index_test::CoinIndexTest, coin_merge_split_test::CoinMergeSplitTest,
26    fullnode_execute_transaction_test::FullNodeExecuteTransactionTest,
27    grpc_publish_transaction_test::GrpcPublishTransactionTest,
28    native_transfer_test::NativeTransferTest, random_beacon_test::RandomBeaconTest,
29    shared_object_test::SharedCounterTest, staking_test::StakingTest,
30};
31use tracing::{error, info};
32use wallet_client::WalletClient;
33
34pub mod cluster;
35pub mod config;
36pub mod faucet;
37pub mod helper;
38pub mod test_case;
39pub mod wallet_client;
40
41#[allow(unused)]
42pub struct TestContext {
43    /// Cluster handle that allows access to various components in a cluster
44    cluster: Box<dyn Cluster + Sync + Send>,
45    /// Client that provides wallet context and gRPC fullnode access
46    client: WalletClient,
47    /// Facuet client that provides faucet access to a test
48    faucet: Arc<dyn FaucetClient + Sync + Send>,
49}
50
51impl TestContext {
52    /// Request coins from the faucet, wait for every faucet transfer to be
53    /// visible over gRPC (`LedgerService`), then read each funded coin object by
54    /// ID and verify ownership. Gas/coin object IDs come straight from the faucet
55    /// response (which returns the exact IDs), so we never depend on owner
56    /// enumeration to fund a transaction — good practice for determinism, not a
57    /// workaround for any missing service.
58    async fn get_sui_from_faucet(&self, minimum_coins: Option<usize>) -> Vec<GasCoin> {
59        let addr = self.get_wallet_address();
60        let minimum_coins = minimum_coins.unwrap_or(1);
61
62        // Coins-per-request varies by faucet (the local test faucet sends
63        // several; gas-station-backed remote faucets send exactly one), so
64        // accumulate requests until the minimum is met.
65        let mut coin_info = Vec::new();
66        for _ in 0..minimum_coins {
67            let faucet_response = self.faucet.request_sui_coins(addr).await;
68            if let RequestStatus::Failure(e) = faucet_response.status {
69                panic!("Failed to get coins from faucet: {e}");
70            }
71            coin_info.extend(faucet_response.coins_sent.unwrap_or_default());
72            if coin_info.len() >= minimum_coins {
73                break;
74            }
75        }
76
77        let digests = coin_info
78            .iter()
79            .map(|coin_info| coin_info.transfer_tx_digest)
80            .collect::<Vec<_>>();
81
82        // Wait (concurrently) for the independent faucet transactions to be
83        // checkpointed before we read the funded objects.
84        self.wait_for_txns(&digests).await;
85
86        let gas_coins = self.check_owner_and_into_gas_coin(coin_info, addr).await;
87
88        if gas_coins.len() < minimum_coins {
89            panic!(
90                "Expect to get at least {minimum_coins} Sui Coins for address {addr}, but only got {}",
91                gas_coins.len()
92            )
93        }
94
95        gas_coins
96    }
97
98    fn get_context(&self) -> &WalletClient {
99        &self.client
100    }
101
102    /// The shared gRPC client (owned, cheaply cloned). Backed by the wallet's
103    /// cached connection.
104    fn get_grpc_client(&self) -> GrpcClient {
105        self.client.grpc_client()
106    }
107
108    fn get_wallet(&self) -> &WalletContext {
109        self.client.get_wallet()
110    }
111
112    async fn get_latest_sui_system_state(&self) -> SuiSystemStateSummary {
113        self.get_grpc_client()
114            .get_system_state_summary(None)
115            .await
116            .unwrap()
117    }
118
119    async fn get_reference_gas_price(&self) -> u64 {
120        self.get_grpc_client()
121            .get_reference_gas_price()
122            .await
123            .unwrap()
124    }
125
126    fn get_wallet_address(&self) -> SuiAddress {
127        self.client.get_wallet_address()
128    }
129
130    /// Fetch the current `ObjectRef` (id, version, digest) for a known object ID
131    /// over `LedgerService`. Object references must be refreshed before every
132    /// transaction because version + digest change on each mutation.
133    pub async fn current_object_ref(
134        &self,
135        object_id: sui_types::base_types::ObjectID,
136    ) -> ObjectRef {
137        self.get_grpc_client()
138            .get_object(object_id)
139            .await
140            .unwrap_or_else(|e| panic!("Failed to fetch object {object_id}: {e}"))
141            .compute_object_reference()
142    }
143
144    /// Build up to `max_txn_num` simple transfer-SUI transactions, each paying a
145    /// tiny amount to a fresh recipient. Gas is sourced explicitly from the
146    /// faucet-funded coins (one coin per transaction), keeping construction
147    /// deterministic without relying on gas enumeration.
148    pub async fn make_transactions(&self, max_txn_num: usize) -> Vec<Transaction> {
149        let sender = self.get_wallet_address();
150        let gas_price = self.get_reference_gas_price().await;
151        // Fund enough coins so each transaction has its own gas coin.
152        let coins = self.get_sui_from_faucet(Some(max_txn_num)).await;
153
154        let mut txns = Vec::with_capacity(max_txn_num);
155        for coin in coins.into_iter().take(max_txn_num) {
156            let recipient = SuiAddress::random_for_testing_only();
157            let gas_ref = self.current_object_ref(*coin.id()).await;
158            let data = TestTransactionBuilder::new(sender, gas_ref, gas_price)
159                .transfer_sui(Some(1), recipient)
160                .build();
161            let signature = self.get_context().sign(&data, "make_transactions").await;
162            txns.push(Transaction::from_data(data, vec![signature]));
163        }
164        txns
165    }
166
167    /// Sign and execute a transaction over gRPC (`TransactionExecutionService`),
168    /// waiting for it to be checkpointed, and assert success. Returns the native
169    /// `ExecutedTransaction` (effects, events, balance changes, changed objects).
170    /// Does not refetch the transaction afterwards.
171    async fn sign_and_execute(&self, txn_data: TransactionData, desc: &str) -> ExecutedTransaction {
172        let signature = self.get_context().sign(&txn_data, desc).await;
173        let tx = Transaction::from_data(txn_data, vec![signature]);
174        self.get_wallet().execute_transaction_must_succeed(tx).await
175    }
176
177    pub async fn setup(options: ClusterTestOpt) -> Result<Self, anyhow::Error> {
178        let cluster = ClusterFactory::start(&options).await?;
179        let wallet_client = WalletClient::new_from_cluster(&cluster).await;
180        let faucet = FaucetClientFactory::new_from_cluster(&cluster).await;
181        Ok(Self {
182            cluster,
183            client: wallet_client,
184            faucet,
185        })
186    }
187
188    /// Wait (concurrently) for each transaction digest to be indexed into a
189    /// checkpoint on the fullnode using the standard gRPC transaction-wait
190    /// mechanism (`Client::wait_for_transaction`, 30s timeout each).
191    pub async fn wait_for_txns(&self, digests: &[TransactionDigest]) {
192        let client = self.get_grpc_client();
193        let waits = digests.iter().map(|digest| {
194            let client = client.clone();
195            async move {
196                client
197                    .wait_for_transaction(digest)
198                    .await
199                    .unwrap_or_else(|e| panic!("Fullnode did not observe {digest}: {e}"));
200            }
201        });
202        join_all(waits).await;
203    }
204
205    async fn check_owner_and_into_gas_coin(
206        &self,
207        coin_info: Vec<CoinInfo>,
208        owner: SuiAddress,
209    ) -> Vec<GasCoin> {
210        let client = self.get_grpc_client();
211        join_all(coin_info.iter().map(|coin_info| {
212            let client = client.clone();
213            async move {
214                ObjectChecker::new(coin_info.id)
215                    .owner(Owner::AddressOwner(owner))
216                    .check_into_gas_coin(&client)
217                    .await
218            }
219        }))
220        .await
221    }
222}
223
224pub struct TestCase<'a> {
225    test_case: Box<dyn TestCaseImpl + 'a>,
226}
227
228impl<'a> TestCase<'a> {
229    pub fn new(test_case: impl TestCaseImpl + 'a) -> Self {
230        TestCase {
231            test_case: (Box::new(test_case)),
232        }
233    }
234
235    pub async fn run(self, ctx: &mut TestContext) -> bool {
236        let test_name = self.test_case.name();
237        info!("Running test {}.", test_name);
238
239        // TODO: unwind panic and fail gracefully?
240
241        match self.test_case.run(ctx).await {
242            Ok(()) => {
243                info!("Test {test_name} succeeded.");
244                true
245            }
246            Err(e) => {
247                error!("Test {test_name} failed with error: {e}.");
248                false
249            }
250        }
251    }
252}
253
254#[async_trait]
255pub trait TestCaseImpl {
256    fn name(&self) -> &'static str;
257    fn description(&self) -> &'static str;
258    async fn run(&self, ctx: &mut TestContext) -> Result<(), anyhow::Error>;
259}
260
261pub struct ClusterTest;
262
263impl ClusterTest {
264    pub async fn run(options: ClusterTestOpt) {
265        let mut ctx = TestContext::setup(options)
266            .await
267            .unwrap_or_else(|e| panic!("Failed to set up TestContext, e: {e}"));
268
269        // TODO: collect tests from each test_case file instead.
270        let tests = vec![
271            TestCase::new(NativeTransferTest {}),
272            TestCase::new(CoinMergeSplitTest {}),
273            TestCase::new(SharedCounterTest {}),
274            TestCase::new(FullNodeExecuteTransactionTest {}),
275            TestCase::new(GrpcPublishTransactionTest {}),
276            TestCase::new(CoinIndexTest {}),
277            TestCase::new(RandomBeaconTest {}),
278            TestCase::new(StakingTest {}),
279        ];
280
281        // TODO: improve the runner parallelism for efficiency
282        // For now we run tests serially
283        let mut success_cnt = 0;
284        let total_cnt = tests.len() as i32;
285        for t in tests {
286            let is_success = t.run(&mut ctx).await as i32;
287            success_cnt += is_success;
288        }
289        if success_cnt < total_cnt {
290            // If any test failed, panic to bubble up the signal
291            panic!("{success_cnt} of {total_cnt} tests passed.");
292        }
293        info!("{success_cnt} of {total_cnt} tests passed.");
294    }
295}