clnrm_core/validation/
orchestrator.rs1use crate::error::{CleanroomError, Result};
6use crate::validation::count_validator::CountExpectation;
7use crate::validation::graph_validator::GraphExpectation;
8use crate::validation::hermeticity_validator::HermeticityExpectation;
9use crate::validation::span_validator::SpanData;
10use crate::validation::window_validator::WindowExpectation;
11
12#[derive(Debug, Clone, Default)]
14pub struct PrdExpectations {
15 pub graph: Option<GraphExpectation>,
17 pub counts: Option<CountExpectation>,
19 pub windows: Vec<WindowExpectation>,
21 pub hermeticity: Option<HermeticityExpectation>,
23}
24
25impl PrdExpectations {
26 pub fn new() -> Self {
28 Self::default()
29 }
30
31 pub fn with_graph(mut self, graph: GraphExpectation) -> Self {
33 self.graph = Some(graph);
34 self
35 }
36
37 pub fn with_counts(mut self, counts: CountExpectation) -> Self {
39 self.counts = Some(counts);
40 self
41 }
42
43 pub fn add_window(mut self, window: WindowExpectation) -> Self {
45 self.windows.push(window);
46 self
47 }
48
49 pub fn with_hermeticity(mut self, hermeticity: HermeticityExpectation) -> Self {
51 self.hermeticity = Some(hermeticity);
52 self
53 }
54
55 pub fn validate_all(&self, spans: &[SpanData]) -> Result<ValidationReport> {
69 let mut report = ValidationReport::new();
70
71 if let Some(ref graph) = self.graph {
73 match graph.validate(spans) {
74 Ok(_) => report.add_pass("graph_topology"),
75 Err(e) => report.add_fail("graph_topology", e.to_string()),
76 }
77 }
78
79 if let Some(ref counts) = self.counts {
81 match counts.validate(spans) {
82 Ok(_) => report.add_pass("span_counts"),
83 Err(e) => report.add_fail("span_counts", e.to_string()),
84 }
85 }
86
87 for (idx, window) in self.windows.iter().enumerate() {
89 let name = format!("window_{}_outer_{}", idx, window.outer);
90 match window.validate(spans) {
91 Ok(_) => report.add_pass(&name),
92 Err(e) => report.add_fail(&name, e.to_string()),
93 }
94 }
95
96 if let Some(ref hermetic) = self.hermeticity {
98 match hermetic.validate(spans) {
99 Ok(_) => report.add_pass("hermeticity"),
100 Err(e) => report.add_fail("hermeticity", e.to_string()),
101 }
102 }
103
104 Ok(report)
105 }
106
107 pub fn validate_strict(&self, spans: &[SpanData]) -> Result<()> {
109 let report = self.validate_all(spans)?;
110 if report.is_success() {
111 Ok(())
112 } else {
113 Err(CleanroomError::validation_error(format!(
114 "Validation failed with {} errors: {}",
115 report.failure_count(),
116 report.first_error().unwrap_or("unknown error")
117 )))
118 }
119 }
120}
121
122#[derive(Debug, Clone, Default)]
124pub struct ValidationReport {
125 passes: Vec<String>,
127 failures: Vec<(String, String)>,
129}
130
131impl ValidationReport {
132 pub fn new() -> Self {
134 Self::default()
135 }
136
137 pub fn add_pass(&mut self, name: &str) {
139 self.passes.push(name.to_string());
140 }
141
142 pub fn add_fail(&mut self, name: &str, error: String) {
144 self.failures.push((name.to_string(), error));
145 }
146
147 pub fn is_success(&self) -> bool {
149 self.failures.is_empty()
150 }
151
152 pub fn pass_count(&self) -> usize {
154 self.passes.len()
155 }
156
157 pub fn failure_count(&self) -> usize {
159 self.failures.len()
160 }
161
162 pub fn passes(&self) -> &[String] {
164 &self.passes
165 }
166
167 pub fn failures(&self) -> &[(String, String)] {
169 &self.failures
170 }
171
172 pub fn first_error(&self) -> Option<&str> {
174 self.failures.first().map(|(_, msg)| msg.as_str())
175 }
176
177 pub fn summary(&self) -> String {
179 if self.is_success() {
180 format!("✓ All {} validations passed", self.pass_count())
181 } else {
182 format!(
183 "✗ {} passed, {} failed\n{}",
184 self.pass_count(),
185 self.failure_count(),
186 self.failures
187 .iter()
188 .map(|(name, err)| format!(" - {}: {}", name, err))
189 .collect::<Vec<_>>()
190 .join("\n")
191 )
192 }
193 }
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199 use crate::validation::count_validator::{CountBound, CountExpectation};
200 use crate::validation::graph_validator::GraphExpectation;
201 use crate::validation::hermeticity_validator::HermeticityExpectation;
202 use std::collections::HashMap;
203
204 fn create_test_span(name: &str, span_id: &str, parent_id: Option<&str>) -> SpanData {
205 SpanData {
206 name: name.to_string(),
207 trace_id: "test_trace".to_string(),
208 span_id: span_id.to_string(),
209 parent_span_id: parent_id.map(|s| s.to_string()),
210 attributes: HashMap::new(),
211 start_time_unix_nano: Some(1000000),
212 end_time_unix_nano: Some(2000000),
213 kind: None,
214 events: None,
215 resource_attributes: HashMap::new(),
216 }
217 }
218
219 #[test]
220 fn test_orchestrator_all_validations_pass() {
221 let spans = vec![
223 create_test_span("root", "s1", None),
224 create_test_span("child", "s2", Some("s1")),
225 ];
226
227 let graph = GraphExpectation::new(vec![("root".to_string(), "child".to_string())]);
228
229 let counts = CountExpectation::new()
230 .with_name_count("root".to_string(), CountBound::eq(1))
231 .with_name_count("child".to_string(), CountBound::eq(1));
232
233 let hermeticity = HermeticityExpectation::default();
234
235 let expectations = PrdExpectations::new()
236 .with_graph(graph)
237 .with_counts(counts)
238 .with_hermeticity(hermeticity);
239
240 let report = expectations.validate_all(&spans).unwrap();
242
243 assert!(report.is_success());
245 assert!(report.pass_count() >= 2); }
247
248 #[test]
249 fn test_orchestrator_graph_validation_fails() {
250 let spans = vec![create_test_span("root", "s1", None)];
252
253 let graph = GraphExpectation::new(vec![("root".to_string(), "missing_child".to_string())]);
254
255 let expectations = PrdExpectations::new().with_graph(graph);
256
257 let report = expectations.validate_all(&spans).unwrap();
259
260 assert!(!report.is_success());
262 assert_eq!(report.failure_count(), 1);
263 assert!(report.first_error().unwrap().contains("missing_child"));
264 }
265
266 #[test]
267 fn test_orchestrator_count_validation_fails() {
268 let spans = vec![create_test_span("root", "s1", None)];
270
271 let counts = CountExpectation::new().with_name_count("root".to_string(), CountBound::eq(2)); let expectations = PrdExpectations::new().with_counts(counts);
274
275 let report = expectations.validate_all(&spans).unwrap();
277
278 assert!(!report.is_success());
280 assert_eq!(report.failure_count(), 1);
281 }
282
283 #[test]
284 fn test_validation_report_summary() {
285 let mut report = ValidationReport::new();
287 report.add_pass("test1");
288 report.add_pass("test2");
289 report.add_fail("test3", "Error message".to_string());
290
291 let summary = report.summary();
293
294 assert!(summary.contains("2 passed"));
296 assert!(summary.contains("1 failed"));
297 assert!(summary.contains("test3"));
298 assert!(summary.contains("Error message"));
299 }
300
301 #[test]
302 fn test_validate_strict_fails_on_error() {
303 let spans = vec![create_test_span("root", "s1", None)];
305
306 let counts = CountExpectation::new().with_name_count("root".to_string(), CountBound::eq(2));
307
308 let expectations = PrdExpectations::new().with_counts(counts);
309
310 let result = expectations.validate_strict(&spans);
312
313 assert!(result.is_err());
315 }
316}