use super::labels::LabelMap;
use super::metrics::Metrics;
use super::model::{OutputColumn, ReportResult, Trend, Verdict};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct RowFacts {
pub differs: bool,
pub verdict: Option<Verdict>,
pub trend: Option<Trend>,
}
impl RowFacts {
pub fn of(result: &ReportResult, r: usize) -> RowFacts {
let Some(row) = result.rows.get(r) else {
return RowFacts::default();
};
let cell = |name: &str| row.cells.get(name).map(String::as_str);
RowFacts {
differs: cell(super::compare::RESULT_COLUMN)
.is_some_and(|v| !v.trim().is_empty() && v != super::compare::MATCH),
verdict: cell(super::compare::CORRECT_COLUMN).and_then(|v| match v {
v if v == Verdict::Correct.as_str() => Some(Verdict::Correct),
v if v == Verdict::Incorrect.as_str() => Some(Verdict::Incorrect),
v if v == Verdict::Untested.as_str() => Some(Verdict::Untested),
_ => None,
}),
trend: result.row_trend(r).or_else(|| {
cell(super::compare::TREND_COLUMN).and_then(|v| match v {
v if v == Trend::Fixed.as_str() => Some(Trend::Fixed),
v if v == Trend::Regressed.as_str() => Some(Trend::Regressed),
_ => None,
})
}),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RowFilter {
All,
Differ,
Incorrect,
Regressed,
MatrixCell {
column: String,
truth: String,
answer: String,
},
}
impl RowFilter {
pub fn label(&self) -> String {
match self {
RowFilter::All => "All".to_string(),
RowFilter::Differ => "Differences".to_string(),
RowFilter::Incorrect => "Incorrect".to_string(),
RowFilter::Regressed => "Regressions".to_string(),
RowFilter::MatrixCell { truth, answer, .. } => format!("{truth} → {answer}"),
}
}
pub fn available(result: &ReportResult) -> Vec<RowFilter> {
let mut out = vec![RowFilter::All];
let facts: Vec<RowFacts> = (0..result.rows.len())
.map(|r| RowFacts::of(result, r))
.collect();
if facts.iter().any(|f| f.differs) {
out.push(RowFilter::Differ);
}
if facts.iter().any(|f| f.verdict == Some(Verdict::Incorrect)) {
out.push(RowFilter::Incorrect);
}
if facts.iter().any(|f| f.trend == Some(Trend::Regressed)) {
out.push(RowFilter::Regressed);
}
out
}
pub fn matches(
&self,
result: &ReportResult,
columns: &[OutputColumn],
labels: &LabelMap,
r: usize,
) -> bool {
match self {
RowFilter::All => true,
RowFilter::Differ => RowFacts::of(result, r).differs,
RowFilter::Incorrect => RowFacts::of(result, r).verdict == Some(Verdict::Incorrect),
RowFilter::Regressed => RowFacts::of(result, r).trend == Some(Trend::Regressed),
RowFilter::MatrixCell {
column,
truth,
answer,
} => {
let key = (r, column.clone());
let Some(expected) = result.truths.get(&key) else {
return false;
};
let Some(row) = result.rows.get(r) else {
return false;
};
let Some(col) = columns.iter().find(|c| &c.header == column) else {
return false;
};
labels.label_of(expected) == *truth
&& labels.label_of(&col.value(row, &result.no_match_marker)) == *answer
}
}
}
}
pub fn all_filters(result: &ReportResult, metrics: Option<&Metrics>) -> (Vec<RowFilter>, usize) {
let mut filters = RowFilter::available(result);
let buttons = filters.len();
if let Some(metrics) = metrics {
for m in &metrics.columns {
let Some(matrix) = &m.matrix else { continue };
for (t, truth) in matrix.axis.iter().enumerate() {
for (p, answer) in matrix.axis.iter().enumerate() {
if matrix.counts[t][p] > 0 {
filters.push(RowFilter::MatrixCell {
column: m.header.clone(),
truth: truth.clone(),
answer: answer.clone(),
});
}
}
}
}
}
(filters, buttons)
}
#[cfg_attr(not(feature = "gui"), allow(dead_code))]
pub fn visible_rows(
result: &ReportResult,
columns: &[OutputColumn],
labels: &LabelMap,
filter: &RowFilter,
text: &str,
) -> Vec<usize> {
let needle = text.trim().to_lowercase();
(0..result.rows.len())
.filter(|r| !result.pending.contains(r))
.filter(|&r| filter.matches(result, columns, labels, r))
.filter(|&r| {
needle.is_empty()
|| result.rows.get(r).is_some_and(|row| {
columns.iter().any(|c| {
c.value(row, &result.no_match_marker)
.to_lowercase()
.contains(&needle)
})
})
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::report::compare::{CORRECT_COLUMN, MATCH, RESULT_COLUMN, TREND_COLUMN};
use crate::report::model::ReportRow;
fn row(cells: &[(&str, &str)]) -> ReportRow {
ReportRow {
cells: cells
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect(),
..Default::default()
}
}
fn col(header: &str) -> OutputColumn {
OutputColumn {
header: header.to_string(),
sources: vec![header.to_string()],
stats: Vec::new(),
image: None,
truth: None,
detail: false,
}
}
fn fixture() -> ReportResult {
ReportResult {
column_order: vec![
RESULT_COLUMN.into(),
CORRECT_COLUMN.into(),
TREND_COLUMN.into(),
"Name".into(),
"Verdict".into(),
],
rows: vec![
row(&[
(RESULT_COLUMN, MATCH),
(CORRECT_COLUMN, "correct"),
(TREND_COLUMN, "unchanged"),
("Name", "alpha"),
("Verdict", "Low Risk"),
]),
row(&[
(RESULT_COLUMN, "Verdict: a≠b"),
(CORRECT_COLUMN, "correct"),
(TREND_COLUMN, "fixed"),
("Name", "beta"),
("Verdict", "Low Risk"),
]),
row(&[
(RESULT_COLUMN, "Verdict: a≠b"),
(CORRECT_COLUMN, "incorrect"),
(TREND_COLUMN, "regressed"),
("Name", "gamma"),
("Verdict", "High Risk"),
]),
row(&[
(RESULT_COLUMN, "Verdict: a≠b"),
(CORRECT_COLUMN, "incorrect"),
(TREND_COLUMN, "unchanged"),
("Name", "delta"),
("Verdict", "High Risk"),
]),
],
trends: [
((0, "Verdict".to_string()), Trend::Unchanged),
((1, "Verdict".to_string()), Trend::Fixed),
((2, "Verdict".to_string()), Trend::Regressed),
((3, "Verdict".to_string()), Trend::StillWrong),
]
.into_iter()
.collect(),
..Default::default()
}
}
#[test]
fn a_still_wrong_row_is_told_apart_from_a_still_right_one() {
let res = fixture();
assert_eq!(RowFacts::of(&res, 0).trend, Some(Trend::Unchanged));
assert_eq!(RowFacts::of(&res, 3).trend, Some(Trend::StillWrong));
assert_eq!(
res.rows[0].cells.get(TREND_COLUMN),
res.rows[3].cells.get(TREND_COLUMN),
"even though the column says the same thing about both"
);
}
fn columns() -> Vec<OutputColumn> {
vec![col("Name"), col("Verdict")]
}
#[test]
fn each_filter_selects_exactly_its_class() {
let res = fixture();
let cols = columns();
let labels = LabelMap::parse(&[]);
let pick = |f: RowFilter| visible_rows(&res, &cols, &labels, &f, "");
assert_eq!(pick(RowFilter::All), vec![0, 1, 2, 3]);
assert_eq!(
pick(RowFilter::Differ),
vec![1, 2, 3],
"the matched row is not a difference"
);
assert_eq!(
pick(RowFilter::Incorrect),
vec![2, 3],
"both wrong rows, whether or not the wrongness is new"
);
assert_eq!(
pick(RowFilter::Regressed),
vec![2],
"`still wrong` is failing, but it is not a regression"
);
}
#[test]
fn an_untested_row_is_not_incorrect() {
let mut res = fixture();
res.rows.push(row(&[
(RESULT_COLUMN, MATCH),
(CORRECT_COLUMN, "untested"),
("Name", "epsilon"),
]));
let cols = columns();
let labels = LabelMap::parse(&[]);
assert_eq!(
visible_rows(&res, &cols, &labels, &RowFilter::Incorrect, ""),
vec![2, 3]
);
}
#[test]
fn the_text_filter_searches_the_shown_columns_and_combines_with_the_class() {
let res = fixture();
let cols = columns();
let labels = LabelMap::parse(&[]);
assert_eq!(
visible_rows(&res, &cols, &labels, &RowFilter::All, "GAM"),
vec![2],
"case-insensitive substring of a shown value"
);
assert_eq!(
visible_rows(&res, &cols, &labels, &RowFilter::Incorrect, "delta"),
vec![3],
"the text and the class narrow together"
);
assert!(
visible_rows(&res, &cols, &labels, &RowFilter::All, "unchanged").is_empty(),
"a value only in a column the caller isn't showing is not searched"
);
}
#[test]
fn a_matrix_cell_selects_the_rows_it_counted_through_the_label_classes() {
let mut res = fixture();
let labels = LabelMap::parse(&[
"Pass = pass, real, low risk",
"Fail = fail, fake, high risk",
]);
for (r, truth) in [(0, "real"), (1, "real"), (2, "real"), (3, "fake")] {
res.truths.insert((r, "Verdict".to_string()), truth.into());
}
let cols = columns();
let cell = |truth: &str, answer: &str| {
visible_rows(
&res,
&cols,
&labels,
&RowFilter::MatrixCell {
column: "Verdict".into(),
truth: truth.into(),
answer: answer.into(),
},
"",
)
};
assert_eq!(
cell("Pass", "Pass"),
vec![0, 1],
"`real` and `Low Risk` are both the Pass class"
);
assert_eq!(cell("Pass", "Fail"), vec![2], "the off-diagonal cell");
assert_eq!(cell("Fail", "Fail"), vec![3]);
assert!(cell("Fail", "Pass").is_empty());
}
#[test]
fn only_the_filters_a_report_can_answer_are_offered() {
assert_eq!(
RowFilter::available(&fixture()),
vec![
RowFilter::All,
RowFilter::Differ,
RowFilter::Incorrect,
RowFilter::Regressed
]
);
let plain = ReportResult {
rows: vec![row(&[("Name", "a")])],
..Default::default()
};
assert_eq!(RowFilter::available(&plain), vec![RowFilter::All]);
}
#[test]
fn rows_that_have_not_run_yet_are_never_shown() {
let mut res = fixture();
res.pending.insert(2);
let cols = columns();
let labels = LabelMap::parse(&[]);
assert_eq!(
visible_rows(&res, &cols, &labels, &RowFilter::All, ""),
vec![0, 1, 3]
);
}
}