mockforge_core/
network_profiles.rs

1//! Pre-configured network condition profiles for easy simulation of various network scenarios
2//!
3//! This module provides user-friendly presets that package latency, bandwidth, and
4//! packet loss settings into common network scenarios like "3G", "Slow 2G", "Satellite", etc.
5
6use crate::latency::LatencyProfile;
7use crate::traffic_shaping::{BandwidthConfig, BurstLossConfig, TrafficShapingConfig};
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11/// Pre-configured network condition profile
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct NetworkProfile {
14    /// Profile name
15    pub name: String,
16    /// Profile description
17    pub description: String,
18    /// Latency configuration
19    pub latency: LatencyProfile,
20    /// Traffic shaping configuration
21    pub traffic_shaping: TrafficShapingConfig,
22}
23
24impl NetworkProfile {
25    /// Create a custom network profile
26    pub fn custom(
27        name: String,
28        description: String,
29        latency: LatencyProfile,
30        traffic_shaping: TrafficShapingConfig,
31    ) -> Self {
32        Self {
33            name,
34            description,
35            latency,
36            traffic_shaping,
37        }
38    }
39
40    /// Apply this profile to get its configurations
41    pub fn apply(&self) -> (LatencyProfile, TrafficShapingConfig) {
42        (self.latency.clone(), self.traffic_shaping.clone())
43    }
44}
45
46/// Network profile catalog with built-in presets
47#[derive(Debug, Clone)]
48pub struct NetworkProfileCatalog {
49    profiles: HashMap<String, NetworkProfile>,
50}
51
52impl Default for NetworkProfileCatalog {
53    fn default() -> Self {
54        Self::new()
55    }
56}
57
58impl NetworkProfileCatalog {
59    /// Create a new catalog with built-in profiles
60    pub fn new() -> Self {
61        let mut catalog = Self {
62            profiles: HashMap::new(),
63        };
64
65        // Add built-in profiles
66        catalog.add_builtin_profiles();
67        catalog
68    }
69
70    /// Add all built-in network profiles
71    fn add_builtin_profiles(&mut self) {
72        // Perfect network (no degradation)
73        self.add_profile(Self::perfect_network());
74
75        // Mobile networks
76        self.add_profile(Self::mobile_5g());
77        self.add_profile(Self::mobile_4g());
78        self.add_profile(Self::mobile_3g());
79        self.add_profile(Self::mobile_2g());
80        self.add_profile(Self::mobile_edge());
81
82        // Satellite connections
83        self.add_profile(Self::satellite_leo()); // Low Earth Orbit (like Starlink)
84        self.add_profile(Self::satellite_geo()); // Geostationary (traditional satellite)
85
86        // Impaired networks
87        self.add_profile(Self::congested_network());
88        self.add_profile(Self::lossy_network());
89        self.add_profile(Self::high_latency());
90
91        // Edge cases
92        self.add_profile(Self::intermittent_connection());
93        self.add_profile(Self::extremely_poor());
94    }
95
96    /// Add a custom profile to the catalog
97    pub fn add_profile(&mut self, profile: NetworkProfile) {
98        self.profiles.insert(profile.name.clone(), profile);
99    }
100
101    /// Get a profile by name
102    pub fn get(&self, name: &str) -> Option<&NetworkProfile> {
103        self.profiles.get(name)
104    }
105
106    /// Get all available profile names
107    pub fn list_profiles(&self) -> Vec<String> {
108        let mut names: Vec<String> = self.profiles.keys().cloned().collect();
109        names.sort();
110        names
111    }
112
113    /// Get all profiles with descriptions
114    pub fn list_profiles_with_description(&self) -> Vec<(String, String)> {
115        let mut profiles: Vec<_> = self
116            .profiles
117            .values()
118            .map(|p| (p.name.clone(), p.description.clone()))
119            .collect();
120        profiles.sort_by(|a, b| a.0.cmp(&b.0));
121        profiles
122    }
123
124    // ========================================================================
125    // Built-in Profile Definitions
126    // ========================================================================
127
128    /// Perfect network (no degradation)
129    fn perfect_network() -> NetworkProfile {
130        NetworkProfile {
131            name: "perfect".to_string(),
132            description: "Perfect network with no degradation".to_string(),
133            latency: LatencyProfile::new(0, 0),
134            traffic_shaping: TrafficShapingConfig::default(),
135        }
136    }
137
138    /// 5G mobile network
139    fn mobile_5g() -> NetworkProfile {
140        NetworkProfile {
141            name: "5g".to_string(),
142            description: "5G mobile network (10-30ms latency, ~100 Mbps)".to_string(),
143            latency: LatencyProfile::with_normal_distribution(20, 5.0)
144                .with_min_ms(10)
145                .with_max_ms(30),
146            traffic_shaping: TrafficShapingConfig {
147                bandwidth: BandwidthConfig::new(
148                    100_000_000 / 8, // 100 Mbps in bytes/sec
149                    10_000_000,      // 10MB burst
150                ),
151                burst_loss: BurstLossConfig {
152                    enabled: false,
153                    ..Default::default()
154                },
155            },
156        }
157    }
158
159    /// 4G/LTE mobile network
160    fn mobile_4g() -> NetworkProfile {
161        NetworkProfile {
162            name: "4g".to_string(),
163            description: "4G/LTE mobile network (30-60ms latency, ~20 Mbps)".to_string(),
164            latency: LatencyProfile::with_normal_distribution(45, 10.0)
165                .with_min_ms(30)
166                .with_max_ms(70),
167            traffic_shaping: TrafficShapingConfig {
168                bandwidth: BandwidthConfig::new(
169                    20_000_000 / 8, // 20 Mbps in bytes/sec
170                    2_500_000,      // 2.5MB burst
171                ),
172                burst_loss: BurstLossConfig {
173                    enabled: true,
174                    burst_probability: 0.05,     // 5% chance of burst
175                    burst_duration_ms: 2000,     // 2 second bursts
176                    loss_rate_during_burst: 0.1, // 10% loss during burst
177                    recovery_time_ms: 30000,     // 30 second recovery
178                    ..Default::default()
179                },
180            },
181        }
182    }
183
184    /// 3G mobile network
185    fn mobile_3g() -> NetworkProfile {
186        NetworkProfile {
187            name: "3g".to_string(),
188            description: "3G mobile network (100-200ms latency, ~1 Mbps)".to_string(),
189            latency: LatencyProfile::with_normal_distribution(150, 30.0)
190                .with_min_ms(100)
191                .with_max_ms(250),
192            traffic_shaping: TrafficShapingConfig {
193                bandwidth: BandwidthConfig::new(
194                    1_000_000 / 8, // 1 Mbps in bytes/sec
195                    125_000,       // 125KB burst
196                ),
197                burst_loss: BurstLossConfig {
198                    enabled: true,
199                    burst_probability: 0.1,
200                    burst_duration_ms: 3000,
201                    loss_rate_during_burst: 0.15,
202                    recovery_time_ms: 20000,
203                    ..Default::default()
204                },
205            },
206        }
207    }
208
209    /// 2G mobile network (EDGE)
210    fn mobile_2g() -> NetworkProfile {
211        NetworkProfile {
212            name: "2g".to_string(),
213            description: "2G/EDGE mobile network (300-500ms latency, ~250 Kbps)".to_string(),
214            latency: LatencyProfile::with_normal_distribution(400, 80.0)
215                .with_min_ms(300)
216                .with_max_ms(600),
217            traffic_shaping: TrafficShapingConfig {
218                bandwidth: BandwidthConfig::new(
219                    250_000 / 8, // 250 Kbps in bytes/sec
220                    31_250,      // 31KB burst
221                ),
222                burst_loss: BurstLossConfig {
223                    enabled: true,
224                    burst_probability: 0.15,
225                    burst_duration_ms: 5000,
226                    loss_rate_during_burst: 0.2,
227                    recovery_time_ms: 15000,
228                    ..Default::default()
229                },
230            },
231        }
232    }
233
234    /// EDGE mobile network (worst case)
235    fn mobile_edge() -> NetworkProfile {
236        NetworkProfile {
237            name: "edge".to_string(),
238            description: "EDGE mobile network (500-800ms latency, ~100 Kbps)".to_string(),
239            latency: LatencyProfile::with_normal_distribution(650, 120.0)
240                .with_min_ms(500)
241                .with_max_ms(1000),
242            traffic_shaping: TrafficShapingConfig {
243                bandwidth: BandwidthConfig::new(
244                    100_000 / 8, // 100 Kbps in bytes/sec
245                    12_500,      // 12.5KB burst
246                ),
247                burst_loss: BurstLossConfig {
248                    enabled: true,
249                    burst_probability: 0.2,
250                    burst_duration_ms: 8000,
251                    loss_rate_during_burst: 0.25,
252                    recovery_time_ms: 10000,
253                    ..Default::default()
254                },
255            },
256        }
257    }
258
259    /// Low Earth Orbit satellite (like Starlink)
260    fn satellite_leo() -> NetworkProfile {
261        NetworkProfile {
262            name: "satellite_leo".to_string(),
263            description: "LEO satellite (20-40ms latency, ~100 Mbps, variable)".to_string(),
264            latency: LatencyProfile::with_pareto_distribution(30, 2.5)
265                .with_min_ms(20)
266                .with_max_ms(150), // Occasional higher latency
267            traffic_shaping: TrafficShapingConfig {
268                bandwidth: BandwidthConfig::new(
269                    100_000_000 / 8, // 100 Mbps in bytes/sec
270                    10_000_000,      // 10MB burst
271                ),
272                burst_loss: BurstLossConfig {
273                    enabled: true,
274                    burst_probability: 0.08,
275                    burst_duration_ms: 3000,
276                    loss_rate_during_burst: 0.15,
277                    recovery_time_ms: 25000,
278                    ..Default::default()
279                },
280            },
281        }
282    }
283
284    /// Geostationary satellite (traditional)
285    fn satellite_geo() -> NetworkProfile {
286        NetworkProfile {
287            name: "satellite_geo".to_string(),
288            description: "GEO satellite (550-750ms latency, ~15 Mbps)".to_string(),
289            latency: LatencyProfile::with_normal_distribution(650, 80.0)
290                .with_min_ms(550)
291                .with_max_ms(850),
292            traffic_shaping: TrafficShapingConfig {
293                bandwidth: BandwidthConfig::new(
294                    15_000_000 / 8, // 15 Mbps in bytes/sec
295                    1_875_000,      // 1.875MB burst
296                ),
297                burst_loss: BurstLossConfig {
298                    enabled: true,
299                    burst_probability: 0.1,
300                    burst_duration_ms: 5000,
301                    loss_rate_during_burst: 0.2,
302                    recovery_time_ms: 20000,
303                    ..Default::default()
304                },
305            },
306        }
307    }
308
309    /// Congested network with high variable latency
310    fn congested_network() -> NetworkProfile {
311        NetworkProfile {
312            name: "congested".to_string(),
313            description: "Congested network (100-500ms latency, ~2 Mbps, high jitter)".to_string(),
314            latency: LatencyProfile::with_pareto_distribution(150, 1.8)
315                .with_min_ms(100)
316                .with_max_ms(800),
317            traffic_shaping: TrafficShapingConfig {
318                bandwidth: BandwidthConfig::new(
319                    2_000_000 / 8, // 2 Mbps in bytes/sec
320                    250_000,       // 250KB burst
321                ),
322                burst_loss: BurstLossConfig {
323                    enabled: true,
324                    burst_probability: 0.12,
325                    burst_duration_ms: 4000,
326                    loss_rate_during_burst: 0.2,
327                    recovery_time_ms: 18000,
328                    ..Default::default()
329                },
330            },
331        }
332    }
333
334    /// Network with significant packet loss
335    fn lossy_network() -> NetworkProfile {
336        NetworkProfile {
337            name: "lossy".to_string(),
338            description: "Lossy network (50-100ms latency, 20% packet loss)".to_string(),
339            latency: LatencyProfile::with_normal_distribution(75, 15.0)
340                .with_min_ms(50)
341                .with_max_ms(120),
342            traffic_shaping: TrafficShapingConfig {
343                bandwidth: BandwidthConfig::new(
344                    10_000_000 / 8, // 10 Mbps in bytes/sec
345                    1_250_000,      // 1.25MB burst
346                ),
347                burst_loss: BurstLossConfig {
348                    enabled: true,
349                    burst_probability: 0.3, // High probability of loss bursts
350                    burst_duration_ms: 2000,
351                    loss_rate_during_burst: 0.5, // 50% loss during burst
352                    recovery_time_ms: 8000,
353                    ..Default::default()
354                },
355            },
356        }
357    }
358
359    /// High latency network
360    fn high_latency() -> NetworkProfile {
361        NetworkProfile {
362            name: "high_latency".to_string(),
363            description: "High latency network (500-1000ms latency, normal bandwidth)".to_string(),
364            latency: LatencyProfile::with_normal_distribution(750, 150.0)
365                .with_min_ms(500)
366                .with_max_ms(1200),
367            traffic_shaping: TrafficShapingConfig {
368                bandwidth: BandwidthConfig::new(
369                    10_000_000 / 8, // 10 Mbps in bytes/sec
370                    1_250_000,      // 1.25MB burst
371                ),
372                burst_loss: BurstLossConfig {
373                    enabled: false,
374                    ..Default::default()
375                },
376            },
377        }
378    }
379
380    /// Intermittent connection (frequent disconnections)
381    fn intermittent_connection() -> NetworkProfile {
382        NetworkProfile {
383            name: "intermittent".to_string(),
384            description: "Intermittent connection (100-300ms latency, frequent drops)".to_string(),
385            latency: LatencyProfile::with_normal_distribution(200, 50.0)
386                .with_min_ms(100)
387                .with_max_ms(400),
388            traffic_shaping: TrafficShapingConfig {
389                bandwidth: BandwidthConfig::new(
390                    5_000_000 / 8, // 5 Mbps in bytes/sec
391                    625_000,       // 625KB burst
392                ),
393                burst_loss: BurstLossConfig {
394                    enabled: true,
395                    burst_probability: 0.4,      // Very frequent drops
396                    burst_duration_ms: 5000,     // Long outages
397                    loss_rate_during_burst: 0.8, // 80% loss during burst
398                    recovery_time_ms: 10000,
399                    ..Default::default()
400                },
401            },
402        }
403    }
404
405    /// Extremely poor network conditions
406    fn extremely_poor() -> NetworkProfile {
407        NetworkProfile {
408            name: "extremely_poor".to_string(),
409            description: "Extremely poor network (1000ms+ latency, <50 Kbps, high loss)"
410                .to_string(),
411            latency: LatencyProfile::with_pareto_distribution(1000, 1.5)
412                .with_min_ms(800)
413                .with_max_ms(3000),
414            traffic_shaping: TrafficShapingConfig {
415                bandwidth: BandwidthConfig::new(
416                    50_000 / 8, // 50 Kbps in bytes/sec
417                    6_250,      // 6.25KB burst
418                ),
419                burst_loss: BurstLossConfig {
420                    enabled: true,
421                    burst_probability: 0.5,
422                    burst_duration_ms: 10000,
423                    loss_rate_during_burst: 0.7,
424                    recovery_time_ms: 5000,
425                    ..Default::default()
426                },
427            },
428        }
429    }
430}
431
432#[cfg(test)]
433mod tests {
434    use super::*;
435
436    #[test]
437    fn test_network_profile_creation() {
438        let profile = NetworkProfile::custom(
439            "test".to_string(),
440            "Test profile".to_string(),
441            LatencyProfile::new(100, 20),
442            TrafficShapingConfig::default(),
443        );
444
445        assert_eq!(profile.name, "test");
446        assert_eq!(profile.description, "Test profile");
447    }
448
449    #[test]
450    fn test_catalog_has_builtin_profiles() {
451        let catalog = NetworkProfileCatalog::new();
452        let profiles = catalog.list_profiles();
453
454        // Check that we have all expected profiles
455        assert!(profiles.contains(&"perfect".to_string()));
456        assert!(profiles.contains(&"5g".to_string()));
457        assert!(profiles.contains(&"4g".to_string()));
458        assert!(profiles.contains(&"3g".to_string()));
459        assert!(profiles.contains(&"2g".to_string()));
460        assert!(profiles.contains(&"edge".to_string()));
461        assert!(profiles.contains(&"satellite_leo".to_string()));
462        assert!(profiles.contains(&"satellite_geo".to_string()));
463        assert!(profiles.contains(&"congested".to_string()));
464        assert!(profiles.contains(&"lossy".to_string()));
465        assert!(profiles.contains(&"high_latency".to_string()));
466        assert!(profiles.contains(&"intermittent".to_string()));
467        assert!(profiles.contains(&"extremely_poor".to_string()));
468
469        assert!(profiles.len() >= 13);
470    }
471
472    #[test]
473    fn test_get_profile() {
474        let catalog = NetworkProfileCatalog::new();
475
476        let profile_3g = catalog.get("3g");
477        assert!(profile_3g.is_some());
478        assert_eq!(profile_3g.unwrap().name, "3g");
479
480        let profile_nonexistent = catalog.get("nonexistent");
481        assert!(profile_nonexistent.is_none());
482    }
483
484    #[test]
485    fn test_apply_profile() {
486        let catalog = NetworkProfileCatalog::new();
487        let profile = catalog.get("4g").unwrap();
488
489        let (latency, traffic_shaping) = profile.apply();
490
491        // 4G should have latency around 30-70ms
492        assert!(latency.base_ms >= 30);
493        assert!(latency.base_ms <= 70);
494
495        // 4G should have bandwidth enabled
496        assert!(traffic_shaping.bandwidth.enabled);
497    }
498
499    #[test]
500    fn test_list_profiles_with_description() {
501        let catalog = NetworkProfileCatalog::new();
502        let profiles = catalog.list_profiles_with_description();
503
504        // Check that we get tuples with names and descriptions
505        assert!(!profiles.is_empty());
506
507        for (name, desc) in &profiles {
508            assert!(!name.is_empty());
509            assert!(!desc.is_empty());
510        }
511    }
512
513    #[test]
514    fn test_custom_profile_addition() {
515        let mut catalog = NetworkProfileCatalog::new();
516
517        let custom = NetworkProfile::custom(
518            "custom_test".to_string(),
519            "Custom test profile".to_string(),
520            LatencyProfile::new(50, 10),
521            TrafficShapingConfig::default(),
522        );
523
524        catalog.add_profile(custom);
525
526        let profiles = catalog.list_profiles();
527        assert!(profiles.contains(&"custom_test".to_string()));
528    }
529
530    #[test]
531    fn test_profile_characteristics() {
532        let catalog = NetworkProfileCatalog::new();
533
534        // Test 5G has lower latency than 3G
535        let profile_5g = catalog.get("5g").unwrap();
536        let profile_3g = catalog.get("3g").unwrap();
537        assert!(profile_5g.latency.base_ms < profile_3g.latency.base_ms);
538
539        // Test satellite_geo has high latency
540        let profile_sat = catalog.get("satellite_geo").unwrap();
541        assert!(profile_sat.latency.base_ms >= 550);
542
543        // Test lossy network has burst loss enabled
544        let profile_lossy = catalog.get("lossy").unwrap();
545        assert!(profile_lossy.traffic_shaping.burst_loss.enabled);
546    }
547}