Skip to content

Commit f44a42d

Browse files
committed
format
1 parent 82857da commit f44a42d

File tree

63 files changed

+947
-478
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

63 files changed

+947
-478
lines changed

INTEGRATION_TESTING.md

Lines changed: 161 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@ All integration tests in this repository must follow these mandatory requirement
6262
**Functional test for every usage flow** - Each user workflow must have a corresponding test
6363
**Failing test for every error condition** - Every error case must have a test that verifies the expected failure
6464
**Complete output verification** - Assert the entire output struct in a single `assert_eq!` against the expected struct
65+
**Before + Changes = After pattern** - Test exact state transitions, not arbitrary end states
66+
**Complete struct assertions** - Single comprehensive `assert_eq!` on complete structs, not individual fields
67+
**Proper test documentation** - Numbered SUCCESS/FAIL test case lists for each test function
6568

6669
## Assertion Utilities
6770

@@ -121,22 +124,170 @@ async fn test_invalid_authority_fails() {
121124
### Complete Output Verification
122125
**Assert complete output structures in single `assert_eq!` against expected structs**
123126

127+
## **CRITICAL: Ideal Assertion Pattern**
128+
129+
**❌ WRONG: Creating arbitrary expected end states**
124130
```rust
125-
#[tokio::test]
126-
async fn test_operation_output() {
127-
let result = perform_operation(&mut rpc, test_params).await.unwrap();
131+
// Anti-pattern: Creating expected state from scratch
132+
let expected_end_state = create_expected_state(field1, field2, field3);
133+
assert_eq!(actual_state, expected_end_state);
134+
```
135+
136+
**✅ CORRECT: Before State + Expected Changes = After State**
137+
```rust
138+
// IDEAL: Parse actual before state, apply expected changes, compare to after state
139+
{
140+
// Parse complete state before operation
141+
let mut expected_after_state = parse_state_before(&state_data_before);
128142

129-
let expected = ExpectedOperationResult {
130-
transaction_signature: result.signature,
131-
modified_accounts: expected_account_changes,
132-
emitted_events: expected_events,
133-
// ... all expected outputs
134-
};
143+
// Apply the expected changes to the before state
144+
expected_after_state.field1 = new_value; // Only change what should change
145+
expected_after_state.amount -= transfer_amount;
135146

136-
assert_eq!(result, expected);
147+
// Parse actual state after operation
148+
let actual_after_state = parse_state_after(&state_data_after);
149+
150+
// Single comprehensive assertion: after = before + changes
151+
assert_eq!(actual_after_state, expected_after_state);
152+
}
153+
```
154+
155+
## **Real Example from Codebase**
156+
From `/program-tests/utils/src/assert_decompressed_token_transfer.rs`:
157+
158+
```rust
159+
{
160+
// Parse as SPL token accounts first
161+
let mut sender_token_before =
162+
spl_token_2022::state::Account::unpack(&sender_data_before[..165]).unwrap();
163+
sender_token_before.amount -= transfer_amount;
164+
let mut recipient_token_before =
165+
spl_token_2022::state::Account::unpack(&recipient_data_before[..165]).unwrap();
166+
recipient_token_before.amount += transfer_amount;
167+
168+
// Parse as SPL token accounts first
169+
let sender_account_after =
170+
spl_token_2022::state::Account::unpack(&sender_account_data.data[..165]).unwrap();
171+
let recipient_account_after =
172+
spl_token_2022::state::Account::unpack(&recipient_account_data.data[..165]).unwrap();
173+
assert_eq!(sender_account_after, sender_token_before);
174+
assert_eq!(recipient_account_after, recipient_token_before);
175+
}
176+
```
177+
178+
This pattern ensures you're testing **exact state transitions** rather than arbitrary end states.
179+
180+
## **Common Pitfalls and Solutions**
181+
182+
### **❌ Assertion Anti-Patterns to Avoid**
183+
184+
```rust
185+
// ❌ WRONG: Individual field assertions
186+
assert_eq!(actual.field1, expected_field1);
187+
assert_eq!(actual.field2, expected_field2);
188+
assert_eq!(actual.field3, expected_field3);
189+
190+
// ❌ WRONG: Creating expected end states from scratch
191+
let expected = ExpectedState {
192+
field1: "hardcoded_value",
193+
field2: 42,
194+
field3: vec![1, 2, 3],
195+
};
196+
197+
// ❌ WRONG: Not capturing actual before state
198+
let expected_before = create_expected_state(/* guess what before state was */);
199+
```
200+
201+
### **✅ Correct Patterns**
202+
203+
```rust
204+
// ✅ CORRECT: Parse actual before state, apply changes, assert after
205+
let actual_before = parse_complete_state(&account_data_before);
206+
let mut expected_after = actual_before.clone();
207+
expected_after.field1 = new_value; // Apply only the expected change
208+
let actual_after = parse_complete_state(&account_data_after);
209+
assert_eq!(actual_after, expected_after);
210+
```
211+
212+
### **Test Documentation Requirements**
213+
214+
**❌ WRONG: Vague test descriptions**
215+
```rust
216+
/// Test metadata operations
217+
#[tokio::test]
218+
async fn test_metadata() {
219+
```
220+
221+
**✅ CORRECT: Numbered SUCCESS/FAIL lists**
222+
```rust
223+
/// Test:
224+
/// 1. SUCCESS: Create mint with additional metadata keys
225+
/// 2. SUCCESS: Update metadata name field
226+
/// 3. FAIL: Update metadata field with invalid authority
227+
#[tokio::test]
228+
#[serial]
229+
async fn test_metadata_field_operations() -> Result<(), RpcError> {
230+
```
231+
232+
### **Error Propagation Patterns**
233+
234+
**❌ WRONG: Using .unwrap() everywhere**
235+
```rust
236+
let result = operation(&mut rpc, params).await.unwrap();
237+
```
238+
239+
**✅ CORRECT: Proper error propagation**
240+
```rust
241+
async fn test_operation() -> Result<(), RpcError> {
242+
let result = operation(&mut rpc, params).await?;
243+
Ok(())
244+
}
245+
```
246+
247+
### **Helper Function Best Practices**
248+
249+
**❌ WRONG: Hiding errors in helpers**
250+
```rust
251+
async fn create_mint_helper(rpc: &mut RPC) {
252+
create_mint(rpc, params).await.unwrap(); // Hides errors!
253+
}
254+
```
255+
256+
**✅ CORRECT: Propagate errors from helpers**
257+
```rust
258+
async fn create_mint_helper(rpc: &mut RPC) -> Result<Signature, RpcError> {
259+
create_mint(rpc, params).await
137260
}
138261
```
139262

263+
### **Struct Parsing Best Practices**
264+
265+
**✅ CORRECT: Use borsh deserialization for easier type handling**
266+
```rust
267+
// Parse complete structs using borsh for easier handling
268+
let mint_data: CompressedMint =
269+
BorshDeserialize::deserialize(&mut account_data.as_slice())
270+
.expect("Failed to deserialize CompressedMint");
271+
272+
// Work with the complete struct
273+
assert_eq!(actual_mint, expected_mint);
274+
```
275+
276+
**✅ CORRECT: Parse complete state, not partial data**
277+
```rust
278+
// Get complete account state before and after
279+
let complete_state_before = get_complete_account_state(&mut rpc, address).await;
280+
// ... perform operation ...
281+
let complete_state_after = get_complete_account_state(&mut rpc, address).await;
282+
283+
// Apply expected changes to before state
284+
let mut expected_after = complete_state_before.clone();
285+
expected_after.some_field = new_value;
286+
287+
// Assert complete state transition
288+
assert_eq!(complete_state_after, expected_after);
289+
```
290+
140291
### Integration Test Pattern
141292
```rust
142293
use light_test_utils::assert_operation_result;

program-libs/ctoken-types/tests/solana_ctoken.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@ use light_ctoken_types::state::{
33
solana_ctoken::{CompressedToken, CompressedTokenConfig, ZCompressedToken},
44
ExtensionStructConfig,
55
};
6-
use light_zero_copy::borsh_mut::DeserializeMut;
7-
use light_zero_copy::{borsh::Deserialize, init_mut::ZeroCopyNew};
6+
use light_zero_copy::{borsh::Deserialize, borsh_mut::DeserializeMut, init_mut::ZeroCopyNew};
87
use rand::Rng;
98
use spl_pod::{bytemuck::pod_from_bytes, primitives::PodU64, solana_program_option::COption};
109
use spl_token_2022::{

program-libs/zero-copy-derive/src/shared/utils.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ use std::{
55

66
use proc_macro2::TokenStream;
77
use quote::{format_ident, quote};
8-
use syn::{Attribute, Data, DataEnum, DeriveInput, Field, Fields, FieldsNamed, Ident, Type, TypePath};
8+
use syn::{
9+
Attribute, Data, DataEnum, DeriveInput, Field, Fields, FieldsNamed, Ident, Type, TypePath,
10+
};
911

1012
// Global cache for storing whether a struct implements Copy
1113
lazy_static::lazy_static! {

program-libs/zero-copy-derive/src/zero_copy.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ use syn::{parse_quote, DeriveInput, Field, Ident};
55

66
use crate::shared::{
77
meta_struct, utils,
8-
z_enum::{generate_enum_deserialize_impl, generate_enum_zero_copy_struct_inner, generate_z_enum},
8+
z_enum::{
9+
generate_enum_deserialize_impl, generate_enum_zero_copy_struct_inner, generate_z_enum,
10+
},
911
z_struct::{analyze_struct_fields, generate_z_struct, FieldType},
1012
};
1113

@@ -245,7 +247,11 @@ pub fn derive_zero_copy_impl(input: ProcTokenStream) -> syn::Result<proc_macro2:
245247
let (meta_fields, struct_fields) = utils::process_fields(fields);
246248

247249
let meta_struct_def = if !meta_fields.is_empty() {
248-
meta_struct::generate_meta_struct::<false>(&z_struct_meta_name, &meta_fields, hasher)?
250+
meta_struct::generate_meta_struct::<false>(
251+
&z_struct_meta_name,
252+
&meta_fields,
253+
hasher,
254+
)?
249255
} else {
250256
quote! {}
251257
};
@@ -284,7 +290,8 @@ pub fn derive_zero_copy_impl(input: ProcTokenStream) -> syn::Result<proc_macro2:
284290

285291
let z_enum_def = generate_z_enum(&z_enum_name, enum_data)?;
286292
let deserialize_impl = generate_enum_deserialize_impl(name, &z_enum_name, enum_data)?;
287-
let zero_copy_struct_inner_impl = generate_enum_zero_copy_struct_inner(name, &z_enum_name)?;
293+
let zero_copy_struct_inner_impl =
294+
generate_enum_zero_copy_struct_inner(name, &z_enum_name)?;
288295

289296
// Combine all implementations
290297
Ok(quote! {

program-libs/zero-copy-derive/tests/action_enum_test.rs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,15 @@ pub enum Action {
1919
#[cfg(test)]
2020
mod tests {
2121
use light_zero_copy::borsh::Deserialize;
22+
2223
use super::*;
2324

2425
#[test]
2526
fn test_action_enum_unit_variants() {
2627
// Test Update variant (discriminant 1)
2728
let data = [1u8];
2829
let (result, remaining) = Action::zero_copy_at(&data).unwrap();
29-
30+
3031
// We can't pattern match without importing the generated type,
3132
// but we can verify it doesn't panic and processes correctly
3233
println!("Successfully deserialized Update variant");
@@ -37,17 +38,17 @@ mod tests {
3738
fn test_action_enum_data_variant() {
3839
// Test MintTo variant (discriminant 0)
3940
let mut data = vec![0u8]; // discriminant 0 for MintTo
40-
41+
4142
// Add MintToAction serialized data
4243
// amount: 1000
4344
data.extend_from_slice(&1000u64.to_le_bytes());
44-
45+
4546
// recipient: "alice" (5 bytes length + "alice")
4647
data.extend_from_slice(&5u32.to_le_bytes());
4748
data.extend_from_slice(b"alice");
4849

4950
let (result, remaining) = Action::zero_copy_at(&data).unwrap();
50-
51+
5152
// We can't easily pattern match without the generated type imported,
5253
// but we can verify it processes without errors
5354
println!("Successfully deserialized MintTo variant");
@@ -59,18 +60,18 @@ mod tests {
5960
// Test all unit variants
6061
let variants = [
6162
(1u8, "Update"),
62-
(2u8, "CreateSplMint"),
63+
(2u8, "CreateSplMint"),
6364
(3u8, "UpdateMetadata"),
6465
];
6566

6667
for (discriminant, name) in variants {
6768
let data = [discriminant];
6869
let result = Action::zero_copy_at(&data);
69-
70+
7071
assert!(result.is_ok(), "Failed to deserialize {} variant", name);
7172
let (_, remaining) = result.unwrap();
7273
assert_eq!(remaining.len(), 0);
7374
println!("Successfully deserialized {} variant", name);
7475
}
7576
}
76-
}
77+
}

program-libs/zero-copy-derive/tests/comprehensive_enum_example.rs

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ pub enum ZAction<'a> {
2626
2727
impl<'a> Deserialize<'a> for Action {
2828
type Output = ZAction<'a>;
29-
29+
3030
fn zero_copy_at(data: &'a [u8]) -> Result<(Self::Output, &'a [u8]), ZeroCopyError> {
3131
match data[0] {
3232
0 => {
@@ -83,36 +83,41 @@ pub enum Action {
8383
#[cfg(test)]
8484
mod tests {
8585
use light_zero_copy::borsh::Deserialize;
86+
8687
use super::*;
8788

8889
#[test]
8990
fn test_generated_enum_structure() {
9091
// The macro should generate ZAction<'a> with concrete variants
91-
92+
9293
// Test unit variants
93-
for (discriminant, expected_name) in [(1u8, "Update"), (2u8, "CreateSplMint"), (3u8, "UpdateMetadata")] {
94+
for (discriminant, expected_name) in [
95+
(1u8, "Update"),
96+
(2u8, "CreateSplMint"),
97+
(3u8, "UpdateMetadata"),
98+
] {
9499
let data = [discriminant];
95100
let (result, remaining) = Action::zero_copy_at(&data).unwrap();
96101
assert_eq!(remaining.len(), 0);
97102
println!("✓ {}: {:?}", expected_name, result);
98103
}
99-
104+
100105
// Test data variant
101106
let mut data = vec![0u8]; // MintTo discriminant
102107
data.extend_from_slice(&42u64.to_le_bytes()); // amount
103108
data.extend_from_slice(&4u32.to_le_bytes()); // recipient length
104109
data.extend_from_slice(b"test"); // recipient data
105-
110+
106111
let (result, remaining) = Action::zero_copy_at(&data).unwrap();
107112
assert_eq!(remaining.len(), 0);
108113
println!("✓ MintTo: {:?}", result);
109114
}
110-
115+
111116
#[test]
112117
fn test_pattern_matching_example() {
113118
// This demonstrates the exact usage pattern the user wants
114119
let mut actions_data = Vec::new();
115-
120+
116121
// Create some test actions
117122
// Action 1: MintTo
118123
actions_data.push({
@@ -122,20 +127,20 @@ mod tests {
122127
data.extend_from_slice(b"alice");
123128
data
124129
});
125-
126-
// Action 2: Update
130+
131+
// Action 2: Update
127132
actions_data.push(vec![1u8]);
128-
133+
129134
// Action 3: CreateSplMint
130135
actions_data.push(vec![2u8]);
131136

132137
// Process each action (simulating the user's use case)
133138
for (i, action_data) in actions_data.iter().enumerate() {
134139
let (action, _) = Action::zero_copy_at(action_data).unwrap();
135-
140+
136141
// This is what the user wants to be able to write:
137142
println!("Processing action {}: {:?}", i, action);
138-
143+
139144
// In the user's real code, this would be:
140145
// match action {
141146
// ZAction::MintTo(mint_action) => {
@@ -153,4 +158,4 @@ mod tests {
153158
// }
154159
}
155160
}
156-
}
161+
}

0 commit comments

Comments
 (0)