1use 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: Box<dyn Cluster + Sync + Send>,
45 client: WalletClient,
47 faucet: Arc<dyn FaucetClient + Sync + Send>,
49}
50
51impl TestContext {
52 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 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 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 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 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 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 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 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 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 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 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 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 panic!("{success_cnt} of {total_cnt} tests passed.");
292 }
293 info!("{success_cnt} of {total_cnt} tests passed.");
294 }
295}