use std::collections::{BTreeMap, BTreeSet};
use serde::{Deserialize, Serialize};
use crate::review_corpus::{CLASSES, Corpus, CorpusRow, DefectClass, Verdict};
pub const LINE_WINDOW: u32 = 10;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct CandidateFinding {
pub reviewed_sha: String,
pub path: String,
pub line: u32,
pub description: String,
#[serde(default)]
pub claims_compile_failure: bool,
#[serde(default)]
pub defect_class: Option<DefectClass>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct CandidateRun {
#[serde(default = "run_schema")]
pub schema: String,
pub attempted_shas: BTreeSet<String>,
pub findings: Vec<CandidateFinding>,
#[serde(default)]
pub suppressed: Vec<CandidateFinding>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub arm: Option<RunArm>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RunArm {
pub context: String,
pub model: String,
}
pub const RUN_SCHEMA: &str = "roteiro.review-run/v1";
fn run_schema() -> String {
RUN_SCHEMA.to_owned()
}
impl Default for CandidateRun {
fn default() -> Self {
Self {
schema: run_schema(),
attempted_shas: BTreeSet::new(),
findings: Vec::new(),
suppressed: Vec::new(),
arm: None,
}
}
}
impl CandidateRun {
pub fn parse(text: &str) -> Result<Self, ScoreError> {
let run: Self = serde_json::from_str(text).map_err(|e| ScoreError::Unreadable {
message: e.to_string(),
})?;
if run.schema != RUN_SCHEMA {
return Err(ScoreError::WrongSchema { got: run.schema });
}
Ok(run)
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum ScoreError {
#[error(
"{what} names commit {sha}, which is in no corpus row. The corpus is keyed \
by each comment's `reviewed_sha` (its `original_commit_id`); a merged PR \
head contains the fix commits, so scoring against one measures recall on \
code that is already repaired and silently reports zero"
)]
UnknownSha {
what: &'static str,
sha: String,
},
#[error(
"a finding names commit {sha}, which is not in `attempted_shas` — the \
attempted set decides the denominator, so it must list every commit the \
candidate reviewed"
)]
UndeclaredSha {
sha: String,
},
#[error("the run attempted no commit, so there is nothing to score")]
NothingAttempted,
#[error("not a `{RUN_SCHEMA}` document: {message}")]
Unreadable {
message: String,
},
#[error("run document declares schema {got:?}, but this build scores `{RUN_SCHEMA}`")]
WrongSchema {
got: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct Missed {
pub id: u64,
pub path: String,
pub line: u32,
pub description: String,
pub comment_url: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct ClassRecall {
pub class: DefectClass,
pub real: usize,
pub found: usize,
pub misclassified: usize,
pub missed: Vec<Missed>,
}
impl ClassRecall {
#[must_use]
#[expect(
clippy::cast_precision_loss,
reason = "counts here are corpus rows — 26 today, and a corpus large \
enough to lose f64 precision would have other problems"
)]
pub fn recall(&self) -> Option<f64> {
(self.real > 0).then(|| self.found as f64 / self.real as f64)
}
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct Score {
pub schema: &'static str,
pub attempted_shas: usize,
pub corpus_shas: usize,
pub per_class: Vec<ClassRecall>,
pub found: usize,
pub real_in_scope: usize,
pub known_false_reproduced: usize,
pub known_false_in_scope: usize,
pub unadjudicated: usize,
pub suppressed_real: usize,
pub suppressed_known_false: usize,
pub suppressed_unadjudicated: usize,
pub expected_by_position: Option<f64>,
}
pub const SCORE_SCHEMA: &str = "roteiro.review-score/v1";
impl Score {
#[must_use]
#[expect(
clippy::cast_precision_loss,
reason = "counts here are corpus rows; see ClassRecall::recall"
)]
pub fn corpus_precision(&self) -> Option<f64> {
let adjudicated = self.found + self.known_false_reproduced;
(adjudicated > 0).then(|| self.found as f64 / adjudicated as f64)
}
#[must_use]
pub fn caveats(&self) -> Vec<String> {
let mut out = Vec::new();
if self.attempted_shas < self.corpus_shas {
out.push(format!(
"partial run: {} of {} corpus commits attempted, so rows on the \
other {} are excluded from every denominator rather than counted \
as misses",
self.attempted_shas,
self.corpus_shas,
self.corpus_shas - self.attempted_shas
));
}
let thin: Vec<&str> = self
.per_class
.iter()
.filter(|c| c.real == 1)
.map(|c| c.class.as_str())
.collect();
if !thin.is_empty() {
out.push(format!(
"{} class(es) have a single real row ({}), so their recall is one \
bit rather than a rate and should not be compared as a percentage",
thin.len(),
thin.join(", ")
));
}
#[expect(
clippy::cast_precision_loss,
reason = "a count of corpus rows found; see ClassRecall::recall"
)]
let found = self.found as f64;
if let Some(expected) = self.expected_by_position
&& found <= expected * 2.0
{
out.push(format!(
"RECALL IS NOT CLEARLY ABOVE CHANCE AT THIS FINDING DENSITY: a \
candidate emitting these findings in these places would match \
~{expected:.1} real row(s) by position alone, and this one matched \
{}. Scoring credits a finding to a row on (commit, path, line \
\u{b1}{}) and NEVER on what the finding says, so a reviewer dense \
enough to blanket a diff scores recall it did not earn. That \
baseline is approximate; confirm with a permutation null before \
comparing two candidates, and lower the finding rate first",
self.found, LINE_WINDOW
));
}
if self.unadjudicated > 0 {
out.push(format!(
"{} finding(s) match no corpus row. These are UNADJUDICATED, not \
false positives — the corpus records what one reviewer said about \
these trees, not every defect in them. They become a precision \
figure only once a human adjudicates them and the rows are added",
self.unadjudicated
));
}
if self.suppressed_real > 0 {
out.push(format!(
"the suppression filter withheld {} finding(s) that match a REAL \
row — it is discarding true findings and its licence (zero cost \
on this corpus) no longer holds",
self.suppressed_real
));
}
out
}
}
pub fn score(corpus: &Corpus, run: &CandidateRun) -> Result<Score, ScoreError> {
let known: BTreeSet<&str> = corpus.reviewed_shas();
if run.attempted_shas.is_empty() {
return Err(ScoreError::NothingAttempted);
}
for sha in &run.attempted_shas {
if !known.contains(sha.as_str()) {
return Err(ScoreError::UnknownSha {
what: "attempted_shas",
sha: sha.clone(),
});
}
}
for finding in run.findings.iter().chain(&run.suppressed) {
if !known.contains(finding.reviewed_sha.as_str()) {
return Err(ScoreError::UnknownSha {
what: "a finding",
sha: finding.reviewed_sha.clone(),
});
}
if !run.attempted_shas.contains(&finding.reviewed_sha) {
return Err(ScoreError::UndeclaredSha {
sha: finding.reviewed_sha.clone(),
});
}
}
let in_scope: Vec<&CorpusRow> = corpus
.rows()
.iter()
.filter(|r| run.attempted_shas.contains(&r.reviewed_sha))
.collect();
let emitted = match_findings(&in_scope, &run.findings);
let withheld = match_findings(&in_scope, &run.suppressed);
let mut per_class: Vec<ClassRecall> = Vec::with_capacity(CLASSES.len());
for class in CLASSES {
let rows: Vec<&&CorpusRow> = in_scope
.iter()
.filter(|r| r.defect_class == class && r.verdict == Verdict::Real)
.collect();
let mut found = 0;
let mut misclassified = 0;
let mut missed = Vec::new();
for row in &rows {
match emitted.by_row.get(&row.id) {
Some(finding) => {
found += 1;
if finding.defect_class.is_some_and(|c| c != class) {
misclassified += 1;
}
}
None => missed.push(Missed {
id: row.id,
path: row.path.clone(),
line: row.line,
description: row.description.clone(),
comment_url: row.comment_url.clone(),
}),
}
}
per_class.push(ClassRecall {
class,
real: rows.len(),
found,
misclassified,
missed,
});
}
let real_in_scope = in_scope
.iter()
.filter(|r| r.verdict == Verdict::Real)
.count();
let known_false_in_scope = in_scope
.iter()
.filter(|r| r.verdict == Verdict::False)
.count();
let verdicts: BTreeMap<u64, Verdict> = in_scope.iter().map(|r| (r.id, r.verdict)).collect();
let count_by_verdict = |m: &Matched, want: Verdict| {
m.by_row
.keys()
.filter(|id| verdicts.get(id) == Some(&want))
.count()
};
Ok(Score {
schema: SCORE_SCHEMA,
attempted_shas: run.attempted_shas.len(),
corpus_shas: known.len(),
per_class,
found: count_by_verdict(&emitted, Verdict::Real),
real_in_scope,
known_false_reproduced: count_by_verdict(&emitted, Verdict::False),
known_false_in_scope,
unadjudicated: run.findings.len() - emitted.by_row.len(),
suppressed_real: count_by_verdict(&withheld, Verdict::Real),
suppressed_known_false: count_by_verdict(&withheld, Verdict::False),
suppressed_unadjudicated: run.suppressed.len() - withheld.by_row.len(),
expected_by_position: expected_by_position(&in_scope, &run.findings),
})
}
#[expect(
clippy::cast_precision_loss,
reason = "line numbers and finding counts on one file; a file long enough to lose f64 precision is not reviewable at all"
)]
fn expected_by_position(rows: &[&CorpusRow], findings: &[CandidateFinding]) -> Option<f64> {
let mut by_file: BTreeMap<(&str, &str), Vec<u32>> = BTreeMap::new();
for f in findings {
by_file
.entry((f.reviewed_sha.as_str(), f.path.as_str()))
.or_default()
.push(f.line);
}
let mut total = 0.0;
let mut any = false;
for row in rows.iter().filter(|r| r.verdict == Verdict::Real) {
let Some(lines) = by_file.get(&(row.reviewed_sha.as_str(), row.path.as_str())) else {
continue;
};
any = true;
let (lo, hi) = (
lines.iter().copied().min().unwrap_or(0),
lines.iter().copied().max().unwrap_or(0),
);
let span = f64::from(hi - lo + 1);
let mut sorted = lines.clone();
sorted.sort_unstable();
let mut covered = 0u64;
let mut open: Option<(u32, u32)> = None;
for line in sorted {
let (start, end) = (line.saturating_sub(LINE_WINDOW), line + LINE_WINDOW);
match open {
Some((s, e)) if start <= e + 1 => open = Some((s, e.max(end))),
Some((s, e)) => {
covered += u64::from(e - s + 1);
open = Some((start, end));
}
None => open = Some((start, end)),
}
}
if let Some((s, e)) = open {
covered += u64::from(e - s + 1);
}
let reach = covered as f64;
total += (reach / span).min(1.0);
}
any.then_some(total)
}
struct Matched<'a> {
by_row: BTreeMap<u64, &'a CandidateFinding>,
}
fn match_findings<'a>(rows: &[&'a CorpusRow], findings: &'a [CandidateFinding]) -> Matched<'a> {
let mut pairs: Vec<(u32, u64, u32, usize)> = Vec::new();
for (idx, finding) in findings.iter().enumerate() {
for row in rows {
if row.reviewed_sha != finding.reviewed_sha || row.path != finding.path {
continue;
}
let distance = row.line.abs_diff(finding.line);
if distance <= LINE_WINDOW {
pairs.push((distance, row.id, finding.line, idx));
}
}
}
pairs.sort_unstable();
let mut by_row: BTreeMap<u64, &CandidateFinding> = BTreeMap::new();
let mut used: BTreeSet<usize> = BTreeSet::new();
for (_, row_id, _, idx) in pairs {
if by_row.contains_key(&row_id) || used.contains(&idx) {
continue;
}
by_row.insert(row_id, &findings[idx]);
used.insert(idx);
}
Matched { by_row }
}
#[cfg(test)]
mod tests {
use super::{CandidateFinding, CandidateRun, LINE_WINDOW, SCORE_SCHEMA, ScoreError, score};
use crate::review_corpus::{Corpus, DefectClass, Verdict};
const SHA_A: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
const SHA_B: &str = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
fn row(id: u64, sha: &str, path: &str, line: u32, verdict: &str, class: &str) -> String {
format!(
"{{\"id\": {id}, \"pr\": 300, \"reviewer\": \"github-copilot\", \
\"reviewed_sha\": {sha:?}, \"path\": {path:?}, \"line\": {line}, \
\"verdict\": {verdict:?}, \"defect_class\": {class:?}, \
\"fix_commit\": \"\", \"description\": \"d\", \
\"comment_url\": \"https://example.invalid/{id}\"}}"
)
}
fn corpus() -> Corpus {
let text = [
row(1, SHA_A, "src/a.rs", 100, "real", "contract-drift"),
row(2, SHA_A, "src/a.rs", 200, "real", "contract-drift"),
row(3, SHA_A, "src/b.rs", 10, "real", "vacuous-test"),
row(4, SHA_B, "src/c.rs", 50, "real", "ordering-bug"),
row(5, SHA_B, "src/c.rs", 300, "false", "false-compile-claim"),
]
.join("\n");
Corpus::parse(&text).expect("the test corpus parses")
}
fn finding(sha: &str, path: &str, line: u32) -> CandidateFinding {
CandidateFinding {
reviewed_sha: sha.to_owned(),
path: path.to_owned(),
line,
description: "a finding".to_owned(),
claims_compile_failure: false,
defect_class: None,
}
}
#[test]
fn a_blanketing_candidate_is_flagged_as_not_clearly_above_chance() {
let dense: Vec<CandidateFinding> = (0..12)
.map(|i| finding(SHA_A, "src/a.rs", 90 + i * 3))
.collect();
let scored = score(&corpus(), &run(&[SHA_A, SHA_B], dense)).expect("scores");
let expected = scored
.expected_by_position
.expect("findings landed on a file carrying a row");
assert!(
expected > 0.5,
"a candidate blanketing a row's file scored a chance baseline of only \
{expected}"
);
assert!(
scored
.caveats()
.iter()
.any(|c| c.contains("NOT CLEARLY ABOVE CHANCE")),
"the density caveat did not fire: {:?}",
scored.caveats()
);
}
#[test]
fn a_sparse_candidate_that_misses_is_not_blamed_on_density() {
let sparse = vec![finding(SHA_A, "src/a.rs", 100)];
let scored = score(&corpus(), &run(&[SHA_A, SHA_B], sparse)).expect("scores");
assert_eq!(scored.found, 1);
assert!(scored.expected_by_position.is_some());
}
#[test]
fn a_run_touching_no_anchored_file_has_no_chance_baseline() {
let elsewhere = vec![finding(SHA_A, "src/nowhere.rs", 10)];
let scored = score(&corpus(), &run(&[SHA_A, SHA_B], elsewhere)).expect("scores");
assert_eq!(scored.expected_by_position, None);
assert!(
!scored
.caveats()
.iter()
.any(|c| c.contains("NOT CLEARLY ABOVE CHANCE")),
"a caveat about density fired with no findings to be dense"
);
}
#[test]
fn overlapping_windows_count_once() {
let mut clustered: Vec<CandidateFinding> = (0..10)
.map(|i| finding(SHA_A, "src/a.rs", 100 + i))
.collect();
clustered.push(finding(SHA_A, "src/a.rs", 1_000));
let scored = score(&corpus(), &run(&[SHA_A, SHA_B], clustered)).expect("scores");
let expected = scored.expected_by_position.expect("some");
assert!(
expected < 0.25,
"overlapping windows were summed rather than merged: {expected}"
);
}
fn run(shas: &[&str], findings: Vec<CandidateFinding>) -> CandidateRun {
CandidateRun {
attempted_shas: shas.iter().map(|s| (*s).to_owned()).collect(),
findings,
..CandidateRun::default()
}
}
#[test]
fn a_found_row_counts_in_its_own_class() {
let scored = score(
&corpus(),
&run(
&[SHA_A],
vec![
finding(SHA_A, "src/a.rs", 100),
finding(SHA_A, "src/a.rs", 200),
finding(SHA_A, "src/b.rs", 10),
],
),
)
.expect("scores");
assert_eq!(scored.schema, SCORE_SCHEMA);
assert_eq!(scored.found, 3);
assert_eq!(scored.real_in_scope, 3);
let drift = scored
.per_class
.iter()
.find(|c| c.class == DefectClass::ContractDrift)
.expect("every class is present");
assert_eq!((drift.real, drift.found), (2, 2));
assert_eq!(drift.recall(), Some(1.0));
let cleanup = scored
.per_class
.iter()
.find(|c| c.class == DefectClass::CleanupGap)
.expect("present with real: 0");
assert_eq!(cleanup.real, 0);
assert_eq!(cleanup.recall(), None);
}
#[test]
fn rows_outside_the_attempted_commits_are_out_of_scope_not_missed() {
let scored = score(&corpus(), &run(&[SHA_A], vec![])).expect("scores");
assert_eq!(scored.real_in_scope, 3, "only SHA_A's real rows");
assert_eq!(scored.found, 0);
assert_eq!(scored.known_false_in_scope, 0, "the false row is on SHA_B");
assert_eq!((scored.attempted_shas, scored.corpus_shas), (1, 2));
assert!(
scored.caveats().iter().any(|c| c.contains("partial run")),
"{:?}",
scored.caveats()
);
}
#[test]
fn a_sha_the_corpus_does_not_know_is_refused_with_the_reason() {
let head = "cccccccccccccccccccccccccccccccccccccccc";
let err = score(&corpus(), &run(&[head], vec![])).expect_err("not a corpus commit");
let ScoreError::UnknownSha { what, .. } = err else {
panic!("expected UnknownSha, got {err:?}");
};
assert_eq!(what, "attempted_shas");
let text = err.to_string();
assert!(text.contains("reviewed_sha"), "{text}");
assert!(
text.contains("fix commits") && text.contains("silently reports zero"),
"says what goes wrong, not just that it did: {text}"
);
let mut r = run(&[SHA_A], vec![finding(head, "src/a.rs", 100)]);
r.attempted_shas.insert(SHA_A.to_owned());
let err = score(&corpus(), &r).expect_err("a finding on an unknown commit");
assert!(
matches!(
err,
ScoreError::UnknownSha {
what: "a finding",
..
}
),
"{err:?}"
);
}
#[test]
fn a_finding_outside_the_attempted_set_is_refused() {
let err = score(
&corpus(),
&run(&[SHA_A], vec![finding(SHA_B, "src/c.rs", 50)]),
)
.expect_err("SHA_B was not attempted");
assert!(matches!(err, ScoreError::UndeclaredSha { .. }), "{err:?}");
}
#[test]
fn an_empty_run_is_refused_rather_than_scored_as_zero() {
let err = score(&corpus(), &CandidateRun::default()).expect_err("nothing attempted");
assert!(matches!(err, ScoreError::NothingAttempted), "{err:?}");
}
#[test]
fn an_unmatched_finding_is_unadjudicated_not_false() {
let scored = score(
&corpus(),
&run(&[SHA_A], vec![finding(SHA_A, "src/z.rs", 7)]),
)
.expect("scores");
assert_eq!(scored.unadjudicated, 1);
assert_eq!(scored.known_false_reproduced, 0);
assert_eq!(scored.found, 0);
assert_eq!(
scored.corpus_precision(),
None,
"no adjudicated finding means no precision, not 1.0 and not 0.0"
);
let caveat = scored.caveats().join(" ");
assert!(caveat.contains("UNADJUDICATED"), "{caveat}");
assert!(
caveat.contains("not every defect in them"),
"says why it is not precision: {caveat}"
);
}
#[test]
fn reproducing_a_known_false_row_costs_precision() {
let scored = score(
&corpus(),
&run(
&[SHA_B],
vec![
finding(SHA_B, "src/c.rs", 50), finding(SHA_B, "src/c.rs", 300), ],
),
)
.expect("scores");
assert_eq!((scored.found, scored.known_false_reproduced), (1, 1));
assert_eq!(scored.corpus_precision(), Some(0.5));
assert_eq!(scored.unadjudicated, 0);
}
#[test]
fn matching_tolerates_a_near_miss_but_not_a_far_one() {
let near = score(
&corpus(),
&run(
&[SHA_A],
vec![finding(SHA_A, "src/a.rs", 100 + LINE_WINDOW)],
),
)
.expect("scores");
assert_eq!(near.found, 1, "at the window edge");
let far = score(
&corpus(),
&run(
&[SHA_A],
vec![finding(SHA_A, "src/a.rs", 100 + LINE_WINDOW + 1)],
),
)
.expect("scores");
assert_eq!(far.found, 0, "one line past the window");
assert_eq!(far.unadjudicated, 1);
}
#[test]
fn the_window_bounds_which_row_a_distant_finding_can_claim() {
let text = [
row(1, SHA_A, "tests/t.rs", 75, "real", "vacuous-test"),
row(2, SHA_A, "tests/t.rs", 125, "real", "vacuous-test"),
row(3, SHA_A, "tests/t.rs", 176, "real", "vacuous-test"),
]
.join("\n");
let corpus = Corpus::parse(&text).expect("parses");
const { assert!(LINE_WINDOW * 2 < 50, "the window would span two #299 rows") };
let scored = score(
&corpus,
&run(&[SHA_A], vec![finding(SHA_A, "tests/t.rs", 100)]),
)
.expect("scores");
assert_eq!(
scored.found, 0,
"a finding 25 lines from the nearest row has not found it"
);
assert_eq!(scored.unadjudicated, 1);
let vacuous = scored
.per_class
.iter()
.find(|c| c.class == DefectClass::VacuousTest)
.expect("present");
let missed: Vec<u64> = vacuous.missed.iter().map(|m| m.id).collect();
assert_eq!(missed, vec![1, 2, 3], "names what to read next");
assert!(
vacuous
.missed
.iter()
.all(|m| !m.comment_url.is_empty() && m.line > 0),
"a miss carries enough to go and look at it, not just an id"
);
let all = score(
&corpus,
&run(
&[SHA_A],
vec![
finding(SHA_A, "tests/t.rs", 75),
finding(SHA_A, "tests/t.rs", 125),
finding(SHA_A, "tests/t.rs", 176),
],
),
)
.expect("scores");
assert_eq!(all.found, 3);
}
#[test]
fn one_finding_cannot_claim_two_rows_in_the_same_window() {
let text = [
row(1, SHA_A, "src/a.rs", 100, "real", "contract-drift"),
row(2, SHA_A, "src/a.rs", 105, "real", "contract-drift"),
]
.join("\n");
let corpus = Corpus::parse(&text).expect("parses");
let scored = score(
&corpus,
&run(&[SHA_A], vec![finding(SHA_A, "src/a.rs", 102)]),
)
.expect("scores");
assert_eq!(
scored.found, 1,
"one comment is one finding, however many rows it is near"
);
let drift = scored
.per_class
.iter()
.find(|c| c.class == DefectClass::ContractDrift)
.expect("present");
assert_eq!((drift.real, drift.found), (2, 1));
assert_eq!(
drift.missed.iter().map(|m| m.id).collect::<Vec<_>>(),
vec![2]
);
}
#[test]
fn extra_findings_in_one_window_do_not_inflate_recall() {
let scored = score(
&corpus(),
&run(
&[SHA_A],
vec![
finding(SHA_A, "src/a.rs", 98),
finding(SHA_A, "src/a.rs", 100),
finding(SHA_A, "src/a.rs", 102),
],
),
)
.expect("scores");
assert_eq!(scored.found, 1, "one row, so one credit");
assert_eq!(scored.unadjudicated, 2);
}
#[test]
fn the_score_is_independent_of_finding_order() {
let findings = vec![
finding(SHA_A, "src/a.rs", 98),
finding(SHA_A, "src/a.rs", 205),
finding(SHA_A, "src/b.rs", 10),
];
let forward = score(&corpus(), &run(&[SHA_A], findings.clone())).expect("scores");
let mut reversed = findings;
reversed.reverse();
let backward = score(&corpus(), &run(&[SHA_A], reversed)).expect("scores");
assert_eq!(forward, backward);
assert_eq!(forward.found, 3);
}
#[test]
fn a_misclassified_finding_still_counts_as_found() {
let mut f = finding(SHA_A, "src/b.rs", 10);
f.defect_class = Some(DefectClass::ProseClarity); let scored = score(&corpus(), &run(&[SHA_A], vec![f])).expect("scores");
let vacuous = scored
.per_class
.iter()
.find(|c| c.class == DefectClass::VacuousTest)
.expect("present");
assert_eq!(
(vacuous.real, vacuous.found, vacuous.misclassified),
(1, 1, 1)
);
}
#[test]
fn suppressed_findings_are_scored_separately_and_a_true_one_raises_a_caveat() {
let mut good = CandidateRun {
attempted_shas: [SHA_B.to_owned()].into_iter().collect(),
suppressed: vec![finding(SHA_B, "src/c.rs", 300)],
..CandidateRun::default()
};
let scored = score(&corpus(), &good).expect("scores");
assert_eq!(scored.suppressed_known_false, 1);
assert_eq!(scored.suppressed_real, 0);
assert_eq!(
scored.known_false_reproduced, 0,
"withheld, so not reproduced"
);
assert!(
!scored.caveats().iter().any(|c| c.contains("REAL row")),
"nothing true was withheld: {:?}",
scored.caveats()
);
good.suppressed.push(finding(SHA_B, "src/c.rs", 50));
let bad = score(&corpus(), &good).expect("scores");
assert_eq!(bad.suppressed_real, 1);
let caveat = bad.caveats().join(" ");
assert!(
caveat.contains("REAL row") && caveat.contains("licence"),
"says the filter's licence no longer holds: {caveat}"
);
}
#[test]
fn a_withheld_finding_is_not_an_unadjudicated_emitted_one() {
let scored = score(
&corpus(),
&CandidateRun {
attempted_shas: [SHA_A.to_owned()].into_iter().collect(),
suppressed: vec![finding(SHA_A, "src/z.rs", 7)],
..CandidateRun::default()
},
)
.expect("scores");
assert_eq!(scored.unadjudicated, 0);
assert_eq!(scored.suppressed_unadjudicated, 1);
}
#[test]
fn the_report_shape_does_not_change_with_the_run() {
let scored = score(&corpus(), &run(&[SHA_A], vec![])).expect("scores");
let classes: Vec<_> = scored.per_class.iter().map(|c| c.class).collect();
assert_eq!(classes, crate::review_corpus::CLASSES.to_vec());
}
#[test]
fn avoiding_a_false_row_out_of_scope_is_not_a_credit() {
let scored = score(&corpus(), &run(&[SHA_A], vec![])).expect("scores");
assert_eq!(scored.known_false_in_scope, 0);
let with_b = score(&corpus(), &run(&[SHA_A, SHA_B], vec![])).expect("scores");
assert_eq!(with_b.known_false_in_scope, 1);
assert_eq!(with_b.known_false_reproduced, 0);
}
#[test]
fn scope_splits_on_verdict_exhaustively() {
let scored = score(&corpus(), &run(&[SHA_A, SHA_B], vec![])).expect("scores");
assert_eq!(
scored.real_in_scope + scored.known_false_in_scope,
corpus().rows().len(),
"every in-scope row is in exactly one denominator"
);
assert_eq!(
corpus().with_verdict(Verdict::False).count(),
scored.known_false_in_scope
);
}
}