Skip to content

Commit dd592cf

Browse files
committed
assertions cleanup
1 parent 75625d7 commit dd592cf

File tree

4 files changed

+419
-204
lines changed

4 files changed

+419
-204
lines changed

crates/testsvm/src/assertions.rs

Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
//! # Transaction Assertions
2+
//!
3+
//! Assertion helpers for testing transaction results in TestSVM.
4+
//!
5+
//! This module provides traits and types for asserting expected transaction outcomes,
6+
//! including methods for verifying that transactions succeed or fail with specific errors.
7+
//! These assertions are particularly useful in test environments where you need to
8+
//! verify that your program behaves correctly under various conditions.
9+
//!
10+
//! ## Features
11+
//!
12+
//! - **Success/Failure Assertions**: Verify transactions succeed or fail as expected
13+
//! - **Error Matching**: Check for specific error types including Anchor errors
14+
//! - **Type-safe API**: Compile-time guarantees for assertion chains
15+
16+
use anyhow::*;
17+
use litesvm::types::TransactionMetadata;
18+
use solana_sdk::instruction::InstructionError;
19+
20+
use crate::{TXError, TXResult};
21+
22+
/// Provides assertion methods for failed transactions.
23+
///
24+
/// This struct wraps a transaction error and provides helper methods
25+
/// for asserting specific error conditions in tests.
26+
pub struct TXErrorAssertions {
27+
/// Underlying transaction error.
28+
pub(crate) error: TXError,
29+
}
30+
31+
impl TXErrorAssertions {
32+
/// Asserts that the transaction failed with a specific Anchor error.
33+
///
34+
/// This uses string matching to find the error in the transaction logs, looking for
35+
/// the last program log containing the string "AnchorError" and matching the error name.
36+
pub fn with_anchor_error(&self, error_name: &str) -> Result<()> {
37+
match self.error.metadata.err.clone() {
38+
solana_sdk::transaction::TransactionError::InstructionError(
39+
_,
40+
InstructionError::Custom(_error_code),
41+
) => {
42+
let maybe_error_message = self
43+
.error
44+
.metadata
45+
.meta
46+
.logs
47+
.iter()
48+
.rev()
49+
.find(|line| line.contains("AnchorError"));
50+
if let Some(error_message) = maybe_error_message {
51+
if error_message.contains(&format!("{error_name}. Error Number:")) {
52+
Ok(())
53+
} else {
54+
Err(anyhow!(
55+
"Expected Anchor error '{}', got '{}'",
56+
error_name,
57+
error_message
58+
))
59+
}
60+
} else {
61+
Err(anyhow!(
62+
"Expected Anchor error '{}', but nothing was found in the logs",
63+
error_name
64+
))
65+
}
66+
}
67+
_ => Err(anyhow!(
68+
"Expected error containing '{}', but got '{}'",
69+
error_name,
70+
self.error.metadata.err.to_string()
71+
)),
72+
}
73+
}
74+
75+
/// Asserts that the transaction failed with a specific error message.
76+
///
77+
/// This method checks the transaction logs for an error message containing
78+
/// the specified error name and error code.
79+
pub fn with_error(&self, error_name: &str) -> Result<()> {
80+
match self.error.metadata.err.clone() {
81+
solana_sdk::transaction::TransactionError::InstructionError(
82+
_,
83+
InstructionError::Custom(error_code),
84+
) => {
85+
if self
86+
.error
87+
.metadata
88+
.meta
89+
.pretty_logs()
90+
.contains(format!("{error_name}. Error Number: {error_code}").as_str())
91+
{
92+
Ok(())
93+
} else {
94+
Err(anyhow!("Expected error '{}'", error_name))
95+
}
96+
}
97+
_ => Err(anyhow!(
98+
"Expected error containing '{}', but got '{}'",
99+
error_name,
100+
self.error.metadata.err.to_string()
101+
)),
102+
}
103+
}
104+
105+
/// Asserts that the transaction failed with a specific custom error code.
106+
///
107+
/// This is useful for checking SPL Token errors and other program-specific error codes.
108+
pub fn with_custom_error(&self, error_code: u32) -> Result<()> {
109+
match self.error.metadata.err.clone() {
110+
solana_sdk::transaction::TransactionError::InstructionError(
111+
_,
112+
InstructionError::Custom(code),
113+
) => {
114+
if code == error_code {
115+
Ok(())
116+
} else {
117+
Err(anyhow!(
118+
"Expected custom error code {}, got {}",
119+
error_code,
120+
code
121+
))
122+
}
123+
}
124+
_ => Err(anyhow!(
125+
"Expected custom error code {}, but got '{}'",
126+
error_code,
127+
self.error.metadata.err.to_string()
128+
)),
129+
}
130+
}
131+
132+
/// Returns the underlying transaction error for custom assertions.
133+
pub fn error(&self) -> &TXError {
134+
&self.error
135+
}
136+
}
137+
138+
/// Assertions for successful transactions.
139+
pub struct TXSuccessAssertions {
140+
/// The successful transaction metadata
141+
pub metadata: TransactionMetadata,
142+
}
143+
144+
impl TXSuccessAssertions {
145+
/// Returns the compute units consumed by the transaction.
146+
pub fn compute_units(&self) -> u64 {
147+
self.metadata.compute_units_consumed
148+
}
149+
150+
/// Returns the transaction logs.
151+
pub fn logs(&self) -> &Vec<String> {
152+
&self.metadata.logs
153+
}
154+
}
155+
156+
/// Extension trait for transaction results providing assertion methods.
157+
///
158+
/// This trait adds convenient assertion methods to `TXResult` for testing
159+
/// whether transactions succeed or fail as expected.
160+
pub trait TXResultAssertions {
161+
/// Asserts that the transaction fails, converting a successful transaction to an error.
162+
///
163+
/// This method is used in tests to verify that a transaction is expected to fail.
164+
/// It returns a `TXErrorAssertions` struct that provides additional assertion methods
165+
/// for checking specific error conditions.
166+
///
167+
/// # Returns
168+
///
169+
/// Returns `Ok(TXErrorAssertions)` if the transaction failed as expected, or an error
170+
/// if the transaction unexpectedly succeeded.
171+
///
172+
/// # Example
173+
///
174+
/// ```rust
175+
/// # use testsvm::{TestSVM, TXResultAssertions};
176+
/// # use solana_sdk::signature::Signer;
177+
/// # use anyhow::Result;
178+
/// # fn main() -> Result<()> {
179+
/// # let mut env = TestSVM::init()?;
180+
/// # let owner = env.new_wallet("owner")?;
181+
/// # let unauthorized_user = env.new_wallet("unauthorized")?;
182+
/// #
183+
/// # // Create a mint owned by 'owner'
184+
/// # let mint = env.create_mint("test_mint", 6, &owner.pubkey())?;
185+
/// #
186+
/// # // Create token accounts
187+
/// # let (owner_ata_ix, owner_ata) = env.create_ata_ix("owner_ata", &owner.pubkey(), &mint.key)?;
188+
/// # let (user_ata_ix, user_ata) = env.create_ata_ix("user_ata", &unauthorized_user.pubkey(), &mint.key)?;
189+
/// # env.execute_ixs(&[owner_ata_ix, user_ata_ix])?;
190+
/// #
191+
/// // Try to mint tokens from unauthorized user (should fail)
192+
/// let mint_ix = anchor_spl::token::spl_token::instruction::mint_to(
193+
/// &anchor_spl::token::ID,
194+
/// &mint.key,
195+
/// &user_ata.key,
196+
/// &unauthorized_user.pubkey(), // Wrong authority!
197+
/// &[],
198+
/// 1_000_000,
199+
/// )?;
200+
///
201+
/// // Test that unauthorized minting fails
202+
/// let result = env.execute_ixs_with_signers(&[mint_ix], &[&unauthorized_user]);
203+
/// result
204+
/// .fails()? // Assert the transaction fails
205+
/// .with_custom_error(4)?; // SPL Token error: OwnerMismatch
206+
/// # Ok(())
207+
/// # }
208+
/// ```
209+
fn fails(self) -> Result<TXErrorAssertions>;
210+
211+
/// Asserts that the transaction succeeds, converting a failed transaction to an error.
212+
///
213+
/// This method is used in tests to verify that a transaction is expected to succeed.
214+
/// It returns a `TXSuccessAssertions` struct that contains the successful transaction
215+
/// metadata for further validation if needed.
216+
///
217+
/// # Returns
218+
///
219+
/// Returns `Ok(TXSuccessAssertions)` if the transaction succeeded as expected, or an error
220+
/// if the transaction unexpectedly failed.
221+
///
222+
/// # Example
223+
///
224+
/// ```rust
225+
/// # use testsvm::{TestSVM, TXResultAssertions};
226+
/// # use solana_sdk::signature::Signer;
227+
/// # use anyhow::Result;
228+
/// # fn main() -> Result<()> {
229+
/// # let mut env = TestSVM::init()?;
230+
/// # let owner = env.new_wallet("owner")?;
231+
/// #
232+
/// # // Create a mint owned by 'owner'
233+
/// # let mint = env.create_mint("test_mint", 6, &owner.pubkey())?;
234+
/// #
235+
/// # // Create token account
236+
/// # let (owner_ata_ix, owner_ata) = env.create_ata_ix("owner_ata", &owner.pubkey(), &mint.key)?;
237+
/// # env.execute_ixs(&[owner_ata_ix])?;
238+
/// #
239+
/// // Mint tokens from the authorized owner (should succeed)
240+
/// let mint_ix = anchor_spl::token::spl_token::instruction::mint_to(
241+
/// &anchor_spl::token::ID,
242+
/// &mint.key,
243+
/// &owner_ata.key,
244+
/// &owner.pubkey(), // Correct authority
245+
/// &[],
246+
/// 1_000_000,
247+
/// )?;
248+
///
249+
/// // Test that authorized minting succeeds
250+
/// let result = env.execute_ixs_with_signers(&[mint_ix], &[&owner]);
251+
/// let assertions = result.succeeds()?;
252+
///
253+
/// // Can access transaction metadata
254+
/// println!("Used {} compute units", assertions.compute_units());
255+
/// # Ok(())
256+
/// # }
257+
/// ```
258+
fn succeeds(self) -> Result<TXSuccessAssertions>;
259+
}
260+
261+
impl TXResultAssertions for TXResult {
262+
fn fails(self) -> Result<TXErrorAssertions> {
263+
let err = self
264+
.err()
265+
.ok_or(anyhow::anyhow!("Unexpected successful transaction"))?;
266+
Ok(TXErrorAssertions { error: *err })
267+
}
268+
269+
fn succeeds(self) -> Result<TXSuccessAssertions> {
270+
let metadata = self?;
271+
Ok(TXSuccessAssertions { metadata })
272+
}
273+
}

0 commit comments

Comments
 (0)