159 lines
5.2 KiB
Rust
159 lines
5.2 KiB
Rust
//! Turning answers into the result table and the overall verdict, as the
|
||
//! architecture doc's "Aggregation into the verdict" states it.
|
||
|
||
use swarm_wire::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<Answer, String>) -> 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);
|
||
}
|
||
}
|