From 68b4b4c7230bb4d2ef6958317c8d5d066715feef Mon Sep 17 00:00:00 2001 From: Andreas Bigger Date: Sun, 1 Feb 2026 18:20:14 -0500 Subject: [PATCH 1/4] style: use inline format args for improved readability Replace format!("{}", var) with format!("{var}") across cache, backend, server, and cli crates. This follows the Rust 2021 edition style and improves code readability. --- crates/types/src/error.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/types/src/error.rs b/crates/types/src/error.rs index d18fc87..a3bd393 100644 --- a/crates/types/src/error.rs +++ b/crates/types/src/error.rs @@ -54,7 +54,7 @@ pub enum RoxyError { } impl RoxyError { - /// Convert to an alloy ErrorPayload for JSON-RPC responses. + /// Convert to an alloy [`ErrorPayload`] for JSON-RPC responses. #[must_use] pub fn to_error_payload(&self) -> ErrorPayload { match self { From 2888d257d60456ba882088bf9b010adab9f60ddc Mon Sep 17 00:00:00 2001 From: Andreas Bigger Date: Sun, 1 Feb 2026 18:20:42 -0500 Subject: [PATCH 2/4] docs: add # Errors sections and fix doc_markdown warnings - Add backticks to ErrorPayload in error.rs doc (clippy doc_markdown) - Inline format args in to_error_payload() (clippy uninlined_format_args) - Add # Errors documentation to Decode and Encode trait methods (clippy missing_errors_doc) - Add # Errors documentation to Handle::join() (clippy missing_errors_doc) --- crates/traits/src/codec.rs | 18 ++++++++++++++++++ crates/traits/src/runtime.rs | 4 ++++ 2 files changed, 22 insertions(+) diff --git a/crates/traits/src/codec.rs b/crates/traits/src/codec.rs index 32137f4..26dd0c6 100644 --- a/crates/traits/src/codec.rs +++ b/crates/traits/src/codec.rs @@ -110,9 +110,19 @@ impl CodecConfig for DefaultCodecConfig { /// Trait for decoding bytes into a type with configuration. pub trait Decode: Sized { /// Decode from bytes using the given configuration. + /// + /// # Errors + /// + /// Returns [`CodecError`] if decoding fails due to invalid data, + /// size limits exceeded, or other configuration violations. fn decode(bytes: &[u8], config: &C) -> Result; /// Decode from Bytes using the given configuration. + /// + /// # Errors + /// + /// Returns [`CodecError`] if decoding fails due to invalid data, + /// size limits exceeded, or other configuration violations. fn decode_bytes(bytes: &Bytes, config: &C) -> Result { Self::decode(bytes.as_ref(), config) } @@ -121,9 +131,17 @@ pub trait Decode: Sized { /// Trait for encoding a type into bytes. pub trait Encode { /// Encode into bytes. + /// + /// # Errors + /// + /// Returns [`CodecError`] if encoding fails. fn encode(&self) -> Result; /// Encode into a `Vec`. + /// + /// # Errors + /// + /// Returns [`CodecError`] if encoding fails. fn encode_vec(&self) -> Result, CodecError> { self.encode().map(|b| b.to_vec()) } diff --git a/crates/traits/src/runtime.rs b/crates/traits/src/runtime.rs index d14537d..253b9fb 100644 --- a/crates/traits/src/runtime.rs +++ b/crates/traits/src/runtime.rs @@ -22,6 +22,10 @@ impl Handle { } /// Wait for the task to complete. + /// + /// # Errors + /// + /// Returns [`JoinError`] if the task was cancelled or panicked. pub async fn join(self) -> Result { self.inner.await.map_err(JoinError) } From 0fec382b2027e9cb593203fe1092d95907616bfd Mon Sep 17 00:00:00 2001 From: Andreas Bigger Date: Sun, 1 Feb 2026 18:21:17 -0500 Subject: [PATCH 3/4] test(runtime): add comprehensive tests for TokioContext Add 16 tests covering the TokioContext implementation: - Constructor tests (new, default) - Spawner trait (spawn, with_label, stop, stopped) - Clock trait (now, sleep) - Clone/state sharing behavior - Debug implementation --- crates/runtime/src/tokio_runtime.rs | 169 ++++++++++++++++++++++++++++ 1 file changed, 169 insertions(+) diff --git a/crates/runtime/src/tokio_runtime.rs b/crates/runtime/src/tokio_runtime.rs index 4a35496..1d90110 100644 --- a/crates/runtime/src/tokio_runtime.rs +++ b/crates/runtime/src/tokio_runtime.rs @@ -87,3 +87,172 @@ impl Clock for TokioContext { tokio::time::sleep(duration).await; } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_new_creates_context() { + let ctx = TokioContext::new(); + assert!(ctx.label.is_empty()); + } + + #[test] + fn test_default_creates_context() { + let ctx = TokioContext::default(); + assert!(ctx.label.is_empty()); + } + + #[tokio::test] + async fn test_spawn_returns_result() { + let ctx = TokioContext::new(); + let handle = ctx.spawn(|_| async { 42 }); + let result = handle.join().await.unwrap(); + assert_eq!(result, 42); + } + + #[tokio::test] + async fn test_spawn_receives_context() { + let ctx = TokioContext::new().with_label("parent"); + let handle = ctx.spawn(|c| async move { c.label.clone() }); + let result = handle.join().await.unwrap(); + assert_eq!(result, "parent"); + } + + #[tokio::test] + async fn test_spawn_multiple_tasks() { + let ctx = TokioContext::new(); + + let h1 = ctx.spawn(|_| async { 1 }); + let h2 = ctx.spawn(|_| async { 2 }); + let h3 = ctx.spawn(|_| async { 3 }); + + let r1 = h1.join().await.unwrap(); + let r2 = h2.join().await.unwrap(); + let r3 = h3.join().await.unwrap(); + + assert_eq!(r1 + r2 + r3, 6); + } + + #[test] + fn test_with_label_single() { + let ctx = TokioContext::new(); + let labeled = ctx.with_label("test"); + assert_eq!(labeled.label, "test"); + } + + #[test] + fn test_with_label_nested() { + let ctx = TokioContext::new(); + let first = ctx.with_label("parent"); + let second = first.with_label("child"); + assert_eq!(second.label, "parent:child"); + } + + #[test] + fn test_with_label_deeply_nested() { + let ctx = TokioContext::new(); + let labeled = ctx.with_label("a").with_label("b").with_label("c"); + assert_eq!(labeled.label, "a:b:c"); + } + + #[test] + fn test_with_label_preserves_stop_channel() { + let ctx = TokioContext::new(); + let labeled = ctx.with_label("test"); + assert!(Arc::ptr_eq(&ctx.stop_tx, &labeled.stop_tx)); + } + + #[tokio::test] + async fn test_stop_signals_stopped() { + let ctx = TokioContext::new(); + let ctx_clone = ctx.clone(); + + let handle = ctx.spawn(|c| async move { + match c.stopped().await { + Signal::Closed(code) => code, + Signal::Open => -999, + } + }); + + ctx_clone.stop(42, None).await; + + let result = handle.join().await.unwrap(); + assert_eq!(result, 42); + } + + #[tokio::test] + async fn test_stop_with_different_codes() { + for code in [0, 1, -1, 100, i32::MAX, i32::MIN] { + let ctx = TokioContext::new(); + let ctx_clone = ctx.clone(); + + let handle = ctx.spawn(|c| async move { + match c.stopped().await { + Signal::Closed(c) => c, + Signal::Open => -999, + } + }); + + ctx_clone.stop(code, None).await; + + let result = handle.join().await.unwrap(); + assert_eq!(result, code); + } + } + + #[tokio::test] + async fn test_stop_propagates_to_labeled_context() { + let ctx = TokioContext::new(); + let labeled = ctx.with_label("child"); + + let handle = labeled.spawn(|c| async move { + match c.stopped().await { + Signal::Closed(code) => code, + Signal::Open => -999, + } + }); + + ctx.stop(123, None).await; + + let result = handle.join().await.unwrap(); + assert_eq!(result, 123); + } + + #[tokio::test] + async fn test_now_returns_increasing_time() { + let ctx = TokioContext::new(); + let t1 = ctx.now(); + tokio::time::sleep(Duration::from_millis(10)).await; + let t2 = ctx.now(); + assert!(t2 > t1); + } + + #[tokio::test] + async fn test_sleep_waits_minimum_duration() { + let ctx = TokioContext::new(); + let sleep_duration = Duration::from_millis(50); + + let start = Instant::now(); + ctx.sleep(sleep_duration).await; + let elapsed = start.elapsed(); + + assert!(elapsed >= sleep_duration); + } + + #[tokio::test] + async fn test_clone_shares_state() { + let ctx1 = TokioContext::new(); + let ctx2 = ctx1.clone(); + assert!(Arc::ptr_eq(&ctx1.stop_tx, &ctx2.stop_tx)); + } + + #[tokio::test] + async fn test_debug_impl() { + let ctx = TokioContext::new().with_label("test"); + let debug_str = format!("{:?}", ctx); + assert!(debug_str.contains("TokioContext")); + assert!(debug_str.contains("test")); + } +} From ea7ce592bfe5e6edcad08d296b972a3b615fd488 Mon Sep 17 00:00:00 2001 From: Andreas Bigger Date: Sun, 1 Feb 2026 18:39:22 -0500 Subject: [PATCH 4/4] style: use inline format args for improved readability Replace format!("{}", var) with format!("{var}") in cache, backend, server, and cli crates. This follows the Rust 2021 edition style. --- crates/backend/src/http.rs | 2 +- crates/cache/src/compressed.rs | 6 ++++-- crates/cache/src/memory.rs | 6 +++--- crates/cache/src/redis.rs | 10 +++++----- crates/cache/src/rpc_cache.rs | 1 + crates/cli/src/server.rs | 4 ++-- crates/server/src/websocket.rs | 4 ++-- 7 files changed, 18 insertions(+), 15 deletions(-) diff --git a/crates/backend/src/http.rs b/crates/backend/src/http.rs index 009d3ab..3d6d3c6 100644 --- a/crates/backend/src/http.rs +++ b/crates/backend/src/http.rs @@ -84,7 +84,7 @@ impl HttpBackend { response .json() .await - .map_err(|e| RoxyError::Internal(format!("failed to parse response: {}", e))) + .map_err(|e| RoxyError::Internal(format!("failed to parse response: {e}"))) } } diff --git a/crates/cache/src/compressed.rs b/crates/cache/src/compressed.rs index 84dfb3c..e919927 100644 --- a/crates/cache/src/compressed.rs +++ b/crates/cache/src/compressed.rs @@ -27,6 +27,7 @@ pub struct CompressedCache { impl CompressedCache { /// Create a new compressed cache with default compression quality. + #[must_use] pub fn new(inner: C) -> Self { Self::with_quality(inner, DEFAULT_QUALITY) } @@ -36,6 +37,7 @@ impl CompressedCache { /// # Arguments /// * `inner` - The inner cache to wrap /// * `quality` - Brotli compression quality (0-11, where 11 is best compression) + #[must_use] pub fn with_quality(inner: C, quality: u32) -> Self { Self { inner, quality: quality.min(11) } } @@ -50,7 +52,7 @@ impl CompressedCache { self.quality, DEFAULT_LG_WINDOW_SIZE, ); - writer.write_all(data).map_err(|e| CacheError(format!("compression failed: {}", e)))?; + writer.write_all(data).map_err(|e| CacheError(format!("compression failed: {e}")))?; } Ok(compressed) } @@ -61,7 +63,7 @@ impl CompressedCache { let mut decompressor = brotli::Decompressor::new(data, 4096); decompressor .read_to_end(&mut decompressed) - .map_err(|e| CacheError(format!("decompression failed: {}", e)))?; + .map_err(|e| CacheError(format!("decompression failed: {e}")))?; Ok(decompressed) } } diff --git a/crates/cache/src/memory.rs b/crates/cache/src/memory.rs index 5577987..d073da7 100644 --- a/crates/cache/src/memory.rs +++ b/crates/cache/src/memory.rs @@ -40,7 +40,7 @@ impl std::fmt::Debug for MemoryCache { impl Cache for MemoryCache { async fn get(&self, key: &str) -> Result, CacheError> { let mut cache = - self.cache.lock().map_err(|e| CacheError(format!("lock poisoned: {}", e)))?; + self.cache.lock().map_err(|e| CacheError(format!("lock poisoned: {e}")))?; if let Some(entry) = cache.get(key) { if entry.expires_at > Instant::now() { @@ -54,7 +54,7 @@ impl Cache for MemoryCache { async fn put(&self, key: &str, value: Bytes, ttl: Duration) -> Result<(), CacheError> { let mut cache = - self.cache.lock().map_err(|e| CacheError(format!("lock poisoned: {}", e)))?; + self.cache.lock().map_err(|e| CacheError(format!("lock poisoned: {e}")))?; let entry = CacheEntry { value, expires_at: Instant::now() + ttl }; @@ -64,7 +64,7 @@ impl Cache for MemoryCache { async fn delete(&self, key: &str) -> Result<(), CacheError> { let mut cache = - self.cache.lock().map_err(|e| CacheError(format!("lock poisoned: {}", e)))?; + self.cache.lock().map_err(|e| CacheError(format!("lock poisoned: {e}")))?; cache.pop(key); Ok(()) diff --git a/crates/cache/src/redis.rs b/crates/cache/src/redis.rs index 9a4c2c3..1211785 100644 --- a/crates/cache/src/redis.rs +++ b/crates/cache/src/redis.rs @@ -26,7 +26,7 @@ impl RedisCache { /// Returns a `CacheError` if the URL is invalid or connection cannot be established. pub fn new(url: &str) -> Result { let client = - Client::open(url).map_err(|e| CacheError(format!("failed to create client: {}", e)))?; + Client::open(url).map_err(|e| CacheError(format!("failed to create client: {e}")))?; Ok(Self { client }) } @@ -35,7 +35,7 @@ impl RedisCache { self.client .get_multiplexed_async_connection() .await - .map_err(|e| CacheError(format!("connection error: {}", e))) + .map_err(|e| CacheError(format!("connection error: {e}"))) } } @@ -44,7 +44,7 @@ impl Cache for RedisCache { let mut conn = self.get_connection().await?; let result: Option> = - conn.get(key).await.map_err(|e| CacheError(format!("get error: {}", e)))?; + conn.get(key).await.map_err(|e| CacheError(format!("get error: {e}")))?; Ok(result.map(Bytes::from)) } @@ -58,7 +58,7 @@ impl Cache for RedisCache { // Use SETEX for atomic set with expiration conn.set_ex::<_, _, ()>(key, value.as_ref(), ttl_secs) .await - .map_err(|e| CacheError(format!("put error: {}", e)))?; + .map_err(|e| CacheError(format!("put error: {e}")))?; Ok(()) } @@ -66,7 +66,7 @@ impl Cache for RedisCache { async fn delete(&self, key: &str) -> Result<(), CacheError> { let mut conn = self.get_connection().await?; - conn.del::<_, ()>(key).await.map_err(|e| CacheError(format!("delete error: {}", e)))?; + conn.del::<_, ()>(key).await.map_err(|e| CacheError(format!("delete error: {e}")))?; Ok(()) } diff --git a/crates/cache/src/rpc_cache.rs b/crates/cache/src/rpc_cache.rs index f7ca6be..2d4ce9a 100644 --- a/crates/cache/src/rpc_cache.rs +++ b/crates/cache/src/rpc_cache.rs @@ -36,6 +36,7 @@ impl RpcCache { /// /// This initializes the cache with sensible default policies for common /// Ethereum JSON-RPC methods. + #[must_use] pub fn new(inner: C) -> Self { let mut policies = HashMap::new(); diff --git a/crates/cli/src/server.rs b/crates/cli/src/server.rs index 21df150..bc3510b 100644 --- a/crates/cli/src/server.rs +++ b/crates/cli/src/server.rs @@ -53,7 +53,7 @@ pub async fn run_server(app: roxy_server::Router, config: &RoxyConfig) -> Result let addr = format!("{}:{}", config.server.host, config.server.port); let listener = tokio::net::TcpListener::bind(&addr) .await - .wrap_err_with(|| format!("failed to bind to {}", addr))?; + .wrap_err_with(|| format!("failed to bind to {addr}"))?; info!(address = %addr, "Roxy RPC proxy listening"); @@ -63,7 +63,7 @@ pub async fn run_server(app: roxy_server::Router, config: &RoxyConfig) -> Result let metrics_addr = format!("{}:{}", config.metrics.host, config.metrics.port); let metrics_listener = tokio::net::TcpListener::bind(&metrics_addr) .await - .wrap_err_with(|| format!("failed to bind metrics server to {}", metrics_addr))?; + .wrap_err_with(|| format!("failed to bind metrics server to {metrics_addr}"))?; info!(address = %metrics_addr, "Metrics server listening"); diff --git a/crates/server/src/websocket.rs b/crates/server/src/websocket.rs index e45325b..b2fa29d 100644 --- a/crates/server/src/websocket.rs +++ b/crates/server/src/websocket.rs @@ -527,7 +527,7 @@ async fn handle_message( Ok(req) => req, Err(e) => { let response = - WsResponse::error(serde_json::Value::Null, -32700, format!("Parse error: {}", e)); + WsResponse::error(serde_json::Value::Null, -32700, format!("Parse error: {e}")); return serde_json::to_string(&response).ok(); } }; @@ -579,7 +579,7 @@ async fn handle_subscribe( return WsResponse::error( request.id.clone(), -32602, - format!("Unknown subscription type: {}", subscription_type), + format!("Unknown subscription type: {subscription_type}"), ); } }