/home/noah/src/trueno/src/brick/connection.rs
Line | Count | Source |
1 | | //! Connection Management |
2 | | //! |
3 | | //! AWP-06: Managed connections with TTL, idle timeout, and health tracking. |
4 | | //! AWP-10: Keep-Alive normalization for HTTP connections. |
5 | | //! AWP-12: Bitflags connection state for efficient state tracking. |
6 | | |
7 | | use std::time::{Duration, Instant}; |
8 | | |
9 | | // ---------------------------------------------------------------------------- |
10 | | // AWP-06: Connection TTL + Health Check |
11 | | // ---------------------------------------------------------------------------- |
12 | | |
13 | | /// Connection with TTL and health tracking. |
14 | | #[derive(Debug)] |
15 | | pub struct ManagedConnection<T> { |
16 | | /// The underlying connection |
17 | | inner: T, |
18 | | /// When the connection was created |
19 | | created_at: Instant, |
20 | | /// When the connection was last used |
21 | | last_used: Instant, |
22 | | /// Maximum lifetime (TTL) |
23 | | max_lifetime: Duration, |
24 | | /// Maximum idle time |
25 | | max_idle: Duration, |
26 | | /// Health check failures |
27 | | health_failures: usize, |
28 | | } |
29 | | |
30 | | impl<T> ManagedConnection<T> { |
31 | | /// Create a new managed connection. |
32 | 0 | pub fn new(inner: T, max_lifetime: Duration, max_idle: Duration) -> Self { |
33 | 0 | let now = Instant::now(); |
34 | 0 | Self { |
35 | 0 | inner, |
36 | 0 | created_at: now, |
37 | 0 | last_used: now, |
38 | 0 | max_lifetime, |
39 | 0 | max_idle, |
40 | 0 | health_failures: 0, |
41 | 0 | } |
42 | 0 | } |
43 | | |
44 | | /// Check if the connection is still valid. |
45 | | #[must_use] |
46 | 0 | pub fn is_valid(&self) -> bool { |
47 | 0 | let now = Instant::now(); |
48 | 0 | let not_expired = now.duration_since(self.created_at) < self.max_lifetime; |
49 | 0 | let not_idle = now.duration_since(self.last_used) < self.max_idle; |
50 | 0 | let healthy = self.health_failures < 3; |
51 | 0 | not_expired && not_idle && healthy |
52 | 0 | } |
53 | | |
54 | | /// Check if the connection has expired (TTL exceeded). |
55 | | #[must_use] |
56 | 0 | pub fn is_expired(&self) -> bool { |
57 | 0 | self.created_at.elapsed() >= self.max_lifetime |
58 | 0 | } |
59 | | |
60 | | /// Check if the connection is idle. |
61 | | #[must_use] |
62 | 0 | pub fn is_idle(&self) -> bool { |
63 | 0 | self.last_used.elapsed() >= self.max_idle |
64 | 0 | } |
65 | | |
66 | | /// Mark the connection as used. |
67 | 0 | pub fn touch(&mut self) { |
68 | 0 | self.last_used = Instant::now(); |
69 | 0 | } |
70 | | |
71 | | /// Record a health check failure. |
72 | 0 | pub fn record_health_failure(&mut self) { |
73 | 0 | self.health_failures += 1; |
74 | 0 | } |
75 | | |
76 | | /// Reset health failure count. |
77 | 0 | pub fn reset_health(&mut self) { |
78 | 0 | self.health_failures = 0; |
79 | 0 | } |
80 | | |
81 | | /// Get the underlying connection. |
82 | 0 | pub fn inner(&self) -> &T { |
83 | 0 | &self.inner |
84 | 0 | } |
85 | | |
86 | | /// Get mutable access to the underlying connection. |
87 | 0 | pub fn inner_mut(&mut self) -> &mut T { |
88 | 0 | &mut self.inner |
89 | 0 | } |
90 | | |
91 | | /// Consume and return the underlying connection. |
92 | 0 | pub fn into_inner(self) -> T { |
93 | 0 | self.inner |
94 | 0 | } |
95 | | |
96 | | /// Get connection age. |
97 | | #[must_use] |
98 | 0 | pub fn age(&self) -> Duration { |
99 | 0 | self.created_at.elapsed() |
100 | 0 | } |
101 | | |
102 | | /// Get idle time. |
103 | | #[must_use] |
104 | 0 | pub fn idle_time(&self) -> Duration { |
105 | 0 | self.last_used.elapsed() |
106 | 0 | } |
107 | | |
108 | | /// Get health failure count (for testing/diagnostics). |
109 | | #[must_use] |
110 | 0 | pub fn health_failures(&self) -> usize { |
111 | 0 | self.health_failures |
112 | 0 | } |
113 | | } |
114 | | |
115 | | // ---------------------------------------------------------------------------- |
116 | | // AWP-10: Keep-Alive Normalization |
117 | | // ---------------------------------------------------------------------------- |
118 | | |
119 | | /// Normalized keep-alive configuration. |
120 | | /// |
121 | | /// Canonicalizes various keep-alive settings into a standard form. |
122 | | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
123 | | pub struct KeepAliveConfig { |
124 | | /// Whether keep-alive is enabled |
125 | | pub enabled: bool, |
126 | | /// Timeout duration in seconds |
127 | | pub timeout_secs: u32, |
128 | | /// Maximum number of requests per connection |
129 | | pub max_requests: u32, |
130 | | } |
131 | | |
132 | | impl KeepAliveConfig { |
133 | | /// Create with default values. |
134 | 0 | pub fn new() -> Self { |
135 | 0 | Self { |
136 | 0 | enabled: true, |
137 | 0 | timeout_secs: 60, |
138 | 0 | max_requests: 100, |
139 | 0 | } |
140 | 0 | } |
141 | | |
142 | | /// Disabled keep-alive. |
143 | 0 | pub fn disabled() -> Self { |
144 | 0 | Self { |
145 | 0 | enabled: false, |
146 | 0 | timeout_secs: 0, |
147 | 0 | max_requests: 0, |
148 | 0 | } |
149 | 0 | } |
150 | | |
151 | | /// Parse from HTTP header value (e.g., "timeout=5, max=100"). |
152 | 0 | pub fn from_header(header: &str) -> Self { |
153 | 0 | let mut config = Self::new(); |
154 | | |
155 | 0 | for part in header.split(',') { |
156 | 0 | let part = part.trim(); |
157 | 0 | if let Some((key, val)) = part.split_once('=') { |
158 | 0 | let key = key.trim().to_lowercase(); |
159 | 0 | let val = val.trim(); |
160 | | |
161 | 0 | match key.as_str() { |
162 | 0 | "timeout" => { |
163 | 0 | if let Ok(t) = val.parse() { |
164 | 0 | config.timeout_secs = t; |
165 | 0 | } |
166 | | } |
167 | 0 | "max" => { |
168 | 0 | if let Ok(m) = val.parse() { |
169 | 0 | config.max_requests = m; |
170 | 0 | } |
171 | | } |
172 | 0 | _ => {} |
173 | | } |
174 | 0 | } |
175 | | } |
176 | | |
177 | 0 | config |
178 | 0 | } |
179 | | |
180 | | /// Check if connection should be kept alive after n requests. |
181 | | #[must_use] |
182 | 0 | pub fn should_keep_alive(&self, request_count: u32) -> bool { |
183 | 0 | self.enabled && request_count < self.max_requests |
184 | 0 | } |
185 | | } |
186 | | |
187 | | impl Default for KeepAliveConfig { |
188 | 0 | fn default() -> Self { |
189 | 0 | Self::new() |
190 | 0 | } |
191 | | } |
192 | | |
193 | | // ---------------------------------------------------------------------------- |
194 | | // AWP-12: Bitflags Connection State |
195 | | // ---------------------------------------------------------------------------- |
196 | | |
197 | | /// Compact connection state using bitflags. |
198 | | /// |
199 | | /// Efficiently represents multiple boolean states in a single byte. |
200 | | #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] |
201 | | pub struct ConnectionState(u8); |
202 | | |
203 | | impl ConnectionState { |
204 | | /// Connection is open |
205 | | pub const OPEN: u8 = 0b0000_0001; |
206 | | /// Connection is readable |
207 | | pub const READABLE: u8 = 0b0000_0010; |
208 | | /// Connection is writable |
209 | | pub const WRITABLE: u8 = 0b0000_0100; |
210 | | /// Connection has pending data |
211 | | pub const HAS_PENDING: u8 = 0b0000_1000; |
212 | | /// Connection is in keep-alive mode |
213 | | pub const KEEP_ALIVE: u8 = 0b0001_0000; |
214 | | /// Connection upgrade requested (e.g., WebSocket) |
215 | | pub const UPGRADE: u8 = 0b0010_0000; |
216 | | /// Connection is closing |
217 | | pub const CLOSING: u8 = 0b0100_0000; |
218 | | /// Connection has error |
219 | | pub const ERROR: u8 = 0b1000_0000; |
220 | | |
221 | | /// Create new state with no flags set. |
222 | | #[must_use] |
223 | 0 | pub fn new() -> Self { |
224 | 0 | Self(0) |
225 | 0 | } |
226 | | |
227 | | /// Create state with initial open + writable. |
228 | | #[must_use] |
229 | 0 | pub fn open_connection() -> Self { |
230 | 0 | Self(Self::OPEN | Self::WRITABLE) |
231 | 0 | } |
232 | | |
233 | | /// Set a flag. |
234 | 0 | pub fn set(&mut self, flag: u8) { |
235 | 0 | self.0 |= flag; |
236 | 0 | } |
237 | | |
238 | | /// Clear a flag. |
239 | 0 | pub fn clear(&mut self, flag: u8) { |
240 | 0 | self.0 &= !flag; |
241 | 0 | } |
242 | | |
243 | | /// Check if flag is set. |
244 | | #[must_use] |
245 | 0 | pub fn is_set(&self, flag: u8) -> bool { |
246 | 0 | self.0 & flag != 0 |
247 | 0 | } |
248 | | |
249 | | /// Check if connection is open and healthy. |
250 | | #[must_use] |
251 | 0 | pub fn is_healthy(&self) -> bool { |
252 | 0 | self.is_set(Self::OPEN) && !self.is_set(Self::ERROR) && !self.is_set(Self::CLOSING) |
253 | 0 | } |
254 | | |
255 | | /// Check if connection can read. |
256 | | #[must_use] |
257 | 0 | pub fn can_read(&self) -> bool { |
258 | 0 | self.is_set(Self::OPEN) && self.is_set(Self::READABLE) |
259 | 0 | } |
260 | | |
261 | | /// Check if connection can write. |
262 | | #[must_use] |
263 | 0 | pub fn can_write(&self) -> bool { |
264 | 0 | self.is_set(Self::OPEN) && self.is_set(Self::WRITABLE) && !self.is_set(Self::CLOSING) |
265 | 0 | } |
266 | | |
267 | | /// Get raw bits. |
268 | | #[must_use] |
269 | 0 | pub fn bits(&self) -> u8 { |
270 | 0 | self.0 |
271 | 0 | } |
272 | | } |
273 | | |
274 | | #[cfg(test)] |
275 | | mod tests { |
276 | | use super::*; |
277 | | |
278 | | #[test] |
279 | | fn test_managed_connection_new() { |
280 | | let conn = ManagedConnection::new( |
281 | | "test_connection", |
282 | | Duration::from_secs(60), |
283 | | Duration::from_secs(10), |
284 | | ); |
285 | | |
286 | | assert_eq!(conn.inner(), &"test_connection"); |
287 | | assert!(conn.is_valid()); |
288 | | assert!(!conn.is_expired()); |
289 | | assert!(!conn.is_idle()); |
290 | | } |
291 | | |
292 | | #[test] |
293 | | fn test_managed_connection_inner_mut() { |
294 | | let mut conn = ManagedConnection::new( |
295 | | vec![1, 2, 3], |
296 | | Duration::from_secs(60), |
297 | | Duration::from_secs(10), |
298 | | ); |
299 | | |
300 | | conn.inner_mut().push(4); |
301 | | assert_eq!(conn.inner(), &vec![1, 2, 3, 4]); |
302 | | } |
303 | | |
304 | | #[test] |
305 | | fn test_managed_connection_into_inner() { |
306 | | let conn = ManagedConnection::new(42u32, Duration::from_secs(60), Duration::from_secs(10)); |
307 | | |
308 | | let value = conn.into_inner(); |
309 | | assert_eq!(value, 42); |
310 | | } |
311 | | |
312 | | #[test] |
313 | | fn test_managed_connection_touch() { |
314 | | let mut conn = ManagedConnection::new( |
315 | | "test", |
316 | | Duration::from_secs(60), |
317 | | Duration::from_millis(10), |
318 | | ); |
319 | | |
320 | | std::thread::sleep(Duration::from_millis(5)); |
321 | | let idle_before = conn.idle_time(); |
322 | | |
323 | | conn.touch(); |
324 | | let idle_after = conn.idle_time(); |
325 | | |
326 | | assert!(idle_after < idle_before); |
327 | | } |
328 | | |
329 | | #[test] |
330 | | fn test_managed_connection_health_failures() { |
331 | | let mut conn = |
332 | | ManagedConnection::new("test", Duration::from_secs(60), Duration::from_secs(10)); |
333 | | |
334 | | assert!(conn.is_valid()); |
335 | | |
336 | | conn.record_health_failure(); |
337 | | conn.record_health_failure(); |
338 | | assert!(conn.is_valid()); // 2 failures, still valid |
339 | | |
340 | | conn.record_health_failure(); |
341 | | assert!(!conn.is_valid()); // 3 failures, now invalid |
342 | | |
343 | | conn.reset_health(); |
344 | | assert!(conn.is_valid()); // Reset, valid again |
345 | | } |
346 | | |
347 | | #[test] |
348 | | fn test_managed_connection_age() { |
349 | | let conn = |
350 | | ManagedConnection::new("test", Duration::from_secs(60), Duration::from_secs(10)); |
351 | | |
352 | | let age = conn.age(); |
353 | | assert!(age < Duration::from_millis(100)); |
354 | | } |
355 | | |
356 | | #[test] |
357 | | fn test_managed_connection_ttl_expiry() { |
358 | | let conn = |
359 | | ManagedConnection::new("test", Duration::from_millis(10), Duration::from_secs(60)); |
360 | | |
361 | | assert!(!conn.is_expired()); |
362 | | |
363 | | std::thread::sleep(Duration::from_millis(15)); |
364 | | |
365 | | assert!(conn.is_expired()); |
366 | | assert!(!conn.is_valid()); |
367 | | } |
368 | | |
369 | | #[test] |
370 | | fn test_managed_connection_idle_expiry() { |
371 | | let conn = |
372 | | ManagedConnection::new("test", Duration::from_secs(60), Duration::from_millis(10)); |
373 | | |
374 | | assert!(!conn.is_idle()); |
375 | | |
376 | | std::thread::sleep(Duration::from_millis(15)); |
377 | | |
378 | | assert!(conn.is_idle()); |
379 | | assert!(!conn.is_valid()); |
380 | | } |
381 | | |
382 | | /// FALSIFICATION TEST: Verify all three conditions must pass for validity |
383 | | /// |
384 | | /// A connection is valid only if: not_expired AND not_idle AND healthy. |
385 | | /// If any one fails, the connection should be invalid. |
386 | | #[test] |
387 | | fn test_falsify_validity_requires_all_conditions() { |
388 | | // Test 1: Healthy and not idle, but expired |
389 | | let expired = ManagedConnection::new( |
390 | | "test", |
391 | | Duration::from_millis(5), |
392 | | Duration::from_secs(60), |
393 | | ); |
394 | | std::thread::sleep(Duration::from_millis(10)); |
395 | | assert!( |
396 | | !expired.is_valid(), |
397 | | "FALSIFICATION FAILED: Expired connection should be invalid" |
398 | | ); |
399 | | |
400 | | // Test 2: Healthy and not expired, but idle |
401 | | let idle = ManagedConnection::new( |
402 | | "test", |
403 | | Duration::from_secs(60), |
404 | | Duration::from_millis(5), |
405 | | ); |
406 | | std::thread::sleep(Duration::from_millis(10)); |
407 | | assert!( |
408 | | !idle.is_valid(), |
409 | | "FALSIFICATION FAILED: Idle connection should be invalid" |
410 | | ); |
411 | | |
412 | | // Test 3: Not expired and not idle, but unhealthy |
413 | | let mut unhealthy = |
414 | | ManagedConnection::new("test", Duration::from_secs(60), Duration::from_secs(60)); |
415 | | unhealthy.record_health_failure(); |
416 | | unhealthy.record_health_failure(); |
417 | | unhealthy.record_health_failure(); |
418 | | assert!( |
419 | | !unhealthy.is_valid(), |
420 | | "FALSIFICATION FAILED: Unhealthy connection should be invalid" |
421 | | ); |
422 | | |
423 | | // Test 4: All conditions pass |
424 | | let valid = |
425 | | ManagedConnection::new("test", Duration::from_secs(60), Duration::from_secs(60)); |
426 | | assert!( |
427 | | valid.is_valid(), |
428 | | "FALSIFICATION FAILED: Fresh connection should be valid" |
429 | | ); |
430 | | } |
431 | | |
432 | | /// FALSIFICATION TEST: Touch must reset idle timer |
433 | | /// |
434 | | /// Uses 200ms timeout with 100ms sleeps to provide 100ms margin against timing jitter. |
435 | | /// Previous 20ms/15ms (5ms margin) caused flaky failures on Mac. |
436 | | #[test] |
437 | | fn test_falsify_touch_resets_idle() { |
438 | | let mut conn = |
439 | | ManagedConnection::new("test", Duration::from_secs(60), Duration::from_millis(200)); |
440 | | |
441 | | // Wait until almost idle (100ms of 200ms timeout = 50% margin) |
442 | | std::thread::sleep(Duration::from_millis(100)); |
443 | | assert!( |
444 | | !conn.is_idle(), |
445 | | "Should not be idle yet at 100ms with 200ms timeout" |
446 | | ); |
447 | | |
448 | | // Touch to reset |
449 | | conn.touch(); |
450 | | |
451 | | // Wait another 100ms (would be 200ms total without touch, but only 100ms since touch) |
452 | | std::thread::sleep(Duration::from_millis(100)); |
453 | | assert!( |
454 | | !conn.is_idle(), |
455 | | "FALSIFICATION FAILED: Touch should have reset idle timer" |
456 | | ); |
457 | | |
458 | | // Now wait until actually idle (another 150ms = 250ms since touch > 200ms timeout) |
459 | | std::thread::sleep(Duration::from_millis(150)); |
460 | | assert!(conn.is_idle(), "Should be idle now (250ms since touch > 200ms timeout)"); |
461 | | } |
462 | | |
463 | | // ========================================================================= |
464 | | // KeepAliveConfig Tests |
465 | | // ========================================================================= |
466 | | |
467 | | #[test] |
468 | | fn test_keep_alive_config_new() { |
469 | | let config = KeepAliveConfig::new(); |
470 | | assert!(config.enabled); |
471 | | assert_eq!(config.timeout_secs, 60); |
472 | | assert_eq!(config.max_requests, 100); |
473 | | } |
474 | | |
475 | | #[test] |
476 | | fn test_keep_alive_config_disabled() { |
477 | | let config = KeepAliveConfig::disabled(); |
478 | | assert!(!config.enabled); |
479 | | assert_eq!(config.timeout_secs, 0); |
480 | | assert_eq!(config.max_requests, 0); |
481 | | } |
482 | | |
483 | | #[test] |
484 | | fn test_keep_alive_config_from_header() { |
485 | | let config = KeepAliveConfig::from_header("timeout=30, max=50"); |
486 | | assert!(config.enabled); |
487 | | assert_eq!(config.timeout_secs, 30); |
488 | | assert_eq!(config.max_requests, 50); |
489 | | } |
490 | | |
491 | | #[test] |
492 | | fn test_keep_alive_config_from_header_partial() { |
493 | | // Only timeout specified |
494 | | let config = KeepAliveConfig::from_header("timeout=15"); |
495 | | assert_eq!(config.timeout_secs, 15); |
496 | | assert_eq!(config.max_requests, 100); // Default |
497 | | |
498 | | // Only max specified |
499 | | let config = KeepAliveConfig::from_header("max=25"); |
500 | | assert_eq!(config.timeout_secs, 60); // Default |
501 | | assert_eq!(config.max_requests, 25); |
502 | | } |
503 | | |
504 | | #[test] |
505 | | fn test_keep_alive_config_from_header_invalid() { |
506 | | // Invalid values are ignored |
507 | | let config = KeepAliveConfig::from_header("timeout=abc, max=xyz"); |
508 | | assert_eq!(config.timeout_secs, 60); // Default |
509 | | assert_eq!(config.max_requests, 100); // Default |
510 | | } |
511 | | |
512 | | #[test] |
513 | | fn test_keep_alive_config_should_keep_alive() { |
514 | | let config = KeepAliveConfig::new(); // max_requests = 100 |
515 | | assert!(config.should_keep_alive(0)); |
516 | | assert!(config.should_keep_alive(50)); |
517 | | assert!(config.should_keep_alive(99)); |
518 | | assert!(!config.should_keep_alive(100)); |
519 | | assert!(!config.should_keep_alive(200)); |
520 | | } |
521 | | |
522 | | #[test] |
523 | | fn test_keep_alive_config_should_keep_alive_disabled() { |
524 | | let config = KeepAliveConfig::disabled(); |
525 | | assert!(!config.should_keep_alive(0)); |
526 | | } |
527 | | |
528 | | #[test] |
529 | | fn test_keep_alive_config_default() { |
530 | | let config = KeepAliveConfig::default(); |
531 | | assert_eq!(config, KeepAliveConfig::new()); |
532 | | } |
533 | | |
534 | | // ========================================================================= |
535 | | // ConnectionState Tests |
536 | | // ========================================================================= |
537 | | |
538 | | #[test] |
539 | | fn test_connection_state_new() { |
540 | | let state = ConnectionState::new(); |
541 | | assert_eq!(state.bits(), 0); |
542 | | assert!(!state.is_set(ConnectionState::OPEN)); |
543 | | } |
544 | | |
545 | | #[test] |
546 | | fn test_connection_state_open_connection() { |
547 | | let state = ConnectionState::open_connection(); |
548 | | assert!(state.is_set(ConnectionState::OPEN)); |
549 | | assert!(state.is_set(ConnectionState::WRITABLE)); |
550 | | assert!(!state.is_set(ConnectionState::READABLE)); |
551 | | } |
552 | | |
553 | | #[test] |
554 | | fn test_connection_state_set_clear() { |
555 | | let mut state = ConnectionState::new(); |
556 | | |
557 | | state.set(ConnectionState::OPEN); |
558 | | assert!(state.is_set(ConnectionState::OPEN)); |
559 | | |
560 | | state.set(ConnectionState::READABLE); |
561 | | assert!(state.is_set(ConnectionState::OPEN)); |
562 | | assert!(state.is_set(ConnectionState::READABLE)); |
563 | | |
564 | | state.clear(ConnectionState::OPEN); |
565 | | assert!(!state.is_set(ConnectionState::OPEN)); |
566 | | assert!(state.is_set(ConnectionState::READABLE)); |
567 | | } |
568 | | |
569 | | #[test] |
570 | | fn test_connection_state_is_healthy() { |
571 | | let mut state = ConnectionState::open_connection(); |
572 | | assert!(state.is_healthy()); |
573 | | |
574 | | state.set(ConnectionState::ERROR); |
575 | | assert!(!state.is_healthy()); |
576 | | |
577 | | state.clear(ConnectionState::ERROR); |
578 | | state.set(ConnectionState::CLOSING); |
579 | | assert!(!state.is_healthy()); |
580 | | } |
581 | | |
582 | | #[test] |
583 | | fn test_connection_state_can_read() { |
584 | | let mut state = ConnectionState::open_connection(); |
585 | | assert!(!state.can_read()); // Not readable yet |
586 | | |
587 | | state.set(ConnectionState::READABLE); |
588 | | assert!(state.can_read()); |
589 | | |
590 | | state.clear(ConnectionState::OPEN); |
591 | | assert!(!state.can_read()); // Not open |
592 | | } |
593 | | |
594 | | #[test] |
595 | | fn test_connection_state_can_write() { |
596 | | let mut state = ConnectionState::open_connection(); |
597 | | assert!(state.can_write()); |
598 | | |
599 | | state.set(ConnectionState::CLOSING); |
600 | | assert!(!state.can_write()); // Can't write when closing |
601 | | |
602 | | state.clear(ConnectionState::CLOSING); |
603 | | state.clear(ConnectionState::WRITABLE); |
604 | | assert!(!state.can_write()); // Not writable |
605 | | } |
606 | | |
607 | | #[test] |
608 | | fn test_connection_state_bits() { |
609 | | let mut state = ConnectionState::new(); |
610 | | state.set(ConnectionState::OPEN); |
611 | | state.set(ConnectionState::WRITABLE); |
612 | | assert_eq!(state.bits(), 0b0000_0101); |
613 | | } |
614 | | |
615 | | #[test] |
616 | | fn test_connection_state_default() { |
617 | | let state = ConnectionState::default(); |
618 | | assert_eq!(state.bits(), 0); |
619 | | } |
620 | | |
621 | | /// FALSIFICATION TEST: ConnectionState bitflags must be non-overlapping |
622 | | #[test] |
623 | | fn test_falsify_connection_state_flags_unique() { |
624 | | let flags = [ |
625 | | ConnectionState::OPEN, |
626 | | ConnectionState::READABLE, |
627 | | ConnectionState::WRITABLE, |
628 | | ConnectionState::HAS_PENDING, |
629 | | ConnectionState::KEEP_ALIVE, |
630 | | ConnectionState::UPGRADE, |
631 | | ConnectionState::CLOSING, |
632 | | ConnectionState::ERROR, |
633 | | ]; |
634 | | |
635 | | // Each flag must be a power of 2 (single bit set) |
636 | | for flag in &flags { |
637 | | assert!( |
638 | | flag.is_power_of_two(), |
639 | | "FALSIFICATION FAILED: Flag {:08b} is not a power of 2", |
640 | | flag |
641 | | ); |
642 | | } |
643 | | |
644 | | // No two flags should overlap |
645 | | for i in 0..flags.len() { |
646 | | for j in (i + 1)..flags.len() { |
647 | | assert_eq!( |
648 | | flags[i] & flags[j], |
649 | | 0, |
650 | | "FALSIFICATION FAILED: Flags {:08b} and {:08b} overlap", |
651 | | flags[i], |
652 | | flags[j] |
653 | | ); |
654 | | } |
655 | | } |
656 | | } |
657 | | |
658 | | /// FALSIFICATION TEST: KeepAliveConfig should_keep_alive boundary |
659 | | #[test] |
660 | | fn test_falsify_keep_alive_boundary() { |
661 | | let config = KeepAliveConfig { |
662 | | enabled: true, |
663 | | timeout_secs: 60, |
664 | | max_requests: 100, |
665 | | }; |
666 | | |
667 | | // At exactly max_requests, should NOT keep alive |
668 | | assert!( |
669 | | !config.should_keep_alive(100), |
670 | | "FALSIFICATION FAILED: should_keep_alive should return false at max_requests" |
671 | | ); |
672 | | |
673 | | // One below max should still be true |
674 | | assert!( |
675 | | config.should_keep_alive(99), |
676 | | "FALSIFICATION FAILED: should_keep_alive should return true below max_requests" |
677 | | ); |
678 | | } |
679 | | } |