From 81a362ab915680b9b00e31aa69e30eea0e0381ae Mon Sep 17 00:00:00 2001 From: Nurullah Giray Kuru Date: Mon, 31 Jul 2023 14:36:14 -0700 Subject: [PATCH] [api] Add threads and timestamps to api tests (#9349) This commit builds threads and run ID on top of the consistent API testing introduced here. See proposal for more high level information. Changes: Refactored tests into dedicated file tests.rs. Switched to creating accounts for every tests instead of using the same and created pre test set up routines in testsetups.rs. Added run ID start_time to metrics. --- Cargo.lock | 1 + crates/aptos-api-tester/Cargo.toml | 1 + crates/aptos-api-tester/src/counters.rs | 30 +- crates/aptos-api-tester/src/main.rs | 620 +++------------------- crates/aptos-api-tester/src/tests.rs | 489 +++++++++++++++++ crates/aptos-api-tester/src/testsetups.rs | 70 +++ crates/aptos-api-tester/src/utils.rs | 77 ++- 7 files changed, 711 insertions(+), 577 deletions(-) create mode 100644 crates/aptos-api-tester/src/tests.rs create mode 100644 crates/aptos-api-tester/src/testsetups.rs diff --git a/Cargo.lock b/Cargo.lock index 658e03eaa101d..8f4ed32bd1b14 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -408,6 +408,7 @@ dependencies = [ "aptos-rest-client", "aptos-sdk", "aptos-types", + "futures", "move-core-types", "once_cell", "prometheus", diff --git a/crates/aptos-api-tester/Cargo.toml b/crates/aptos-api-tester/Cargo.toml index 706f4b4b09a67..01db4237b1caa 100644 --- a/crates/aptos-api-tester/Cargo.toml +++ b/crates/aptos-api-tester/Cargo.toml @@ -23,6 +23,7 @@ aptos-push-metrics = { workspace = true } aptos-rest-client = { workspace = true } aptos-sdk = { workspace = true } aptos-types = { workspace = true } +futures = { workspace = true } move-core-types = { workspace = true } once_cell = { workspace = true } prometheus = { workspace = true } diff --git a/crates/aptos-api-tester/src/counters.rs b/crates/aptos-api-tester/src/counters.rs index 58515ce3b4a16..79798807d85e7 100644 --- a/crates/aptos-api-tester/src/counters.rs +++ b/crates/aptos-api-tester/src/counters.rs @@ -7,49 +7,55 @@ pub static API_TEST_SUCCESS: Lazy = Lazy::new(|| { register_histogram_vec!( "api_test_success", "Number of user flows which succesfully passed", - &["test_name", "network_name"], + &["test_name", "network_name", "start_time"], ) .unwrap() }); -pub fn test_success(test_name: &str, network_name: &str) -> Histogram { - API_TEST_SUCCESS.with_label_values(&[test_name, network_name]) +pub fn test_success(test_name: &str, network_name: &str, start_time: &str) -> Histogram { + API_TEST_SUCCESS.with_label_values(&[test_name, network_name, start_time]) } pub static API_TEST_FAIL: Lazy = Lazy::new(|| { register_histogram_vec!( "api_test_fail", "Number of user flows which failed checks", - &["test_name", "network_name"], + &["test_name", "network_name", "start_time"], ) .unwrap() }); -pub fn test_fail(test_name: &str, network_name: &str) -> Histogram { - API_TEST_FAIL.with_label_values(&[test_name, network_name]) +pub fn test_fail(test_name: &str, network_name: &str, start_time: &str) -> Histogram { + API_TEST_FAIL.with_label_values(&[test_name, network_name, start_time]) } pub static API_TEST_ERROR: Lazy = Lazy::new(|| { register_histogram_vec!("api_test_error", "Number of user flows which crashed", &[ "test_name", "network_name", - ]) + "start_time" + ],) .unwrap() }); -pub fn test_error(test_name: &str, network_name: &str) -> Histogram { - API_TEST_ERROR.with_label_values(&[test_name, network_name]) +pub fn test_error(test_name: &str, network_name: &str, start_time: &str) -> Histogram { + API_TEST_ERROR.with_label_values(&[test_name, network_name, start_time]) } pub static API_TEST_LATENCY: Lazy = Lazy::new(|| { register_histogram_vec!( "api_test_latency", "Time it takes to complete a user flow", - &["test_name", "network_name", "result"], + &["test_name", "network_name", "start_time", "result"], ) .unwrap() }); -pub fn test_latency(test_name: &str, network_name: &str, result: &str) -> Histogram { - API_TEST_LATENCY.with_label_values(&[test_name, network_name, result]) +pub fn test_latency( + test_name: &str, + network_name: &str, + start_time: &str, + result: &str, +) -> Histogram { + API_TEST_LATENCY.with_label_values(&[test_name, network_name, start_time, result]) } diff --git a/crates/aptos-api-tester/src/main.rs b/crates/aptos-api-tester/src/main.rs index 3476389e83184..06a13713ffb57 100644 --- a/crates/aptos-api-tester/src/main.rs +++ b/crates/aptos-api-tester/src/main.rs @@ -4,57 +4,33 @@ #![forbid(unsafe_code)] mod counters; +mod tests; +mod testsetups; mod utils; -use crate::utils::{ - set_metrics, NetworkName, TestFailure, TestName, TestResult, DEVNET_FAUCET_URL, - DEVNET_NODE_URL, TESTNET_FAUCET_URL, TESTNET_NODE_URL, +use crate::{ + testsetups::{ + setup_and_run_cointransfer, setup_and_run_newaccount, setup_and_run_nfttransfer, + setup_and_run_publishmodule, + }, + utils::{set_metrics, NetworkName, TestFailure, TestName, TestResult}, }; -use anyhow::{anyhow, Result}; -use aptos_api_types::{HexEncodedBytes, U64}; -use aptos_cached_packages::aptos_stdlib::EntryFunctionCall; -use aptos_framework::{BuildOptions, BuiltPackage}; +use anyhow::Result; use aptos_logger::{info, Level, Logger}; use aptos_push_metrics::MetricsPusher; -use aptos_rest_client::{Account, Client, FaucetClient}; -use aptos_sdk::{ - bcs, - coin_client::CoinClient, - token_client::{ - build_and_submit_transaction, CollectionData, CollectionMutabilityConfig, RoyaltyOptions, - TokenClient, TokenData, TokenMutabilityConfig, TransactionOptions, - }, - types::LocalAccount, +use futures::future::join_all; +use std::{ + future::Future, + time::{Instant, SystemTime, UNIX_EPOCH}, }; -use aptos_types::{ - account_address::AccountAddress, - transaction::{EntryFunction, TransactionPayload}, -}; -use move_core_types::{ident_str, language_storage::ModuleId}; -use std::{collections::BTreeMap, future::Future, path::PathBuf, time::Instant}; - -// fail messages -static FAIL_ACCOUNT_DATA: &str = "wrong account data"; -static FAIL_BALANCE: &str = "wrong balance"; -static FAIL_BALANCE_AFTER_TRANSACTION: &str = "wrong balance after transaction"; -static FAIL_BALANCE_BEFORE_TRANSACTION: &str = "wrong balance before transaction"; -static FAIL_COLLECTION_DATA: &str = "wrong collection data"; -static FAIL_TOKEN_DATA: &str = "wrong token data"; -static FAIL_TOKEN_BALANCE: &str = "wrong token balance"; -static FAIL_TOKENS_BEFORE_CLAIM: &str = "found tokens for receiver when shouldn't"; -static FAIL_TOKEN_BALANCE_AFTER_TRANSACTION: &str = "wrong token balance after transaction"; -static FAIL_BYTECODE: &str = "wrong bytecode"; -static FAIL_MODULE_INTERACTION: &str = "module interaction isn't reflected correctly"; -static ERROR_NO_VERSION: &str = "transaction did not return version"; -static ERROR_NO_BYTECODE: &str = "error while getting bytecode from blobs"; -static ERROR_MODULE_INTERACTION: &str = "module interaction isn't reflected"; // Processes a test result. -async fn handle_result>>( +async fn process_result>>( test_name: TestName, - network_type: NetworkName, + network_name: NetworkName, + start_time: &str, fut: Fut, -) -> Result { +) { // start timer let start = Instant::now(); @@ -74,522 +50,78 @@ async fn handle_result>>( set_metrics( &output, &test_name.to_string(), - &network_type.to_string(), + &network_name.to_string(), + start_time, time, ); info!( "{} {} result:{:?} in time:{:?}", - network_type.to_string(), + network_name.to_string(), test_name.to_string(), output, time, ); - - Ok(output) -} - -/// Tests new account creation. Checks that: -/// - account data exists -/// - account balance reflects funded amount -async fn test_newaccount( - client: &Client, - account: &LocalAccount, - amount_funded: u64, -) -> Result<(), TestFailure> { - // ask for account data - let response = client.get_account(account.address()).await?; - - // check account data - let expected_account = Account { - authentication_key: account.authentication_key(), - sequence_number: account.sequence_number(), - }; - let actual_account = response.inner(); - - if &expected_account != actual_account { - info!( - "fail: {}, expected {:?}, got {:?}", - FAIL_ACCOUNT_DATA, expected_account, actual_account - ); - return Err(TestFailure::Fail(FAIL_ACCOUNT_DATA)); - } - - // check account balance - let expected_balance = U64(amount_funded); - let actual_balance = client - .get_account_balance(account.address()) - .await? - .inner() - .coin - .value; - - if expected_balance != actual_balance { - info!( - "fail: {}, expected {:?}, got {:?}", - FAIL_BALANCE, expected_balance, actual_balance - ); - return Err(TestFailure::Fail(FAIL_BALANCE)); - } - - Ok(()) -} - -/// Tests coin transfer. Checks that: -/// - receiver balance reflects transferred amount -/// - receiver balance shows correct amount at the previous version -async fn test_cointransfer( - client: &Client, - coin_client: &CoinClient<'_>, - account: &mut LocalAccount, - receiver: AccountAddress, - amount: u64, -) -> Result<(), TestFailure> { - // get starting balance - let starting_receiver_balance = u64::from( - client - .get_account_balance(receiver) - .await? - .inner() - .coin - .value, - ); - - // transfer coins to second account - let pending_txn = coin_client - .transfer(account, receiver, amount, None) - .await?; - let response = client.wait_for_transaction(&pending_txn).await?; - - // check receiver balance - let expected_receiver_balance = U64(starting_receiver_balance + amount); - let actual_receiver_balance = client - .get_account_balance(receiver) - .await? - .inner() - .coin - .value; - - if expected_receiver_balance != actual_receiver_balance { - info!( - "fail: {}, expected {:?}, got {:?}", - FAIL_BALANCE_AFTER_TRANSACTION, expected_receiver_balance, actual_receiver_balance - ); - return Err(TestFailure::Fail(FAIL_BALANCE_AFTER_TRANSACTION)); - } - - // check account balance with a lower version number - let version = match response.inner().version() { - Some(version) => version, - _ => { - info!("error: {}", ERROR_MODULE_INTERACTION); - return Err(TestFailure::Error(anyhow!(ERROR_NO_VERSION))); - }, - }; - - let expected_balance_at_version = U64(starting_receiver_balance); - let actual_balance_at_version = client - .get_account_balance_at_version(receiver, version - 1) - .await? - .inner() - .coin - .value; - - if expected_balance_at_version != actual_balance_at_version { - info!( - "fail: {}, expected {:?}, got {:?}", - FAIL_BALANCE_BEFORE_TRANSACTION, expected_balance_at_version, actual_balance_at_version - ); - return Err(TestFailure::Fail(FAIL_BALANCE_BEFORE_TRANSACTION)); - } - - Ok(()) -} - -/// Tests nft transfer. Checks that: -/// - collection data exists -/// - token data exists -/// - token balance reflects transferred amount -async fn test_mintnft( - client: &Client, - token_client: &TokenClient<'_>, - account: &mut LocalAccount, - receiver: &mut LocalAccount, -) -> Result<(), TestFailure> { - // create collection - let collection_name = "test collection".to_string(); - let collection_description = "collection description".to_string(); - let collection_uri = "collection uri".to_string(); - let collection_maximum = 1000; - - let pending_txn = token_client - .create_collection( - account, - &collection_name, - &collection_description, - &collection_uri, - collection_maximum, - None, - ) - .await?; - client.wait_for_transaction(&pending_txn).await?; - - // create token - let token_name = "test token".to_string(); - let token_description = "token description".to_string(); - let token_uri = "token uri".to_string(); - let token_maximum = 1000; - let token_supply = 10; - - let pending_txn = token_client - .create_token( - account, - &collection_name, - &token_name, - &token_description, - token_supply, - &token_uri, - token_maximum, - None, - None, - ) - .await?; - client.wait_for_transaction(&pending_txn).await?; - - // check collection metadata - let expected_collection_data = CollectionData { - name: collection_name.clone(), - description: collection_description, - uri: collection_uri, - maximum: U64(collection_maximum), - mutability_config: CollectionMutabilityConfig { - description: false, - maximum: false, - uri: false, - }, - }; - let actual_collection_data = token_client - .get_collection_data(account.address(), &collection_name) - .await?; - - if expected_collection_data != actual_collection_data { - info!( - "fail: {}, expected {:?}, got {:?}", - FAIL_COLLECTION_DATA, expected_collection_data, actual_collection_data - ); - return Err(TestFailure::Fail(FAIL_COLLECTION_DATA)); - } - - // check token metadata - let expected_token_data = TokenData { - name: token_name.clone(), - description: token_description, - uri: token_uri, - maximum: U64(token_maximum), - mutability_config: TokenMutabilityConfig { - description: false, - maximum: false, - properties: false, - royalty: false, - uri: false, - }, - supply: U64(token_supply), - royalty: RoyaltyOptions { - payee_address: account.address(), - royalty_points_denominator: U64(0), - royalty_points_numerator: U64(0), - }, - largest_property_version: U64(0), - }; - let actual_token_data = token_client - .get_token_data(account.address(), &collection_name, &token_name) - .await?; - - if expected_token_data != actual_token_data { - info!( - "fail: {}, expected {:?}, got {:?}", - FAIL_TOKEN_DATA, expected_token_data, actual_token_data - ); - return Err(TestFailure::Fail(FAIL_TOKEN_DATA)); - } - - // offer token - let pending_txn = token_client - .offer_token( - account, - receiver.address(), - account.address(), - &collection_name, - &token_name, - 2, - None, - None, - ) - .await?; - client.wait_for_transaction(&pending_txn).await?; - - // check token balance for the sender - let expected_sender_token_balance = U64(8); - let actual_sender_token_balance = token_client - .get_token( - account.address(), - account.address(), - &collection_name, - &token_name, - ) - .await? - .amount; - - if expected_sender_token_balance != actual_sender_token_balance { - info!( - "fail: {}, expected {:?}, got {:?}", - FAIL_TOKEN_BALANCE, expected_sender_token_balance, actual_sender_token_balance - ); - return Err(TestFailure::Fail(FAIL_TOKEN_BALANCE)); - } - - // check that token store isn't initialized for the receiver - if token_client - .get_token( - receiver.address(), - account.address(), - &collection_name, - &token_name, - ) - .await - .is_ok() - { - info!( - "fail: {}, expected no token client resource for the receiver", - FAIL_TOKENS_BEFORE_CLAIM - ); - return Err(TestFailure::Fail(FAIL_TOKENS_BEFORE_CLAIM)); - } - - // claim token - let pending_txn = token_client - .claim_token( - receiver, - account.address(), - account.address(), - &collection_name, - &token_name, - None, - None, - ) - .await?; - client.wait_for_transaction(&pending_txn).await?; - - // check token balance for the receiver - let expected_receiver_token_balance = U64(2); - let actual_receiver_token_balance = token_client - .get_token( - receiver.address(), - account.address(), - &collection_name, - &token_name, - ) - .await? - .amount; - - if expected_receiver_token_balance != actual_receiver_token_balance { - info!( - "{}, expected {:?}, got {:?}", - FAIL_TOKEN_BALANCE_AFTER_TRANSACTION, - expected_receiver_token_balance, - actual_receiver_token_balance - ); - return Err(TestFailure::Fail(FAIL_TOKEN_BALANCE_AFTER_TRANSACTION)); - } - - Ok(()) -} - -/// Helper function that publishes module and returns the bytecode. -async fn publish_module(client: &Client, account: &mut LocalAccount) -> Result { - // get file to compile - let move_dir = PathBuf::from("./aptos-move/move-examples/hello_blockchain"); - - // insert address - let mut named_addresses: BTreeMap = BTreeMap::new(); - named_addresses.insert("hello_blockchain".to_string(), account.address()); - - // build options - let options = BuildOptions { - named_addresses, - ..BuildOptions::default() - }; - - // build module - let package = BuiltPackage::build(move_dir, options)?; - let blobs = package.extract_code(); - let metadata = package.extract_metadata()?; - - // create payload - let payload: aptos_types::transaction::TransactionPayload = - EntryFunctionCall::CodePublishPackageTxn { - metadata_serialized: bcs::to_bytes(&metadata).expect("PackageMetadata has BCS"), - code: blobs.clone(), - } - .encode(); - - // create and submit transaction - let pending_txn = - build_and_submit_transaction(client, account, payload, TransactionOptions::default()) - .await?; - client.wait_for_transaction(&pending_txn).await?; - - let blob = match blobs.get(0) { - Some(bytecode) => bytecode.clone(), - None => { - info!("error: {}", ERROR_NO_BYTECODE); - return Err(anyhow!(ERROR_NO_BYTECODE)); - }, - }; - - Ok(HexEncodedBytes::from(blob)) } -/// Helper function that interacts with the message module. -async fn set_message(client: &Client, account: &mut LocalAccount, message: &str) -> Result<()> { - // create payload - let payload = TransactionPayload::EntryFunction(EntryFunction::new( - ModuleId::new(account.address(), ident_str!("message").to_owned()), - ident_str!("set_message").to_owned(), - vec![], - vec![bcs::to_bytes(message)?], - )); - - // create and submit transaction - let pending_txn = - build_and_submit_transaction(client, account, payload, TransactionOptions::default()) - .await?; - client.wait_for_transaction(&pending_txn).await?; - - Ok(()) -} - -/// Helper function that gets back the result of the interaction. -async fn get_message(client: &Client, address: AccountAddress) -> Option { - let resource = match client - .get_account_resource( - address, - format!("{}::message::MessageHolder", address.to_hex_literal()).as_str(), - ) - .await - { - Ok(response) => response.into_inner()?, - Err(_) => return None, - }; - - Some(resource.data.get("message")?.as_str()?.to_owned()) -} - -/// Tests module publishing and interaction. Checks that: -/// - module data exists -/// - can interact with module -/// - resources reflect interaction -async fn test_module(client: &Client, account: &mut LocalAccount) -> Result<(), TestFailure> { - // publish module - let blob = publish_module(client, account).await?; - - // check module data - let response = client - .get_account_module(account.address(), "message") - .await?; - - let expected_bytecode = &blob; - let actual_bytecode = &response.inner().bytecode; - - if expected_bytecode != actual_bytecode { - info!( - "fail: {}, expected {:?}, got {:?}", - FAIL_BYTECODE, expected_bytecode, actual_bytecode - ); - return Err(TestFailure::Fail(FAIL_BYTECODE)); - } - - // interact with module - let message = "test message"; - set_message(client, account, message).await?; - - // check that the message is sent - let expected_message = message.to_string(); - let actual_message = match get_message(client, account.address()).await { - Some(message) => message, - None => { - info!("error: {}", ERROR_MODULE_INTERACTION); - return Err(TestFailure::Error(anyhow!(ERROR_MODULE_INTERACTION))); - }, - }; - - if expected_message != actual_message { - info!( - "fail: {}, expected {:?}, got {:?}", - FAIL_MODULE_INTERACTION, expected_message, actual_message - ); - return Err(TestFailure::Fail(FAIL_MODULE_INTERACTION)); - } - - Ok(()) -} - -async fn test_flows( - network_type: NetworkName, - client: Client, - faucet_client: FaucetClient, -) -> Result<()> { - info!("testing {}", network_type.to_string()); - - // create clients - let coin_client = CoinClient::new(&client); - let token_client = TokenClient::new(&client); - - // create and fund account for tests - let mut giray = LocalAccount::generate(&mut rand::rngs::OsRng); - faucet_client.fund(giray.address(), 100_000_000).await?; - info!("{:?}", giray.address()); - - let mut giray2 = LocalAccount::generate(&mut rand::rngs::OsRng); - faucet_client.fund(giray2.address(), 100_000_000).await?; - info!("{:?}", giray2.address()); +async fn test_flows(network_name: NetworkName) -> Result<()> { + let start_time = SystemTime::now() + .duration_since(UNIX_EPOCH)? + .as_secs() + .to_string(); + info!("testing {} at {}", network_name.to_string(), start_time); // Test new account creation and funding - // this test is critical to pass for the next tests - match handle_result( - TestName::NewAccount, - network_type, - test_newaccount(&client, &giray, 100_000_000), - ) - .await? - { - TestResult::Success => {}, - _ => return Err(anyhow!("returning early because new account test failed")), - } + let test_time = start_time.clone(); + let handle_newaccount = tokio::spawn(async move { + process_result( + TestName::NewAccount, + network_name, + &test_time, + setup_and_run_newaccount(network_name), + ) + .await; + }); // Flow 1: Coin transfer - let _ = handle_result( - TestName::CoinTransfer, - network_type, - test_cointransfer(&client, &coin_client, &mut giray, giray2.address(), 1_000), - ) - .await; + let test_time = start_time.clone(); + let handle_cointransfer = tokio::spawn(async move { + process_result( + TestName::CoinTransfer, + network_name, + &test_time, + setup_and_run_cointransfer(network_name), + ) + .await; + }); // Flow 2: NFT transfer - let _ = handle_result( - TestName::NftTransfer, - network_type, - test_mintnft(&client, &token_client, &mut giray, &mut giray2), - ) - .await; + let test_time = start_time.clone(); + let handle_nfttransfer = tokio::spawn(async move { + process_result( + TestName::NftTransfer, + network_name, + &test_time, + setup_and_run_nfttransfer(network_name), + ) + .await; + }); // Flow 3: Publishing module - let _ = handle_result( + let test_time = start_time.clone(); + process_result( TestName::PublishModule, - network_type, - test_module(&client, &mut giray), + network_name, + &test_time, + setup_and_run_publishmodule(network_name), ) .await; + join_all(vec![ + handle_newaccount, + handle_cointransfer, + handle_nfttransfer, + ]) + .await; Ok(()) } @@ -599,21 +131,9 @@ async fn main() -> Result<()> { Logger::builder().level(Level::Info).build(); let _mp = MetricsPusher::start_for_local_run("api-tester"); - // test flows on testnet - let _ = test_flows( - NetworkName::Testnet, - Client::new(TESTNET_NODE_URL.clone()), - FaucetClient::new(TESTNET_FAUCET_URL.clone(), TESTNET_NODE_URL.clone()), - ) - .await; - - // test flows on devnet - let _ = test_flows( - NetworkName::Devnet, - Client::new(DEVNET_NODE_URL.clone()), - FaucetClient::new(DEVNET_FAUCET_URL.clone(), DEVNET_NODE_URL.clone()), - ) - .await; + // test flows + let _ = test_flows(NetworkName::Testnet).await; + let _ = test_flows(NetworkName::Devnet).await; Ok(()) } diff --git a/crates/aptos-api-tester/src/tests.rs b/crates/aptos-api-tester/src/tests.rs new file mode 100644 index 0000000000000..d7071f441ad24 --- /dev/null +++ b/crates/aptos-api-tester/src/tests.rs @@ -0,0 +1,489 @@ +// Copyright © Aptos Foundation + +use crate::utils::TestFailure; +use anyhow::{anyhow, Result}; +use aptos_api_types::{HexEncodedBytes, U64}; +use aptos_cached_packages::aptos_stdlib::EntryFunctionCall; +use aptos_framework::{BuildOptions, BuiltPackage}; +use aptos_logger::info; +use aptos_rest_client::{Account, Client}; +use aptos_sdk::{ + bcs, + coin_client::CoinClient, + token_client::{ + build_and_submit_transaction, CollectionData, CollectionMutabilityConfig, RoyaltyOptions, + TokenClient, TokenData, TokenMutabilityConfig, TransactionOptions, + }, + types::LocalAccount, +}; +use aptos_types::{ + account_address::AccountAddress, + transaction::{EntryFunction, TransactionPayload}, +}; +use move_core_types::{ident_str, language_storage::ModuleId}; +use std::{collections::BTreeMap, path::PathBuf}; + +// fail messages +static FAIL_ACCOUNT_DATA: &str = "wrong account data"; +static FAIL_BALANCE: &str = "wrong balance"; +static FAIL_BALANCE_AFTER_TRANSACTION: &str = "wrong balance after transaction"; +static FAIL_BALANCE_BEFORE_TRANSACTION: &str = "wrong balance before transaction"; +static FAIL_COLLECTION_DATA: &str = "wrong collection data"; +static FAIL_TOKEN_DATA: &str = "wrong token data"; +static FAIL_TOKEN_BALANCE: &str = "wrong token balance"; +static FAIL_TOKENS_BEFORE_CLAIM: &str = "found tokens for receiver when shouldn't"; +static FAIL_TOKEN_BALANCE_AFTER_TRANSACTION: &str = "wrong token balance after transaction"; +static FAIL_BYTECODE: &str = "wrong bytecode"; +static FAIL_MODULE_INTERACTION: &str = "module interaction isn't reflected correctly"; +static ERROR_NO_VERSION: &str = "transaction did not return version"; +static ERROR_NO_BYTECODE: &str = "error while getting bytecode from blobs"; +static ERROR_MODULE_INTERACTION: &str = "module interaction isn't reflected"; + +/// Tests new account creation. Checks that: +/// - account data exists +/// - account balance reflects funded amount +pub async fn test_newaccount( + client: &Client, + account: &LocalAccount, + amount_funded: u64, +) -> Result<(), TestFailure> { + // ask for account data + let response = client.get_account(account.address()).await?; + + // check account data + let expected_account = Account { + authentication_key: account.authentication_key(), + sequence_number: account.sequence_number(), + }; + let actual_account = response.inner(); + + if &expected_account != actual_account { + info!( + "fail: {}, expected {:?}, got {:?}", + FAIL_ACCOUNT_DATA, expected_account, actual_account + ); + return Err(TestFailure::Fail(FAIL_ACCOUNT_DATA)); + } + + // check account balance + let expected_balance = U64(amount_funded); + let actual_balance = client + .get_account_balance(account.address()) + .await? + .inner() + .coin + .value; + + if expected_balance != actual_balance { + info!( + "fail: {}, expected {:?}, got {:?}", + FAIL_BALANCE, expected_balance, actual_balance + ); + return Err(TestFailure::Fail(FAIL_BALANCE)); + } + + Ok(()) +} + +/// Tests coin transfer. Checks that: +/// - receiver balance reflects transferred amount +/// - receiver balance shows correct amount at the previous version +pub async fn test_cointransfer( + client: &Client, + coin_client: &CoinClient<'_>, + account: &mut LocalAccount, + receiver: AccountAddress, + amount: u64, +) -> Result<(), TestFailure> { + // get starting balance + let starting_receiver_balance = u64::from( + client + .get_account_balance(receiver) + .await? + .inner() + .coin + .value, + ); + + // transfer coins to second account + let pending_txn = coin_client + .transfer(account, receiver, amount, None) + .await?; + let response = client.wait_for_transaction(&pending_txn).await?; + + // check receiver balance + let expected_receiver_balance = U64(starting_receiver_balance + amount); + let actual_receiver_balance = client + .get_account_balance(receiver) + .await? + .inner() + .coin + .value; + + if expected_receiver_balance != actual_receiver_balance { + info!( + "fail: {}, expected {:?}, got {:?}", + FAIL_BALANCE_AFTER_TRANSACTION, expected_receiver_balance, actual_receiver_balance + ); + return Err(TestFailure::Fail(FAIL_BALANCE_AFTER_TRANSACTION)); + } + + // check account balance with a lower version number + let version = match response.inner().version() { + Some(version) => version, + _ => { + info!("error: {}", ERROR_MODULE_INTERACTION); + return Err(TestFailure::Error(anyhow!(ERROR_NO_VERSION))); + }, + }; + + let expected_balance_at_version = U64(starting_receiver_balance); + let actual_balance_at_version = client + .get_account_balance_at_version(receiver, version - 1) + .await? + .inner() + .coin + .value; + + if expected_balance_at_version != actual_balance_at_version { + info!( + "fail: {}, expected {:?}, got {:?}", + FAIL_BALANCE_BEFORE_TRANSACTION, expected_balance_at_version, actual_balance_at_version + ); + return Err(TestFailure::Fail(FAIL_BALANCE_BEFORE_TRANSACTION)); + } + + Ok(()) +} + +/// Tests nft transfer. Checks that: +/// - collection data exists +/// - token data exists +/// - token balance reflects transferred amount +pub async fn test_nfttransfer( + client: &Client, + token_client: &TokenClient<'_>, + account: &mut LocalAccount, + receiver: &mut LocalAccount, +) -> Result<(), TestFailure> { + // create collection + let collection_name = "test collection".to_string(); + let collection_description = "collection description".to_string(); + let collection_uri = "collection uri".to_string(); + let collection_maximum = 1000; + + let pending_txn = token_client + .create_collection( + account, + &collection_name, + &collection_description, + &collection_uri, + collection_maximum, + None, + ) + .await?; + client.wait_for_transaction(&pending_txn).await?; + + // create token + let token_name = "test token".to_string(); + let token_description = "token description".to_string(); + let token_uri = "token uri".to_string(); + let token_maximum = 1000; + let token_supply = 10; + + let pending_txn = token_client + .create_token( + account, + &collection_name, + &token_name, + &token_description, + token_supply, + &token_uri, + token_maximum, + None, + None, + ) + .await?; + client.wait_for_transaction(&pending_txn).await?; + + // check collection metadata + let expected_collection_data = CollectionData { + name: collection_name.clone(), + description: collection_description, + uri: collection_uri, + maximum: U64(collection_maximum), + mutability_config: CollectionMutabilityConfig { + description: false, + maximum: false, + uri: false, + }, + }; + let actual_collection_data = token_client + .get_collection_data(account.address(), &collection_name) + .await?; + + if expected_collection_data != actual_collection_data { + info!( + "fail: {}, expected {:?}, got {:?}", + FAIL_COLLECTION_DATA, expected_collection_data, actual_collection_data + ); + return Err(TestFailure::Fail(FAIL_COLLECTION_DATA)); + } + + // check token metadata + let expected_token_data = TokenData { + name: token_name.clone(), + description: token_description, + uri: token_uri, + maximum: U64(token_maximum), + mutability_config: TokenMutabilityConfig { + description: false, + maximum: false, + properties: false, + royalty: false, + uri: false, + }, + supply: U64(token_supply), + royalty: RoyaltyOptions { + payee_address: account.address(), + royalty_points_denominator: U64(0), + royalty_points_numerator: U64(0), + }, + largest_property_version: U64(0), + }; + let actual_token_data = token_client + .get_token_data(account.address(), &collection_name, &token_name) + .await?; + + if expected_token_data != actual_token_data { + info!( + "fail: {}, expected {:?}, got {:?}", + FAIL_TOKEN_DATA, expected_token_data, actual_token_data + ); + return Err(TestFailure::Fail(FAIL_TOKEN_DATA)); + } + + // offer token + let pending_txn = token_client + .offer_token( + account, + receiver.address(), + account.address(), + &collection_name, + &token_name, + 2, + None, + None, + ) + .await?; + client.wait_for_transaction(&pending_txn).await?; + + // check token balance for the sender + let expected_sender_token_balance = U64(8); + let actual_sender_token_balance = token_client + .get_token( + account.address(), + account.address(), + &collection_name, + &token_name, + ) + .await? + .amount; + + if expected_sender_token_balance != actual_sender_token_balance { + info!( + "fail: {}, expected {:?}, got {:?}", + FAIL_TOKEN_BALANCE, expected_sender_token_balance, actual_sender_token_balance + ); + return Err(TestFailure::Fail(FAIL_TOKEN_BALANCE)); + } + + // check that token store isn't initialized for the receiver + if token_client + .get_token( + receiver.address(), + account.address(), + &collection_name, + &token_name, + ) + .await + .is_ok() + { + info!( + "fail: {}, expected no token client resource for the receiver", + FAIL_TOKENS_BEFORE_CLAIM + ); + return Err(TestFailure::Fail(FAIL_TOKENS_BEFORE_CLAIM)); + } + + // claim token + let pending_txn = token_client + .claim_token( + receiver, + account.address(), + account.address(), + &collection_name, + &token_name, + None, + None, + ) + .await?; + client.wait_for_transaction(&pending_txn).await?; + + // check token balance for the receiver + let expected_receiver_token_balance = U64(2); + let actual_receiver_token_balance = token_client + .get_token( + receiver.address(), + account.address(), + &collection_name, + &token_name, + ) + .await? + .amount; + + if expected_receiver_token_balance != actual_receiver_token_balance { + info!( + "{}, expected {:?}, got {:?}", + FAIL_TOKEN_BALANCE_AFTER_TRANSACTION, + expected_receiver_token_balance, + actual_receiver_token_balance + ); + return Err(TestFailure::Fail(FAIL_TOKEN_BALANCE_AFTER_TRANSACTION)); + } + + Ok(()) +} + +/// Helper function that publishes module and returns the bytecode. +async fn publish_module(client: &Client, account: &mut LocalAccount) -> Result { + // get file to compile + let move_dir = PathBuf::from("./aptos-move/move-examples/hello_blockchain"); + + // insert address + let mut named_addresses: BTreeMap = BTreeMap::new(); + named_addresses.insert("hello_blockchain".to_string(), account.address()); + + // build options + let options = BuildOptions { + named_addresses, + ..BuildOptions::default() + }; + + // build module + let package = BuiltPackage::build(move_dir, options)?; + let blobs = package.extract_code(); + let metadata = package.extract_metadata()?; + + // create payload + let payload: aptos_types::transaction::TransactionPayload = + EntryFunctionCall::CodePublishPackageTxn { + metadata_serialized: bcs::to_bytes(&metadata) + .expect("PackageMetadata should deserialize"), + code: blobs.clone(), + } + .encode(); + + // create and submit transaction + let pending_txn = + build_and_submit_transaction(client, account, payload, TransactionOptions::default()) + .await?; + client.wait_for_transaction(&pending_txn).await?; + + let blob = match blobs.get(0) { + Some(bytecode) => bytecode.clone(), + None => { + info!("error: {}", ERROR_NO_BYTECODE); + return Err(anyhow!(ERROR_NO_BYTECODE)); + }, + }; + + Ok(HexEncodedBytes::from(blob)) +} + +/// Helper function that interacts with the message module. +async fn set_message(client: &Client, account: &mut LocalAccount, message: &str) -> Result<()> { + // create payload + let payload = TransactionPayload::EntryFunction(EntryFunction::new( + ModuleId::new(account.address(), ident_str!("message").to_owned()), + ident_str!("set_message").to_owned(), + vec![], + vec![bcs::to_bytes(message)?], + )); + + // create and submit transaction + let pending_txn = + build_and_submit_transaction(client, account, payload, TransactionOptions::default()) + .await?; + client.wait_for_transaction(&pending_txn).await?; + + Ok(()) +} + +/// Helper function that gets back the result of the interaction. +async fn get_message(client: &Client, address: AccountAddress) -> Option { + let resource = match client + .get_account_resource( + address, + format!("{}::message::MessageHolder", address.to_hex_literal()).as_str(), + ) + .await + { + Ok(response) => response.into_inner()?, + Err(_) => return None, + }; + + Some(resource.data.get("message")?.as_str()?.to_owned()) +} + +/// Tests module publishing and interaction. Checks that: +/// - module data exists +/// - can interact with module +/// - resources reflect interaction +pub async fn test_publishmodule( + client: &Client, + account: &mut LocalAccount, +) -> Result<(), TestFailure> { + // publish module + let blob = publish_module(client, account).await?; + + // check module data + let response = client + .get_account_module(account.address(), "message") + .await?; + + let expected_bytecode = &blob; + let actual_bytecode = &response.inner().bytecode; + + if expected_bytecode != actual_bytecode { + info!( + "fail: {}, expected {:?}, got {:?}", + FAIL_BYTECODE, expected_bytecode, actual_bytecode + ); + return Err(TestFailure::Fail(FAIL_BYTECODE)); + } + + // interact with module + let message = "test message"; + set_message(client, account, message).await?; + + // check that the message is sent + let expected_message = message.to_string(); + let actual_message = match get_message(client, account.address()).await { + Some(message) => message, + None => { + info!("error: {}", ERROR_MODULE_INTERACTION); + return Err(TestFailure::Error(anyhow!(ERROR_MODULE_INTERACTION))); + }, + }; + + if expected_message != actual_message { + info!( + "fail: {}, expected {:?}, got {:?}", + FAIL_MODULE_INTERACTION, expected_message, actual_message + ); + return Err(TestFailure::Fail(FAIL_MODULE_INTERACTION)); + } + + Ok(()) +} diff --git a/crates/aptos-api-tester/src/testsetups.rs b/crates/aptos-api-tester/src/testsetups.rs new file mode 100644 index 0000000000000..78ff4f5cb5153 --- /dev/null +++ b/crates/aptos-api-tester/src/testsetups.rs @@ -0,0 +1,70 @@ +// Copyright © Aptos Foundation + +use crate::{ + tests::{test_cointransfer, test_newaccount, test_nfttransfer, test_publishmodule}, + utils::{ + create_account, create_and_fund_account, get_client, get_faucet_client, NetworkName, + TestFailure, + }, +}; +use anyhow::Result; +use aptos_sdk::{coin_client::CoinClient, token_client::TokenClient}; + +pub async fn setup_and_run_newaccount(network_name: NetworkName) -> Result<(), TestFailure> { + // spin up clients + let client = get_client(network_name); + let faucet_client = get_faucet_client(network_name); + + // create and fund account + let account = create_and_fund_account(&faucet_client).await?; + + // run test + test_newaccount(&client, &account, 100_000_000).await +} + +pub async fn setup_and_run_cointransfer(network_name: NetworkName) -> Result<(), TestFailure> { + // spin up clients + let client = get_client(network_name); + let faucet_client = get_faucet_client(network_name); + let coin_client = CoinClient::new(&client); + + // create and fund accounts + let mut account = create_and_fund_account(&faucet_client).await?; + let receiver = create_account(&faucet_client).await?; + + // run test + test_cointransfer( + &client, + &coin_client, + &mut account, + receiver.address(), + 1_000, + ) + .await +} + +pub async fn setup_and_run_nfttransfer(network_name: NetworkName) -> Result<(), TestFailure> { + // spin up clients + let client = get_client(network_name); + let faucet_client = get_faucet_client(network_name); + let token_client = TokenClient::new(&client); + + // create and fund accounts + let mut account = create_and_fund_account(&faucet_client).await?; + let mut receiver = create_and_fund_account(&faucet_client).await?; + + // run test + test_nfttransfer(&client, &token_client, &mut account, &mut receiver).await +} + +pub async fn setup_and_run_publishmodule(network_name: NetworkName) -> Result<(), TestFailure> { + // spin up clients + let client = get_client(network_name); + let faucet_client = get_faucet_client(network_name); + + // create and fund accounts + let mut account = create_and_fund_account(&faucet_client).await?; + + // run test + test_publishmodule(&client, &mut account).await +} diff --git a/crates/aptos-api-tester/src/utils.rs b/crates/aptos-api-tester/src/utils.rs index 55d42cf52a4a8..d0c482d26eff2 100644 --- a/crates/aptos-api-tester/src/utils.rs +++ b/crates/aptos-api-tester/src/utils.rs @@ -1,7 +1,10 @@ // Copyright © Aptos Foundation use crate::counters::{test_error, test_fail, test_latency, test_success}; -use aptos_rest_client::error::RestError; +use anyhow::Result; +use aptos_logger::info; +use aptos_rest_client::{error::RestError, Client, FaucetClient}; +use aptos_sdk::types::LocalAccount; use once_cell::sync::Lazy; use url::Url; @@ -82,26 +85,70 @@ impl ToString for NetworkName { } } -// Helper function to set metrics based on the result. -pub fn set_metrics(output: &TestResult, test_name: &str, network_name: &str, time: f64) { +// Set metrics based on the result. +pub fn set_metrics( + output: &TestResult, + test_name: &str, + network_name: &str, + start_time: &str, + time: f64, +) { match output { TestResult::Success => { - test_success(test_name, network_name).observe(1_f64); - test_fail(test_name, network_name).observe(0_f64); - test_error(test_name, network_name).observe(0_f64); - test_latency(test_name, network_name, "success").observe(time); + test_success(test_name, network_name, start_time).observe(1_f64); + test_fail(test_name, network_name, start_time).observe(0_f64); + test_error(test_name, network_name, start_time).observe(0_f64); + test_latency(test_name, network_name, start_time, "success").observe(time); }, TestResult::Fail(_) => { - test_success(test_name, network_name).observe(0_f64); - test_fail(test_name, network_name).observe(1_f64); - test_error(test_name, network_name).observe(0_f64); - test_latency(test_name, network_name, "fail").observe(time); + test_success(test_name, network_name, start_time).observe(0_f64); + test_fail(test_name, network_name, start_time).observe(1_f64); + test_error(test_name, network_name, start_time).observe(0_f64); + test_latency(test_name, network_name, start_time, "fail").observe(time); }, TestResult::Error(_) => { - test_success(test_name, network_name).observe(0_f64); - test_fail(test_name, network_name).observe(0_f64); - test_error(test_name, network_name).observe(1_f64); - test_latency(test_name, network_name, "error").observe(time); + test_success(test_name, network_name, start_time).observe(0_f64); + test_fail(test_name, network_name, start_time).observe(0_f64); + test_error(test_name, network_name, start_time).observe(1_f64); + test_latency(test_name, network_name, start_time, "error").observe(time); }, } } + +// Create a REST client. +pub fn get_client(network_name: NetworkName) -> Client { + match network_name { + NetworkName::Testnet => Client::new(TESTNET_NODE_URL.clone()), + NetworkName::Devnet => Client::new(DEVNET_NODE_URL.clone()), + } +} + +// Create a faucet client. +pub fn get_faucet_client(network_name: NetworkName) -> FaucetClient { + match network_name { + NetworkName::Testnet => { + FaucetClient::new(TESTNET_FAUCET_URL.clone(), TESTNET_NODE_URL.clone()) + }, + NetworkName::Devnet => { + FaucetClient::new(DEVNET_FAUCET_URL.clone(), DEVNET_NODE_URL.clone()) + }, + } +} + +// Create an account with zero balance. +pub async fn create_account(faucet_client: &FaucetClient) -> Result { + let account = LocalAccount::generate(&mut rand::rngs::OsRng); + faucet_client.create_account(account.address()).await?; + info!("{:?}", account.address()); + + Ok(account) +} + +// Create an account with 100_000_000 balance. +pub async fn create_and_fund_account(faucet_client: &FaucetClient) -> Result { + let account = LocalAccount::generate(&mut rand::rngs::OsRng); + faucet_client.fund(account.address(), 100_000_000).await?; + info!("{:?}", account.address()); + + Ok(account) +}