use crate::core::verification::{VerificationCheck, VerificationResult};
use crate::orchestrator::apply::{ApplyStatus, ApplySummary};
use serde::{Deserialize, Serialize};
use std::fmt;
pub trait VerifyExecutor: Send + Sync {
fn verify_component(&self, component_id: &str, expected_version: &str) -> VerificationResult;
}
pub struct NoOpVerifyExecutor;
impl VerifyExecutor for NoOpVerifyExecutor {
fn verify_component(&self, component_id: &str, _expected_version: &str) -> VerificationResult {
VerificationResult::new(
component_id,
vec![VerificationCheck::passed(
"default_check",
"no-op verification passed",
)],
)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct VerifyItem {
pub component_id: String,
pub apply_status: ApplyStatus,
pub verification_result: Option<VerificationResult>,
pub verified: VerifyStatus,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum VerifyStatus {
Verified,
VerifyFailed,
NotVerified,
}
impl VerifyStatus {
pub fn label(self) -> &'static str {
match self {
VerifyStatus::Verified => "verified",
VerifyStatus::VerifyFailed => "verify_failed",
VerifyStatus::NotVerified => "not_verified",
}
}
}
impl fmt::Display for VerifyStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.label())
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct VerifySummary {
pub verified: Vec<VerifyItem>,
pub failed: Vec<VerifyItem>,
pub not_verified: Vec<VerifyItem>,
}
impl VerifySummary {
pub fn new() -> Self {
Self {
verified: Vec::new(),
failed: Vec::new(),
not_verified: Vec::new(),
}
}
pub fn total(&self) -> usize {
self.verified.len() + self.failed.len() + self.not_verified.len()
}
pub fn is_complete_partition(&self) -> bool {
let all_ids: HashSet<&str> = self
.verified
.iter()
.chain(self.failed.iter())
.chain(self.not_verified.iter())
.map(|i| i.component_id.as_str())
.collect();
let total_count = self.verified.len() + self.failed.len() + self.not_verified.len();
all_ids.len() == total_count
}
}
impl Default for VerifySummary {
fn default() -> Self {
Self::new()
}
}
pub struct VerifyRunner {
executor: Box<dyn VerifyExecutor>,
}
impl VerifyRunner {
pub fn new(executor: impl VerifyExecutor + 'static) -> Self {
Self {
executor: Box::new(executor),
}
}
pub fn new_noop() -> Self {
Self::new(NoOpVerifyExecutor)
}
pub fn verify(&self, apply_summary: &ApplySummary) -> VerifySummary {
let mut summary = VerifySummary::new();
for item in &apply_summary.success {
let result = self
.executor
.verify_component(&item.component_id, &item.proposed_version);
let verified = result.success;
let verify_item = VerifyItem {
component_id: item.component_id.clone(),
apply_status: ApplyStatus::Success,
verification_result: Some(result),
verified: if verified {
VerifyStatus::Verified
} else {
VerifyStatus::VerifyFailed
},
};
if verified {
summary.verified.push(verify_item);
} else {
summary.failed.push(verify_item);
}
}
for item in apply_summary
.failed
.iter()
.chain(apply_summary.blocked.iter())
.chain(apply_summary.held_back.iter())
.chain(apply_summary.skipped.iter())
{
summary.not_verified.push(VerifyItem {
component_id: item.component_id.clone(),
apply_status: item.status,
verification_result: None,
verified: VerifyStatus::NotVerified,
});
}
summary
}
}
use std::collections::HashSet;
#[cfg(test)]
mod tests {
use super::*;
use crate::core::verification::{CheckStatus, VerificationCheck};
use crate::orchestrator::apply::ApplyStatus;
fn make_apply_summary(
success_ids: &[&str],
failed_ids: &[&str],
held_back_ids: &[&str],
blocked_ids: &[&str],
) -> ApplySummary {
use crate::orchestrator::apply::ApplyItem;
let mut summary = ApplySummary::new();
for &id in success_ids {
summary
.success
.push(ApplyItem::new(id, "1.0.0", "1.1.0", ApplyStatus::Success));
}
for &id in failed_ids {
let mut item = ApplyItem::new(id, "1.0.0", "1.1.0", ApplyStatus::Failed);
item.error_message = "test failure".to_string();
summary.failed.push(item);
}
for &id in held_back_ids {
let mut item = ApplyItem::new(id, "1.0.0", "1.1.0", ApplyStatus::HeldBack);
item.was_selected = false;
summary.held_back.push(item);
}
for &id in blocked_ids {
let mut item = ApplyItem::new(id, "1.0.0", "1.1.0", ApplyStatus::BlockedByFailure);
item.error_message = "blocked by dependency".to_string();
summary.blocked.push(item);
}
summary
}
#[test]
fn test_verify_runs_for_successful_applies() {
let apply_summary = make_apply_summary(&["a", "b", "c"], &[], &[], &[]);
let runner = VerifyRunner::new_noop();
let verify_summary = runner.verify(&apply_summary);
assert_eq!(verify_summary.verified.len(), 3);
assert_eq!(verify_summary.failed.len(), 0);
assert_eq!(verify_summary.not_verified.len(), 0);
for item in &verify_summary.verified {
assert!(item.verification_result.is_some());
assert!(item.verification_result.as_ref().unwrap().success);
}
}
#[test]
fn test_verify_not_run_for_failed_applies() {
let apply_summary = make_apply_summary(&[], &["a"], &[], &[]);
let runner = VerifyRunner::new_noop();
let verify_summary = runner.verify(&apply_summary);
assert_eq!(verify_summary.verified.len(), 0);
assert_eq!(verify_summary.not_verified.len(), 1);
assert_eq!(verify_summary.not_verified[0].component_id, "a");
assert_eq!(
verify_summary.not_verified[0].verified,
VerifyStatus::NotVerified
);
}
#[test]
fn test_verify_summary_complete_partitioning() {
let apply_summary = make_apply_summary(&["a", "b"], &["c"], &["d"], &["e"]);
let runner = VerifyRunner::new_noop();
let verify_summary = runner.verify(&apply_summary);
assert!(verify_summary.is_complete_partition());
assert_eq!(verify_summary.total(), 5);
assert_eq!(verify_summary.verified.len(), 2);
assert_eq!(verify_summary.failed.len(), 0);
assert_eq!(verify_summary.not_verified.len(), 3);
}
#[test]
fn test_verify_summary_partition_no_overlaps() {
let apply_summary = make_apply_summary(&["a"], &["b"], &["c"], &["d"]);
let runner = VerifyRunner::new_noop();
let verify_summary = runner.verify(&apply_summary);
let verified_ids: HashSet<&str> = verify_summary
.verified
.iter()
.map(|i| i.component_id.as_str())
.collect();
let failed_ids: HashSet<&str> = verify_summary
.failed
.iter()
.map(|i| i.component_id.as_str())
.collect();
let not_verified_ids: HashSet<&str> = verify_summary
.not_verified
.iter()
.map(|i| i.component_id.as_str())
.collect();
assert!(verified_ids.is_disjoint(&failed_ids));
assert!(verified_ids.is_disjoint(¬_verified_ids));
assert!(failed_ids.is_disjoint(¬_verified_ids));
let all: HashSet<&str> = verified_ids
.union(&failed_ids)
.cloned()
.collect::<HashSet<&str>>()
.union(¬_verified_ids)
.cloned()
.collect();
assert_eq!(all.len(), 4);
}
#[test]
fn test_verify_failure_after_successful_apply() {
struct FailForB;
impl VerifyExecutor for FailForB {
fn verify_component(
&self,
component_id: &str,
_expected_version: &str,
) -> VerificationResult {
if component_id == "b" {
VerificationResult::new(
component_id,
vec![VerificationCheck::failed("import_test", "import failed")],
)
} else {
VerificationResult::new(
component_id,
vec![VerificationCheck::passed("ok", "ok")],
)
}
}
}
let apply_summary = make_apply_summary(&["a", "b", "c"], &[], &[], &[]);
let runner = VerifyRunner::new(FailForB);
let verify_summary = runner.verify(&apply_summary);
assert_eq!(verify_summary.verified.len(), 2);
assert_eq!(verify_summary.failed.len(), 1);
assert_eq!(verify_summary.failed[0].component_id, "b");
assert_eq!(
verify_summary.failed[0].verified,
VerifyStatus::VerifyFailed
);
assert!(verify_summary.failed[0]
.verification_result
.as_ref()
.unwrap()
.checks
.iter()
.any(|c| c.status == CheckStatus::Failed));
}
#[test]
fn test_verify_empty_apply_summary() {
let apply_summary = ApplySummary::new();
let runner = VerifyRunner::new_noop();
let verify_summary = runner.verify(&apply_summary);
assert_eq!(verify_summary.total(), 0);
assert!(verify_summary.is_complete_partition());
}
#[test]
fn test_verify_status_serde_roundtrip() {
let statuses = [
VerifyStatus::Verified,
VerifyStatus::VerifyFailed,
VerifyStatus::NotVerified,
];
for status in &statuses {
let json = serde_json::to_string(status).unwrap();
let back: VerifyStatus = serde_json::from_str(&json).unwrap();
assert_eq!(*status, back);
}
}
#[test]
fn test_verify_summary_serde_roundtrip() {
let apply_summary = make_apply_summary(&["a"], &["b"], &["c"], &[]);
let runner = VerifyRunner::new_noop();
let verify_summary = runner.verify(&apply_summary);
let json = serde_json::to_string(&verify_summary).unwrap();
let back: VerifySummary = serde_json::from_str(&json).unwrap();
assert_eq!(verify_summary, back);
}
#[test]
fn test_verify_item_serde_roundtrip() {
let item = VerifyItem {
component_id: "pytorch".to_string(),
apply_status: ApplyStatus::Success,
verification_result: Some(VerificationResult::new(
"pytorch",
vec![VerificationCheck::passed("import", "ok")],
)),
verified: VerifyStatus::Verified,
};
let json = serde_json::to_string(&item).unwrap();
let back: VerifyItem = serde_json::from_str(&json).unwrap();
assert_eq!(item, back);
}
#[test]
fn test_verify_status_display() {
assert_eq!(format!("{}", VerifyStatus::Verified), "verified");
assert_eq!(format!("{}", VerifyStatus::VerifyFailed), "verify_failed");
assert_eq!(format!("{}", VerifyStatus::NotVerified), "not_verified");
}
#[test]
fn test_full_apply_verify_flow() {
let apply_summary = make_apply_summary(&["a"], &["b"], &["c"], &[]);
let runner = VerifyRunner::new_noop();
let verify_summary = runner.verify(&apply_summary);
assert_eq!(verify_summary.verified.len(), 1);
assert_eq!(verify_summary.verified[0].component_id, "a");
assert_eq!(verify_summary.not_verified.len(), 2);
let not_verified_ids: HashSet<&str> = verify_summary
.not_verified
.iter()
.map(|i| i.component_id.as_str())
.collect();
assert!(not_verified_ids.contains("b"));
assert!(not_verified_ids.contains("c"));
assert!(verify_summary.is_complete_partition());
assert_eq!(verify_summary.total(), 3);
}
}