1use std::collections::VecDeque;
2use std::time::Instant;
3
4use bincode::{Decode, Encode};
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Encode, Decode)]
9pub struct NetworkInterfaceSnapshot {
10 pub name: String,
11 pub bytes_rx: u64,
12 pub bytes_tx: u64,
13 pub packets_rx: u64,
14 pub packets_tx: u64,
15 pub errors_rx: u64,
16 pub errors_tx: u64,
17 pub mac_address: String,
18 pub is_up: bool,
22}
23
24#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Encode, Decode)]
26pub struct NetworkSnapshot {
27 pub interfaces: Vec<NetworkInterfaceSnapshot>,
28 pub total_rx: u64,
29 pub total_tx: u64,
30}
31
32impl NetworkSnapshot {
33 pub fn collect(networks: &sysinfo::Networks) -> Self {
35 let mut total_rx: u64 = 0;
36 let mut total_tx: u64 = 0;
37
38 let interfaces: Vec<NetworkInterfaceSnapshot> = networks
39 .iter()
40 .map(|(name, data)| {
41 let bytes_rx = data.total_received();
42 let bytes_tx = data.total_transmitted();
43 total_rx = total_rx.saturating_add(bytes_rx);
44 total_tx = total_tx.saturating_add(bytes_tx);
45
46 NetworkInterfaceSnapshot {
47 name: name.clone(),
48 bytes_rx,
49 bytes_tx,
50 packets_rx: data.total_packets_received(),
51 packets_tx: data.total_packets_transmitted(),
52 errors_rx: data.total_errors_on_received(),
53 errors_tx: data.total_errors_on_transmitted(),
54 mac_address: data.mac_address().to_string(),
55 is_up: bytes_rx > 0 || bytes_tx > 0,
56 }
57 })
58 .collect();
59
60 Self {
61 interfaces,
62 total_rx,
63 total_tx,
64 }
65 }
66}
67
68#[derive(Debug, Clone)]
70struct TimestampedSnapshot {
71 snapshot: NetworkSnapshot,
72 timestamp: Instant,
73}
74
75#[derive(Debug, Clone)]
81pub struct NetworkHistory {
82 samples: VecDeque<TimestampedSnapshot>,
83 capacity: usize,
84}
85
86impl NetworkHistory {
87 pub fn new(capacity: usize) -> Self {
90 let capacity = capacity.max(2);
91 Self {
92 samples: VecDeque::with_capacity(capacity),
93 capacity,
94 }
95 }
96
97 pub fn push(&mut self, snapshot: NetworkSnapshot) {
99 if self.samples.len() >= self.capacity {
100 self.samples.pop_front();
101 }
102 self.samples.push_back(TimestampedSnapshot {
103 snapshot,
104 timestamp: Instant::now(),
105 });
106 }
107
108 pub fn len(&self) -> usize {
110 self.samples.len()
111 }
112
113 pub fn is_empty(&self) -> bool {
115 self.samples.is_empty()
116 }
117
118 pub fn bandwidth_rx(&self, iface: &str) -> f64 {
121 self.bandwidth(iface, |i| i.bytes_rx)
122 }
123
124 pub fn bandwidth_tx(&self, iface: &str) -> f64 {
127 self.bandwidth(iface, |i| i.bytes_tx)
128 }
129
130 pub fn sparkline_rx(&self, iface: &str, points: usize) -> Vec<u64> {
133 self.sparkline(iface, points, |i| i.bytes_rx)
134 }
135
136 pub fn sparkline_tx(&self, iface: &str, points: usize) -> Vec<u64> {
138 self.sparkline(iface, points, |i| i.bytes_tx)
139 }
140
141 fn find_iface_value(
142 snapshot: &NetworkSnapshot,
143 iface: &str,
144 extract: &impl Fn(&NetworkInterfaceSnapshot) -> u64,
145 ) -> Option<u64> {
146 snapshot
147 .interfaces
148 .iter()
149 .find(|i| i.name == iface)
150 .map(extract)
151 }
152
153 fn bandwidth(&self, iface: &str, extract: impl Fn(&NetworkInterfaceSnapshot) -> u64) -> f64 {
154 if self.samples.len() < 2 {
155 return 0.0;
156 }
157 let prev = &self.samples[self.samples.len() - 2];
158 let curr = &self.samples[self.samples.len() - 1];
159
160 let prev_val = Self::find_iface_value(&prev.snapshot, iface, &extract).unwrap_or(0);
161 let curr_val = Self::find_iface_value(&curr.snapshot, iface, &extract).unwrap_or(0);
162
163 let delta = curr_val.saturating_sub(prev_val) as f64;
165 let elapsed = curr.timestamp.duration_since(prev.timestamp).as_secs_f64();
166
167 if elapsed > 0.0 { delta / elapsed } else { 0.0 }
168 }
169
170 fn sparkline(
171 &self,
172 iface: &str,
173 points: usize,
174 extract: impl Fn(&NetworkInterfaceSnapshot) -> u64,
175 ) -> Vec<u64> {
176 if self.samples.len() < 2 || points == 0 {
177 return Vec::new();
178 }
179
180 let n = points.min(self.samples.len() - 1);
181 let start = self.samples.len() - n - 1;
182 let mut result = Vec::with_capacity(n);
183
184 for i in start..self.samples.len() - 1 {
185 let prev_val =
186 Self::find_iface_value(&self.samples[i].snapshot, iface, &extract).unwrap_or(0);
187 let curr_val =
188 Self::find_iface_value(&self.samples[i + 1].snapshot, iface, &extract).unwrap_or(0);
189 result.push(curr_val.saturating_sub(prev_val));
190 }
191
192 result
193 }
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199
200 #[test]
201 fn test_network_types_send_clone() {
202 fn assert_send_clone<T: Send + Clone>() {}
203 assert_send_clone::<NetworkInterfaceSnapshot>();
204 assert_send_clone::<NetworkSnapshot>();
205 assert_send_clone::<NetworkHistory>();
206 }
207
208 #[test]
209 fn test_interface_snapshot_from_sysinfo() {
210 let networks = sysinfo::Networks::new_with_refreshed_list();
211 let snapshot = NetworkSnapshot::collect(&networks);
212 assert!(
214 !snapshot.interfaces.is_empty(),
215 "should have at least one network interface"
216 );
217 for iface in &snapshot.interfaces {
218 assert!(!iface.name.is_empty(), "interface name should not be empty");
219 }
220 }
221
222 #[test]
223 fn test_network_snapshot_totals_consistent() {
224 let networks = sysinfo::Networks::new_with_refreshed_list();
225 let snapshot = NetworkSnapshot::collect(&networks);
226
227 let sum_rx: u64 = snapshot.interfaces.iter().map(|i| i.bytes_rx).sum();
228 let sum_tx: u64 = snapshot.interfaces.iter().map(|i| i.bytes_tx).sum();
229 assert_eq!(
230 snapshot.total_rx, sum_rx,
231 "total_rx should equal sum of interface bytes_rx"
232 );
233 assert_eq!(
234 snapshot.total_tx, sum_tx,
235 "total_tx should equal sum of interface bytes_tx"
236 );
237 }
238
239 fn make_snapshot(iface: &str, rx: u64, tx: u64) -> NetworkSnapshot {
241 NetworkSnapshot {
242 interfaces: vec![NetworkInterfaceSnapshot {
243 name: iface.into(),
244 bytes_rx: rx,
245 bytes_tx: tx,
246 packets_rx: 0,
247 packets_tx: 0,
248 errors_rx: 0,
249 errors_tx: 0,
250 mac_address: "00:00:00:00:00:00".into(),
251 is_up: rx > 0 || tx > 0,
252 }],
253 total_rx: rx,
254 total_tx: tx,
255 }
256 }
257
258 #[test]
259 fn test_history_empty() {
260 let history = NetworkHistory::new(60);
261 assert!(history.is_empty());
262 assert_eq!(history.len(), 0);
263 assert_eq!(history.bandwidth_rx("eth0"), 0.0);
264 assert_eq!(history.bandwidth_tx("eth0"), 0.0);
265 assert!(history.sparkline_rx("eth0", 30).is_empty());
266 assert!(history.sparkline_tx("eth0", 30).is_empty());
267 }
268
269 #[test]
270 fn test_history_single_snapshot() {
271 let mut history = NetworkHistory::new(60);
272 history.push(make_snapshot("eth0", 1000, 500));
273 assert_eq!(history.len(), 1);
274 assert_eq!(history.bandwidth_rx("eth0"), 0.0);
275 assert!(history.sparkline_rx("eth0", 30).is_empty());
276 }
277
278 #[test]
279 fn test_bandwidth_calculation() {
280 let mut history = NetworkHistory::new(60);
281 history.push(make_snapshot("eth0", 1000, 500));
282 std::thread::sleep(std::time::Duration::from_millis(10));
284 history.push(make_snapshot("eth0", 2000, 800));
285
286 let bw_rx = history.bandwidth_rx("eth0");
287 let bw_tx = history.bandwidth_tx("eth0");
288 assert!(bw_rx > 0.0, "bandwidth_rx should be positive, got {bw_rx}");
291 assert!(bw_tx > 0.0, "bandwidth_tx should be positive, got {bw_tx}");
292 }
293
294 #[test]
295 fn test_bandwidth_counter_reset() {
296 let mut history = NetworkHistory::new(60);
297 history.push(make_snapshot("eth0", 5000, 3000));
298 std::thread::sleep(std::time::Duration::from_millis(10));
299 history.push(make_snapshot("eth0", 100, 50));
301
302 assert_eq!(history.bandwidth_rx("eth0"), 0.0);
304 assert_eq!(history.bandwidth_tx("eth0"), 0.0);
305 }
306
307 #[test]
308 fn test_bandwidth_unknown_interface() {
309 let mut history = NetworkHistory::new(60);
310 history.push(make_snapshot("eth0", 1000, 500));
311 std::thread::sleep(std::time::Duration::from_millis(10));
312 history.push(make_snapshot("eth0", 2000, 800));
313
314 assert_eq!(history.bandwidth_rx("nonexistent"), 0.0);
315 }
316
317 #[test]
318 fn test_history_capacity_eviction() {
319 let mut history = NetworkHistory::new(60);
320 for i in 0..70 {
321 history.push(make_snapshot("eth0", i * 100, i * 50));
322 }
323 assert_eq!(history.len(), 60);
324 }
325
326 #[test]
327 fn test_history_capacity_minimum() {
328 let history = NetworkHistory::new(0);
330 assert_eq!(history.capacity, 2);
331
332 let history = NetworkHistory::new(1);
333 assert_eq!(history.capacity, 2);
334 }
335
336 #[test]
337 fn test_sparkline_data() {
338 let mut history = NetworkHistory::new(60);
339 for &rx in &[0u64, 100, 300, 600, 1000] {
341 history.push(make_snapshot("eth0", rx, 0));
342 }
343
344 let spark = history.sparkline_rx("eth0", 10);
345 assert_eq!(spark, vec![100, 200, 300, 400]);
347 }
348
349 #[test]
350 fn test_sparkline_limited_points() {
351 let mut history = NetworkHistory::new(60);
352 for &rx in &[0u64, 100, 300, 600, 1000] {
353 history.push(make_snapshot("eth0", rx, 0));
354 }
355
356 let spark = history.sparkline_rx("eth0", 2);
357 assert_eq!(spark, vec![300, 400]);
359 }
360
361 #[test]
362 fn test_sparkline_zero_points() {
363 let mut history = NetworkHistory::new(60);
364 history.push(make_snapshot("eth0", 0, 0));
365 history.push(make_snapshot("eth0", 100, 50));
366
367 let spark = history.sparkline_rx("eth0", 0);
368 assert!(spark.is_empty());
369 }
370
371 #[test]
372 fn test_sparkline_unknown_interface() {
373 let mut history = NetworkHistory::new(60);
374 history.push(make_snapshot("eth0", 0, 0));
375 history.push(make_snapshot("eth0", 100, 50));
376
377 let spark = history.sparkline_rx("nonexistent", 10);
378 assert_eq!(spark, vec![0]);
380 }
381
382 #[test]
383 fn test_network_snapshot_empty_interfaces() {
384 let snapshot = NetworkSnapshot {
385 interfaces: vec![],
386 total_rx: 0,
387 total_tx: 0,
388 };
389 assert!(snapshot.interfaces.is_empty());
390 assert_eq!(snapshot.total_rx, 0);
391 assert_eq!(snapshot.total_tx, 0);
392 }
393
394 #[test]
395 fn test_multi_interface_snapshot() {
396 let snap = NetworkSnapshot {
397 interfaces: vec![
398 NetworkInterfaceSnapshot {
399 name: "eth0".into(),
400 bytes_rx: 1000,
401 bytes_tx: 500,
402 packets_rx: 10,
403 packets_tx: 5,
404 errors_rx: 0,
405 errors_tx: 0,
406 mac_address: "aa:bb:cc:dd:ee:f0".into(),
407 is_up: true,
408 },
409 NetworkInterfaceSnapshot {
410 name: "wlan0".into(),
411 bytes_rx: 2000,
412 bytes_tx: 1000,
413 packets_rx: 20,
414 packets_tx: 10,
415 errors_rx: 1,
416 errors_tx: 0,
417 mac_address: "aa:bb:cc:dd:ee:f1".into(),
418 is_up: true,
419 },
420 NetworkInterfaceSnapshot {
421 name: "lo".into(),
422 bytes_rx: 500,
423 bytes_tx: 500,
424 packets_rx: 5,
425 packets_tx: 5,
426 errors_rx: 0,
427 errors_tx: 0,
428 mac_address: "00:00:00:00:00:00".into(),
429 is_up: true,
430 },
431 ],
432 total_rx: 3500,
433 total_tx: 2000,
434 };
435 assert_eq!(snap.interfaces.len(), 3);
436 let sum_rx: u64 = snap.interfaces.iter().map(|i| i.bytes_rx).sum();
437 let sum_tx: u64 = snap.interfaces.iter().map(|i| i.bytes_tx).sum();
438 assert_eq!(sum_rx, snap.total_rx);
439 assert_eq!(sum_tx, snap.total_tx);
440 }
441
442 fn make_multi_iface_snapshot(
443 eth0_rx: u64,
444 eth0_tx: u64,
445 wlan0_rx: u64,
446 wlan0_tx: u64,
447 ) -> NetworkSnapshot {
448 NetworkSnapshot {
449 interfaces: vec![
450 NetworkInterfaceSnapshot {
451 name: "eth0".into(),
452 bytes_rx: eth0_rx,
453 bytes_tx: eth0_tx,
454 packets_rx: 0,
455 packets_tx: 0,
456 errors_rx: 0,
457 errors_tx: 0,
458 mac_address: "00:00:00:00:00:00".into(),
459 is_up: eth0_rx > 0 || eth0_tx > 0,
460 },
461 NetworkInterfaceSnapshot {
462 name: "wlan0".into(),
463 bytes_rx: wlan0_rx,
464 bytes_tx: wlan0_tx,
465 packets_rx: 0,
466 packets_tx: 0,
467 errors_rx: 0,
468 errors_tx: 0,
469 mac_address: "00:00:00:00:00:01".into(),
470 is_up: wlan0_rx > 0 || wlan0_tx > 0,
471 },
472 ],
473 total_rx: eth0_rx + wlan0_rx,
474 total_tx: eth0_tx + wlan0_tx,
475 }
476 }
477
478 #[test]
479 fn test_history_multi_interface_bandwidth() {
480 let mut history = NetworkHistory::new(60);
481 history.push(make_multi_iface_snapshot(1000, 500, 2000, 1000));
482 std::thread::sleep(std::time::Duration::from_millis(10));
483 history.push(make_multi_iface_snapshot(2000, 800, 2500, 1200));
484
485 let eth0_bw = history.bandwidth_rx("eth0");
486 let wlan0_bw = history.bandwidth_rx("wlan0");
487
488 assert!(eth0_bw > 0.0, "eth0 rx bandwidth should be positive");
491 assert!(wlan0_bw > 0.0, "wlan0 rx bandwidth should be positive");
492 assert!(
493 eth0_bw > wlan0_bw,
494 "eth0 (delta 1000) should have higher bandwidth than wlan0 (delta 500)"
495 );
496 }
497
498 #[test]
499 fn test_sparkline_tx_data() {
500 let mut history = NetworkHistory::new(60);
501 for &tx in &[0u64, 100, 300, 700, 1500] {
502 history.push(make_snapshot("eth0", 0, tx));
503 }
504 let spark = history.sparkline_tx("eth0", 10);
505 assert_eq!(spark, vec![100, 200, 400, 800]);
507 }
508
509 #[test]
510 fn test_history_multi_interface_sparkline() {
511 let mut history = NetworkHistory::new(60);
512 history.push(make_multi_iface_snapshot(0, 0, 0, 0));
514 history.push(make_multi_iface_snapshot(100, 50, 200, 100));
515 history.push(make_multi_iface_snapshot(300, 150, 500, 250));
516
517 let eth0_spark = history.sparkline_rx("eth0", 10);
518 let wlan0_spark = history.sparkline_rx("wlan0", 10);
519
520 assert_eq!(eth0_spark, vec![100, 200]); assert_eq!(wlan0_spark, vec![200, 300]); }
523
524 #[test]
525 fn test_interface_is_up_heuristic() {
526 let down = NetworkInterfaceSnapshot {
527 name: "eth1".into(),
528 bytes_rx: 0,
529 bytes_tx: 0,
530 packets_rx: 0,
531 packets_tx: 0,
532 errors_rx: 0,
533 errors_tx: 0,
534 mac_address: "00:00:00:00:00:00".into(),
535 is_up: false,
536 };
537 assert!(!down.is_up, "interface with zero traffic should be down");
538
539 let up = NetworkInterfaceSnapshot {
540 name: "eth0".into(),
541 bytes_rx: 100,
542 bytes_tx: 0,
543 packets_rx: 1,
544 packets_tx: 0,
545 errors_rx: 0,
546 errors_tx: 0,
547 mac_address: "00:00:00:00:00:00".into(),
548 is_up: true,
549 };
550 assert!(up.is_up, "interface with rx traffic should be up");
551 }
552
553 #[test]
554 fn test_all_structs_are_debug() {
555 let iface = NetworkInterfaceSnapshot {
556 name: "eth0".into(),
557 bytes_rx: 1000,
558 bytes_tx: 500,
559 packets_rx: 10,
560 packets_tx: 5,
561 errors_rx: 0,
562 errors_tx: 0,
563 mac_address: "00:00:00:00:00:00".into(),
564 is_up: true,
565 };
566 assert!(!format!("{iface:?}").is_empty());
567
568 let snap = NetworkSnapshot {
569 interfaces: vec![iface],
570 total_rx: 1000,
571 total_tx: 500,
572 };
573 assert!(!format!("{snap:?}").is_empty());
574 }
575}