mockforge_http/
latency_profiles.rs1use globwalk::GlobWalkerBuilder;
3use rand::{rng, Rng};
4use serde::Deserialize;
5use std::{collections::HashMap, time::Duration};
6use tokio::time::sleep;
7
8#[derive(Debug, Clone, Deserialize)]
9pub struct Profile {
10 pub fixed_ms: Option<u64>,
11 pub jitter_ms: Option<u64>,
12 pub fail_p: Option<f64>,
13 pub fail_status: Option<u16>,
14}
15
16#[derive(Debug, Default, Clone)]
17pub struct LatencyProfiles {
18 by_operation: HashMap<String, Profile>,
19 by_tag: HashMap<String, Profile>,
20}
21
22impl LatencyProfiles {
23 pub async fn load_from_glob(pattern: &str) -> anyhow::Result<Self> {
24 let mut result = LatencyProfiles::default();
25 for dir_entry in GlobWalkerBuilder::from_patterns(".", &[pattern]).build()? {
26 let path = dir_entry?.path().to_path_buf();
27 if path.extension().map(|e| e == "yaml" || e == "yml").unwrap_or(false) {
28 let text = tokio::fs::read_to_string(&path).await?;
29 let cfg: HashMap<String, Profile> = serde_yaml::from_str(&text)?;
30 for (k, v) in cfg {
31 if let Some(rest) = k.strip_prefix("operation:") {
32 result.by_operation.insert(rest.to_string(), v);
33 } else if let Some(rest) = k.strip_prefix("tag:") {
34 result.by_tag.insert(rest.to_string(), v);
35 }
36 }
37 }
38 }
39 Ok(result)
40 }
41
42 pub async fn maybe_fault(&self, operation_id: &str, tags: &[String]) -> Option<(u16, String)> {
43 let profile = self
44 .by_operation
45 .get(operation_id)
46 .or_else(|| tags.iter().find_map(|t| self.by_tag.get(t)));
47 if let Some(p) = profile {
48 let base = p.fixed_ms.unwrap_or(0);
49 let jitter = p.jitter_ms.unwrap_or(0);
50 let mut rng = rng();
51 let extra: u64 = if jitter > 0 {
52 rng.random_range(0..=jitter)
53 } else {
54 0
55 };
56 sleep(Duration::from_millis(base + extra)).await;
57 if let Some(fp) = p.fail_p {
58 let roll: f64 = rng.random();
59 if roll < fp {
60 return Some((
61 p.fail_status.unwrap_or(500),
62 format!("Injected failure (p={:.2})", fp),
63 ));
64 }
65 }
66 }
67 None
68 }
69}
70
71#[cfg(test)]
72mod tests {
73 use super::*;
74
75 #[test]
76 fn test_profile_creation() {
77 let profile = Profile {
78 fixed_ms: Some(100),
79 jitter_ms: Some(20),
80 fail_p: Some(0.1),
81 fail_status: Some(503),
82 };
83
84 assert_eq!(profile.fixed_ms, Some(100));
85 assert_eq!(profile.jitter_ms, Some(20));
86 assert_eq!(profile.fail_p, Some(0.1));
87 assert_eq!(profile.fail_status, Some(503));
88 }
89
90 #[test]
91 fn test_latency_profiles_default() {
92 let profiles = LatencyProfiles::default();
93 assert!(profiles.by_operation.is_empty());
94 assert!(profiles.by_tag.is_empty());
95 }
96
97 #[tokio::test]
98 async fn test_maybe_fault_no_profile() {
99 let profiles = LatencyProfiles::default();
100 let result = profiles.maybe_fault("test_op", &[]).await;
101 assert!(result.is_none());
102 }
103
104 #[tokio::test]
105 async fn test_maybe_fault_with_operation_profile_no_failure() {
106 let mut profiles = LatencyProfiles::default();
107 profiles.by_operation.insert(
108 "test_op".to_string(),
109 Profile {
110 fixed_ms: Some(1),
111 jitter_ms: Some(1),
112 fail_p: Some(0.0),
113 fail_status: Some(500),
114 },
115 );
116
117 let result = profiles.maybe_fault("test_op", &[]).await;
118 assert!(result.is_none());
119 }
120
121 #[tokio::test]
122 async fn test_maybe_fault_with_tag_profile() {
123 let mut profiles = LatencyProfiles::default();
124 profiles.by_tag.insert(
125 "slow".to_string(),
126 Profile {
127 fixed_ms: Some(1),
128 jitter_ms: None,
129 fail_p: Some(0.0),
130 fail_status: None,
131 },
132 );
133
134 let tags = vec!["slow".to_string()];
135 let result = profiles.maybe_fault("unknown_op", &tags).await;
136 assert!(result.is_none());
137 }
138
139 #[tokio::test]
140 async fn test_maybe_fault_guaranteed_failure() {
141 let mut profiles = LatencyProfiles::default();
142 profiles.by_operation.insert(
143 "failing_op".to_string(),
144 Profile {
145 fixed_ms: Some(0),
146 jitter_ms: None,
147 fail_p: Some(1.0),
148 fail_status: Some(503),
149 },
150 );
151
152 let result = profiles.maybe_fault("failing_op", &[]).await;
153 assert!(result.is_some());
154 let (status, _message) = result.unwrap();
155 assert_eq!(status, 503);
156 }
157
158 #[tokio::test]
159 async fn test_maybe_fault_operation_priority_over_tag() {
160 let mut profiles = LatencyProfiles::default();
161
162 profiles.by_operation.insert(
163 "test_op".to_string(),
164 Profile {
165 fixed_ms: Some(1),
166 jitter_ms: None,
167 fail_p: Some(0.0),
168 fail_status: Some(500),
169 },
170 );
171
172 profiles.by_tag.insert(
173 "test_tag".to_string(),
174 Profile {
175 fixed_ms: Some(100),
176 jitter_ms: None,
177 fail_p: Some(1.0),
178 fail_status: Some(503),
179 },
180 );
181
182 let tags = vec!["test_tag".to_string()];
183 let result = profiles.maybe_fault("test_op", &tags).await;
184
185 assert!(result.is_none());
187 }
188
189 #[test]
190 fn test_profile_deserialization() {
191 let yaml = r#"
192 fixed_ms: 100
193 jitter_ms: 20
194 fail_p: 0.1
195 fail_status: 503
196 "#;
197
198 let profile: Profile = serde_yaml::from_str(yaml).unwrap();
199 assert_eq!(profile.fixed_ms, Some(100));
200 assert_eq!(profile.jitter_ms, Some(20));
201 assert_eq!(profile.fail_p, Some(0.1));
202 assert_eq!(profile.fail_status, Some(503));
203 }
204
205 #[test]
206 fn test_profile_partial_deserialization() {
207 let yaml = r#"
208 fixed_ms: 50
209 "#;
210
211 let profile: Profile = serde_yaml::from_str(yaml).unwrap();
212 assert_eq!(profile.fixed_ms, Some(50));
213 assert!(profile.jitter_ms.is_none());
214 assert!(profile.fail_p.is_none());
215 assert!(profile.fail_status.is_none());
216 }
217}