rusty_stack/orchestrator/
verify.rs1use crate::core::verification::{VerificationCheck, VerificationResult};
21use crate::orchestrator::apply::{ApplyStatus, ApplySummary};
22use serde::{Deserialize, Serialize};
23use std::fmt;
24
25pub trait VerifyExecutor: Send + Sync {
34 fn verify_component(&self, component_id: &str, expected_version: &str) -> VerificationResult;
38}
39
40pub struct NoOpVerifyExecutor;
47
48impl VerifyExecutor for NoOpVerifyExecutor {
49 fn verify_component(&self, component_id: &str, _expected_version: &str) -> VerificationResult {
50 VerificationResult::new(
51 component_id,
52 vec![VerificationCheck::passed(
53 "default_check",
54 "no-op verification passed",
55 )],
56 )
57 }
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
66pub struct VerifyItem {
67 pub component_id: String,
69 pub apply_status: ApplyStatus,
71 pub verification_result: Option<VerificationResult>,
73 pub verified: VerifyStatus,
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
79#[serde(rename_all = "snake_case")]
80pub enum VerifyStatus {
81 Verified,
83 VerifyFailed,
85 NotVerified,
87}
88
89impl VerifyStatus {
90 pub fn label(self) -> &'static str {
92 match self {
93 VerifyStatus::Verified => "verified",
94 VerifyStatus::VerifyFailed => "verify_failed",
95 VerifyStatus::NotVerified => "not_verified",
96 }
97 }
98}
99
100impl fmt::Display for VerifyStatus {
101 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102 f.write_str(self.label())
103 }
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
117pub struct VerifySummary {
118 pub verified: Vec<VerifyItem>,
120 pub failed: Vec<VerifyItem>,
122 pub not_verified: Vec<VerifyItem>,
124}
125
126impl VerifySummary {
127 pub fn new() -> Self {
129 Self {
130 verified: Vec::new(),
131 failed: Vec::new(),
132 not_verified: Vec::new(),
133 }
134 }
135
136 pub fn total(&self) -> usize {
138 self.verified.len() + self.failed.len() + self.not_verified.len()
139 }
140
141 pub fn is_complete_partition(&self) -> bool {
143 let all_ids: HashSet<&str> = self
144 .verified
145 .iter()
146 .chain(self.failed.iter())
147 .chain(self.not_verified.iter())
148 .map(|i| i.component_id.as_str())
149 .collect();
150
151 let total_count = self.verified.len() + self.failed.len() + self.not_verified.len();
152 all_ids.len() == total_count
153 }
154}
155
156impl Default for VerifySummary {
157 fn default() -> Self {
158 Self::new()
159 }
160}
161
162pub struct VerifyRunner {
175 executor: Box<dyn VerifyExecutor>,
177}
178
179impl VerifyRunner {
180 pub fn new(executor: impl VerifyExecutor + 'static) -> Self {
182 Self {
183 executor: Box::new(executor),
184 }
185 }
186
187 pub fn new_noop() -> Self {
189 Self::new(NoOpVerifyExecutor)
190 }
191
192 pub fn verify(&self, apply_summary: &ApplySummary) -> VerifySummary {
196 let mut summary = VerifySummary::new();
197
198 for item in &apply_summary.success {
200 let result = self
201 .executor
202 .verify_component(&item.component_id, &item.proposed_version);
203
204 let verified = result.success;
205
206 let verify_item = VerifyItem {
207 component_id: item.component_id.clone(),
208 apply_status: ApplyStatus::Success,
209 verification_result: Some(result),
210 verified: if verified {
211 VerifyStatus::Verified
212 } else {
213 VerifyStatus::VerifyFailed
214 },
215 };
216
217 if verified {
218 summary.verified.push(verify_item);
219 } else {
220 summary.failed.push(verify_item);
221 }
222 }
223
224 for item in apply_summary
226 .failed
227 .iter()
228 .chain(apply_summary.blocked.iter())
229 .chain(apply_summary.held_back.iter())
230 .chain(apply_summary.skipped.iter())
231 {
232 summary.not_verified.push(VerifyItem {
233 component_id: item.component_id.clone(),
234 apply_status: item.status,
235 verification_result: None,
236 verified: VerifyStatus::NotVerified,
237 });
238 }
239
240 summary
241 }
242}
243
244use std::collections::HashSet;
246
247#[cfg(test)]
252mod tests {
253 use super::*;
254 use crate::core::verification::{CheckStatus, VerificationCheck};
255 use crate::orchestrator::apply::ApplyStatus;
256
257 fn make_apply_summary(
262 success_ids: &[&str],
263 failed_ids: &[&str],
264 held_back_ids: &[&str],
265 blocked_ids: &[&str],
266 ) -> ApplySummary {
267 use crate::orchestrator::apply::ApplyItem;
268 let mut summary = ApplySummary::new();
269 for &id in success_ids {
270 summary
271 .success
272 .push(ApplyItem::new(id, "1.0.0", "1.1.0", ApplyStatus::Success));
273 }
274 for &id in failed_ids {
275 let mut item = ApplyItem::new(id, "1.0.0", "1.1.0", ApplyStatus::Failed);
276 item.error_message = "test failure".to_string();
277 summary.failed.push(item);
278 }
279 for &id in held_back_ids {
280 let mut item = ApplyItem::new(id, "1.0.0", "1.1.0", ApplyStatus::HeldBack);
281 item.was_selected = false;
282 summary.held_back.push(item);
283 }
284 for &id in blocked_ids {
285 let mut item = ApplyItem::new(id, "1.0.0", "1.1.0", ApplyStatus::BlockedByFailure);
286 item.error_message = "blocked by dependency".to_string();
287 summary.blocked.push(item);
288 }
289 summary
290 }
291
292 #[test]
297 fn test_verify_runs_for_successful_applies() {
298 let apply_summary = make_apply_summary(&["a", "b", "c"], &[], &[], &[]);
299 let runner = VerifyRunner::new_noop();
300 let verify_summary = runner.verify(&apply_summary);
301
302 assert_eq!(verify_summary.verified.len(), 3);
304 assert_eq!(verify_summary.failed.len(), 0);
305 assert_eq!(verify_summary.not_verified.len(), 0);
306
307 for item in &verify_summary.verified {
309 assert!(item.verification_result.is_some());
310 assert!(item.verification_result.as_ref().unwrap().success);
311 }
312 }
313
314 #[test]
315 fn test_verify_not_run_for_failed_applies() {
316 let apply_summary = make_apply_summary(&[], &["a"], &[], &[]);
317 let runner = VerifyRunner::new_noop();
318 let verify_summary = runner.verify(&apply_summary);
319
320 assert_eq!(verify_summary.verified.len(), 0);
321 assert_eq!(verify_summary.not_verified.len(), 1);
322 assert_eq!(verify_summary.not_verified[0].component_id, "a");
323 assert_eq!(
324 verify_summary.not_verified[0].verified,
325 VerifyStatus::NotVerified
326 );
327 }
328
329 #[test]
334 fn test_verify_summary_complete_partitioning() {
335 let apply_summary = make_apply_summary(&["a", "b"], &["c"], &["d"], &["e"]);
336 let runner = VerifyRunner::new_noop();
337 let verify_summary = runner.verify(&apply_summary);
338
339 assert!(verify_summary.is_complete_partition());
340 assert_eq!(verify_summary.total(), 5);
341 assert_eq!(verify_summary.verified.len(), 2);
342 assert_eq!(verify_summary.failed.len(), 0);
343 assert_eq!(verify_summary.not_verified.len(), 3);
344 }
345
346 #[test]
347 fn test_verify_summary_partition_no_overlaps() {
348 let apply_summary = make_apply_summary(&["a"], &["b"], &["c"], &["d"]);
349 let runner = VerifyRunner::new_noop();
350 let verify_summary = runner.verify(&apply_summary);
351
352 let verified_ids: HashSet<&str> = verify_summary
353 .verified
354 .iter()
355 .map(|i| i.component_id.as_str())
356 .collect();
357 let failed_ids: HashSet<&str> = verify_summary
358 .failed
359 .iter()
360 .map(|i| i.component_id.as_str())
361 .collect();
362 let not_verified_ids: HashSet<&str> = verify_summary
363 .not_verified
364 .iter()
365 .map(|i| i.component_id.as_str())
366 .collect();
367
368 assert!(verified_ids.is_disjoint(&failed_ids));
370 assert!(verified_ids.is_disjoint(¬_verified_ids));
371 assert!(failed_ids.is_disjoint(¬_verified_ids));
372
373 let all: HashSet<&str> = verified_ids
375 .union(&failed_ids)
376 .cloned()
377 .collect::<HashSet<&str>>()
378 .union(¬_verified_ids)
379 .cloned()
380 .collect();
381 assert_eq!(all.len(), 4);
382 }
383
384 #[test]
389 fn test_verify_failure_after_successful_apply() {
390 struct FailForB;
392 impl VerifyExecutor for FailForB {
393 fn verify_component(
394 &self,
395 component_id: &str,
396 _expected_version: &str,
397 ) -> VerificationResult {
398 if component_id == "b" {
399 VerificationResult::new(
400 component_id,
401 vec![VerificationCheck::failed("import_test", "import failed")],
402 )
403 } else {
404 VerificationResult::new(
405 component_id,
406 vec![VerificationCheck::passed("ok", "ok")],
407 )
408 }
409 }
410 }
411
412 let apply_summary = make_apply_summary(&["a", "b", "c"], &[], &[], &[]);
413 let runner = VerifyRunner::new(FailForB);
414 let verify_summary = runner.verify(&apply_summary);
415
416 assert_eq!(verify_summary.verified.len(), 2);
417 assert_eq!(verify_summary.failed.len(), 1);
418 assert_eq!(verify_summary.failed[0].component_id, "b");
419 assert_eq!(
420 verify_summary.failed[0].verified,
421 VerifyStatus::VerifyFailed
422 );
423 assert!(verify_summary.failed[0]
424 .verification_result
425 .as_ref()
426 .unwrap()
427 .checks
428 .iter()
429 .any(|c| c.status == CheckStatus::Failed));
430 }
431
432 #[test]
437 fn test_verify_empty_apply_summary() {
438 let apply_summary = ApplySummary::new();
439 let runner = VerifyRunner::new_noop();
440 let verify_summary = runner.verify(&apply_summary);
441
442 assert_eq!(verify_summary.total(), 0);
443 assert!(verify_summary.is_complete_partition());
444 }
445
446 #[test]
451 fn test_verify_status_serde_roundtrip() {
452 let statuses = [
453 VerifyStatus::Verified,
454 VerifyStatus::VerifyFailed,
455 VerifyStatus::NotVerified,
456 ];
457 for status in &statuses {
458 let json = serde_json::to_string(status).unwrap();
459 let back: VerifyStatus = serde_json::from_str(&json).unwrap();
460 assert_eq!(*status, back);
461 }
462 }
463
464 #[test]
469 fn test_verify_summary_serde_roundtrip() {
470 let apply_summary = make_apply_summary(&["a"], &["b"], &["c"], &[]);
471 let runner = VerifyRunner::new_noop();
472 let verify_summary = runner.verify(&apply_summary);
473
474 let json = serde_json::to_string(&verify_summary).unwrap();
475 let back: VerifySummary = serde_json::from_str(&json).unwrap();
476 assert_eq!(verify_summary, back);
477 }
478
479 #[test]
484 fn test_verify_item_serde_roundtrip() {
485 let item = VerifyItem {
486 component_id: "pytorch".to_string(),
487 apply_status: ApplyStatus::Success,
488 verification_result: Some(VerificationResult::new(
489 "pytorch",
490 vec![VerificationCheck::passed("import", "ok")],
491 )),
492 verified: VerifyStatus::Verified,
493 };
494 let json = serde_json::to_string(&item).unwrap();
495 let back: VerifyItem = serde_json::from_str(&json).unwrap();
496 assert_eq!(item, back);
497 }
498
499 #[test]
504 fn test_verify_status_display() {
505 assert_eq!(format!("{}", VerifyStatus::Verified), "verified");
506 assert_eq!(format!("{}", VerifyStatus::VerifyFailed), "verify_failed");
507 assert_eq!(format!("{}", VerifyStatus::NotVerified), "not_verified");
508 }
509
510 #[test]
515 fn test_full_apply_verify_flow() {
516 let apply_summary = make_apply_summary(&["a"], &["b"], &["c"], &[]);
518 let runner = VerifyRunner::new_noop();
519 let verify_summary = runner.verify(&apply_summary);
520
521 assert_eq!(verify_summary.verified.len(), 1);
523 assert_eq!(verify_summary.verified[0].component_id, "a");
524
525 assert_eq!(verify_summary.not_verified.len(), 2);
527 let not_verified_ids: HashSet<&str> = verify_summary
528 .not_verified
529 .iter()
530 .map(|i| i.component_id.as_str())
531 .collect();
532 assert!(not_verified_ids.contains("b"));
533 assert!(not_verified_ids.contains("c"));
534
535 assert!(verify_summary.is_complete_partition());
537 assert_eq!(verify_summary.total(), 3);
538 }
539}