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