//! Turning answers into the result table and the overall verdict, as the //! architecture doc's "Aggregation into the verdict" states it. use swarm_queue_client::review_classifier::{Classification, Overall, Polarity, Row, Verdict}; use crate::answer::Answer; /// The row for `c`, from its answer or the reason it has none. /// /// A row counts when it is applicable and its certainty reaches the /// threshold. A counted row passes when its verdict is on the wanted side of /// its polarity. A row without an answer is never counted. #[must_use] pub fn row(c: &Classification, outcome: Result) -> Row { match outcome { Ok(a) => { let counted = a.verdict != Verdict::NotApplicable && a.certainty >= c.threshold; let wanted = match c.polarity { Polarity::Positive => Verdict::Match, Polarity::Negative => Verdict::NoMatch, }; Row { name: c.name.clone(), polarity: c.polarity, verdict: a.verdict, certainty: a.certainty, threshold: c.threshold, counted, passed: counted && a.verdict == wanted, reason: a.reason, error: None, } } Err(error) => Row { name: c.name.clone(), polarity: c.polarity, verdict: Verdict::NotApplicable, certainty: 0, threshold: c.threshold, counted: false, passed: false, reason: String::new(), error: Some(error), }, } } /// Request changes when any counted row failed, approve otherwise, also when /// nothing counted. #[must_use] pub fn overall(rows: &[Row]) -> Overall { if rows.iter().any(|r| r.counted && !r.passed) { Overall::RequestChanges } else { Overall::Approve } } #[cfg(test)] mod tests { use super::*; fn classification(polarity: Polarity, threshold: u8) -> Classification { Classification { name: "c".to_owned(), description: "d".to_owned(), polarity, threshold, } } fn answer(verdict: Verdict, certainty: u8) -> Answer { Answer { verdict, certainty, reason: "r".to_owned(), } } /// Every polarity × verdict at and below the threshold: what counts, /// what passes, and the overall verdict the row alone gives. #[test] fn polarity_verdict_and_threshold_decide_counted_and_passed() { use Overall::{Approve, RequestChanges}; use Polarity::{Negative, Positive}; use Verdict::{Match, NoMatch, NotApplicable}; let cases = [ // polarity, verdict, certainty, counted, passed, overall (Positive, Match, 80, true, true, Approve), (Positive, NoMatch, 80, true, false, RequestChanges), (Negative, Match, 80, true, false, RequestChanges), (Negative, NoMatch, 80, true, true, Approve), (Positive, NoMatch, 79, false, false, Approve), (Negative, Match, 79, false, false, Approve), (Positive, NotApplicable, 100, false, false, Approve), (Negative, NotApplicable, 100, false, false, Approve), ]; for (polarity, verdict, certainty, counted, passed, want) in cases { let r = row( &classification(polarity, 80), Ok(answer(verdict, certainty)), ); assert_eq!( (r.counted, r.passed, overall(std::slice::from_ref(&r))), (counted, passed, want), "{polarity:?} {verdict:?} at {certainty}" ); } } #[test] fn nothing_counted_is_approve() { assert_eq!(overall(&[]), Overall::Approve); let excluded = [ row( &classification(Polarity::Positive, 90), Ok(answer(Verdict::NoMatch, 10)), ), row( &classification(Polarity::Negative, 0), Ok(answer(Verdict::NotApplicable, 100)), ), ]; assert_eq!(overall(&excluded), Overall::Approve); } #[test] fn one_failing_row_among_passing_ones_requests_changes() { let rows = [ row( &classification(Polarity::Positive, 50), Ok(answer(Verdict::Match, 90)), ), row( &classification(Polarity::Negative, 50), Ok(answer(Verdict::Match, 90)), ), ]; assert_eq!(overall(&rows), Overall::RequestChanges); } #[test] fn an_error_row_is_excluded_and_never_passes() { let r = row( &classification(Polarity::Positive, 0), Err("timed out".to_owned()), ); assert!(!r.counted && !r.passed); assert_eq!(r.error.as_deref(), Some("timed out")); // A threshold of 0 would count any applicable answer; an error is // not one. let failing = row( &classification(Polarity::Positive, 0), Ok(answer(Verdict::NoMatch, 0)), ); assert_eq!(overall(&[r, failing]), Overall::RequestChanges); } }