1use crate::error::{CleanroomError, Result};
7use crate::validation::span_validator::SpanData;
8use glob::Pattern;
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
14#[serde(rename_all = "UPPERCASE")]
15pub enum StatusCode {
16 Unset,
18 Ok,
20 Error,
22}
23
24impl StatusCode {
25 #[allow(clippy::should_implement_trait)]
39 pub fn parse(s: &str) -> Result<Self> {
40 match s.to_uppercase().as_str() {
41 "UNSET" => Ok(StatusCode::Unset),
42 "OK" => Ok(StatusCode::Ok),
43 "ERROR" => Ok(StatusCode::Error),
44 _ => Err(CleanroomError::validation_error(format!(
45 "Invalid status code: '{}'. Must be UNSET, OK, or ERROR",
46 s
47 ))),
48 }
49 }
50
51 pub fn as_str(&self) -> &'static str {
53 match self {
54 StatusCode::Unset => "UNSET",
55 StatusCode::Ok => "OK",
56 StatusCode::Error => "ERROR",
57 }
58 }
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct StatusExpectation {
67 pub all: Option<StatusCode>,
69 pub by_name: HashMap<String, StatusCode>,
71}
72
73impl StatusExpectation {
74 pub fn new() -> Self {
76 Self {
77 all: None,
78 by_name: HashMap::new(),
79 }
80 }
81
82 pub fn with_all(mut self, status: StatusCode) -> Self {
90 self.all = Some(status);
91 self
92 }
93
94 pub fn with_name_pattern(mut self, pattern: String, status: StatusCode) -> Self {
103 self.by_name.insert(pattern, status);
104 self
105 }
106
107 pub fn validate(&self, spans: &[SpanData]) -> Result<()> {
120 if let Some(expected_all) = self.all {
122 for span in spans {
123 let actual = self.get_span_status(span)?;
124 if actual != expected_all {
125 return Err(CleanroomError::validation_error(format!(
126 "Status validation failed: span '{}' has status {} but expected {}",
127 span.name,
128 actual.as_str(),
129 expected_all.as_str()
130 )));
131 }
132 }
133 }
134
135 for (pattern, expected_status) in &self.by_name {
137 let glob_pattern = Pattern::new(pattern).map_err(|e| {
138 CleanroomError::validation_error(format!(
139 "Invalid glob pattern '{}': {}",
140 pattern, e
141 ))
142 })?;
143
144 let matching_spans: Vec<_> = spans
146 .iter()
147 .filter(|s| glob_pattern.matches(&s.name))
148 .collect();
149
150 if matching_spans.is_empty() {
151 return Err(CleanroomError::validation_error(format!(
152 "Status validation failed: no spans match pattern '{}'",
153 pattern
154 )));
155 }
156
157 for span in matching_spans {
159 let actual = self.get_span_status(span)?;
160 if actual != *expected_status {
161 return Err(CleanroomError::validation_error(format!(
162 "Status validation failed: span '{}' matching pattern '{}' has status {} but expected {}",
163 span.name, pattern, actual.as_str(), expected_status.as_str()
164 )));
165 }
166 }
167 }
168
169 Ok(())
170 }
171
172 fn get_span_status(&self, span: &SpanData) -> Result<StatusCode> {
188 if let Some(status_val) = span.attributes.get("otel.status_code") {
190 if let Some(status_str) = status_val.as_str() {
191 return StatusCode::parse(status_str);
192 }
193 }
194
195 if let Some(status_val) = span.attributes.get("status") {
197 if let Some(status_str) = status_val.as_str() {
198 return StatusCode::parse(status_str);
199 }
200 }
201
202 Ok(StatusCode::Unset)
204 }
205}
206
207impl Default for StatusExpectation {
208 fn default() -> Self {
209 Self::new()
210 }
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216 use serde_json::json;
217
218 fn create_span_with_status(name: &str, status: &str) -> SpanData {
219 let mut attrs = HashMap::new();
220 attrs.insert("otel.status_code".to_string(), json!(status));
221
222 SpanData {
223 name: name.to_string(),
224 trace_id: "test_trace".to_string(),
225 span_id: format!("span_{}", name),
226 parent_span_id: None,
227 start_time_unix_nano: Some(1000),
228 end_time_unix_nano: Some(2000),
229 attributes: attrs,
230 kind: None,
231 events: None,
232 resource_attributes: HashMap::new(),
233 }
234 }
235
236 fn create_span_without_status(name: &str) -> SpanData {
237 SpanData {
238 name: name.to_string(),
239 trace_id: "test_trace".to_string(),
240 span_id: format!("span_{}", name),
241 parent_span_id: None,
242 start_time_unix_nano: Some(1000),
243 end_time_unix_nano: Some(2000),
244 attributes: HashMap::new(),
245 kind: None,
246 events: None,
247 resource_attributes: HashMap::new(),
248 }
249 }
250
251 #[test]
252 fn test_status_code_parse_valid() -> Result<()> {
253 let unset = StatusCode::parse("UNSET")?;
255 let ok = StatusCode::parse("OK")?;
256 let error = StatusCode::parse("ERROR")?;
257
258 assert_eq!(unset, StatusCode::Unset);
260 assert_eq!(ok, StatusCode::Ok);
261 assert_eq!(error, StatusCode::Error);
262 Ok(())
263 }
264
265 #[test]
266 fn test_status_code_parse_case_insensitive() -> Result<()> {
267 let ok_lower = StatusCode::parse("ok")?;
269 let ok_mixed = StatusCode::parse("Ok")?;
270
271 assert_eq!(ok_lower, StatusCode::Ok);
273 assert_eq!(ok_mixed, StatusCode::Ok);
274 Ok(())
275 }
276
277 #[test]
278 fn test_status_code_parse_invalid() {
279 let result = StatusCode::parse("INVALID");
281
282 assert!(result.is_err());
284 assert!(result
285 .unwrap_err()
286 .to_string()
287 .contains("Invalid status code"));
288 }
289
290 #[test]
291 fn test_all_status_ok() -> Result<()> {
292 let spans = vec![
294 create_span_with_status("span1", "OK"),
295 create_span_with_status("span2", "OK"),
296 ];
297
298 let expectation = StatusExpectation::new().with_all(StatusCode::Ok);
299
300 let result = expectation.validate(&spans);
302
303 assert!(result.is_ok());
305 Ok(())
306 }
307
308 #[test]
309 fn test_all_status_fails() {
310 let spans = vec![
312 create_span_with_status("span1", "OK"),
313 create_span_with_status("span2", "ERROR"),
314 ];
315
316 let expectation = StatusExpectation::new().with_all(StatusCode::Ok);
317
318 let result = expectation.validate(&spans);
320
321 assert!(result.is_err());
323 let err_msg = result.unwrap_err().to_string();
324 assert!(err_msg.contains("span2"));
325 assert!(err_msg.contains("ERROR"));
326 assert!(err_msg.contains("expected OK"));
327 }
328
329 #[test]
330 fn test_glob_pattern_match() -> Result<()> {
331 let spans = vec![
333 create_span_with_status("clnrm.test1", "OK"),
334 create_span_with_status("clnrm.test2", "OK"),
335 create_span_with_status("other", "ERROR"),
336 ];
337
338 let expectation =
339 StatusExpectation::new().with_name_pattern("clnrm.*".to_string(), StatusCode::Ok);
340
341 let result = expectation.validate(&spans);
343
344 assert!(result.is_ok());
346 Ok(())
347 }
348
349 #[test]
350 fn test_glob_pattern_mismatch() {
351 let spans = vec![
353 create_span_with_status("clnrm.test1", "OK"),
354 create_span_with_status("clnrm.test2", "ERROR"),
355 ];
356
357 let expectation =
358 StatusExpectation::new().with_name_pattern("clnrm.*".to_string(), StatusCode::Ok);
359
360 let result = expectation.validate(&spans);
362
363 assert!(result.is_err());
365 let err_msg = result.unwrap_err().to_string();
366 assert!(err_msg.contains("clnrm.test2"));
367 assert!(err_msg.contains("clnrm.*"));
368 }
369
370 #[test]
371 fn test_glob_pattern_no_matches() {
372 let spans = vec![
374 create_span_with_status("span1", "OK"),
375 create_span_with_status("span2", "OK"),
376 ];
377
378 let expectation =
379 StatusExpectation::new().with_name_pattern("clnrm.*".to_string(), StatusCode::Ok);
380
381 let result = expectation.validate(&spans);
383
384 assert!(result.is_err());
386 let err_msg = result.unwrap_err().to_string();
387 assert!(err_msg.contains("no spans match pattern"));
388 assert!(err_msg.contains("clnrm.*"));
389 }
390
391 #[test]
392 fn test_invalid_glob_pattern() {
393 let spans = vec![create_span_with_status("span1", "OK")];
395
396 let expectation =
397 StatusExpectation::new().with_name_pattern("[invalid".to_string(), StatusCode::Ok);
398
399 let result = expectation.validate(&spans);
401
402 assert!(result.is_err());
404 let err_msg = result.unwrap_err().to_string();
405 assert!(err_msg.contains("Invalid glob pattern"));
406 }
407
408 #[test]
409 fn test_multiple_patterns() -> Result<()> {
410 let spans = vec![
412 create_span_with_status("clnrm.run", "OK"),
413 create_span_with_status("clnrm.test", "OK"),
414 create_span_with_status("http.request", "ERROR"),
415 ];
416
417 let expectation = StatusExpectation::new()
418 .with_name_pattern("clnrm.*".to_string(), StatusCode::Ok)
419 .with_name_pattern("http.*".to_string(), StatusCode::Error);
420
421 let result = expectation.validate(&spans);
423
424 assert!(result.is_ok());
426 Ok(())
427 }
428
429 #[test]
430 fn test_wildcard_patterns() -> Result<()> {
431 let spans = vec![
433 create_span_with_status("test_1", "OK"),
434 create_span_with_status("test_2", "OK"),
435 create_span_with_status("test_3", "OK"),
436 ];
437
438 let expectation =
439 StatusExpectation::new().with_name_pattern("test_?".to_string(), StatusCode::Ok);
440
441 let result = expectation.validate(&spans);
443
444 assert!(result.is_ok());
446 Ok(())
447 }
448
449 #[test]
450 fn test_default_unset_status() -> Result<()> {
451 let spans = vec![create_span_without_status("span1")];
453
454 let expectation = StatusExpectation::new().with_all(StatusCode::Unset);
455
456 let result = expectation.validate(&spans);
458
459 assert!(result.is_ok());
461 Ok(())
462 }
463
464 #[test]
465 fn test_alternative_status_attribute() -> Result<()> {
466 let mut attrs = HashMap::new();
468 attrs.insert("status".to_string(), json!("OK"));
469
470 let span = SpanData {
471 name: "span1".to_string(),
472 trace_id: "test_trace".to_string(),
473 span_id: "span1".to_string(),
474 parent_span_id: None,
475 start_time_unix_nano: Some(1000),
476 end_time_unix_nano: Some(2000),
477 attributes: attrs,
478 kind: None,
479 events: None,
480 resource_attributes: HashMap::new(),
481 };
482
483 let expectation = StatusExpectation::new().with_all(StatusCode::Ok);
484
485 let result = expectation.validate(&[span]);
487
488 assert!(result.is_ok());
490 Ok(())
491 }
492
493 #[test]
494 fn test_combining_all_and_pattern() -> Result<()> {
495 let spans = vec![
497 create_span_with_status("clnrm.test", "OK"),
498 create_span_with_status("other", "OK"),
499 ];
500
501 let expectation = StatusExpectation::new()
502 .with_all(StatusCode::Ok)
503 .with_name_pattern("clnrm.*".to_string(), StatusCode::Ok);
504
505 let result = expectation.validate(&spans);
507
508 assert!(result.is_ok());
510 Ok(())
511 }
512
513 #[test]
514 fn test_status_code_as_str() {
515 assert_eq!(StatusCode::Unset.as_str(), "UNSET");
517 assert_eq!(StatusCode::Ok.as_str(), "OK");
518 assert_eq!(StatusCode::Error.as_str(), "ERROR");
519 }
520}