Skip to content

Commit

Permalink
enha: add snapshot tests for external primitives (#2007)
Browse files Browse the repository at this point in the history
* chore: add serde initial tests

* lint

* chore: refac into macro

* doc

* doc

* chore: add more structs

* chore: naming

* chore: doc

* chore: wan comment
  • Loading branch information
gabriel-aranha-cw authored Feb 11, 2025
1 parent c97aede commit b0504ab
Show file tree
Hide file tree
Showing 12 changed files with 601 additions and 5 deletions.
62 changes: 61 additions & 1 deletion src/eth/primitives/external_block.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
use alloy_eips::eip4895::Withdrawals;
use alloy_primitives::Bloom;
use alloy_primitives::Bytes;
use alloy_primitives::B256;
use alloy_primitives::B64;
use alloy_primitives::U256;
use fake::Dummy;
use fake::Fake;
use fake::Faker;
use serde::Deserialize;

use crate::alias::AlloyBlockExternalTransaction;
use crate::alias::JsonValue;
use crate::eth::primitives::external_transaction::ExternalTransaction;
use crate::eth::primitives::Address;
use crate::eth::primitives::BlockNumber;
use crate::eth::primitives::Hash;
use crate::eth::primitives::UnixTime;
use crate::log_and_err;

#[derive(Debug, Clone, derive_more::Deref, derive_more::DerefMut, serde::Deserialize, serde::Serialize)]
#[derive(Debug, Clone, PartialEq, derive_more::Deref, derive_more::DerefMut, serde::Deserialize, serde::Serialize)]
#[serde(transparent)]
pub struct ExternalBlock(#[deref] pub AlloyBlockExternalTransaction);

Expand Down Expand Up @@ -36,9 +46,59 @@ impl ExternalBlock {
}
}

impl Dummy<Faker> for ExternalBlock {
fn dummy_with_rng<R: rand_core::RngCore + ?Sized>(faker: &Faker, rng: &mut R) -> Self {
let mut addr_bytes = [0u8; 20];
let mut hash_bytes = [0u8; 32];
let mut nonce_bytes = [0u8; 8];
rng.fill_bytes(&mut addr_bytes);
rng.fill_bytes(&mut hash_bytes);
rng.fill_bytes(&mut nonce_bytes);

let transaction: ExternalTransaction = faker.fake_with_rng(rng);

let block = alloy_rpc_types_eth::Block {
header: alloy_rpc_types_eth::Header {
hash: B256::from_slice(&hash_bytes),
inner: alloy_consensus::Header {
parent_hash: B256::from_slice(&hash_bytes),
ommers_hash: B256::from_slice(&hash_bytes),
beneficiary: alloy_primitives::Address::from_slice(&addr_bytes),
state_root: B256::from_slice(&hash_bytes),
transactions_root: B256::from_slice(&hash_bytes),
receipts_root: B256::from_slice(&hash_bytes),
withdrawals_root: Some(B256::from_slice(&hash_bytes)),
number: rng.next_u64(),
gas_used: rng.next_u64(),
gas_limit: rng.next_u64(),
extra_data: Bytes::default(),
logs_bloom: Bloom::default(),
timestamp: rng.next_u64(),
difficulty: U256::from(rng.next_u64()),
mix_hash: B256::from_slice(&hash_bytes),
nonce: B64::from_slice(&nonce_bytes),
base_fee_per_gas: Some(rng.next_u64()),
blob_gas_used: None,
excess_blob_gas: None,
parent_beacon_block_root: None,
requests_hash: None,
},
total_difficulty: Some(U256::from(rng.next_u64())),
size: Some(U256::from(rng.next_u64())),
},
uncles: vec![B256::from_slice(&hash_bytes)],
transactions: alloy_rpc_types_eth::BlockTransactions::Full(vec![transaction]),
withdrawals: Some(Withdrawals::default()),
};

ExternalBlock(block)
}
}

// -----------------------------------------------------------------------------
// Conversions: Other -> Self
// -----------------------------------------------------------------------------

impl TryFrom<JsonValue> for ExternalBlock {
type Error = anyhow::Error;

Expand Down
19 changes: 18 additions & 1 deletion src/eth/primitives/external_block_with_receipts.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,33 @@
use fake::Dummy;
use fake::Faker;
use serde::Deserialize;
use serde::Serialize;

use crate::alias::JsonValue;
use crate::eth::primitives::ExternalBlock;
use crate::eth::primitives::ExternalReceipt;
use crate::log_and_err;

#[derive(Debug, Clone, Deserialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub struct ExternalBlockWithReceipts {
pub block: ExternalBlock,
pub receipts: Vec<ExternalReceipt>,
}

impl Dummy<Faker> for ExternalBlockWithReceipts {
fn dummy_with_rng<R: rand_core::RngCore + ?Sized>(faker: &Faker, rng: &mut R) -> Self {
let block = ExternalBlock::dummy_with_rng(faker, rng);

let receipts = match &block.transactions {
alloy_rpc_types_eth::BlockTransactions::Full(txs) => txs.iter().map(|_| ExternalReceipt::dummy_with_rng(faker, rng)).collect(),
alloy_rpc_types_eth::BlockTransactions::Hashes(_) => Vec::new(),
alloy_rpc_types_eth::BlockTransactions::Uncle => Vec::new(),
};

Self { block, receipts }
}
}

// -----------------------------------------------------------------------------
// Conversions: Other -> Self
// -----------------------------------------------------------------------------
Expand Down
61 changes: 60 additions & 1 deletion src/eth/primitives/external_receipt.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
use alloy_consensus::ReceiptEnvelope;
use alloy_primitives::Bloom;
use alloy_primitives::Bytes;
use alloy_primitives::B256;
use ethereum_types::U256;
use fake::Dummy;
use fake::Faker;
use serde::Deserialize;

use crate::alias::AlloyReceipt;
Expand All @@ -8,7 +14,7 @@ use crate::eth::primitives::Hash;
use crate::eth::primitives::Wei;
use crate::log_and_err;

#[derive(Debug, Clone, derive_more::Deref, serde::Serialize)]
#[derive(Debug, Clone, PartialEq, derive_more::Deref, serde::Serialize)]
#[serde(transparent)]
pub struct ExternalReceipt(#[deref] pub AlloyReceipt);

Expand Down Expand Up @@ -43,6 +49,58 @@ impl ExternalReceipt {
}
}

impl Dummy<Faker> for ExternalReceipt {
fn dummy_with_rng<R: rand_core::RngCore + ?Sized>(_faker: &Faker, rng: &mut R) -> Self {
let mut addr_bytes = [0u8; 20];
let mut hash_bytes = [0u8; 32];
rng.fill_bytes(&mut addr_bytes);
rng.fill_bytes(&mut hash_bytes);

let log = alloy_rpc_types_eth::Log {
inner: alloy_primitives::Log {
address: alloy_primitives::Address::from_slice(&addr_bytes),
data: alloy_primitives::LogData::new_unchecked(vec![B256::from_slice(&hash_bytes)], Bytes::default()),
},
block_hash: Some(B256::from_slice(&hash_bytes)),
block_number: Some(rng.next_u64()),
transaction_hash: Some(B256::from_slice(&hash_bytes)),
transaction_index: Some(rng.next_u64()),
log_index: Some(rng.next_u64()),
removed: false,
block_timestamp: Some(rng.next_u64()),
};

let receipt = alloy_consensus::Receipt {
status: alloy_consensus::Eip658Value::Eip658(true),
cumulative_gas_used: rng.next_u64(),
logs: vec![log],
};

let receipt_envelope = ReceiptEnvelope::Legacy(alloy_consensus::ReceiptWithBloom {
receipt,
logs_bloom: Bloom::default(),
});

let receipt = alloy_rpc_types_eth::TransactionReceipt {
inner: receipt_envelope,
transaction_hash: B256::from_slice(&hash_bytes),
transaction_index: Some(rng.next_u64()),
block_hash: Some(B256::from_slice(&hash_bytes)),
block_number: Some(rng.next_u64()),
from: alloy_primitives::Address::from_slice(&addr_bytes),
to: Some(alloy_primitives::Address::from_slice(&addr_bytes)),
contract_address: None,
gas_used: rng.next_u64(),
effective_gas_price: rng.next_u64() as u128,
blob_gas_used: None,
blob_gas_price: None,
authorization_list: None,
};

ExternalReceipt(receipt)
}
}

// -----------------------------------------------------------------------------
// Serialization / Deserialization
// -----------------------------------------------------------------------------
Expand Down Expand Up @@ -91,6 +149,7 @@ impl TryFrom<JsonValue> for ExternalReceipt {

#[cfg(test)]
mod tests {

use alloy_consensus::TxType;

use super::*;
Expand Down
17 changes: 16 additions & 1 deletion src/eth/primitives/external_receipts.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
use std::collections::HashMap;

use anyhow::anyhow;
use fake::Dummy;
use fake::Faker;

use crate::eth::primitives::ExternalReceipt;
use crate::eth::primitives::Hash;

/// A collection of [`ExternalReceipt`].
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct ExternalReceipts(HashMap<Hash, ExternalReceipt>);

impl ExternalReceipts {
Expand All @@ -22,6 +24,19 @@ impl ExternalReceipts {
}
}

impl Dummy<Faker> for ExternalReceipts {
fn dummy_with_rng<R: rand_core::RngCore + ?Sized>(faker: &Faker, rng: &mut R) -> Self {
let count = (rng.next_u32() % 5 + 1) as usize;
let receipts = (0..count).map(|_| ExternalReceipt::dummy_with_rng(faker, rng)).collect::<Vec<_>>();

Self::from(receipts)
}
}

// -----------------------------------------------------------------------------
// Conversions: Other -> Self
// -----------------------------------------------------------------------------

impl From<Vec<ExternalReceipt>> for ExternalReceipts {
fn from(receipts: Vec<ExternalReceipt>) -> Self {
let mut receipts_by_hash = HashMap::with_capacity(receipts.len());
Expand Down
55 changes: 54 additions & 1 deletion src/eth/primitives/external_transaction.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,24 @@
use alloy_consensus::Signed;
use alloy_consensus::TxEnvelope;
use alloy_consensus::TxLegacy;
use alloy_primitives::Bytes;
use alloy_primitives::PrimitiveSignature;
use alloy_primitives::TxKind;
use anyhow::Context;
use anyhow::Result;
use ethereum_types::U256;
use fake::Dummy;
use fake::Fake;
use fake::Faker;

use crate::alias::AlloyTransaction;
use crate::eth::primitives::signature_component::SignatureComponent;
use crate::eth::primitives::Address;
use crate::eth::primitives::BlockNumber;
use crate::eth::primitives::Hash;
#[derive(Debug, Clone, derive_more::Deref, serde::Deserialize, serde::Serialize)]
use crate::eth::primitives::Wei;

#[derive(Debug, Clone, PartialEq, derive_more::Deref, serde::Deserialize, serde::Serialize)]
#[serde(transparent)]
pub struct ExternalTransaction(#[deref] pub AlloyTransaction);

Expand All @@ -20,6 +34,45 @@ impl ExternalTransaction {
}
}

impl Dummy<Faker> for ExternalTransaction {
fn dummy_with_rng<R: rand_core::RngCore + ?Sized>(faker: &Faker, rng: &mut R) -> Self {
let from: Address = faker.fake_with_rng(rng);
let block_hash: Hash = faker.fake_with_rng(rng);

let gas_price: Wei = Wei::from(rng.next_u64());
let value: Wei = Wei::from(rng.next_u64());

let tx = TxLegacy {
chain_id: Some(1),
nonce: rng.next_u64(),
gas_price: gas_price.into(),
gas_limit: rng.next_u64(),
to: TxKind::Call(from.into()),
value: value.into(),
input: Bytes::default(),
};

let r = U256::from(rng.next_u64());
let s = U256::from(rng.next_u64());
let v = rng.next_u64() % 2 == 0;
let signature = PrimitiveSignature::new(SignatureComponent(r).into(), SignatureComponent(s).into(), v);

let hash: Hash = faker.fake_with_rng(rng);
let inner_tx = TxEnvelope::Legacy(Signed::new_unchecked(tx, signature, hash.into()));

let inner = alloy_rpc_types_eth::Transaction {
inner: inner_tx,
block_hash: Some(block_hash.into()),
block_number: Some(rng.next_u64()),
transaction_index: Some(rng.next_u64()),
from: from.into(),
effective_gas_price: Some(gas_price.as_u128()),
};

ExternalTransaction(inner)
}
}

// -----------------------------------------------------------------------------
// Conversions: Other -> Self
// -----------------------------------------------------------------------------
Expand Down
7 changes: 7 additions & 0 deletions src/eth/primitives/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ pub use wei::Wei;
#[cfg(test)]
mod tests {
use super::*;
use crate::gen_test_json;
use crate::gen_test_serde;

type TransactionExecutionValueChangeBytes = ExecutionValueChange<Bytes>;
Expand All @@ -142,6 +143,12 @@ mod tests {
// gen_test_serde!(TransactionExecution);
// gen_test_serde!(TransactionStage);

gen_test_json!(ExternalBlock);
gen_test_json!(ExternalBlockWithReceipts);
gen_test_json!(ExternalReceipt);
gen_test_json!(ExternalReceipts);
gen_test_json!(ExternalTransaction);

gen_test_serde!(Account);
gen_test_serde!(Address);
gen_test_serde!(Block);
Expand Down
52 changes: 52 additions & 0 deletions src/ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,58 @@ macro_rules! gen_test_serde {
};
}

/// Generates a unit test that verifies JSON serialization/deserialization compatibility using snapshots.
#[macro_export]
macro_rules! gen_test_json {
($type:ty) => {
paste::paste! {
#[test]
fn [<test_ $type:snake _json_snapshot>]() -> anyhow::Result<()> {
use anyhow::bail;
use std::path::Path;
use std::{env, fs};

let expected: $type = $crate::utils::test_utils::fake_first::<$type>();
let expected_json = serde_json::to_string_pretty(&expected)?;
let snapshot_parent_path = "tests/fixtures/primitives";
let snapshot_name = format!("{}.json", stringify!($type));
let snapshot_path = format!("{}/{}", snapshot_parent_path, snapshot_name);

// WARNING: If you need to update snapshots (DANGEROUS_UPDATE_SNAPSHOTS=1), you have likely
// broken backwards compatibility! Make sure this is intentional.
if !Path::new(&snapshot_path).exists() {
if env::var("DANGEROUS_UPDATE_SNAPSHOTS").is_ok() {
fs::create_dir_all(snapshot_parent_path)?;
fs::write(&snapshot_path, &expected_json)?;
} else {
bail!("snapshot file at '{snapshot_path}' doesn't exist and DANGEROUS_UPDATE_SNAPSHOTS is not set");
}
}

// Read and attempt to deserialize the snapshot
let snapshot_content = fs::read_to_string(&snapshot_path)?;
let deserialized = match serde_json::from_str::<$type>(&snapshot_content) {
Ok(value) => value,
Err(e) => {
bail!("Failed to deserialize snapshot:\nError: {}\n\nExpected JSON:\n{}\n\nActual JSON from snapshot:\n{}",
e, expected_json, snapshot_content);
}
};

// Compare the values
assert_eq!(
expected, deserialized,
"\nDeserialized value doesn't match expected:\n\nExpected JSON:\n{}\n\nDeserialized JSON:\n{}",
expected_json,
serde_json::to_string_pretty(&deserialized)?
);

Ok(())
}
}
};
}

/// Generates unit test that checks that bincode's serialization and deserialization are compatible
#[macro_export]
macro_rules! gen_test_bincode {
Expand Down
Loading

0 comments on commit b0504ab

Please sign in to comment.