Skip to content

Commit 2a74b92

Browse files
LeoPatOZ0xNeshi
andauthored
Added Simple Unit Tests (#7)
Co-authored-by: 0xNeshi <nenad.misic@openzeppelin.com>
1 parent e838723 commit 2a74b92

File tree

6 files changed

+360
-10
lines changed

6 files changed

+360
-10
lines changed

.github/workflows/test.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ jobs:
2525
- name: Install stable toolchain
2626
uses: actions-rust-lang/setup-rust-toolchain@v1
2727

28+
- name: Install Foundry
29+
uses: foundry-rs/foundry-toolchain@v1
30+
2831
- name: Cache cargo-nextest binary
2932
id: cache-cargo-nextest
3033
uses: actions/cache@v4
@@ -39,7 +42,7 @@ jobs:
3942
tool: cargo-nextest
4043

4144
- name: Cargo test
42-
run: cargo nextest run --locked --all-targets --all-features --no-tests=pass
45+
run: cargo nextest run --locked --all-targets --all-features --no-tests=pass --no-fail-fast
4346

4447
# https://github.com/rust-lang/cargo/issues/6669
4548
- name: Run doc tests

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
/target
22
/examples/**/target
3+
.DS_Store

src/block_scanner.rs

Lines changed: 100 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@ use alloy::{
99
eips::BlockNumberOrTag,
1010
network::Network,
1111
providers::{Provider, RootProvider},
12+
pubsub::PubSubConnect,
1213
rpc::{
1314
client::{ClientBuilder, RpcClient},
1415
types::Header,
1516
},
16-
transports::TransportError,
17+
transports::{TransportError, ipc::IpcConnect, ws::WsConnect},
1718
};
1819

1920
// copied form https://github.com/taikoxyz/taiko-mono/blob/f4b3a0e830e42e2fee54829326389709dd422098/packages/taiko-client/pkg/chain_iterator/block_batch_iterator.go#L19
@@ -132,7 +133,7 @@ impl<N: Network> BlockScannerBuilder<N> {
132133
/// Returns an error if the connection fails
133134
pub async fn connect_ws(
134135
self,
135-
connect: alloy::transports::ws::WsConnect,
136+
connect: WsConnect,
136137
) -> Result<BlockScanner<RootProvider<N>, N>, TransportError> {
137138
let client = ClientBuilder::default().ws(connect).await?;
138139
Ok(self.connect_client(client))
@@ -145,10 +146,10 @@ impl<N: Network> BlockScannerBuilder<N> {
145146
/// Returns an error if the connection fails
146147
pub async fn connect_ipc<T>(
147148
self,
148-
connect: alloy::transports::ipc::IpcConnect<T>,
149+
connect: IpcConnect<T>,
149150
) -> Result<BlockScanner<RootProvider<N>, N>, TransportError>
150151
where
151-
alloy::transports::ipc::IpcConnect<T>: alloy::pubsub::PubSubConnect,
152+
IpcConnect<T>: PubSubConnect,
152153
{
153154
let client = ClientBuilder::default().ipc(connect).await?;
154155
Ok(self.connect_client(client))
@@ -219,3 +220,98 @@ where
219220
receiver_stream
220221
}
221222
}
223+
224+
#[cfg(test)]
225+
mod tests {
226+
use super::*;
227+
use alloy::network::{Ethereum, Network};
228+
use alloy_node_bindings::Anvil;
229+
use tokio_stream::StreamExt;
230+
231+
#[allow(clippy::unnecessary_wraps)]
232+
fn no_op_on_blocks<N: Network>(
233+
_block: <N as Network>::BlockResponse,
234+
_update_current: UpdateCurrentFunc,
235+
_end_iter: EndIterFunc,
236+
) -> anyhow::Result<()> {
237+
Ok(())
238+
}
239+
240+
#[test]
241+
fn test_block_scanner_error_display() {
242+
assert_eq!(format!("{}", BlockScannerError::ErrEOF), "end of block batch iterator");
243+
assert_eq!(format!("{}", BlockScannerError::ErrContinue), "continue");
244+
assert_eq!(
245+
format!("{}", BlockScannerError::TerminalError(42)),
246+
"terminal error at block height 42"
247+
);
248+
}
249+
250+
#[test]
251+
fn test_builder_defaults() {
252+
let builder = BlockScannerBuilder::<Ethereum>::new();
253+
assert_eq!(builder.blocks_read_per_epoch, DEFAULT_BLOCKS_READ_PER_EPOCH);
254+
assert!(matches!(builder.start_height, BlockNumberOrTag::Earliest));
255+
assert!(matches!(builder.end_height, BlockNumberOrTag::Latest));
256+
assert_eq!(builder.reorg_rewind_depth, DEFAULT_REORG_REWIND_DEPTH);
257+
assert_eq!(builder.retry_interval, DEFAULT_RETRY_INTERVAL);
258+
assert_eq!(builder.block_confirmations, DEFAULT_BLOCK_CONFIRMATIONS);
259+
}
260+
261+
#[test]
262+
fn test_builder_setters() {
263+
let mut builder = BlockScannerBuilder::<Ethereum>::new();
264+
builder.with_blocks_read_per_epoch(25);
265+
builder.with_start_height(BlockNumberOrTag::Earliest);
266+
builder.with_end_height(BlockNumberOrTag::Latest);
267+
builder.with_on_blocks(no_op_on_blocks::<Ethereum>);
268+
builder.with_reorg_rewind_depth(5);
269+
let interval = Duration::from_secs(3);
270+
builder.with_retry_interval(interval);
271+
builder.with_block_confirmations(12);
272+
273+
assert_eq!(builder.blocks_read_per_epoch, 25);
274+
assert!(matches!(builder.start_height, BlockNumberOrTag::Earliest));
275+
assert!(matches!(builder.end_height, BlockNumberOrTag::Latest));
276+
assert_eq!(builder.reorg_rewind_depth, 5);
277+
assert_eq!(builder.retry_interval, interval);
278+
assert_eq!(builder.block_confirmations, 12);
279+
}
280+
281+
#[tokio::test]
282+
async fn test_connect_ws_and_start_stream_eof() {
283+
let anvil = Anvil::new().try_spawn().expect("failed to spawn anvil");
284+
let ws = WsConnect::new(anvil.ws_endpoint_url());
285+
286+
let builder = BlockScannerBuilder::<Ethereum>::new();
287+
let scanner = builder.connect_ws(ws).await.expect("failed to connect ws");
288+
289+
let mut stream = scanner.start().await;
290+
let first = stream.next().await;
291+
match first {
292+
Some(Err(BlockScannerError::ErrEOF)) => {}
293+
other => panic!("expected first stream item to be ErrEOF, got: {other:?}"),
294+
}
295+
}
296+
297+
#[tokio::test]
298+
async fn test_channel_buffer_is_equal_to_blocks_read_per_epoch() {
299+
let anvil = Anvil::new().try_spawn().expect("failed to spawn anvil");
300+
let ws = WsConnect::new(anvil.ws_endpoint_url());
301+
302+
let mut builder = BlockScannerBuilder::<Ethereum>::new();
303+
builder.with_blocks_read_per_epoch(5);
304+
305+
let scanner = builder.connect_ws(ws).await.expect("failed to connect ws");
306+
307+
for _ in 0..scanner.blocks_read_per_epoch {
308+
scanner
309+
.sender
310+
.try_send(Err(BlockScannerError::ErrContinue))
311+
.expect("channel should not be full yet");
312+
}
313+
314+
let res = scanner.sender.try_send(Err(BlockScannerError::ErrContinue));
315+
assert!(matches!(res, Err(tokio::sync::mpsc::error::TrySendError::Full(_))));
316+
}
317+
}

src/builder.rs

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,204 @@ impl ScannerBuilder {
7777
.await
7878
}
7979
}
80+
81+
#[cfg(test)]
82+
mod tests {
83+
use super::*;
84+
use crate::callback::EventCallback;
85+
use alloy::{primitives::address, rpc::types::Log};
86+
use async_trait::async_trait;
87+
use std::sync::Arc;
88+
89+
struct MockCallback;
90+
91+
#[async_trait]
92+
impl EventCallback for MockCallback {
93+
async fn on_event(&self, _log: &Log) -> anyhow::Result<()> {
94+
Ok(())
95+
}
96+
}
97+
98+
const WS_URL: &str = "ws://localhost:8545";
99+
100+
#[test]
101+
fn test_builder_new_defaults() {
102+
let builder = ScannerBuilder::new(WS_URL);
103+
assert_eq!(builder.rpc_url, WS_URL);
104+
assert_eq!(builder.start_block, None);
105+
assert_eq!(builder.end_block, None);
106+
assert_eq!(builder.max_blocks_per_filter, 1000);
107+
assert!(builder.tracked_events.is_empty());
108+
}
109+
110+
#[test]
111+
fn test_builder_start_block() {
112+
let start_block = 100;
113+
let builder = ScannerBuilder::new(WS_URL).start_block(start_block);
114+
assert_eq!(builder.start_block, Some(start_block));
115+
}
116+
117+
#[test]
118+
fn test_builder_end_block() {
119+
let end_block = 500;
120+
let builder = ScannerBuilder::new(WS_URL).end_block(end_block);
121+
assert_eq!(builder.end_block, Some(end_block));
122+
}
123+
124+
#[test]
125+
fn test_builder_block_range() {
126+
let start_block = 100;
127+
let end_block = 500;
128+
let builder = ScannerBuilder::new(WS_URL).start_block(start_block).end_block(end_block);
129+
assert_eq!(builder.start_block, Some(start_block));
130+
assert_eq!(builder.end_block, Some(end_block));
131+
}
132+
133+
#[test]
134+
fn test_builder_max_blocks_per_filter() {
135+
let max_blocks = 5000;
136+
let builder = ScannerBuilder::new(WS_URL).max_blocks_per_filter(max_blocks);
137+
assert_eq!(builder.max_blocks_per_filter, max_blocks);
138+
}
139+
140+
#[test]
141+
fn test_builder_callback_config() {
142+
let max_attempts = 5;
143+
let delay_ms = 500;
144+
let config = CallbackConfig { max_attempts, delay_ms };
145+
146+
let builder = ScannerBuilder::new(WS_URL).callback_config(config.clone());
147+
148+
assert_eq!(builder.callback_config.max_attempts, max_attempts);
149+
assert_eq!(builder.callback_config.delay_ms, delay_ms);
150+
}
151+
152+
#[test]
153+
fn test_builder_default_callback_config() {
154+
let builder = ScannerBuilder::new(WS_URL);
155+
156+
assert_eq!(builder.callback_config.max_attempts, 3);
157+
assert_eq!(builder.callback_config.delay_ms, 200);
158+
}
159+
160+
#[test]
161+
fn test_builder_add_event_filter() {
162+
let addr = address!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266");
163+
let event = "Transfer(address,address,uint256)".to_string();
164+
let filter = EventFilter {
165+
contract_address: addr,
166+
event: event.clone(),
167+
callback: Arc::new(MockCallback),
168+
};
169+
let builder = ScannerBuilder::new(WS_URL).add_event_filter(filter.clone());
170+
171+
assert_eq!(builder.tracked_events.len(), 1);
172+
assert_eq!(builder.tracked_events[0].contract_address, addr);
173+
assert_eq!(builder.tracked_events[0].event, event);
174+
}
175+
176+
#[test]
177+
fn test_builder_add_multiple_event_filters() {
178+
let addr1 = address!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266");
179+
let event1 = "Transfer(address,address,uint256)".to_string();
180+
let addr2 = address!("70997970C51812dc3A010C7d01b50e0d17dc79C8");
181+
let event2 = "Approval(address,address,uint256)".to_string();
182+
183+
let filter1 = EventFilter {
184+
contract_address: addr1,
185+
event: event1.clone(),
186+
callback: Arc::new(MockCallback),
187+
};
188+
let filter2 = EventFilter {
189+
contract_address: addr2,
190+
event: event2.clone(),
191+
callback: Arc::new(MockCallback),
192+
};
193+
194+
let builder = ScannerBuilder::new(WS_URL)
195+
.add_event_filter(filter1.clone())
196+
.add_event_filter(filter2.clone());
197+
198+
assert_eq!(builder.tracked_events.len(), 2);
199+
for (i, expected_filter) in builder.tracked_events.iter().enumerate() {
200+
assert_eq!(
201+
builder.tracked_events[i].contract_address,
202+
expected_filter.contract_address
203+
);
204+
assert_eq!(builder.tracked_events[i].event, expected_filter.event);
205+
}
206+
}
207+
208+
#[test]
209+
fn test_builder_add_event_filters_batch() {
210+
let addr1 = address!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266");
211+
let event1 = "Transfer(address,address,uint256)".to_string();
212+
let addr2 = address!("70997970C51812dc3A010C7d01b50e0d17dc79C8");
213+
let event2 = "Approval(address,address,uint256)".to_string();
214+
let addr3 = address!("3C44CdDdB6a900fa2b585dd299e03d12FA4293BC");
215+
let event3 = "Mint(address,uint256)".to_string();
216+
217+
let filter_1 = EventFilter {
218+
contract_address: addr1,
219+
event: event1.clone(),
220+
callback: Arc::new(MockCallback),
221+
};
222+
let filter_2 = EventFilter {
223+
contract_address: addr2,
224+
event: event2.clone(),
225+
callback: Arc::new(MockCallback),
226+
};
227+
let filter_3 = EventFilter {
228+
contract_address: addr3,
229+
event: event3.clone(),
230+
callback: Arc::new(MockCallback),
231+
};
232+
233+
let filters = vec![filter_1.clone(), filter_2.clone(), filter_3.clone()];
234+
let builder = ScannerBuilder::new(WS_URL).add_event_filters(filters.clone());
235+
236+
assert_eq!(builder.tracked_events.len(), filters.len());
237+
238+
for (i, expected_filter) in filters.iter().enumerate() {
239+
assert_eq!(
240+
builder.tracked_events[i].contract_address,
241+
expected_filter.contract_address
242+
);
243+
assert_eq!(builder.tracked_events[i].event, expected_filter.event);
244+
}
245+
}
246+
247+
#[test]
248+
fn test_builder_chain_all_methods() {
249+
let start_block = 100;
250+
let end_block = 500;
251+
252+
let addr = address!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266");
253+
let event = "Transfer(address,address,uint256)".to_string();
254+
255+
let filter = EventFilter {
256+
contract_address: addr,
257+
event: event.clone(),
258+
callback: Arc::new(MockCallback),
259+
};
260+
261+
let max_attempts = 5;
262+
let delay_ms = 500;
263+
let config = CallbackConfig { max_attempts, delay_ms };
264+
265+
let max_blocks_per_filter = 2000;
266+
let builder = ScannerBuilder::new(WS_URL)
267+
.start_block(start_block)
268+
.end_block(end_block)
269+
.max_blocks_per_filter(max_blocks_per_filter)
270+
.add_event_filter(filter.clone())
271+
.callback_config(config.clone());
272+
273+
assert_eq!(builder.start_block, Some(start_block));
274+
assert_eq!(builder.end_block, Some(end_block));
275+
assert_eq!(builder.max_blocks_per_filter, max_blocks_per_filter);
276+
assert_eq!(builder.tracked_events.len(), 1);
277+
assert_eq!(builder.callback_config.max_attempts, max_attempts);
278+
assert_eq!(builder.callback_config.delay_ms, delay_ms);
279+
}
280+
}

0 commit comments

Comments
 (0)