clnrm_core/validation/
window_validator.rs

1//! Temporal window validator for OTEL span containment
2//!
3//! Validates that child spans are temporally contained within parent spans.
4//! This ensures proper span lifecycle management and helps detect timing issues.
5
6use crate::error::{CleanroomError, Result};
7use crate::validation::span_validator::SpanData;
8use serde::{Deserialize, Serialize};
9
10/// Represents a temporal window expectation
11///
12/// Validates that all specified child spans are temporally contained within
13/// an outer span, meaning:
14/// - outer.start_time <= child.start_time
15/// - child.end_time <= outer.end_time
16///
17/// # Example
18///
19/// ```toml
20/// [[expect.window]]
21/// outer = "root_span_name"
22/// contains = ["child_a", "child_b"]
23/// ```
24#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
25pub struct WindowExpectation {
26    /// Name of the outer (parent) span that should contain children
27    pub outer: String,
28    /// Names of child spans that must be temporally contained
29    pub contains: Vec<String>,
30}
31
32impl WindowExpectation {
33    /// Create a new window expectation
34    ///
35    /// # Arguments
36    /// * `outer` - Name of the outer span
37    /// * `contains` - Names of child spans that must be contained
38    pub fn new(outer: impl Into<String>, contains: Vec<String>) -> Self {
39        Self {
40            outer: outer.into(),
41            contains,
42        }
43    }
44
45    /// Validate temporal containment across all spans
46    ///
47    /// # Arguments
48    /// * `spans` - All spans to validate against
49    ///
50    /// # Returns
51    /// * `Ok(())` if all children are temporally contained in outer span
52    /// * `Err` with detailed message if validation fails
53    ///
54    /// # Errors
55    /// * Outer span not found
56    /// * Child span not found
57    /// * Missing timestamps on any span
58    /// * Temporal containment violation (child outside parent window)
59    pub fn validate(&self, spans: &[SpanData]) -> Result<()> {
60        // Find the outer span by name
61        let outer_span = self.find_span_by_name(spans, &self.outer)?;
62
63        // Validate outer span has timestamps
64        let (outer_start, outer_end) = self.extract_timestamps(outer_span, &self.outer)?;
65
66        // Validate each child span
67        for child_name in &self.contains {
68            let child_span = self.find_span_by_name(spans, child_name)?;
69            let (child_start, child_end) = self.extract_timestamps(child_span, child_name)?;
70
71            // Check temporal containment
72            self.validate_containment(
73                &self.outer,
74                outer_start,
75                outer_end,
76                child_name,
77                child_start,
78                child_end,
79            )?;
80        }
81
82        Ok(())
83    }
84
85    /// Find a span by name
86    fn find_span_by_name<'a>(&self, spans: &'a [SpanData], name: &str) -> Result<&'a SpanData> {
87        spans.iter().find(|s| s.name == name).ok_or_else(|| {
88            CleanroomError::validation_error(format!(
89                "Window validation failed: span '{}' not found in trace",
90                name
91            ))
92        })
93    }
94
95    /// Extract and validate timestamps from a span
96    fn extract_timestamps(&self, span: &SpanData, span_name: &str) -> Result<(u64, u64)> {
97        let start_time = span.start_time_unix_nano.ok_or_else(|| {
98            CleanroomError::validation_error(format!(
99                "Window validation failed: span '{}' missing start_time_unix_nano",
100                span_name
101            ))
102        })?;
103
104        let end_time = span.end_time_unix_nano.ok_or_else(|| {
105            CleanroomError::validation_error(format!(
106                "Window validation failed: span '{}' missing end_time_unix_nano",
107                span_name
108            ))
109        })?;
110
111        Ok((start_time, end_time))
112    }
113
114    /// Validate temporal containment between parent and child
115    fn validate_containment(
116        &self,
117        outer_name: &str,
118        outer_start: u64,
119        outer_end: u64,
120        child_name: &str,
121        child_start: u64,
122        child_end: u64,
123    ) -> Result<()> {
124        // Check: outer.start <= child.start
125        if child_start < outer_start {
126            return Err(CleanroomError::validation_error(format!(
127                "Window validation failed: child span '{}' started before outer span '{}' \
128                 (child_start: {}, outer_start: {})",
129                child_name, outer_name, child_start, outer_start
130            )));
131        }
132
133        // Check: child.end <= outer.end
134        if child_end > outer_end {
135            return Err(CleanroomError::validation_error(format!(
136                "Window validation failed: child span '{}' ended after outer span '{}' \
137                 (child_end: {}, outer_end: {})",
138                child_name, outer_name, child_end, outer_end
139            )));
140        }
141
142        Ok(())
143    }
144}
145
146/// Window validator for validating multiple window expectations
147pub struct WindowValidator;
148
149impl WindowValidator {
150    /// Validate multiple window expectations against a set of spans
151    ///
152    /// # Arguments
153    /// * `expectations` - Window expectations to validate
154    /// * `spans` - Spans to validate against
155    ///
156    /// # Returns
157    /// * `Ok(())` if all expectations pass
158    /// * `Err` with the first validation failure
159    pub fn validate_windows(expectations: &[WindowExpectation], spans: &[SpanData]) -> Result<()> {
160        for expectation in expectations {
161            expectation.validate(spans)?;
162        }
163        Ok(())
164    }
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170    use std::collections::HashMap;
171
172    /// Helper to create a span for testing
173    fn create_test_span(name: &str, start_nano: Option<u64>, end_nano: Option<u64>) -> SpanData {
174        SpanData {
175            name: name.to_string(),
176            attributes: HashMap::new(),
177            trace_id: "test-trace-123".to_string(),
178            span_id: format!("span-{}", name),
179            parent_span_id: None,
180            start_time_unix_nano: start_nano,
181            end_time_unix_nano: end_nano,
182            kind: None,
183            events: None,
184            resource_attributes: HashMap::new(),
185        }
186    }
187
188    #[test]
189    fn test_window_expectation_new() {
190        // Arrange & Act
191        let expectation = WindowExpectation::new(
192            "outer_span",
193            vec!["child_a".to_string(), "child_b".to_string()],
194        );
195
196        // Assert
197        assert_eq!(expectation.outer, "outer_span");
198        assert_eq!(expectation.contains.len(), 2);
199    }
200
201    #[test]
202    fn test_valid_temporal_containment() {
203        // Arrange
204        let spans = vec![
205            create_test_span("root", Some(1000), Some(5000)),
206            create_test_span("child_a", Some(1500), Some(3000)),
207            create_test_span("child_b", Some(3500), Some(4500)),
208        ];
209
210        let expectation =
211            WindowExpectation::new("root", vec!["child_a".to_string(), "child_b".to_string()]);
212
213        // Act
214        let result = expectation.validate(&spans);
215
216        // Assert
217        assert!(result.is_ok());
218    }
219
220    #[test]
221    fn test_child_starts_before_parent() {
222        // Arrange
223        let spans = vec![
224            create_test_span("root", Some(2000), Some(5000)),
225            create_test_span("child_a", Some(1000), Some(3000)), // Starts before parent!
226        ];
227
228        let expectation = WindowExpectation::new("root", vec!["child_a".to_string()]);
229
230        // Act
231        let result = expectation.validate(&spans);
232
233        // Assert
234        assert!(result.is_err());
235        let err_msg = result.unwrap_err().to_string();
236        assert!(err_msg.contains("started before outer span"));
237        assert!(err_msg.contains("child_start: 1000"));
238        assert!(err_msg.contains("outer_start: 2000"));
239    }
240
241    #[test]
242    fn test_child_ends_after_parent() {
243        // Arrange
244        let spans = vec![
245            create_test_span("root", Some(1000), Some(4000)),
246            create_test_span("child_a", Some(2000), Some(5000)), // Ends after parent!
247        ];
248
249        let expectation = WindowExpectation::new("root", vec!["child_a".to_string()]);
250
251        // Act
252        let result = expectation.validate(&spans);
253
254        // Assert
255        assert!(result.is_err());
256        let err_msg = result.unwrap_err().to_string();
257        assert!(err_msg.contains("ended after outer span"));
258        assert!(err_msg.contains("child_end: 5000"));
259        assert!(err_msg.contains("outer_end: 4000"));
260    }
261
262    #[test]
263    fn test_outer_span_not_found() {
264        // Arrange
265        let spans = vec![create_test_span("child_a", Some(1000), Some(2000))];
266
267        let expectation = WindowExpectation::new("nonexistent_root", vec!["child_a".to_string()]);
268
269        // Act
270        let result = expectation.validate(&spans);
271
272        // Assert
273        assert!(result.is_err());
274        let err_msg = result.unwrap_err().to_string();
275        assert!(err_msg.contains("span 'nonexistent_root' not found"));
276    }
277
278    #[test]
279    fn test_child_span_not_found() {
280        // Arrange
281        let spans = vec![create_test_span("root", Some(1000), Some(5000))];
282
283        let expectation = WindowExpectation::new("root", vec!["nonexistent_child".to_string()]);
284
285        // Act
286        let result = expectation.validate(&spans);
287
288        // Assert
289        assert!(result.is_err());
290        let err_msg = result.unwrap_err().to_string();
291        assert!(err_msg.contains("span 'nonexistent_child' not found"));
292    }
293
294    #[test]
295    fn test_outer_span_missing_start_time() {
296        // Arrange
297        let spans = vec![
298            create_test_span("root", None, Some(5000)), // Missing start time
299            create_test_span("child_a", Some(1500), Some(3000)),
300        ];
301
302        let expectation = WindowExpectation::new("root", vec!["child_a".to_string()]);
303
304        // Act
305        let result = expectation.validate(&spans);
306
307        // Assert
308        assert!(result.is_err());
309        let err_msg = result.unwrap_err().to_string();
310        assert!(err_msg.contains("missing start_time_unix_nano"));
311        assert!(err_msg.contains("'root'"));
312    }
313
314    #[test]
315    fn test_outer_span_missing_end_time() {
316        // Arrange
317        let spans = vec![
318            create_test_span("root", Some(1000), None), // Missing end time
319            create_test_span("child_a", Some(1500), Some(3000)),
320        ];
321
322        let expectation = WindowExpectation::new("root", vec!["child_a".to_string()]);
323
324        // Act
325        let result = expectation.validate(&spans);
326
327        // Assert
328        assert!(result.is_err());
329        let err_msg = result.unwrap_err().to_string();
330        assert!(err_msg.contains("missing end_time_unix_nano"));
331        assert!(err_msg.contains("'root'"));
332    }
333
334    #[test]
335    fn test_child_span_missing_start_time() {
336        // Arrange
337        let spans = vec![
338            create_test_span("root", Some(1000), Some(5000)),
339            create_test_span("child_a", None, Some(3000)), // Missing start time
340        ];
341
342        let expectation = WindowExpectation::new("root", vec!["child_a".to_string()]);
343
344        // Act
345        let result = expectation.validate(&spans);
346
347        // Assert
348        assert!(result.is_err());
349        let err_msg = result.unwrap_err().to_string();
350        assert!(err_msg.contains("missing start_time_unix_nano"));
351        assert!(err_msg.contains("'child_a'"));
352    }
353
354    #[test]
355    fn test_child_span_missing_end_time() {
356        // Arrange
357        let spans = vec![
358            create_test_span("root", Some(1000), Some(5000)),
359            create_test_span("child_a", Some(1500), None), // Missing end time
360        ];
361
362        let expectation = WindowExpectation::new("root", vec!["child_a".to_string()]);
363
364        // Act
365        let result = expectation.validate(&spans);
366
367        // Assert
368        assert!(result.is_err());
369        let err_msg = result.unwrap_err().to_string();
370        assert!(err_msg.contains("missing end_time_unix_nano"));
371        assert!(err_msg.contains("'child_a'"));
372    }
373
374    #[test]
375    fn test_multiple_children_all_valid() {
376        // Arrange
377        let spans = vec![
378            create_test_span("root", Some(1000), Some(10000)),
379            create_test_span("child_a", Some(1500), Some(3000)),
380            create_test_span("child_b", Some(3500), Some(6000)),
381            create_test_span("child_c", Some(7000), Some(9000)),
382        ];
383
384        let expectation = WindowExpectation::new(
385            "root",
386            vec![
387                "child_a".to_string(),
388                "child_b".to_string(),
389                "child_c".to_string(),
390            ],
391        );
392
393        // Act
394        let result = expectation.validate(&spans);
395
396        // Assert
397        assert!(result.is_ok());
398    }
399
400    #[test]
401    fn test_multiple_children_one_invalid() {
402        // Arrange
403        let spans = vec![
404            create_test_span("root", Some(1000), Some(10000)),
405            create_test_span("child_a", Some(1500), Some(3000)),
406            create_test_span("child_b", Some(500), Some(6000)), // Starts before parent!
407            create_test_span("child_c", Some(7000), Some(9000)),
408        ];
409
410        let expectation = WindowExpectation::new(
411            "root",
412            vec![
413                "child_a".to_string(),
414                "child_b".to_string(),
415                "child_c".to_string(),
416            ],
417        );
418
419        // Act
420        let result = expectation.validate(&spans);
421
422        // Assert
423        assert!(result.is_err());
424        let err_msg = result.unwrap_err().to_string();
425        assert!(err_msg.contains("child_b"));
426        assert!(err_msg.contains("started before"));
427    }
428
429    #[test]
430    fn test_exact_boundary_containment_start_equals() {
431        // Arrange - child starts exactly when parent starts
432        let spans = vec![
433            create_test_span("root", Some(1000), Some(5000)),
434            create_test_span("child_a", Some(1000), Some(3000)), // Same start time
435        ];
436
437        let expectation = WindowExpectation::new("root", vec!["child_a".to_string()]);
438
439        // Act
440        let result = expectation.validate(&spans);
441
442        // Assert
443        assert!(
444            result.is_ok(),
445            "Child starting exactly when parent starts should be valid"
446        );
447    }
448
449    #[test]
450    fn test_exact_boundary_containment_end_equals() {
451        // Arrange - child ends exactly when parent ends
452        let spans = vec![
453            create_test_span("root", Some(1000), Some(5000)),
454            create_test_span("child_a", Some(2000), Some(5000)), // Same end time
455        ];
456
457        let expectation = WindowExpectation::new("root", vec!["child_a".to_string()]);
458
459        // Act
460        let result = expectation.validate(&spans);
461
462        // Assert
463        assert!(
464            result.is_ok(),
465            "Child ending exactly when parent ends should be valid"
466        );
467    }
468
469    #[test]
470    fn test_exact_boundary_containment_both_equal() {
471        // Arrange - child has exact same timing as parent
472        let spans = vec![
473            create_test_span("root", Some(1000), Some(5000)),
474            create_test_span("child_a", Some(1000), Some(5000)), // Exact same window
475        ];
476
477        let expectation = WindowExpectation::new("root", vec!["child_a".to_string()]);
478
479        // Act
480        let result = expectation.validate(&spans);
481
482        // Assert
483        assert!(
484            result.is_ok(),
485            "Child with exact same window as parent should be valid"
486        );
487    }
488
489    #[test]
490    fn test_window_validator_multiple_expectations() {
491        // Arrange
492        let spans = vec![
493            create_test_span("root", Some(1000), Some(10000)),
494            create_test_span("child_a", Some(1500), Some(3000)),
495            create_test_span("child_b", Some(3500), Some(6000)),
496            create_test_span("child_c", Some(7000), Some(9000)),
497        ];
498
499        let expectations = vec![
500            WindowExpectation::new("root", vec!["child_a".to_string()]),
501            WindowExpectation::new("root", vec!["child_b".to_string(), "child_c".to_string()]),
502        ];
503
504        // Act
505        let result = WindowValidator::validate_windows(&expectations, &spans);
506
507        // Assert
508        assert!(result.is_ok());
509    }
510
511    #[test]
512    fn test_window_validator_multiple_expectations_one_fails() {
513        // Arrange
514        let spans = vec![
515            create_test_span("root", Some(1000), Some(10000)),
516            create_test_span("child_a", Some(1500), Some(3000)),
517            create_test_span("child_b", Some(500), Some(6000)), // Invalid!
518        ];
519
520        let expectations = vec![
521            WindowExpectation::new("root", vec!["child_a".to_string()]),
522            WindowExpectation::new("root", vec!["child_b".to_string()]),
523        ];
524
525        // Act
526        let result = WindowValidator::validate_windows(&expectations, &spans);
527
528        // Assert
529        assert!(result.is_err());
530    }
531
532    #[test]
533    fn test_empty_contains_list() {
534        // Arrange
535        let spans = vec![create_test_span("root", Some(1000), Some(5000))];
536
537        let expectation = WindowExpectation::new("root", vec![]);
538
539        // Act
540        let result = expectation.validate(&spans);
541
542        // Assert
543        assert!(
544            result.is_ok(),
545            "Empty contains list should validate successfully"
546        );
547    }
548
549    #[test]
550    fn test_nanosecond_precision() {
551        // Arrange - test with realistic nanosecond timestamps
552        let spans = vec![
553            create_test_span(
554                "root",
555                Some(1_700_000_000_000_000_000), // Jan 2024
556                Some(1_700_000_001_000_000_000),
557            ),
558            create_test_span(
559                "child_a",
560                Some(1_700_000_000_100_000_000),
561                Some(1_700_000_000_900_000_000),
562            ),
563        ];
564
565        let expectation = WindowExpectation::new("root", vec!["child_a".to_string()]);
566
567        // Act
568        let result = expectation.validate(&spans);
569
570        // Assert
571        assert!(result.is_ok());
572    }
573
574    #[test]
575    fn test_off_by_one_nanosecond_violation() {
576        // Arrange - child ends 1 nanosecond after parent
577        let spans = vec![
578            create_test_span("root", Some(1000), Some(5000)),
579            create_test_span("child_a", Some(2000), Some(5001)), // 1ns too late
580        ];
581
582        let expectation = WindowExpectation::new("root", vec!["child_a".to_string()]);
583
584        // Act
585        let result = expectation.validate(&spans);
586
587        // Assert
588        assert!(result.is_err());
589        let err_msg = result.unwrap_err().to_string();
590        assert!(err_msg.contains("ended after"));
591    }
592}