Skip to content

Commit 6617055

Browse files
authored
test(runtime): add comprehensive tests for TokioContext (#18)
* 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. * 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) * 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 * 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. * fix: format memory.rs after inline format args change
1 parent 1e82622 commit 6617055

9 files changed

Lines changed: 188 additions & 19 deletions

File tree

crates/backend/src/http.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ impl HttpBackend {
8484
response
8585
.json()
8686
.await
87-
.map_err(|e| RoxyError::Internal(format!("failed to parse response: {}", e)))
87+
.map_err(|e| RoxyError::Internal(format!("failed to parse response: {e}")))
8888
}
8989
}
9090

crates/cache/src/compressed.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ pub struct CompressedCache<C> {
2727

2828
impl<C: Cache> CompressedCache<C> {
2929
/// Create a new compressed cache with default compression quality.
30+
#[must_use]
3031
pub fn new(inner: C) -> Self {
3132
Self::with_quality(inner, DEFAULT_QUALITY)
3233
}
@@ -36,6 +37,7 @@ impl<C: Cache> CompressedCache<C> {
3637
/// # Arguments
3738
/// * `inner` - The inner cache to wrap
3839
/// * `quality` - Brotli compression quality (0-11, where 11 is best compression)
40+
#[must_use]
3941
pub fn with_quality(inner: C, quality: u32) -> Self {
4042
Self { inner, quality: quality.min(11) }
4143
}
@@ -50,7 +52,7 @@ impl<C: Cache> CompressedCache<C> {
5052
self.quality,
5153
DEFAULT_LG_WINDOW_SIZE,
5254
);
53-
writer.write_all(data).map_err(|e| CacheError(format!("compression failed: {}", e)))?;
55+
writer.write_all(data).map_err(|e| CacheError(format!("compression failed: {e}")))?;
5456
}
5557
Ok(compressed)
5658
}
@@ -61,7 +63,7 @@ impl<C: Cache> CompressedCache<C> {
6163
let mut decompressor = brotli::Decompressor::new(data, 4096);
6264
decompressor
6365
.read_to_end(&mut decompressed)
64-
.map_err(|e| CacheError(format!("decompression failed: {}", e)))?;
66+
.map_err(|e| CacheError(format!("decompression failed: {e}")))?;
6567
Ok(decompressed)
6668
}
6769
}

crates/cache/src/memory.rs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,7 @@ impl std::fmt::Debug for MemoryCache {
3939

4040
impl Cache for MemoryCache {
4141
async fn get(&self, key: &str) -> Result<Option<Bytes>, CacheError> {
42-
let mut cache =
43-
self.cache.lock().map_err(|e| CacheError(format!("lock poisoned: {}", e)))?;
42+
let mut cache = self.cache.lock().map_err(|e| CacheError(format!("lock poisoned: {e}")))?;
4443

4544
if let Some(entry) = cache.get(key) {
4645
if entry.expires_at > Instant::now() {
@@ -53,8 +52,7 @@ impl Cache for MemoryCache {
5352
}
5453

5554
async fn put(&self, key: &str, value: Bytes, ttl: Duration) -> Result<(), CacheError> {
56-
let mut cache =
57-
self.cache.lock().map_err(|e| CacheError(format!("lock poisoned: {}", e)))?;
55+
let mut cache = self.cache.lock().map_err(|e| CacheError(format!("lock poisoned: {e}")))?;
5856

5957
let entry = CacheEntry { value, expires_at: Instant::now() + ttl };
6058

@@ -63,8 +61,7 @@ impl Cache for MemoryCache {
6361
}
6462

6563
async fn delete(&self, key: &str) -> Result<(), CacheError> {
66-
let mut cache =
67-
self.cache.lock().map_err(|e| CacheError(format!("lock poisoned: {}", e)))?;
64+
let mut cache = self.cache.lock().map_err(|e| CacheError(format!("lock poisoned: {e}")))?;
6865

6966
cache.pop(key);
7067
Ok(())

crates/cache/src/redis.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ impl RedisCache {
2626
/// Returns a `CacheError` if the URL is invalid or connection cannot be established.
2727
pub fn new(url: &str) -> Result<Self, CacheError> {
2828
let client =
29-
Client::open(url).map_err(|e| CacheError(format!("failed to create client: {}", e)))?;
29+
Client::open(url).map_err(|e| CacheError(format!("failed to create client: {e}")))?;
3030
Ok(Self { client })
3131
}
3232

@@ -35,7 +35,7 @@ impl RedisCache {
3535
self.client
3636
.get_multiplexed_async_connection()
3737
.await
38-
.map_err(|e| CacheError(format!("connection error: {}", e)))
38+
.map_err(|e| CacheError(format!("connection error: {e}")))
3939
}
4040
}
4141

@@ -44,7 +44,7 @@ impl Cache for RedisCache {
4444
let mut conn = self.get_connection().await?;
4545

4646
let result: Option<Vec<u8>> =
47-
conn.get(key).await.map_err(|e| CacheError(format!("get error: {}", e)))?;
47+
conn.get(key).await.map_err(|e| CacheError(format!("get error: {e}")))?;
4848

4949
Ok(result.map(Bytes::from))
5050
}
@@ -58,15 +58,15 @@ impl Cache for RedisCache {
5858
// Use SETEX for atomic set with expiration
5959
conn.set_ex::<_, _, ()>(key, value.as_ref(), ttl_secs)
6060
.await
61-
.map_err(|e| CacheError(format!("put error: {}", e)))?;
61+
.map_err(|e| CacheError(format!("put error: {e}")))?;
6262

6363
Ok(())
6464
}
6565

6666
async fn delete(&self, key: &str) -> Result<(), CacheError> {
6767
let mut conn = self.get_connection().await?;
6868

69-
conn.del::<_, ()>(key).await.map_err(|e| CacheError(format!("delete error: {}", e)))?;
69+
conn.del::<_, ()>(key).await.map_err(|e| CacheError(format!("delete error: {e}")))?;
7070

7171
Ok(())
7272
}

crates/cache/src/rpc_cache.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ impl<C: Cache> RpcCache<C> {
3636
///
3737
/// This initializes the cache with sensible default policies for common
3838
/// Ethereum JSON-RPC methods.
39+
#[must_use]
3940
pub fn new(inner: C) -> Self {
4041
let mut policies = HashMap::new();
4142

crates/cli/src/server.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ pub async fn run_server(app: roxy_server::Router, config: &RoxyConfig) -> Result
5353
let addr = format!("{}:{}", config.server.host, config.server.port);
5454
let listener = tokio::net::TcpListener::bind(&addr)
5555
.await
56-
.wrap_err_with(|| format!("failed to bind to {}", addr))?;
56+
.wrap_err_with(|| format!("failed to bind to {addr}"))?;
5757

5858
info!(address = %addr, "Roxy RPC proxy listening");
5959

@@ -63,7 +63,7 @@ pub async fn run_server(app: roxy_server::Router, config: &RoxyConfig) -> Result
6363
let metrics_addr = format!("{}:{}", config.metrics.host, config.metrics.port);
6464
let metrics_listener = tokio::net::TcpListener::bind(&metrics_addr)
6565
.await
66-
.wrap_err_with(|| format!("failed to bind metrics server to {}", metrics_addr))?;
66+
.wrap_err_with(|| format!("failed to bind metrics server to {metrics_addr}"))?;
6767

6868
info!(address = %metrics_addr, "Metrics server listening");
6969

crates/runtime/src/tokio_runtime.rs

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,172 @@ impl Clock for TokioContext {
8787
tokio::time::sleep(duration).await;
8888
}
8989
}
90+
91+
#[cfg(test)]
92+
mod tests {
93+
use super::*;
94+
95+
#[test]
96+
fn test_new_creates_context() {
97+
let ctx = TokioContext::new();
98+
assert!(ctx.label.is_empty());
99+
}
100+
101+
#[test]
102+
fn test_default_creates_context() {
103+
let ctx = TokioContext::default();
104+
assert!(ctx.label.is_empty());
105+
}
106+
107+
#[tokio::test]
108+
async fn test_spawn_returns_result() {
109+
let ctx = TokioContext::new();
110+
let handle = ctx.spawn(|_| async { 42 });
111+
let result = handle.join().await.unwrap();
112+
assert_eq!(result, 42);
113+
}
114+
115+
#[tokio::test]
116+
async fn test_spawn_receives_context() {
117+
let ctx = TokioContext::new().with_label("parent");
118+
let handle = ctx.spawn(|c| async move { c.label.clone() });
119+
let result = handle.join().await.unwrap();
120+
assert_eq!(result, "parent");
121+
}
122+
123+
#[tokio::test]
124+
async fn test_spawn_multiple_tasks() {
125+
let ctx = TokioContext::new();
126+
127+
let h1 = ctx.spawn(|_| async { 1 });
128+
let h2 = ctx.spawn(|_| async { 2 });
129+
let h3 = ctx.spawn(|_| async { 3 });
130+
131+
let r1 = h1.join().await.unwrap();
132+
let r2 = h2.join().await.unwrap();
133+
let r3 = h3.join().await.unwrap();
134+
135+
assert_eq!(r1 + r2 + r3, 6);
136+
}
137+
138+
#[test]
139+
fn test_with_label_single() {
140+
let ctx = TokioContext::new();
141+
let labeled = ctx.with_label("test");
142+
assert_eq!(labeled.label, "test");
143+
}
144+
145+
#[test]
146+
fn test_with_label_nested() {
147+
let ctx = TokioContext::new();
148+
let first = ctx.with_label("parent");
149+
let second = first.with_label("child");
150+
assert_eq!(second.label, "parent:child");
151+
}
152+
153+
#[test]
154+
fn test_with_label_deeply_nested() {
155+
let ctx = TokioContext::new();
156+
let labeled = ctx.with_label("a").with_label("b").with_label("c");
157+
assert_eq!(labeled.label, "a:b:c");
158+
}
159+
160+
#[test]
161+
fn test_with_label_preserves_stop_channel() {
162+
let ctx = TokioContext::new();
163+
let labeled = ctx.with_label("test");
164+
assert!(Arc::ptr_eq(&ctx.stop_tx, &labeled.stop_tx));
165+
}
166+
167+
#[tokio::test]
168+
async fn test_stop_signals_stopped() {
169+
let ctx = TokioContext::new();
170+
let ctx_clone = ctx.clone();
171+
172+
let handle = ctx.spawn(|c| async move {
173+
match c.stopped().await {
174+
Signal::Closed(code) => code,
175+
Signal::Open => -999,
176+
}
177+
});
178+
179+
ctx_clone.stop(42, None).await;
180+
181+
let result = handle.join().await.unwrap();
182+
assert_eq!(result, 42);
183+
}
184+
185+
#[tokio::test]
186+
async fn test_stop_with_different_codes() {
187+
for code in [0, 1, -1, 100, i32::MAX, i32::MIN] {
188+
let ctx = TokioContext::new();
189+
let ctx_clone = ctx.clone();
190+
191+
let handle = ctx.spawn(|c| async move {
192+
match c.stopped().await {
193+
Signal::Closed(c) => c,
194+
Signal::Open => -999,
195+
}
196+
});
197+
198+
ctx_clone.stop(code, None).await;
199+
200+
let result = handle.join().await.unwrap();
201+
assert_eq!(result, code);
202+
}
203+
}
204+
205+
#[tokio::test]
206+
async fn test_stop_propagates_to_labeled_context() {
207+
let ctx = TokioContext::new();
208+
let labeled = ctx.with_label("child");
209+
210+
let handle = labeled.spawn(|c| async move {
211+
match c.stopped().await {
212+
Signal::Closed(code) => code,
213+
Signal::Open => -999,
214+
}
215+
});
216+
217+
ctx.stop(123, None).await;
218+
219+
let result = handle.join().await.unwrap();
220+
assert_eq!(result, 123);
221+
}
222+
223+
#[tokio::test]
224+
async fn test_now_returns_increasing_time() {
225+
let ctx = TokioContext::new();
226+
let t1 = ctx.now();
227+
tokio::time::sleep(Duration::from_millis(10)).await;
228+
let t2 = ctx.now();
229+
assert!(t2 > t1);
230+
}
231+
232+
#[tokio::test]
233+
async fn test_sleep_waits_minimum_duration() {
234+
let ctx = TokioContext::new();
235+
let sleep_duration = Duration::from_millis(50);
236+
237+
let start = Instant::now();
238+
ctx.sleep(sleep_duration).await;
239+
let elapsed = start.elapsed();
240+
241+
assert!(elapsed >= sleep_duration);
242+
}
243+
244+
#[tokio::test]
245+
async fn test_clone_shares_state() {
246+
let ctx1 = TokioContext::new();
247+
let ctx2 = ctx1.clone();
248+
assert!(Arc::ptr_eq(&ctx1.stop_tx, &ctx2.stop_tx));
249+
}
250+
251+
#[tokio::test]
252+
async fn test_debug_impl() {
253+
let ctx = TokioContext::new().with_label("test");
254+
let debug_str = format!("{:?}", ctx);
255+
assert!(debug_str.contains("TokioContext"));
256+
assert!(debug_str.contains("test"));
257+
}
258+
}

crates/server/src/websocket.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -527,7 +527,7 @@ async fn handle_message(
527527
Ok(req) => req,
528528
Err(e) => {
529529
let response =
530-
WsResponse::error(serde_json::Value::Null, -32700, format!("Parse error: {}", e));
530+
WsResponse::error(serde_json::Value::Null, -32700, format!("Parse error: {e}"));
531531
return serde_json::to_string(&response).ok();
532532
}
533533
};
@@ -579,7 +579,7 @@ async fn handle_subscribe(
579579
return WsResponse::error(
580580
request.id.clone(),
581581
-32602,
582-
format!("Unknown subscription type: {}", subscription_type),
582+
format!("Unknown subscription type: {subscription_type}"),
583583
);
584584
}
585585
}

crates/types/src/error.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ pub enum RoxyError {
5454
}
5555

5656
impl RoxyError {
57-
/// Convert to an alloy ErrorPayload for JSON-RPC responses.
57+
/// Convert to an alloy [`ErrorPayload`] for JSON-RPC responses.
5858
#[must_use]
5959
pub fn to_error_payload(&self) -> ErrorPayload {
6060
match self {

0 commit comments

Comments
 (0)