Skip to main content

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}