1use crate::error::{CleanroomError, Result};
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10use std::path::Path;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
14#[serde(rename_all = "lowercase")]
15pub enum SpanKind {
16 Internal,
18 Server,
20 Client,
22 Producer,
24 Consumer,
26}
27
28impl SpanKind {
29 pub fn parse_kind(s: &str) -> Result<Self> {
31 match s.to_lowercase().as_str() {
32 "internal" => Ok(SpanKind::Internal),
33 "server" => Ok(SpanKind::Server),
34 "client" => Ok(SpanKind::Client),
35 "producer" => Ok(SpanKind::Producer),
36 "consumer" => Ok(SpanKind::Consumer),
37 _ => Err(CleanroomError::validation_error(format!(
38 "Invalid span kind: '{}'. Must be one of: internal, server, client, producer, consumer",
39 s
40 ))),
41 }
42 }
43
44 pub fn to_otel_int(&self) -> i32 {
46 match self {
47 SpanKind::Internal => 1,
48 SpanKind::Server => 2,
49 SpanKind::Client => 3,
50 SpanKind::Producer => 4,
51 SpanKind::Consumer => 5,
52 }
53 }
54
55 pub fn from_otel_int(i: i32) -> Result<Self> {
57 match i {
58 1 => Ok(SpanKind::Internal),
59 2 => Ok(SpanKind::Server),
60 3 => Ok(SpanKind::Client),
61 4 => Ok(SpanKind::Producer),
62 5 => Ok(SpanKind::Consumer),
63 _ => Err(CleanroomError::validation_error(format!(
64 "Invalid OTEL span kind integer: {}",
65 i
66 ))),
67 }
68 }
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct SpanData {
74 pub name: String,
76 pub attributes: HashMap<String, serde_json::Value>,
78 pub trace_id: String,
80 pub span_id: String,
82 pub parent_span_id: Option<String>,
84 pub start_time_unix_nano: Option<u64>,
86 pub end_time_unix_nano: Option<u64>,
88 pub kind: Option<SpanKind>,
90 pub events: Option<Vec<String>>,
92 #[serde(default)]
94 pub resource_attributes: HashMap<String, serde_json::Value>,
95}
96
97impl SpanData {
98 pub fn duration_ms(&self) -> Option<f64> {
100 match (self.start_time_unix_nano, self.end_time_unix_nano) {
101 (Some(start), Some(end)) => {
102 if end >= start {
103 Some((end - start) as f64 / 1_000_000.0)
104 } else {
105 None
106 }
107 }
108 _ => None,
109 }
110 }
111}
112
113#[derive(Debug, Clone)]
115pub enum SpanAssertion {
116 SpanExists { name: String },
118 SpanCount { name: String, count: usize },
120 SpanAttribute {
122 name: String,
123 attribute_key: String,
124 attribute_value: String,
125 },
126 SpanHierarchy { parent: String, child: String },
128
129 SpanKind { name: String, kind: SpanKind },
132
133 SpanAllAttributes {
136 name: String,
137 attributes: HashMap<String, String>,
138 },
139
140 SpanAnyAttributes {
144 name: String,
145 attribute_patterns: Vec<String>,
146 },
147
148 SpanEvents { name: String, events: Vec<String> },
151
152 SpanDuration {
155 name: String,
156 min_ms: Option<u64>,
157 max_ms: Option<u64>,
158 },
159}
160
161#[derive(Debug, Clone, Serialize, Deserialize)]
163pub struct FailureDetails {
164 pub rule: String,
166 pub span_name: String,
168 pub expected: String,
170 pub actual: Option<String>,
172 pub message: String,
174}
175
176#[derive(Debug, Clone, Serialize, Deserialize)]
178pub struct ValidationResult {
179 pub passed: bool,
181 pub failures: Vec<FailureDetails>,
183 pub validations_count: usize,
185}
186
187impl ValidationResult {
188 pub fn success(validations_count: usize) -> Self {
190 Self {
191 passed: true,
192 failures: Vec::new(),
193 validations_count,
194 }
195 }
196
197 pub fn failure(failure: FailureDetails) -> Self {
199 Self {
200 passed: false,
201 failures: vec![failure],
202 validations_count: 1,
203 }
204 }
205
206 pub fn add_failure(&mut self, failure: FailureDetails) {
208 self.passed = false;
209 self.failures.push(failure);
210 }
211
212 pub fn merge(results: Vec<ValidationResult>) -> Self {
214 let passed = results.iter().all(|r| r.passed);
215 let failures: Vec<FailureDetails> = results
216 .iter()
217 .flat_map(|r| r.failures.clone())
218 .collect();
219 let validations_count: usize = results.iter().map(|r| r.validations_count).sum();
220
221 Self {
222 passed,
223 failures,
224 validations_count,
225 }
226 }
227}
228
229pub struct SpanValidator {
231 pub(crate) spans: Vec<SpanData>,
233}
234
235impl SpanValidator {
236 pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
250 let content = std::fs::read_to_string(path.as_ref()).map_err(|e| {
251 CleanroomError::config_error(format!("Failed to read spans file: {}", e))
252 })?;
253
254 Self::from_json(&content)
255 }
256
257 pub fn from_json(json: &str) -> Result<Self> {
268 let mut all_spans = Vec::new();
271
272 for line in json.lines() {
273 let line = line.trim();
274 if line.is_empty() {
275 continue;
276 }
277
278 if let Ok(spans) = serde_json::from_str::<Vec<SpanData>>(line) {
280 all_spans.extend(spans);
281 } else if let Ok(span) = serde_json::from_str::<SpanData>(line) {
282 all_spans.push(span);
284 } else {
285 if let Ok(value) = serde_json::from_str::<serde_json::Value>(line) {
287 if let Some(spans) = Self::extract_spans_from_otel_format(&value) {
288 all_spans.extend(spans);
289 }
290 }
291 }
292 }
293
294 Ok(Self { spans: all_spans })
295 }
296
297 fn extract_spans_from_otel_format(value: &serde_json::Value) -> Option<Vec<SpanData>> {
299 let mut spans = Vec::new();
300
301 if let Some(resource_spans) = value.get("resourceSpans").and_then(|v| v.as_array()) {
303 for resource_span in resource_spans {
304 if let Some(scope_spans) =
305 resource_span.get("scopeSpans").and_then(|v| v.as_array())
306 {
307 for scope_span in scope_spans {
308 if let Some(span_array) = scope_span.get("spans").and_then(|v| v.as_array())
309 {
310 for span_obj in span_array {
311 if let Some(span) = Self::parse_otel_span(span_obj) {
312 spans.push(span);
313 }
314 }
315 }
316 }
317 }
318 }
319 }
320
321 if spans.is_empty() {
322 None
323 } else {
324 Some(spans)
325 }
326 }
327
328 fn parse_otel_span(span_obj: &serde_json::Value) -> Option<SpanData> {
330 let name = span_obj.get("name")?.as_str()?.to_string();
331 let trace_id = span_obj.get("traceId")?.as_str()?.to_string();
332 let span_id = span_obj.get("spanId")?.as_str()?.to_string();
333 let parent_span_id = span_obj
334 .get("parentSpanId")
335 .and_then(|v| v.as_str())
336 .map(|s| s.to_string());
337
338 let start_time_unix_nano = span_obj
339 .get("startTimeUnixNano")
340 .and_then(|v| v.as_str())
341 .and_then(|s| s.parse::<u64>().ok());
342
343 let end_time_unix_nano = span_obj
344 .get("endTimeUnixNano")
345 .and_then(|v| v.as_str())
346 .and_then(|s| s.parse::<u64>().ok());
347
348 let kind = span_obj
350 .get("kind")
351 .and_then(|v| v.as_i64())
352 .and_then(|i| SpanKind::from_otel_int(i as i32).ok());
353
354 let mut attributes = HashMap::new();
356 if let Some(attrs) = span_obj.get("attributes").and_then(|v| v.as_array()) {
357 for attr in attrs {
358 if let (Some(key), Some(value)) =
359 (attr.get("key").and_then(|k| k.as_str()), attr.get("value"))
360 {
361 attributes.insert(key.to_string(), value.clone());
362 }
363 }
364 }
365
366 let events = span_obj
368 .get("events")
369 .and_then(|v| v.as_array())
370 .map(|events_array| {
371 events_array
372 .iter()
373 .filter_map(|event| {
374 event.get("name").and_then(|n| n.as_str()).map(String::from)
375 })
376 .collect()
377 });
378
379 Some(SpanData {
380 name,
381 attributes,
382 trace_id,
383 span_id,
384 parent_span_id,
385 start_time_unix_nano,
386 end_time_unix_nano,
387 kind,
388 events,
389 resource_attributes: HashMap::new(),
390 })
391 }
392
393 pub fn spans(&self) -> &[SpanData] {
395 &self.spans
396 }
397
398 pub fn find_spans_by_name(&self, name: &str) -> Vec<&SpanData> {
400 self.spans.iter().filter(|s| s.name == name).collect()
401 }
402
403 pub fn find_span(&self, trace_id: &str, span_id: &str) -> Option<&SpanData> {
405 self.spans
406 .iter()
407 .find(|s| s.trace_id == trace_id && s.span_id == span_id)
408 }
409
410 pub fn has_span(&self, name: &str) -> bool {
412 self.spans.iter().any(|s| s.name == name)
413 }
414
415 pub fn count_spans(&self, name: &str) -> usize {
417 self.spans.iter().filter(|s| s.name == name).count()
418 }
419
420 pub fn validate_expectations(
431 &self,
432 expectations: &[crate::config::SpanExpectationConfig],
433 ) -> Result<ValidationResult> {
434 let mut results = Vec::new();
435
436 for expectation in expectations {
437 let result = self.validate_single_expectation(expectation)?;
438 results.push(result);
439 }
440
441 Ok(ValidationResult::merge(results))
442 }
443
444 fn validate_single_expectation(
446 &self,
447 expectation: &crate::config::SpanExpectationConfig,
448 ) -> Result<ValidationResult> {
449 let span_name = &expectation.name;
450
451 let matching_spans = self.find_spans_by_name(span_name);
453 if matching_spans.is_empty() {
454 return Ok(ValidationResult::failure(FailureDetails {
455 rule: format!("expect.span[{}].existence", span_name),
456 span_name: span_name.clone(),
457 expected: format!("Span '{}' to exist", span_name),
458 actual: None,
459 message: format!("Span '{}' not found in trace", span_name),
460 }));
461 }
462
463 let mut validation_count = 1; let mut failures = Vec::new();
465
466 let span = matching_spans[0];
469
470 if let Some(ref parent_name) = expectation.parent {
472 validation_count += 1;
473 if let Some(failure) = self.validate_parent_relationship(span, parent_name, span_name)
474 {
475 failures.push(failure);
476 }
477 }
478
479 if let Some(ref kind_str) = expectation.kind {
481 validation_count += 1;
482 if let Some(failure) = self.validate_span_kind(span, kind_str, span_name)? {
483 failures.push(failure);
484 }
485 }
486
487 if let Some(ref attrs_config) = expectation.attrs {
489 if let Some(ref all_attrs) = attrs_config.all {
491 validation_count += all_attrs.len();
492 if let Some(failure) = self.validate_attrs_all(span, all_attrs, span_name) {
493 failures.push(failure);
494 }
495 }
496
497 if let Some(ref any_attrs) = attrs_config.any {
499 validation_count += 1;
500 if let Some(failure) = self.validate_attrs_any(span, any_attrs, span_name) {
501 failures.push(failure);
502 }
503 }
504 }
505
506 if let Some(ref events_config) = expectation.events {
508 if let Some(ref any_events) = events_config.any {
509 validation_count += 1;
510 if let Some(failure) = self.validate_events_any(span, any_events, span_name) {
511 failures.push(failure);
512 }
513 }
514
515 if let Some(ref all_events) = events_config.all {
516 validation_count += all_events.len();
517 if let Some(failure) = self.validate_events_all(span, all_events, span_name) {
518 failures.push(failure);
519 }
520 }
521 }
522
523 if let Some(ref duration_config) = expectation.duration_ms {
525 validation_count += 1;
526 if let Some(failure) = self.validate_duration(span, duration_config, span_name) {
527 failures.push(failure);
528 }
529 }
530
531 if failures.is_empty() {
532 Ok(ValidationResult::success(validation_count))
533 } else {
534 Ok(ValidationResult {
535 passed: false,
536 failures,
537 validations_count: validation_count,
538 })
539 }
540 }
541
542 fn validate_parent_relationship(
544 &self,
545 span: &SpanData,
546 parent_name: &str,
547 span_name: &str,
548 ) -> Option<FailureDetails> {
549 let parent_spans = self.find_spans_by_name(parent_name);
551 if parent_spans.is_empty() {
552 return Some(FailureDetails {
553 rule: format!("expect.span[{}].parent", span_name),
554 span_name: span_name.to_string(),
555 expected: format!("Parent span '{}'", parent_name),
556 actual: None,
557 message: format!(
558 "Parent span '{}' not found for span '{}'",
559 parent_name, span_name
560 ),
561 });
562 }
563
564 if let Some(ref parent_id) = span.parent_span_id {
566 if parent_spans.iter().any(|p| &p.span_id == parent_id) {
567 return None; }
569
570 Some(FailureDetails {
572 rule: format!("expect.span[{}].parent", span_name),
573 span_name: span_name.to_string(),
574 expected: format!("Parent span '{}'", parent_name),
575 actual: Some(format!("Different parent (ID: {})", parent_id)),
576 message: format!(
577 "Span '{}' parent mismatch: expected '{}', found different parent",
578 span_name, parent_name
579 ),
580 })
581 } else {
582 Some(FailureDetails {
584 rule: format!("expect.span[{}].parent", span_name),
585 span_name: span_name.to_string(),
586 expected: format!("Parent span '{}'", parent_name),
587 actual: Some("none".to_string()),
588 message: format!(
589 "Span '{}' parent mismatch: expected '{}', found none",
590 span_name, parent_name
591 ),
592 })
593 }
594 }
595
596 fn validate_span_kind(
598 &self,
599 span: &SpanData,
600 kind_str: &str,
601 span_name: &str,
602 ) -> Result<Option<FailureDetails>> {
603 let expected_kind = SpanKind::parse_kind(kind_str)?;
604
605 match span.kind {
606 Some(actual_kind) if actual_kind == expected_kind => Ok(None),
607 Some(actual_kind) => Ok(Some(FailureDetails {
608 rule: format!("expect.span[{}].kind", span_name),
609 span_name: span_name.to_string(),
610 expected: format!("{:?}", expected_kind),
611 actual: Some(format!("{:?}", actual_kind)),
612 message: format!(
613 "Span '{}' kind mismatch: expected {:?}, found {:?}",
614 span_name, expected_kind, actual_kind
615 ),
616 })),
617 None => Ok(Some(FailureDetails {
618 rule: format!("expect.span[{}].kind", span_name),
619 span_name: span_name.to_string(),
620 expected: format!("{:?}", expected_kind),
621 actual: None,
622 message: format!(
623 "Span '{}' kind mismatch: expected {:?}, found none",
624 span_name, expected_kind
625 ),
626 })),
627 }
628 }
629
630 fn validate_attrs_all(
632 &self,
633 span: &SpanData,
634 all_attrs: &HashMap<String, String>,
635 span_name: &str,
636 ) -> Option<FailureDetails> {
637 let mut missing = Vec::new();
638
639 for (key, expected_value) in all_attrs {
640 let matches = span
641 .attributes
642 .get(key)
643 .and_then(|v| v.as_str())
644 .map(|v| v == expected_value)
645 .unwrap_or(false);
646
647 if !matches {
648 let actual = span
649 .attributes
650 .get(key)
651 .and_then(|v| v.as_str())
652 .map(|s| s.to_string());
653
654 if actual.is_none() {
655 missing.push(format!("{}={}", key, expected_value));
656 } else {
657 missing.push(format!(
658 "{}={} (found: {})",
659 key,
660 expected_value,
661 actual.unwrap_or_default()
662 ));
663 }
664 }
665 }
666
667 if missing.is_empty() {
668 None
669 } else {
670 Some(FailureDetails {
671 rule: format!("expect.span[{}].attrs.all", span_name),
672 span_name: span_name.to_string(),
673 expected: format!("All attributes: {:?}", all_attrs),
674 actual: Some(format!("Missing/incorrect: [{}]", missing.join(", "))),
675 message: format!(
676 "Span '{}' missing required attributes: [{}]",
677 span_name,
678 missing.join(", ")
679 ),
680 })
681 }
682 }
683
684 fn validate_attrs_any(
686 &self,
687 span: &SpanData,
688 any_attrs: &HashMap<String, String>,
689 span_name: &str,
690 ) -> Option<FailureDetails> {
691 let has_any = any_attrs.iter().any(|(key, expected_value)| {
692 span.attributes
693 .get(key)
694 .and_then(|v| v.as_str())
695 .map(|v| v == expected_value)
696 .unwrap_or(false)
697 });
698
699 if has_any {
700 None
701 } else {
702 let patterns: Vec<String> = any_attrs
703 .iter()
704 .map(|(k, v)| format!("{}={}", k, v))
705 .collect();
706
707 Some(FailureDetails {
708 rule: format!("expect.span[{}].attrs.any", span_name),
709 span_name: span_name.to_string(),
710 expected: format!("Any of: [{}]", patterns.join(", ")),
711 actual: None,
712 message: format!(
713 "Span '{}' missing any of required attributes: [{}]",
714 span_name,
715 patterns.join(", ")
716 ),
717 })
718 }
719 }
720
721 fn validate_events_any(
723 &self,
724 span: &SpanData,
725 any_events: &[String],
726 span_name: &str,
727 ) -> Option<FailureDetails> {
728 if let Some(ref span_events) = span.events {
729 let has_any = any_events.iter().any(|event| span_events.contains(event));
730
731 if has_any {
732 return None;
733 }
734 }
735
736 Some(FailureDetails {
737 rule: format!("expect.span[{}].events.any", span_name),
738 span_name: span_name.to_string(),
739 expected: format!("Any of: [{}]", any_events.join(", ")),
740 actual: span.events.as_ref().map(|events| format!("{:?}", events)),
741 message: format!(
742 "Span '{}' missing required events: [{}]",
743 span_name,
744 any_events.join(", ")
745 ),
746 })
747 }
748
749 fn validate_events_all(
751 &self,
752 span: &SpanData,
753 all_events: &[String],
754 span_name: &str,
755 ) -> Option<FailureDetails> {
756 if let Some(ref span_events) = span.events {
757 let missing: Vec<&String> = all_events
758 .iter()
759 .filter(|event| !span_events.contains(event))
760 .collect();
761
762 if missing.is_empty() {
763 return None;
764 }
765
766 return Some(FailureDetails {
767 rule: format!("expect.span[{}].events.all", span_name),
768 span_name: span_name.to_string(),
769 expected: format!("All of: [{}]", all_events.join(", ")),
770 actual: Some(format!("Missing: {:?}", missing)),
771 message: format!(
772 "Span '{}' missing required events: {:?}",
773 span_name, missing
774 ),
775 });
776 }
777
778 Some(FailureDetails {
779 rule: format!("expect.span[{}].events.all", span_name),
780 span_name: span_name.to_string(),
781 expected: format!("All of: [{}]", all_events.join(", ")),
782 actual: None,
783 message: format!("Span '{}' has no events", span_name),
784 })
785 }
786
787 fn validate_duration(
789 &self,
790 span: &SpanData,
791 duration_config: &crate::config::DurationBoundConfig,
792 span_name: &str,
793 ) -> Option<FailureDetails> {
794 let duration_ms = span.duration_ms()?;
795
796 if let Some(min) = duration_config.min {
798 if duration_ms < min {
799 return Some(FailureDetails {
800 rule: format!("expect.span[{}].duration_ms.min", span_name),
801 span_name: span_name.to_string(),
802 expected: format!("duration >= {}ms", min),
803 actual: Some(format!("{}ms", duration_ms)),
804 message: format!(
805 "Span '{}' duration {}ms < min {}ms",
806 span_name, duration_ms, min
807 ),
808 });
809 }
810 }
811
812 if let Some(max) = duration_config.max {
814 if duration_ms > max {
815 return Some(FailureDetails {
816 rule: format!("expect.span[{}].duration_ms.max", span_name),
817 span_name: span_name.to_string(),
818 expected: format!("duration <= {}ms", max),
819 actual: Some(format!("{}ms", duration_ms)),
820 message: format!(
821 "Span '{}' duration {}ms > max {}ms",
822 span_name, duration_ms, max
823 ),
824 });
825 }
826 }
827
828 None
829 }
830
831 pub fn first_failure(result: &ValidationResult) -> Option<&FailureDetails> {
833 result.failures.first()
834 }
835
836 pub fn validate_assertion(&self, assertion: &SpanAssertion) -> Result<()> {
838 match assertion {
839 SpanAssertion::SpanExists { name } => {
840 if !self.has_span(name) {
841 return Err(CleanroomError::validation_error(format!(
842 "Span assertion failed: span '{}' does not exist",
843 name
844 )));
845 }
846 Ok(())
847 }
848 SpanAssertion::SpanCount { name, count } => {
849 let actual_count = self.count_spans(name);
850 if actual_count != *count {
851 return Err(CleanroomError::validation_error(format!(
852 "Span count assertion failed: expected {} spans named '{}', found {}",
853 count, name, actual_count
854 )));
855 }
856 Ok(())
857 }
858 SpanAssertion::SpanAttribute {
859 name,
860 attribute_key,
861 attribute_value,
862 } => {
863 let spans = self.find_spans_by_name(name);
864 if spans.is_empty() {
865 return Err(CleanroomError::validation_error(format!(
866 "Span attribute assertion failed: span '{}' does not exist",
867 name
868 )));
869 }
870
871 let has_attribute = spans.iter().any(|span| {
873 span.attributes
875 .get(attribute_key)
876 .and_then(|v| v.as_str())
877 .map(|v| v == attribute_value)
878 .unwrap_or(false)
879 });
880
881 if !has_attribute {
882 return Err(CleanroomError::validation_error(format!(
883 "Span attribute assertion failed: no span '{}' has attribute '{}' = '{}'",
884 name, attribute_key, attribute_value
885 )));
886 }
887 Ok(())
888 }
889 SpanAssertion::SpanHierarchy { parent, child } => {
890 let parent_spans = self.find_spans_by_name(parent);
891 let child_spans = self.find_spans_by_name(child);
892
893 if parent_spans.is_empty() {
894 return Err(CleanroomError::validation_error(format!(
895 "Span hierarchy assertion failed: parent span '{}' does not exist",
896 parent
897 )));
898 }
899
900 if child_spans.is_empty() {
901 return Err(CleanroomError::validation_error(format!(
902 "Span hierarchy assertion failed: child span '{}' does not exist",
903 child
904 )));
905 }
906
907 let has_hierarchy = child_spans.iter().any(|child_span| {
909 if let Some(parent_id) = &child_span.parent_span_id {
910 parent_spans.iter().any(|p| &p.span_id == parent_id)
911 } else {
912 false
913 }
914 });
915
916 if !has_hierarchy {
917 return Err(CleanroomError::validation_error(format!(
918 "Span hierarchy assertion failed: no '{}' span is a child of '{}' span",
919 child, parent
920 )));
921 }
922 Ok(())
923 }
924
925 SpanAssertion::SpanKind { name, kind } => {
927 let spans = self.find_spans_by_name(name);
928 if spans.is_empty() {
929 return Err(CleanroomError::validation_error(format!(
930 "Span kind assertion failed: span '{}' does not exist",
931 name
932 )));
933 }
934
935 let has_kind = spans
938 .iter()
939 .any(|span| span.kind.map(|k| k == *kind).unwrap_or(false));
940
941 if !has_kind {
942 return Err(CleanroomError::validation_error(format!(
943 "Span kind assertion failed: no span '{}' has kind '{:?}'",
944 name, kind
945 )));
946 }
947 Ok(())
948 }
949
950 SpanAssertion::SpanAllAttributes { name, attributes } => {
951 let spans = self.find_spans_by_name(name);
952 if spans.is_empty() {
953 return Err(CleanroomError::validation_error(format!(
954 "Span all attributes assertion failed: span '{}' does not exist",
955 name
956 )));
957 }
958
959 let has_all_attributes = spans.iter().any(|span| {
961 attributes.iter().all(|(key, expected_value)| {
962 span.attributes
964 .get(key)
965 .and_then(|v| v.as_str())
966 .map(|v| v == expected_value)
967 .unwrap_or(false)
968 })
969 });
970
971 if !has_all_attributes {
972 let missing: Vec<String> = attributes
973 .iter()
974 .filter(|(key, expected_value)| {
975 !spans.iter().any(|span| {
976 span.attributes
978 .get(*key)
979 .and_then(|v| v.as_str())
980 .map(|v| v == *expected_value)
981 .unwrap_or(false)
982 })
983 })
984 .map(|(k, v)| format!("{}={}", k, v))
985 .collect();
986
987 return Err(CleanroomError::validation_error(format!(
988 "Span all attributes assertion failed: span '{}' is missing attributes: [{}]",
989 name,
990 missing.join(", ")
991 )));
992 }
993 Ok(())
994 }
995
996 SpanAssertion::SpanAnyAttributes {
997 name,
998 attribute_patterns,
999 } => {
1000 let spans = self.find_spans_by_name(name);
1001 if spans.is_empty() {
1002 return Err(CleanroomError::validation_error(format!(
1003 "Span any attributes assertion failed: span '{}' does not exist",
1004 name
1005 )));
1006 }
1007
1008 let has_any_match = spans.iter().any(|span| {
1010 attribute_patterns.iter().any(|pattern| {
1011 if let Some((key, value)) = pattern.split_once('=') {
1012 span.attributes
1014 .get(key)
1015 .and_then(|v| v.as_str())
1016 .map(|v| v == value)
1017 .unwrap_or(false)
1018 } else {
1019 false
1020 }
1021 })
1022 });
1023
1024 if !has_any_match {
1025 return Err(CleanroomError::validation_error(format!(
1026 "Span any attributes assertion failed: span '{}' does not have any of the patterns: [{}]",
1027 name,
1028 attribute_patterns.join(", ")
1029 )));
1030 }
1031 Ok(())
1032 }
1033
1034 SpanAssertion::SpanEvents { name, events } => {
1035 let spans = self.find_spans_by_name(name);
1036 if spans.is_empty() {
1037 return Err(CleanroomError::validation_error(format!(
1038 "Span events assertion failed: span '{}' does not exist",
1039 name
1040 )));
1041 }
1042
1043 let has_any_event = spans.iter().any(|span| {
1045 if let Some(span_events) = &span.events {
1046 events.iter().any(|event| span_events.contains(event))
1047 } else {
1048 false
1049 }
1050 });
1051
1052 if !has_any_event {
1053 return Err(CleanroomError::validation_error(format!(
1054 "Span events assertion failed: span '{}' does not have any of the events: [{}]",
1055 name,
1056 events.join(", ")
1057 )));
1058 }
1059 Ok(())
1060 }
1061
1062 SpanAssertion::SpanDuration {
1063 name,
1064 min_ms,
1065 max_ms,
1066 } => {
1067 let spans = self.find_spans_by_name(name);
1068 if spans.is_empty() {
1069 return Err(CleanroomError::validation_error(format!(
1070 "Span duration assertion failed: span '{}' does not exist",
1071 name
1072 )));
1073 }
1074
1075 let has_valid_duration = spans.iter().any(|span| {
1077 if let Some(duration) = span.duration_ms() {
1078 let duration_u64 = duration as u64;
1079
1080 let min_ok = min_ms.map(|min| duration_u64 >= min).unwrap_or(true);
1081 let max_ok = max_ms.map(|max| duration_u64 <= max).unwrap_or(true);
1082
1083 min_ok && max_ok
1084 } else {
1085 false
1086 }
1087 });
1088
1089 if !has_valid_duration {
1090 let bounds = match (min_ms, max_ms) {
1091 (Some(min), Some(max)) => format!("between {}ms and {}ms", min, max),
1092 (Some(min), None) => format!("at least {}ms", min),
1093 (None, Some(max)) => format!("at most {}ms", max),
1094 (None, None) => "any duration".to_string(),
1095 };
1096
1097 return Err(CleanroomError::validation_error(format!(
1098 "Span duration assertion failed: span '{}' does not have duration {}",
1099 name, bounds
1100 )));
1101 }
1102 Ok(())
1103 }
1104 }
1105 }
1106
1107 pub fn validate_assertions(&self, assertions: &[SpanAssertion]) -> Result<()> {
1109 for assertion in assertions {
1110 self.validate_assertion(assertion)?;
1111 }
1112 Ok(())
1113 }
1114
1115 pub fn get_span(&self, name: &str) -> Option<&SpanData> {
1117 self.spans.iter().find(|s| s.name == name)
1118 }
1119
1120 pub fn get_span_by_id(&self, span_id: &str) -> Option<&SpanData> {
1122 self.spans.iter().find(|s| s.span_id == span_id)
1123 }
1124
1125 pub fn all_spans(&self) -> &[SpanData] {
1127 &self.spans
1128 }
1129}
1130
1131#[cfg(test)]
1132mod tests {
1133 use super::*;
1134 use crate::config::SpanAttributesConfig;
1135
1136 #[test]
1137 fn test_span_validator_from_json_empty() {
1138 let json = "";
1140
1141 let validator = SpanValidator::from_json(json).unwrap();
1143
1144 assert_eq!(validator.spans().len(), 0);
1146 }
1147
1148 #[test]
1149 fn test_span_validator_single_span() {
1150 let json = r#"{"name":"test.span","trace_id":"abc123","span_id":"span1","parent_span_id":null,"attributes":{}}"#;
1152
1153 let validator = SpanValidator::from_json(json).unwrap();
1155
1156 assert_eq!(validator.spans().len(), 1);
1158 assert_eq!(validator.spans()[0].name, "test.span");
1159 }
1160
1161 #[test]
1162 fn test_span_exists_assertion() {
1163 let json = r#"{"name":"clnrm.run","trace_id":"abc123","span_id":"span1","parent_span_id":null,"attributes":{}}"#;
1165 let validator = SpanValidator::from_json(json).unwrap();
1166 let assertion = SpanAssertion::SpanExists {
1167 name: "clnrm.run".to_string(),
1168 };
1169
1170 let result = validator.validate_assertion(&assertion);
1172
1173 assert!(result.is_ok());
1175 }
1176
1177 #[test]
1178 fn test_span_exists_assertion_fails() {
1179 let json = r#"{"name":"clnrm.run","trace_id":"abc123","span_id":"span1","parent_span_id":null,"attributes":{}}"#;
1181 let validator = SpanValidator::from_json(json).unwrap();
1182 let assertion = SpanAssertion::SpanExists {
1183 name: "clnrm.test".to_string(),
1184 };
1185
1186 let result = validator.validate_assertion(&assertion);
1188
1189 assert!(result.is_err());
1191 }
1192
1193 #[test]
1194 fn test_span_count_assertion() {
1195 let json = r#"{"name":"clnrm.test","trace_id":"abc123","span_id":"span1","parent_span_id":null,"attributes":{}}
1197{"name":"clnrm.test","trace_id":"abc123","span_id":"span2","parent_span_id":null,"attributes":{}}"#;
1198 let validator = SpanValidator::from_json(json).unwrap();
1199 let assertion = SpanAssertion::SpanCount {
1200 name: "clnrm.test".to_string(),
1201 count: 2,
1202 };
1203
1204 let result = validator.validate_assertion(&assertion);
1206
1207 assert!(result.is_ok());
1209 }
1210
1211 #[test]
1212 fn test_span_hierarchy_assertion() {
1213 let json = r#"{"name":"clnrm.run","trace_id":"abc123","span_id":"parent1","parent_span_id":null,"attributes":{}}
1215{"name":"clnrm.test","trace_id":"abc123","span_id":"child1","parent_span_id":"parent1","attributes":{}}"#;
1216 let validator = SpanValidator::from_json(json).unwrap();
1217 let assertion = SpanAssertion::SpanHierarchy {
1218 parent: "clnrm.run".to_string(),
1219 child: "clnrm.test".to_string(),
1220 };
1221
1222 let result = validator.validate_assertion(&assertion);
1224
1225 assert!(result.is_ok());
1227 }
1228
1229 #[test]
1234 fn test_span_existence_validation() -> Result<()> {
1235 use crate::config::SpanExpectationConfig;
1237
1238 let expectation = SpanExpectationConfig {
1239 name: "test.span".to_string(),
1240 parent: None,
1241 kind: None,
1242 attrs: None,
1243 events: None,
1244 duration_ms: None,
1245 };
1246
1247 let empty_validator = SpanValidator::from_json("")?;
1249 let result = empty_validator.validate_expectations(&[expectation.clone()])?;
1250
1251 assert!(!result.passed);
1253 assert_eq!(result.failures.len(), 1);
1254 assert_eq!(result.failures[0].span_name, "test.span");
1255 assert!(result.failures[0].message.contains("not found"));
1256
1257 let json = r#"{"name":"test.span","trace_id":"abc123","span_id":"span1","parent_span_id":null,"attributes":{},"start_time_unix_nano":1000000000,"end_time_unix_nano":2000000000}"#;
1259 let validator_with_span = SpanValidator::from_json(json)?;
1260 let result_success = validator_with_span.validate_expectations(&[expectation])?;
1261
1262 assert!(
1264 result_success.passed,
1265 "Expected pass but got failures: {:?}",
1266 result_success.failures
1267 );
1268 assert_eq!(result_success.failures.len(), 0);
1269
1270 Ok(())
1271 }
1272
1273 #[test]
1274 fn test_attrs_all_validation() -> Result<()> {
1275 use crate::config::SpanExpectationConfig;
1277
1278 let mut attrs = HashMap::new();
1279 attrs.insert("result".to_string(), "pass".to_string());
1280
1281 let expectation = SpanExpectationConfig {
1282 name: "test.span".to_string(),
1283 parent: None,
1284 kind: None,
1285 attrs: Some(SpanAttributesConfig {
1286 all: Some(attrs.clone()),
1287 any: None,
1288 }),
1289 events: None,
1290 duration_ms: None,
1291 };
1292
1293 let json_no_attr = r#"{"name":"test.span","trace_id":"abc123","span_id":"span1","parent_span_id":null,"attributes":{}}"#;
1295 let validator_no_attr = SpanValidator::from_json(json_no_attr)?;
1296 let result_fail = validator_no_attr.validate_expectations(&[expectation.clone()])?;
1297
1298 assert!(!result_fail.passed);
1300 assert!(result_fail.failures[0]
1301 .message
1302 .contains("missing required attributes"));
1303
1304 let json_with_attr = r#"{"name":"test.span","trace_id":"abc123","span_id":"span1","parent_span_id":null,"attributes":{"result":"pass"}}"#;
1306 let validator_with_attr = SpanValidator::from_json(json_with_attr)?;
1307 let result_success = validator_with_attr.validate_expectations(&[expectation])?;
1308
1309 assert!(result_success.passed);
1311 assert_eq!(result_success.failures.len(), 0);
1312
1313 Ok(())
1314 }
1315
1316 #[test]
1317 fn test_events_any_validation() -> Result<()> {
1318 use crate::config::{SpanEventsConfig, SpanExpectationConfig};
1320
1321 let expectation = SpanExpectationConfig {
1322 name: "test.span".to_string(),
1323 parent: None,
1324 kind: None,
1325 attrs: None,
1326 events: Some(SpanEventsConfig {
1327 any: Some(vec!["event1".to_string(), "event2".to_string()]),
1328 all: None,
1329 }),
1330 duration_ms: None,
1331 };
1332
1333 let json_no_events = r#"{"name":"test.span","trace_id":"abc123","span_id":"span1","parent_span_id":null,"attributes":{}}"#;
1335 let validator_no_events = SpanValidator::from_json(json_no_events)?;
1336 let result_fail = validator_no_events.validate_expectations(&[expectation.clone()])?;
1337
1338 assert!(!result_fail.passed);
1340 assert!(result_fail.failures[0].message.contains("missing required events"));
1341
1342 let json_with_event = r#"{"name":"test.span","trace_id":"abc123","span_id":"span1","parent_span_id":null,"attributes":{},"events":["event1"]}"#;
1344 let validator_with_event = SpanValidator::from_json(json_with_event)?;
1345 let result_success = validator_with_event.validate_expectations(&[expectation])?;
1346
1347 assert!(result_success.passed);
1349 assert_eq!(result_success.failures.len(), 0);
1350
1351 Ok(())
1352 }
1353
1354 #[test]
1355 fn test_duration_validation() -> Result<()> {
1356 use crate::config::{DurationBoundConfig, SpanExpectationConfig};
1358
1359 let expectation = SpanExpectationConfig {
1360 name: "test.span".to_string(),
1361 parent: None,
1362 kind: None,
1363 attrs: None,
1364 events: None,
1365 duration_ms: Some(DurationBoundConfig {
1366 min: Some(10.0),
1367 max: Some(1000.0),
1368 }),
1369 };
1370
1371 let json_short = r#"{"name":"test.span","trace_id":"abc123","span_id":"span1","parent_span_id":null,"attributes":{},"start_time_unix_nano":1000000000,"end_time_unix_nano":1005000000}"#;
1373 let validator_short = SpanValidator::from_json(json_short)?;
1374 let result_short = validator_short.validate_expectations(&[expectation.clone()])?;
1375
1376 assert!(!result_short.passed);
1378 assert!(result_short.failures[0].message.contains("< min"));
1379
1380 let json_ok = r#"{"name":"test.span","trace_id":"abc123","span_id":"span1","parent_span_id":null,"attributes":{},"start_time_unix_nano":1000000000,"end_time_unix_nano":1100000000}"#;
1382 let validator_ok = SpanValidator::from_json(json_ok)?;
1383 let result_ok = validator_ok.validate_expectations(&[expectation.clone()])?;
1384
1385 assert!(result_ok.passed);
1387 assert_eq!(result_ok.failures.len(), 0);
1388
1389 let json_long = r#"{"name":"test.span","trace_id":"abc123","span_id":"span1","parent_span_id":null,"attributes":{},"start_time_unix_nano":1000000000,"end_time_unix_nano":3000000000}"#;
1391 let validator_long = SpanValidator::from_json(json_long)?;
1392 let result_long = validator_long.validate_expectations(&[expectation])?;
1393
1394 assert!(!result_long.passed);
1396 assert!(result_long.failures[0].message.contains("> max"));
1397
1398 Ok(())
1399 }
1400
1401 #[test]
1402 fn test_parent_relationship_validation() -> Result<()> {
1403 use crate::config::SpanExpectationConfig;
1405
1406 let expectation = SpanExpectationConfig {
1407 name: "clnrm.step:hello_world".to_string(),
1408 parent: Some("clnrm.run".to_string()),
1409 kind: None,
1410 attrs: None,
1411 events: None,
1412 duration_ms: None,
1413 };
1414
1415 let json_no_parent = r#"{"name":"clnrm.step:hello_world","trace_id":"abc123","span_id":"child1","parent_span_id":null,"attributes":{}}"#;
1417 let validator_no_parent = SpanValidator::from_json(json_no_parent)?;
1418 let result_no_parent = validator_no_parent.validate_expectations(&[expectation.clone()])?;
1419
1420 assert!(!result_no_parent.passed);
1422 assert!(result_no_parent.failures[0]
1423 .message
1424 .contains("parent mismatch"));
1425 assert!(result_no_parent.failures[0].message.contains("found none"));
1426
1427 let json_with_parent = r#"{"name":"clnrm.run","trace_id":"abc123","span_id":"parent1","parent_span_id":null,"attributes":{}}
1429{"name":"clnrm.step:hello_world","trace_id":"abc123","span_id":"child1","parent_span_id":"parent1","attributes":{}}"#;
1430 let validator_with_parent = SpanValidator::from_json(json_with_parent)?;
1431 let result_with_parent = validator_with_parent.validate_expectations(&[expectation])?;
1432
1433 assert!(result_with_parent.passed);
1435 assert_eq!(result_with_parent.failures.len(), 0);
1436
1437 Ok(())
1438 }
1439
1440 #[test]
1441 fn test_span_kind_validation() -> Result<()> {
1442 use crate::config::SpanExpectationConfig;
1444
1445 let expectation = SpanExpectationConfig {
1446 name: "test.span".to_string(),
1447 parent: None,
1448 kind: Some("internal".to_string()),
1449 attrs: None,
1450 events: None,
1451 duration_ms: None,
1452 };
1453
1454 let json_wrong_kind = r#"{"name":"test.span","trace_id":"abc123","span_id":"span1","parent_span_id":null,"attributes":{},"kind":3}"#;
1456 let validator_wrong = SpanValidator::from_json(json_wrong_kind)?;
1457 let result_wrong = validator_wrong.validate_expectations(&[expectation.clone()])?;
1458
1459 assert!(!result_wrong.passed);
1461 assert!(result_wrong.failures[0].message.contains("kind mismatch"));
1462
1463 let json_correct_kind = r#"{"name":"test.span","trace_id":"abc123","span_id":"span1","parent_span_id":null,"attributes":{},"kind":1}"#;
1465 let validator_correct = SpanValidator::from_json(json_correct_kind)?;
1466 let result_correct = validator_correct.validate_expectations(&[expectation])?;
1467
1468 assert!(result_correct.passed);
1470 assert_eq!(result_correct.failures.len(), 0);
1471
1472 Ok(())
1473 }
1474
1475 #[test]
1476 fn test_validation_result_merge() {
1477 let result1 = ValidationResult::success(3);
1479 let result2 = ValidationResult::success(2);
1480 let result3 = ValidationResult::failure(FailureDetails {
1481 rule: "test.rule".to_string(),
1482 span_name: "test.span".to_string(),
1483 expected: "something".to_string(),
1484 actual: None,
1485 message: "Test failure".to_string(),
1486 });
1487
1488 let merged_success = ValidationResult::merge(vec![result1.clone(), result2.clone()]);
1490 let merged_failure = ValidationResult::merge(vec![result1, result2, result3]);
1491
1492 assert!(merged_success.passed);
1494 assert_eq!(merged_success.validations_count, 5);
1495 assert_eq!(merged_success.failures.len(), 0);
1496
1497 assert!(!merged_failure.passed);
1498 assert_eq!(merged_failure.validations_count, 6);
1499 assert_eq!(merged_failure.failures.len(), 1);
1500 }
1501
1502 #[test]
1503 fn test_first_failure_helper() {
1504 let result_success = ValidationResult::success(5);
1506 let result_with_failure = ValidationResult::failure(FailureDetails {
1507 rule: "test.rule".to_string(),
1508 span_name: "test.span".to_string(),
1509 expected: "expected".to_string(),
1510 actual: Some("actual".to_string()),
1511 message: "Test failure message".to_string(),
1512 });
1513
1514 let no_failure = SpanValidator::first_failure(&result_success);
1516 let has_failure = SpanValidator::first_failure(&result_with_failure);
1517
1518 assert!(no_failure.is_none());
1520 assert!(has_failure.is_some());
1521 assert_eq!(has_failure.unwrap().message, "Test failure message");
1522 }
1523
1524 #[test]
1525 fn test_multiple_expectations_validation() -> Result<()> {
1526 use crate::config::{
1528 DurationBoundConfig, SpanAttributesConfig, SpanEventsConfig, SpanExpectationConfig,
1529 };
1530
1531 let mut attrs = HashMap::new();
1532 attrs.insert("result".to_string(), "pass".to_string());
1533
1534 let expectations = vec![
1535 SpanExpectationConfig {
1537 name: "clnrm.run".to_string(),
1538 parent: None,
1539 kind: Some("internal".to_string()),
1540 attrs: Some(SpanAttributesConfig {
1541 all: Some(attrs.clone()),
1542 any: None,
1543 }),
1544 events: None,
1545 duration_ms: Some(DurationBoundConfig {
1546 min: Some(10.0),
1547 max: Some(600000.0),
1548 }),
1549 },
1550 SpanExpectationConfig {
1552 name: "clnrm.step:hello_world".to_string(),
1553 parent: Some("clnrm.run".to_string()),
1554 kind: None,
1555 attrs: None,
1556 events: Some(SpanEventsConfig {
1557 any: Some(vec![
1558 "container.start".to_string(),
1559 "container.exec".to_string(),
1560 ]),
1561 all: None,
1562 }),
1563 duration_ms: None,
1564 },
1565 ];
1566
1567 let json = r#"{"name":"clnrm.run","trace_id":"abc123","span_id":"parent1","parent_span_id":null,"attributes":{"result":"pass"},"kind":1,"start_time_unix_nano":1000000000,"end_time_unix_nano":1100000000}
1568{"name":"clnrm.step:hello_world","trace_id":"abc123","span_id":"child1","parent_span_id":"parent1","attributes":{},"events":["container.start","container.exec"]}"#;
1569
1570 let validator = SpanValidator::from_json(json)?;
1571
1572 let result = validator.validate_expectations(&expectations)?;
1574
1575 assert!(result.passed, "Validation should pass: {:?}", result.failures);
1577 assert_eq!(result.failures.len(), 0);
1578 assert!(result.validations_count >= expectations.len());
1579
1580 Ok(())
1581 }
1582}