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