1use crate::error::{CleanroomError, Result};
7use crate::validation::span_validator::SpanData;
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
13pub struct CountBound {
14 #[serde(skip_serializing_if = "Option::is_none")]
16 pub gte: Option<usize>,
17 #[serde(skip_serializing_if = "Option::is_none")]
19 pub lte: Option<usize>,
20 #[serde(skip_serializing_if = "Option::is_none")]
22 pub eq: Option<usize>,
23}
24
25impl CountBound {
26 pub fn gte(value: usize) -> Self {
28 Self {
29 gte: Some(value),
30 lte: None,
31 eq: None,
32 }
33 }
34
35 pub fn lte(value: usize) -> Self {
37 Self {
38 gte: None,
39 lte: Some(value),
40 eq: None,
41 }
42 }
43
44 pub fn eq(value: usize) -> Self {
46 Self {
47 gte: None,
48 lte: None,
49 eq: Some(value),
50 }
51 }
52
53 pub fn range(min: usize, max: usize) -> Result<Self> {
55 if min > max {
56 return Err(CleanroomError::validation_error(format!(
57 "Invalid range: min ({}) > max ({})",
58 min, max
59 )));
60 }
61 Ok(Self {
62 gte: Some(min),
63 lte: Some(max),
64 eq: None,
65 })
66 }
67
68 pub fn validate(&self, actual: usize, context: &str) -> Result<()> {
70 if let Some(expected) = self.eq {
72 if actual != expected {
73 return Err(CleanroomError::validation_error(format!(
74 "{}: expected exactly {} items, found {}",
75 context, expected, actual
76 )));
77 }
78 return Ok(());
79 }
80
81 if let Some(min) = self.gte {
83 if actual < min {
84 return Err(CleanroomError::validation_error(format!(
85 "{}: expected at least {} items, found {}",
86 context, min, actual
87 )));
88 }
89 }
90
91 if let Some(max) = self.lte {
93 if actual > max {
94 return Err(CleanroomError::validation_error(format!(
95 "{}: expected at most {} items, found {}",
96 context, max, actual
97 )));
98 }
99 }
100
101 Ok(())
102 }
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize, Default)]
107pub struct CountExpectation {
108 #[serde(skip_serializing_if = "Option::is_none")]
110 pub spans_total: Option<CountBound>,
111 #[serde(skip_serializing_if = "Option::is_none")]
113 pub events_total: Option<CountBound>,
114 #[serde(skip_serializing_if = "Option::is_none")]
116 pub errors_total: Option<CountBound>,
117 #[serde(skip_serializing_if = "Option::is_none")]
119 pub by_name: Option<HashMap<String, CountBound>>,
120}
121
122impl CountExpectation {
123 pub fn new() -> Self {
125 Self::default()
126 }
127
128 pub fn with_spans_total(mut self, bound: CountBound) -> Self {
130 self.spans_total = Some(bound);
131 self
132 }
133
134 pub fn with_events_total(mut self, bound: CountBound) -> Self {
136 self.events_total = Some(bound);
137 self
138 }
139
140 pub fn with_errors_total(mut self, bound: CountBound) -> Self {
142 self.errors_total = Some(bound);
143 self
144 }
145
146 pub fn with_name_count(mut self, name: String, bound: CountBound) -> Self {
148 self.by_name
149 .get_or_insert_with(HashMap::new)
150 .insert(name, bound);
151 self
152 }
153
154 pub fn validate(&self, spans: &[SpanData]) -> Result<()> {
166 if let Some(bound) = &self.spans_total {
168 bound.validate(spans.len(), "Total span count")?;
169 }
170
171 if let Some(bound) = &self.events_total {
173 let total_events = Self::count_total_events(spans);
174 bound.validate(total_events, "Total event count")?;
175 }
176
177 if let Some(bound) = &self.errors_total {
179 let total_errors = Self::count_error_spans(spans);
180 bound.validate(total_errors, "Total error count")?;
181 }
182
183 if let Some(by_name) = &self.by_name {
185 for (name, bound) in by_name {
186 let count = Self::count_spans_by_name(spans, name);
187 bound.validate(count, &format!("Count for span name '{}'", name))?;
188 }
189 }
190
191 Ok(())
192 }
193
194 fn count_total_events(spans: &[SpanData]) -> usize {
196 spans
197 .iter()
198 .map(|span| {
199 span.attributes
201 .get("event.count")
202 .and_then(|v| v.as_u64())
203 .unwrap_or(0) as usize
204 })
205 .sum()
206 }
207
208 fn count_error_spans(spans: &[SpanData]) -> usize {
210 spans
211 .iter()
212 .filter(|span| {
213 span.attributes
216 .get("otel.status_code")
217 .and_then(|v| v.as_str())
218 .map(|s| s == "ERROR")
219 .unwrap_or(false)
220 || span
222 .attributes
223 .get("error")
224 .and_then(|v| v.as_bool())
225 .unwrap_or(false)
226 })
227 .count()
228 }
229
230 fn count_spans_by_name(spans: &[SpanData], name: &str) -> usize {
232 spans.iter().filter(|span| span.name == name).count()
233 }
234}
235
236#[cfg(test)]
237mod tests {
238 use super::*;
239 use serde_json::json;
240
241 fn create_test_span(name: &str, is_error: bool) -> SpanData {
242 let mut attributes = HashMap::new();
243 if is_error {
244 attributes.insert("otel.status_code".to_string(), json!("ERROR"));
245 }
246
247 SpanData {
248 name: name.to_string(),
249 attributes,
250 trace_id: "test_trace".to_string(),
251 span_id: format!("span_{}", name),
252 parent_span_id: None,
253 start_time_unix_nano: Some(1000000),
254 end_time_unix_nano: Some(2000000),
255 kind: None,
256 events: None,
257 resource_attributes: HashMap::new(),
258 }
259 }
260
261 #[test]
262 fn test_count_bound_eq_valid() {
263 let bound = CountBound::eq(5);
265
266 let result = bound.validate(5, "Test count");
268
269 assert!(result.is_ok());
271 }
272
273 #[test]
274 fn test_count_bound_eq_invalid() {
275 let bound = CountBound::eq(5);
277
278 let result = bound.validate(3, "Test count");
280
281 assert!(result.is_err());
283 let err_msg = result.unwrap_err().to_string();
284 assert!(err_msg.contains("expected exactly 5"));
285 assert!(err_msg.contains("found 3"));
286 }
287
288 #[test]
289 fn test_count_bound_gte_valid() {
290 let bound = CountBound::gte(5);
292
293 assert!(bound.validate(5, "Test count").is_ok());
295 assert!(bound.validate(10, "Test count").is_ok());
296 }
297
298 #[test]
299 fn test_count_bound_gte_invalid() {
300 let bound = CountBound::gte(5);
302
303 let result = bound.validate(3, "Test count");
305
306 assert!(result.is_err());
308 let err_msg = result.unwrap_err().to_string();
309 assert!(err_msg.contains("expected at least 5"));
310 assert!(err_msg.contains("found 3"));
311 }
312
313 #[test]
314 fn test_count_bound_lte_valid() {
315 let bound = CountBound::lte(5);
317
318 assert!(bound.validate(5, "Test count").is_ok());
320 assert!(bound.validate(3, "Test count").is_ok());
321 assert!(bound.validate(0, "Test count").is_ok());
322 }
323
324 #[test]
325 fn test_count_bound_lte_invalid() {
326 let bound = CountBound::lte(5);
328
329 let result = bound.validate(10, "Test count");
331
332 assert!(result.is_err());
334 let err_msg = result.unwrap_err().to_string();
335 assert!(err_msg.contains("expected at most 5"));
336 assert!(err_msg.contains("found 10"));
337 }
338
339 #[test]
340 fn test_count_bound_range_valid() {
341 let bound = CountBound::range(5, 10).unwrap();
343
344 assert!(bound.validate(5, "Test count").is_ok());
346 assert!(bound.validate(7, "Test count").is_ok());
347 assert!(bound.validate(10, "Test count").is_ok());
348 }
349
350 #[test]
351 fn test_count_bound_range_invalid_below() {
352 let bound = CountBound::range(5, 10).unwrap();
354
355 let result = bound.validate(3, "Test count");
357
358 assert!(result.is_err());
360 let err_msg = result.unwrap_err().to_string();
361 assert!(err_msg.contains("expected at least 5"));
362 }
363
364 #[test]
365 fn test_count_bound_range_invalid_above() {
366 let bound = CountBound::range(5, 10).unwrap();
368
369 let result = bound.validate(15, "Test count");
371
372 assert!(result.is_err());
374 let err_msg = result.unwrap_err().to_string();
375 assert!(err_msg.contains("expected at most 10"));
376 }
377
378 #[test]
379 fn test_count_bound_range_invalid_creation() {
380 let result = CountBound::range(10, 5);
382
383 assert!(result.is_err());
385 let err_msg = result.unwrap_err().to_string();
386 assert!(err_msg.contains("Invalid range"));
387 }
388
389 #[test]
390 fn test_count_bound_eq_takes_precedence() {
391 let bound = CountBound {
393 gte: Some(1),
394 lte: Some(10),
395 eq: Some(5),
396 };
397
398 assert!(bound.validate(5, "Test count").is_ok());
400 assert!(bound.validate(7, "Test count").is_err()); }
402
403 #[test]
404 fn test_count_expectation_spans_total() {
405 let spans = vec![
407 create_test_span("span1", false),
408 create_test_span("span2", false),
409 create_test_span("span3", false),
410 ];
411 let expectation = CountExpectation::new().with_spans_total(CountBound::eq(3));
412
413 let result = expectation.validate(&spans);
415
416 assert!(result.is_ok());
418 }
419
420 #[test]
421 fn test_count_expectation_spans_total_invalid() {
422 let spans = vec![
424 create_test_span("span1", false),
425 create_test_span("span2", false),
426 ];
427 let expectation = CountExpectation::new().with_spans_total(CountBound::eq(3));
428
429 let result = expectation.validate(&spans);
431
432 assert!(result.is_err());
434 let err_msg = result.unwrap_err().to_string();
435 assert!(err_msg.contains("Total span count"));
436 }
437
438 #[test]
439 fn test_count_expectation_errors_total() {
440 let spans = vec![
442 create_test_span("span1", false),
443 create_test_span("span2", true),
444 create_test_span("span3", true),
445 ];
446 let expectation = CountExpectation::new().with_errors_total(CountBound::eq(2));
447
448 let result = expectation.validate(&spans);
450
451 assert!(result.is_ok());
453 }
454
455 #[test]
456 fn test_count_expectation_errors_total_zero() {
457 let spans = vec![
459 create_test_span("span1", false),
460 create_test_span("span2", false),
461 ];
462 let expectation = CountExpectation::new().with_errors_total(CountBound::eq(0));
463
464 let result = expectation.validate(&spans);
466
467 assert!(result.is_ok());
469 }
470
471 #[test]
472 fn test_count_expectation_errors_total_invalid() {
473 let spans = vec![
475 create_test_span("span1", false),
476 create_test_span("span2", true),
477 ];
478 let expectation = CountExpectation::new().with_errors_total(CountBound::eq(0));
479
480 let result = expectation.validate(&spans);
482
483 assert!(result.is_err());
485 let err_msg = result.unwrap_err().to_string();
486 assert!(err_msg.contains("Total error count"));
487 assert!(err_msg.contains("expected exactly 0"));
488 assert!(err_msg.contains("found 1"));
489 }
490
491 #[test]
492 fn test_count_expectation_by_name() {
493 let spans = vec![
495 create_test_span("clnrm.run", false),
496 create_test_span("clnrm.test", false),
497 create_test_span("clnrm.test", false),
498 create_test_span("clnrm.test", false),
499 ];
500 let expectation = CountExpectation::new()
501 .with_name_count("clnrm.run".to_string(), CountBound::eq(1))
502 .with_name_count("clnrm.test".to_string(), CountBound::eq(3));
503
504 let result = expectation.validate(&spans);
506
507 assert!(result.is_ok());
509 }
510
511 #[test]
512 fn test_count_expectation_by_name_invalid() {
513 let spans = vec![
515 create_test_span("clnrm.run", false),
516 create_test_span("clnrm.test", false),
517 ];
518 let expectation =
519 CountExpectation::new().with_name_count("clnrm.test".to_string(), CountBound::eq(3));
520
521 let result = expectation.validate(&spans);
523
524 assert!(result.is_err());
526 let err_msg = result.unwrap_err().to_string();
527 assert!(err_msg.contains("Count for span name 'clnrm.test'"));
528 assert!(err_msg.contains("expected exactly 3"));
529 assert!(err_msg.contains("found 1"));
530 }
531
532 #[test]
533 fn test_count_expectation_by_name_missing() {
534 let spans = vec![create_test_span("clnrm.run", false)];
536 let expectation = CountExpectation::new()
537 .with_name_count("clnrm.missing".to_string(), CountBound::gte(1));
538
539 let result = expectation.validate(&spans);
541
542 assert!(result.is_err());
544 let err_msg = result.unwrap_err().to_string();
545 assert!(err_msg.contains("Count for span name 'clnrm.missing'"));
546 assert!(err_msg.contains("expected at least 1"));
547 assert!(err_msg.contains("found 0"));
548 }
549
550 #[test]
551 fn test_count_expectation_multiple_constraints() {
552 let spans = vec![
554 create_test_span("clnrm.run", false),
555 create_test_span("clnrm.test", false),
556 create_test_span("clnrm.test", false),
557 ];
558 let expectation = CountExpectation::new()
559 .with_spans_total(CountBound::range(2, 5).unwrap())
560 .with_errors_total(CountBound::eq(0))
561 .with_name_count("clnrm.run".to_string(), CountBound::gte(1))
562 .with_name_count("clnrm.test".to_string(), CountBound::lte(3));
563
564 let result = expectation.validate(&spans);
566
567 assert!(result.is_ok());
569 }
570
571 #[test]
572 fn test_count_expectation_events_total() {
573 let mut span1 = create_test_span("span1", false);
575 span1.attributes.insert("event.count".to_string(), json!(2));
576 let mut span2 = create_test_span("span2", false);
577 span2.attributes.insert("event.count".to_string(), json!(3));
578 let spans = vec![span1, span2];
579 let expectation = CountExpectation::new().with_events_total(CountBound::eq(5));
580
581 let result = expectation.validate(&spans);
583
584 assert!(result.is_ok());
586 }
587
588 #[test]
589 fn test_count_expectation_empty_spans() {
590 let spans: Vec<SpanData> = vec![];
592 let expectation = CountExpectation::new()
593 .with_spans_total(CountBound::eq(0))
594 .with_errors_total(CountBound::eq(0));
595
596 let result = expectation.validate(&spans);
598
599 assert!(result.is_ok());
601 }
602
603 #[test]
604 fn test_count_expectation_no_constraints() {
605 let spans = vec![create_test_span("span1", false)];
607 let expectation = CountExpectation::new();
608
609 let result = expectation.validate(&spans);
611
612 assert!(result.is_ok());
614 }
615
616 #[test]
617 fn test_serde_count_bound() {
618 let bound = CountBound::range(5, 10).unwrap();
620
621 let json = serde_json::to_string(&bound).unwrap();
623 let deserialized: CountBound = serde_json::from_str(&json).unwrap();
624
625 assert_eq!(bound, deserialized);
627 }
628
629 #[test]
630 fn test_serde_count_expectation() {
631 let expectation = CountExpectation::new()
633 .with_spans_total(CountBound::eq(5))
634 .with_name_count("test".to_string(), CountBound::gte(1));
635
636 let json = serde_json::to_string(&expectation).unwrap();
638 let deserialized: CountExpectation = serde_json::from_str(&json).unwrap();
639
640 assert!(deserialized.spans_total.is_some());
642 assert!(deserialized.by_name.is_some());
643 }
644
645 #[test]
646 fn test_error_detection_via_error_attribute() {
647 let mut span = create_test_span("span1", false);
649 span.attributes.insert("error".to_string(), json!(true));
650 let spans = vec![span];
651 let expectation = CountExpectation::new().with_errors_total(CountBound::eq(1));
652
653 let result = expectation.validate(&spans);
655
656 assert!(result.is_ok());
658 }
659}