aria2_core/engine/peer_stats.rs
1//! Peer statistics tracking with sliding window speed calculation.
2//!
3//! This module provides [`PeerStats`] for tracking per-peer metrics including
4//! upload/download byte counts, speed calculations using Exponential Moving Average (EMA),
5//! choke/interested state management for BT choking algorithm implementation,
6//! and bad peer detection/banning system for handling peers that send invalid data.
7
8use std::net::SocketAddr;
9use std::time::{Duration, Instant};
10
11use crate::constants;
12
13/// EMA smoothing factor (alpha).
14///
15/// Controls responsiveness vs. smoothness of speed estimates.
16/// 0.5 provides balanced behavior: responsive to changes while filtering noise.
17const EMA_ALPHA: f64 = constants::PEER_STATS_EMA_ALPHA;
18
19/// Threshold for banning peers that send too many invalid pieces.
20///
21/// When a peer's `bad_data_count` reaches this value, they are permanently
22/// banned for the remainder of the session.
23pub const BAD_DATA_THRESHOLD: u32 = constants::PEER_STATS_BAD_DATA_THRESHOLD as u32;
24
25/// Per-peer statistics for BitTorrent choking algorithm decisions.
26///
27/// Tracks cumulative byte counts, real-time speeds via EMA, choke/interested states,
28/// timestamps for snubbed detection and unchoke rotation eligibility,
29/// and bad data detection for peer banning system.
30pub struct PeerStats {
31 /// 20-byte peer identifier from the BitTorrent handshake.
32 pub peer_id: [u8; 20],
33
34 /// Network address of this peer.
35 pub addr: SocketAddr,
36
37 // ------------------------------------------------------------------
38 // Cumulative byte counts
39 // ------------------------------------------------------------------
40 /// Total bytes uploaded to this peer (cumulative).
41 pub uploaded_bytes: u64,
42
43 /// Total bytes downloaded from this peer (cumulative).
44 pub downloaded_bytes: u64,
45
46 // ------------------------------------------------------------------
47 // Speed estimates (bytes/sec), updated via EMA
48 // ------------------------------------------------------------------
49 /// Current upload speed estimate in bytes/second.
50 pub upload_speed: f64,
51
52 /// Current download speed estimate in bytes/second.
53 pub download_speed: f64,
54
55 /// Average upload speed over the entire connection (bytes/sec).
56 pub avg_upload_speed: u64,
57
58 /// Average download speed over the entire connection (bytes/sec).
59 pub avg_download_speed: u64,
60
61 // ------------------------------------------------------------------
62 // Choke / Interested state (per BEP-0003)
63 // ------------------------------------------------------------------
64 /// Whether *we* are choking this peer.
65 ///
66 /// Starts as `true` (we choke all peers by default).
67 pub am_choking: bool,
68
69 /// Whether *we* are interested in data from this peer.
70 pub am_interested: bool,
71
72 /// Whether *this peer* is choking us.
73 pub peer_choking: bool,
74
75 /// Whether *this peer* is interested in data from us.
76 pub peer_interested: bool,
77
78 /// Whether this peer has been marked as snubbed (not sending data).
79 pub is_snubbed: bool,
80
81 // ------------------------------------------------------------------
82 // Bad data tracking (for ban system)
83 // ------------------------------------------------------------------
84 /// Number of times this peer sent invalid piece data (hash verification failed).
85 ///
86 /// When this reaches [`BAD_DATA_THRESHOLD`], the peer is permanently banned.
87 pub bad_data_count: u32,
88
89 /// Number of times this peer has been marked as snubbed.
90 pub snub_count: u32,
91
92 /// Whether this peer has been banned for sending too much invalid data.
93 ///
94 /// Banned peers are disconnected, excluded from selection, and not reconnected
95 /// for the remainder of the session.
96 pub is_banned: bool,
97
98 /// Reason why this peer was banned (if `is_banned == true`).
99 pub ban_reason: Option<String>,
100
101 // ------------------------------------------------------------------
102 // Timestamps for speed calculation & snubbed detection
103 // ------------------------------------------------------------------
104 /// Instant of the most recent message received from this peer.
105 pub last_message_received_at: Instant,
106
107 /// Instant of the most recent data received FROM this peer.
108 pub last_data_time: Option<Instant>,
109
110 /// Instant of the most recent data sent TO this peer.
111 pub last_upload_time: Option<Instant>,
112
113 /// Instant when we last unchoked this peer (for rotation round-robin).
114 pub last_unchoke_at: Instant,
115
116 /// Instant when we last optimistically unchoked this peer.
117 pub last_optimistic_unchoke_at: Instant,
118
119 /// When this `PeerStats` was created.
120 created_at: Instant,
121
122 // ------------------------------------------------------------------
123 // Internal: previous timestamp for EMA speed calculation
124 // ------------------------------------------------------------------
125 /// Last time `on_data_sent` was called (for upload speed EMA).
126 last_upload_tick: Instant,
127
128 /// Last time `on_data_received` was called (for download speed EMA).
129 last_download_tick: Instant,
130}
131
132impl PeerStats {
133 /// Create a new `PeerStats` for the given peer.
134 ///
135 /// # Default state
136 ///
137 /// - Byte counters start at 0.
138 /// - Speeds start at 0.0.
139 /// - `am_choking = true` (we choke by default).
140 /// - All other boolean flags are `false`.
141 /// - All timestamps are set to `Instant::now()`.
142 /// - Bad data count starts at 0.
143 ///
144 /// # Example
145 ///
146 /// ```ignore
147 /// use std::net::SocketAddr;
148 /// let addr: SocketAddr = "192.168.1.5:6881".parse().unwrap();
149 /// let stats = PeerStats::new([0u8; 20], addr);
150 /// assert!(stats.am_choking);
151 /// assert_eq!(stats.uploaded_bytes, 0);
152 /// assert!(!stats.is_banned);
153 /// ```
154 pub fn new(peer_id: [u8; 20], addr: SocketAddr) -> Self {
155 let now = Instant::now();
156 Self {
157 peer_id,
158 addr,
159 uploaded_bytes: 0,
160 downloaded_bytes: 0,
161 upload_speed: 0.0,
162 download_speed: 0.0,
163 avg_upload_speed: 0,
164 avg_download_speed: 0,
165 am_choking: true,
166 am_interested: false,
167 peer_choking: true,
168 peer_interested: false,
169 is_snubbed: false,
170 bad_data_count: 0,
171 snub_count: 0,
172 is_banned: false,
173 ban_reason: None,
174 last_message_received_at: now,
175 last_data_time: None,
176 last_upload_time: None,
177 last_unchoke_at: now,
178 last_optimistic_unchoke_at: now,
179 created_at: now,
180 last_upload_tick: now,
181 last_download_tick: now,
182 }
183 }
184
185 // ------------------------------------------------------------------
186 // Data event handlers (update counters + EMA speeds)
187 // ------------------------------------------------------------------
188
189 /// Record that we sent `bytes` to this peer.
190 ///
191 /// Increments [`uploaded_bytes`](Self::uploaded_bytes) and updates
192 /// [`upload_speed`](Self::upload_speed) using an Exponential Moving Average:
193 ///
194 /// ```text
195 /// new_speed = alpha * instant_rate + (1 - alpha) * old_speed
196 /// ```
197 ///
198 /// where `alpha = 0.5`. On the **first** call the raw instant rate is used directly.
199 /// Also updates [`last_upload_time`](Self::last_upload_time).
200 pub fn on_data_sent(&mut self, bytes: u64) {
201 self.uploaded_bytes += bytes;
202 let now = Instant::now();
203 self.last_upload_time = Some(now);
204
205 let elapsed = now - self.last_upload_tick;
206 self.last_upload_tick = now;
207
208 if elapsed.is_zero() {
209 return; // avoid division-by-zero; speed unchanged
210 }
211
212 let instant_rate = bytes as f64 / elapsed.as_secs_f64();
213
214 if self.upload_speed == 0.0 && self.uploaded_bytes == bytes {
215 // First measurement: use raw rate
216 self.upload_speed = instant_rate;
217 } else {
218 // EMA update
219 self.upload_speed = EMA_ALPHA * instant_rate + (1.0 - EMA_ALPHA) * self.upload_speed;
220 }
221
222 // Update average upload speed (cumulative)
223 let total_elapsed = self.created_at.elapsed().as_secs_f64();
224 if total_elapsed > 0.0 {
225 self.avg_upload_speed = (self.uploaded_bytes as f64 / total_elapsed) as u64;
226 }
227 }
228
229 /// Record that we received `bytes` from this peer.
230 ///
231 /// Increments [`downloaded_bytes`](Self::downloaded_bytes),
232 /// resets [`is_snubbed`](Self::is_snubbed) to `false`,
233 /// updates [`last_message_received_at`](Self::last_message_received_at),
234 /// updates [`last_data_time`](Self::last_data_time),
235 /// and refreshes [`download_speed`](Self::download_speed) via EMA.
236 pub fn on_data_received(&mut self, bytes: u64) {
237 self.downloaded_bytes += bytes;
238 let now = Instant::now();
239 self.last_message_received_at = now;
240 self.last_data_time = Some(now);
241 self.is_snubbed = false;
242
243 let elapsed = now - self.last_download_tick;
244 self.last_download_tick = now;
245
246 if elapsed.is_zero() {
247 return;
248 }
249
250 let instant_rate = bytes as f64 / elapsed.as_secs_f64();
251
252 if self.download_speed == 0.0 && self.downloaded_bytes == bytes {
253 // First measurement: use raw rate
254 self.download_speed = instant_rate;
255 } else {
256 // EMA update
257 self.download_speed =
258 EMA_ALPHA * instant_rate + (1.0 - EMA_ALPHA) * self.download_speed;
259 }
260
261 // Update average download speed (cumulative)
262 let total_elapsed = self.created_at.elapsed().as_secs_f64();
263 if total_elapsed > 0.0 {
264 self.avg_download_speed = (self.downloaded_bytes as f64 / total_elapsed) as u64;
265 }
266 }
267
268 // ------------------------------------------------------------------
269 // Snubbed detection
270 // ------------------------------------------------------------------
271
272 /// Check whether this peer should be marked as snubbed due to inactivity.
273 ///
274 /// Returns `true` if the peer has **just** transitioned into the snubbed state
275 /// (i.e., no data for at least `timeout_secs` seconds and was not already snubbed).
276 ///
277 /// Returns `false` if the peer is still active or was already snubbed.
278 /// Also increments [`snub_count`](Self::snub_count) when transitioning to snubbed state.
279 pub fn check_snubbed(&mut self, timeout_secs: u64) -> bool {
280 if self.last_message_received_at.elapsed().as_secs() >= timeout_secs && !self.is_snubbed {
281 self.is_snubbed = true;
282 self.snub_count = self.snub_count.saturating_add(1);
283 return true;
284 }
285 false
286 }
287
288 /// Explicitly reset the snubbed flag (e.g. after an unchoke).
289 pub fn reset_snubbed(&mut self) {
290 self.is_snubbed = false;
291 }
292
293 // ------------------------------------------------------------------
294 // Choke / Unchoke bookkeeping
295 // ------------------------------------------------------------------
296
297 /// Record that we have **unchoked** this peer.
298 ///
299 /// Sets [`am_choking`](Self::am_choking) to `false` and refreshes
300 /// [`last_unchoke_at`](Self::last_unchoke_at).
301 pub fn record_unchoke(&mut self) {
302 self.am_choking = false;
303 self.last_unchoke_at = Instant::now();
304 }
305
306 /// Record that we have **choked** this peer.
307 ///
308 /// Sets [`am_choking`](Self::am_choking) to `true`.
309 pub fn record_choke(&mut self) {
310 self.am_choking = true;
311 }
312
313 /// Record that we performed an **optimistic unchoke** on this peer.
314 ///
315 /// Sets [`am_choking`](Self::am_choking) to `false` and refreshes
316 /// [`last_optimistic_unchoke_at`](Self::last_optimistic_unchoke_at).
317 pub fn record_optimistic_unchoke(&mut self) {
318 self.am_choking = false;
319 self.last_optimistic_unchoke_at = Instant::now();
320 }
321
322 // ------------------------------------------------------------------
323 // Time-since helpers for rotation logic
324 // ------------------------------------------------------------------
325
326 /// Elapsed time since we last unchoked this peer (regular unchoke).
327 ///
328 /// Used by the choking algorithm to determine rotation eligibility
329 /// (peers that have been unchoked longest are candidates for choking).
330 pub fn time_since_last_unchoke(&self) -> Duration {
331 self.last_unchoke_at.elapsed()
332 }
333
334 /// Elapsed time since we last optimistically unchoked this peer.
335 ///
336 /// Used to avoid re-selecting the same peer for optimistic unchoke
337 /// too frequently.
338 pub fn time_since_last_optimistic_unchoke(&self) -> Duration {
339 self.last_optimistic_unchoke_at.elapsed()
340 }
341
342 /// Elapsed time since this `PeerStats` was created.
343 pub fn age(&self) -> Duration {
344 self.created_at.elapsed()
345 }
346
347 /// Get the total duration of this peer connection in seconds.
348 ///
349 /// Returns the number of seconds since this peer was first connected.
350 pub fn connection_duration_secs(&self) -> u64 {
351 self.created_at.elapsed().as_secs()
352 }
353
354 // ------------------------------------------------------------------
355 // Bad data tracking / Ban system
356 // ------------------------------------------------------------------
357
358 /// Increment the bad data counter for this peer.
359 ///
360 /// Called when a piece received from this peer fails hash verification.
361 ///
362 /// # Returns
363 ///
364 /// * `true` if the peer should now be banned (count >= [`BAD_DATA_THRESHOLD`])
365 /// * `false` if the peer is still under the threshold
366 pub fn increment_bad_data(&mut self) -> bool {
367 self.bad_data_count = self.bad_data_count.saturating_add(1);
368 self.bad_data_count >= BAD_DATA_THRESHOLD
369 }
370
371 /// Decrement the bad data counter for this peer (gradual recovery).
372 ///
373 /// Called when a valid, verified piece is successfully received from this peer.
374 /// This allows peers who occasionally send bad data to recover their reputation.
375 /// The count is floored at 0 (never goes negative).
376 pub fn decrement_bad_data(&mut self) {
377 self.bad_data_count = self.bad_data_count.saturating_sub(1);
378 }
379
380 /// Ban this peer with a reason.
381 ///
382 /// Sets [`is_banned`](Self::is_banned) to `true`, stores the reason,
383 /// and logs the ban event. Banned peers are:
384 /// - Disconnected immediately
385 /// - Not reconnected for the rest of the session
386 /// - Excluded from all selection algorithms
387 ///
388 /// # Arguments
389 ///
390 /// * `reason` - Human-readable explanation for why the peer was banned
391 pub fn ban_peer(&mut self, reason: String) {
392 self.is_banned = true;
393 self.ban_reason = Some(reason);
394 }
395
396 /// Check if this peer is eligible for selection in algorithms.
397 ///
398 /// Returns `false` if the peer is banned, regardless of other metrics.
399 /// This should be called before including a peer in any selection logic.
400 pub fn is_eligible_for_selection(&self) -> bool {
401 !self.is_banned
402 }
403}
404
405// ======================================================================
406// Tests
407// ======================================================================
408
409#[cfg(test)]
410mod tests {
411 use super::*;
412 use std::thread;
413 use std::time::Duration;
414
415 fn make_test_peer() -> PeerStats {
416 let addr: SocketAddr = "127.0.0.1:6881".parse().unwrap();
417 PeerStats::new([0x42; 20], addr)
418 }
419
420 #[test]
421 fn test_new_peer_stats() {
422 let stats = make_test_peer();
423
424 // Byte counters should be zero
425 assert_eq!(stats.uploaded_bytes, 0);
426 assert_eq!(stats.downloaded_bytes, 0);
427
428 // Speeds should be zero
429 assert_eq!(stats.upload_speed, 0.0);
430 assert_eq!(stats.download_speed, 0.0);
431
432 // Default choke state: we choke the peer by default
433 assert!(stats.am_choking);
434 assert!(!stats.am_interested);
435
436 // Peer default states
437 assert!(stats.peer_choking); // peer chokes us initially
438 assert!(!stats.peer_interested);
439
440 // Not snubbed initially
441 assert!(!stats.is_snubbed);
442
443 // Peer ID preserved
444 assert_eq!(stats.peer_id, [0x42; 20]);
445 }
446
447 #[test]
448 fn test_on_data_sent_updates_counters() {
449 let mut stats = make_test_peer();
450
451 // Small sleep so elapsed > 0
452 thread::sleep(Duration::from_millis(10));
453
454 stats.on_data_sent(1024);
455
456 assert_eq!(stats.uploaded_bytes, 1024);
457 assert!(
458 stats.upload_speed > 0.0,
459 "upload_speed should be positive after sending data"
460 );
461
462 // Send more data
463 thread::sleep(Duration::from_millis(10));
464 stats.on_data_sent(2048);
465
466 assert_eq!(stats.uploaded_bytes, 1024 + 2048);
467 assert!(stats.upload_speed > 0.0);
468 }
469
470 #[test]
471 fn test_on_data_received_resets_snubbed() {
472 let mut stats = make_test_peer();
473
474 // Mark as snubbed manually
475 stats.is_snubbed = true;
476 assert!(stats.is_snubbed);
477
478 // Receive data -- should reset snubbed flag
479 thread::sleep(Duration::from_millis(10));
480 stats.on_data_received(512);
481
482 assert!(
483 !stats.is_snubbed,
484 "receiving data should reset snubbed status"
485 );
486 assert_eq!(stats.downloaded_bytes, 512);
487 assert!(stats.download_speed > 0.0);
488 }
489
490 #[test]
491 fn test_check_snubbed_timeout() {
492 let mut stats = make_test_peer();
493
494 // Immediately after creation, should NOT be snubbed with a reasonable timeout
495 let result = stats.check_snubbed(10);
496 assert!(!result, "should not be snubbed immediately");
497 assert!(!stats.is_snubbed);
498
499 // Use timeout=0 to guarantee it triggers (elapsed >= 0 always true)
500 let result = stats.check_snubbed(0);
501 assert!(
502 result,
503 "with timeout=0, any elapsed time should trigger snubbed"
504 );
505 assert!(stats.is_snubbed);
506
507 // Calling again should return false (already snubbed)
508 let result2 = stats.check_snubbed(0);
509 assert!(
510 !result2,
511 "second call should return false (already snubbed)"
512 );
513 }
514
515 #[test]
516 fn test_choke_state_transitions() {
517 let mut stats = make_test_peer();
518
519 // Initial state: we are choking
520 assert!(stats.am_choking);
521
522 // Unchoke
523 stats.record_unchoke();
524 assert!(!stats.am_choking);
525
526 // Verify timestamp updated
527 let unchoke_time = stats.time_since_last_unchoke();
528 assert!(unchoke_time < Duration::from_millis(100));
529
530 // Re-choke
531 stats.record_choke();
532 assert!(stats.am_choking);
533
534 // Optimistic unchoke
535 stats.record_optimistic_unchoke();
536 assert!(!stats.am_choking);
537
538 let opt_time = stats.time_since_last_optimistic_unchoke();
539 assert!(opt_time < Duration::from_millis(100));
540 }
541
542 #[test]
543 fn test_ema_speed_smoothing() {
544 let mut stats = make_test_peer();
545
546 // First measurement: raw rate
547 thread::sleep(Duration::from_millis(20));
548 stats.on_data_received(1000);
549
550 let first_speed = stats.download_speed;
551 assert!(first_speed > 0.0);
552
553 // Second measurement at similar interval: EMA should produce
554 // a value close to first_speed (smoothed)
555 thread::sleep(Duration::from_millis(20));
556 stats.on_data_received(1000);
557
558 let second_speed = stats.download_speed;
559 // With alpha=0.5, second_speed ~= 0.5*rate2 + 0.5*first_speed
560 // Since rate1 ~= rate2 (same bytes, same interval), second_speed ~= first_speed
561 let ratio = second_speed / first_speed;
562 assert!(
563 ratio > 0.3 && ratio < 3.0,
564 "EMA should smooth speeds reasonably (ratio={:.2})",
565 ratio
566 );
567 }
568
569 #[test]
570 fn test_cumulative_byte_counts() {
571 let mut stats = make_test_peer();
572
573 for _ in 0..5 {
574 thread::sleep(Duration::from_millis(5));
575 stats.on_data_sent(1024);
576 thread::sleep(Duration::from_millis(5));
577 stats.on_data_received(2048);
578 }
579
580 assert_eq!(stats.uploaded_bytes, 5 * 1024);
581 assert_eq!(stats.downloaded_bytes, 5 * 2048);
582 }
583
584 #[test]
585 fn test_reset_snubbed_explicit() {
586 let mut stats = make_test_peer();
587
588 stats.is_snubbed = true;
589 stats.reset_snubbed();
590 assert!(!stats.is_snubbed);
591 }
592
593 // ==================================================================
594 // H3: Bad Peer Ban System Tests
595 // ==================================================================
596
597 #[test]
598 fn test_new_peer_stats_default_ban_state() {
599 let stats = make_test_peer();
600
601 // Bad data tracking should start at 0
602 assert_eq!(stats.bad_data_count, 0);
603 assert_eq!(stats.snub_count, 0);
604
605 // Peer should not be banned initially
606 assert!(!stats.is_banned);
607 assert!(stats.ban_reason.is_none());
608
609 // Should be eligible for selection
610 assert!(stats.is_eligible_for_selection());
611
612 // Average speeds should be 0
613 assert_eq!(stats.avg_upload_speed, 0);
614 assert_eq!(stats.avg_download_speed, 0);
615
616 // Timestamps should be None
617 assert!(stats.last_data_time.is_none());
618 assert!(stats.last_upload_time.is_none());
619 }
620
621 #[test]
622 fn test_bad_data_incremented_on_invalid_hash() {
623 let mut stats = make_test_peer();
624
625 // First invalid piece
626 let should_ban = stats.increment_bad_data();
627 assert!(!should_ban, "Should not ban after 1 bad piece");
628 assert_eq!(stats.bad_data_count, 1);
629
630 // Second invalid piece
631 let should_ban = stats.increment_bad_data();
632 assert!(!should_ban, "Should not ban after 2 bad pieces");
633 assert_eq!(stats.bad_data_count, 2);
634 }
635
636 #[test]
637 fn test_ban_threshold_reached_peer_banned() {
638 let mut stats = make_test_peer();
639
640 // Increment to threshold (BAD_DATA_THRESHOLD = 3)
641 stats.increment_bad_data(); // count = 1
642 stats.increment_bad_data(); // count = 2
643
644 // Third strike - should trigger ban
645 let should_ban = stats.increment_bad_data();
646 assert!(
647 should_ban,
648 "Should ban after {} bad pieces",
649 BAD_DATA_THRESHOLD
650 );
651 assert_eq!(stats.bad_data_count, BAD_DATA_THRESHOLD);
652 }
653
654 #[test]
655 fn test_successful_piece_decrements_bad_count() {
656 let mut stats = make_test_peer();
657
658 // Simulate some bad pieces
659 stats.increment_bad_data(); // count = 1
660 stats.increment_bad_data(); // count = 2
661 assert_eq!(stats.bad_data_count, 2);
662
663 // Successful piece received - gradual recovery
664 stats.decrement_bad_data();
665 assert_eq!(stats.bad_data_count, 1, "Should decrement by 1");
666
667 // Another successful piece
668 stats.decrement_bad_data();
669 assert_eq!(stats.bad_data_count, 0, "Should reach 0");
670
671 // Decrementing below 0 should floor at 0
672 stats.decrement_bad_data();
673 assert_eq!(stats.bad_data_count, 0, "Should never go negative");
674 }
675
676 #[test]
677 fn test_ban_peer_sets_flags_and_reason() {
678 let mut stats = make_test_peer();
679
680 assert!(!stats.is_banned);
681 assert!(stats.ban_reason.is_none());
682 assert!(stats.is_eligible_for_selection());
683
684 // Ban with reason
685 stats.ban_peer("Too many invalid pieces (3 >= 3)".to_string());
686
687 assert!(stats.is_banned, "Peer should be marked as banned");
688 assert!(
689 stats.ban_reason.as_deref() == Some("Too many invalid pieces (3 >= 3)"),
690 "Ban reason should be preserved"
691 );
692 assert!(
693 !stats.is_eligible_for_selection(),
694 "Banned peer should not be eligible for selection"
695 );
696 }
697
698 #[test]
699 fn test_banned_peer_excluded_from_selection() {
700 let mut stats = make_test_peer();
701
702 // Before banning: eligible
703 assert!(stats.is_eligible_for_selection());
704
705 // After banning: excluded
706 stats.ban_peer("Test ban".to_string());
707 assert!(!stats.is_eligible_for_selection());
708
709 // Even if we try to unchoke them, they remain banned
710 stats.record_unchoke();
711 assert!(
712 !stats.is_eligible_for_selection(),
713 "Banned status persists after unchoke"
714 );
715 }
716
717 #[test]
718 fn test_snub_count_increments_on_snubbed() {
719 let mut stats = make_test_peer();
720
721 assert_eq!(stats.snub_count, 0);
722
723 // Trigger snub detection with timeout=0
724 let snubbed = stats.check_snubbed(0);
725 assert!(snubbed);
726 assert_eq!(
727 stats.snub_count, 1,
728 "Snub count should increment on first snub"
729 );
730
731 // Second call should NOT increment (already snubbed)
732 let snubbed_again = stats.check_snubbed(0);
733 assert!(!snubbed_again, "Already snubbed, should return false");
734 assert_eq!(stats.snub_count, 1, "Snub count should NOT increment again");
735
736 // Reset and re-check
737 stats.reset_snubbed();
738 let snubbed_third = stats.check_snubbed(0);
739 assert!(snubbed_third);
740 assert_eq!(
741 stats.snub_count, 2,
742 "Snub count should increment after reset+re-snub"
743 );
744 }
745
746 #[test]
747 fn test_connection_duration_increases() {
748 let stats = make_test_peer();
749
750 // Duration should be very small immediately after creation
751 let initial_duration = stats.connection_duration_secs();
752 assert!(initial_duration < 2, "Initial duration should be near 0");
753
754 // Sleep briefly and check duration increased
755 std::thread::sleep(Duration::from_millis(100));
756 let later_duration = stats.connection_duration_secs();
757 assert!(
758 later_duration >= initial_duration,
759 "Duration should increase over time"
760 );
761 }
762
763 #[test]
764 fn test_on_data_sent_updates_last_upload_time() {
765 let mut stats = make_test_peer();
766
767 assert!(stats.last_upload_time.is_none(), "No upload time initially");
768
769 thread::sleep(Duration::from_millis(10));
770 stats.on_data_sent(1024);
771
772 assert!(
773 stats.last_upload_time.is_some(),
774 "Upload time should be set after sending data"
775 );
776 }
777
778 #[test]
779 fn test_on_data_received_updates_last_data_time() {
780 let mut stats = make_test_peer();
781
782 assert!(stats.last_data_time.is_none(), "No data time initially");
783
784 thread::sleep(Duration::from_millis(10));
785 stats.on_data_received(512);
786
787 assert!(
788 stats.last_data_time.is_some(),
789 "Data time should be set after receiving data"
790 );
791 }
792
793 #[test]
794 fn test_avg_speeds_updated_after_data_transfer() {
795 let mut stats = make_test_peer();
796
797 // Initially average speeds are 0
798 assert_eq!(stats.avg_upload_speed, 0);
799 assert_eq!(stats.avg_download_speed, 0);
800
801 // Send some data
802 thread::sleep(Duration::from_millis(20));
803 stats.on_data_sent(2048);
804 assert!(
805 stats.avg_upload_speed > 0,
806 "Average upload speed should be positive"
807 );
808
809 // Receive some data
810 thread::sleep(Duration::from_millis(20));
811 stats.on_data_received(4096);
812 assert!(
813 stats.avg_download_speed > 0,
814 "Average download speed should be positive"
815 );
816 }
817}