1use crate::error::{CleanroomError, Result};
7use crate::validation::span_validator::SpanData;
8use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
25pub struct WindowExpectation {
26 pub outer: String,
28 pub contains: Vec<String>,
30}
31
32impl WindowExpectation {
33 pub fn new(outer: impl Into<String>, contains: Vec<String>) -> Self {
39 Self {
40 outer: outer.into(),
41 contains,
42 }
43 }
44
45 pub fn validate(&self, spans: &[SpanData]) -> Result<()> {
60 let outer_span = self.find_span_by_name(spans, &self.outer)?;
62
63 let (outer_start, outer_end) = self.extract_timestamps(outer_span, &self.outer)?;
65
66 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 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 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 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 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 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 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
146pub struct WindowValidator;
148
149impl WindowValidator {
150 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 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 let expectation = WindowExpectation::new(
192 "outer_span",
193 vec!["child_a".to_string(), "child_b".to_string()],
194 );
195
196 assert_eq!(expectation.outer, "outer_span");
198 assert_eq!(expectation.contains.len(), 2);
199 }
200
201 #[test]
202 fn test_valid_temporal_containment() {
203 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 let result = expectation.validate(&spans);
215
216 assert!(result.is_ok());
218 }
219
220 #[test]
221 fn test_child_starts_before_parent() {
222 let spans = vec![
224 create_test_span("root", Some(2000), Some(5000)),
225 create_test_span("child_a", Some(1000), Some(3000)), ];
227
228 let expectation = WindowExpectation::new("root", vec!["child_a".to_string()]);
229
230 let result = expectation.validate(&spans);
232
233 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 let spans = vec![
245 create_test_span("root", Some(1000), Some(4000)),
246 create_test_span("child_a", Some(2000), Some(5000)), ];
248
249 let expectation = WindowExpectation::new("root", vec!["child_a".to_string()]);
250
251 let result = expectation.validate(&spans);
253
254 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 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 let result = expectation.validate(&spans);
271
272 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 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 let result = expectation.validate(&spans);
287
288 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 let spans = vec![
298 create_test_span("root", None, Some(5000)), create_test_span("child_a", Some(1500), Some(3000)),
300 ];
301
302 let expectation = WindowExpectation::new("root", vec!["child_a".to_string()]);
303
304 let result = expectation.validate(&spans);
306
307 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 let spans = vec![
318 create_test_span("root", Some(1000), None), create_test_span("child_a", Some(1500), Some(3000)),
320 ];
321
322 let expectation = WindowExpectation::new("root", vec!["child_a".to_string()]);
323
324 let result = expectation.validate(&spans);
326
327 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 let spans = vec![
338 create_test_span("root", Some(1000), Some(5000)),
339 create_test_span("child_a", None, Some(3000)), ];
341
342 let expectation = WindowExpectation::new("root", vec!["child_a".to_string()]);
343
344 let result = expectation.validate(&spans);
346
347 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 let spans = vec![
358 create_test_span("root", Some(1000), Some(5000)),
359 create_test_span("child_a", Some(1500), None), ];
361
362 let expectation = WindowExpectation::new("root", vec!["child_a".to_string()]);
363
364 let result = expectation.validate(&spans);
366
367 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 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 let result = expectation.validate(&spans);
395
396 assert!(result.is_ok());
398 }
399
400 #[test]
401 fn test_multiple_children_one_invalid() {
402 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)), 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 let result = expectation.validate(&spans);
421
422 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 let spans = vec![
433 create_test_span("root", Some(1000), Some(5000)),
434 create_test_span("child_a", Some(1000), Some(3000)), ];
436
437 let expectation = WindowExpectation::new("root", vec!["child_a".to_string()]);
438
439 let result = expectation.validate(&spans);
441
442 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 let spans = vec![
453 create_test_span("root", Some(1000), Some(5000)),
454 create_test_span("child_a", Some(2000), Some(5000)), ];
456
457 let expectation = WindowExpectation::new("root", vec!["child_a".to_string()]);
458
459 let result = expectation.validate(&spans);
461
462 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 let spans = vec![
473 create_test_span("root", Some(1000), Some(5000)),
474 create_test_span("child_a", Some(1000), Some(5000)), ];
476
477 let expectation = WindowExpectation::new("root", vec!["child_a".to_string()]);
478
479 let result = expectation.validate(&spans);
481
482 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 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 let result = WindowValidator::validate_windows(&expectations, &spans);
506
507 assert!(result.is_ok());
509 }
510
511 #[test]
512 fn test_window_validator_multiple_expectations_one_fails() {
513 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)), ];
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 let result = WindowValidator::validate_windows(&expectations, &spans);
527
528 assert!(result.is_err());
530 }
531
532 #[test]
533 fn test_empty_contains_list() {
534 let spans = vec![create_test_span("root", Some(1000), Some(5000))];
536
537 let expectation = WindowExpectation::new("root", vec![]);
538
539 let result = expectation.validate(&spans);
541
542 assert!(
544 result.is_ok(),
545 "Empty contains list should validate successfully"
546 );
547 }
548
549 #[test]
550 fn test_nanosecond_precision() {
551 let spans = vec![
553 create_test_span(
554 "root",
555 Some(1_700_000_000_000_000_000), 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 let result = expectation.validate(&spans);
569
570 assert!(result.is_ok());
572 }
573
574 #[test]
575 fn test_off_by_one_nanosecond_violation() {
576 let spans = vec![
578 create_test_span("root", Some(1000), Some(5000)),
579 create_test_span("child_a", Some(2000), Some(5001)), ];
581
582 let expectation = WindowExpectation::new("root", vec!["child_a".to_string()]);
583
584 let result = expectation.validate(&spans);
586
587 assert!(result.is_err());
589 let err_msg = result.unwrap_err().to_string();
590 assert!(err_msg.contains("ended after"));
591 }
592}