use crate::core::config::warning_ack::{AcknowledgeableWarningCategory, WarningAcknowledgement};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AckOutcome {
Acknowledged { matched_entry: String },
NotAcknowledged { would_acknowledge: String },
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct AcknowledgementReport {
pub matched_count: usize,
pub matched_entries: Vec<String>,
}
#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
pub enum WarningAckError {
#[error(
"{count} acknowledged warning(s) matched nothing this run and must be removed or \
corrected (the warning may have been fixed, or never fired for that identity/target): \
{entries}"
)]
Stale { count: usize, entries: String },
#[error(
"`{category}` cannot be acknowledged here; this location only accepts: {allowed}. \
Offending entry: identity = \"{identity}\", target = \"{target}\""
)]
OutOfScope {
category: AcknowledgeableWarningCategory,
identity: String,
target: String,
allowed: String,
},
}
#[derive(Debug)]
pub struct AcknowledgementLedger {
scope: Vec<AcknowledgeableWarningCategory>,
entries: Vec<WarningAcknowledgement>,
matched_counts: Vec<usize>,
}
impl AcknowledgementLedger {
pub fn new(
scope: &[AcknowledgeableWarningCategory],
entries: Vec<WarningAcknowledgement>,
) -> Result<Self, WarningAckError> {
if let Some(entry) = entries.iter().find(|entry| !scope.contains(&entry.category)) {
return Err(WarningAckError::OutOfScope {
category: entry.category,
identity: entry.identity.clone(),
target: entry.target.clone(),
allowed: scope
.iter()
.map(|category| category.config_value())
.collect::<Vec<_>>()
.join(", "),
});
}
let matched_counts = vec![0; entries.len()];
Ok(Self {
scope: scope.to_vec(),
entries,
matched_counts,
})
}
pub fn check(&mut self, category: AcknowledgeableWarningCategory, identity: &str, target: &str) -> AckOutcome {
debug_assert!(
self.scope.contains(&category),
"checked warning category `{category}` outside this ledger's declared scope; add it \
to the scope passed to AcknowledgementLedger::new"
);
if let Some(index) = self
.entries
.iter()
.position(|entry| entry.category == category && entry.identity == identity && entry.target == target)
{
self.matched_counts[index] += 1;
let entry = &self.entries[index];
return AckOutcome::Acknowledged {
matched_entry: WarningAcknowledgement::config_entry_for(entry.category, &entry.identity, &entry.target),
};
}
AckOutcome::NotAcknowledged {
would_acknowledge: WarningAcknowledgement::config_entry_for(category, identity, target),
}
}
pub fn finish(self) -> Result<AcknowledgementReport, WarningAckError> {
let stale: Vec<&WarningAcknowledgement> = self
.entries
.iter()
.zip(&self.matched_counts)
.filter_map(|(entry, count)| (*count == 0).then_some(entry))
.collect();
if !stale.is_empty() {
let entries = stale
.iter()
.map(|entry| WarningAcknowledgement::config_entry_for(entry.category, &entry.identity, &entry.target))
.collect::<Vec<_>>()
.join(", ");
return Err(WarningAckError::Stale {
count: stale.len(),
entries,
});
}
let matched_entries = self
.entries
.iter()
.zip(&self.matched_counts)
.filter(|(_, count)| **count > 0)
.map(|(entry, count)| {
format!(
"{} (matched {count}x)",
WarningAcknowledgement::config_entry_for(entry.category, &entry.identity, &entry.target)
)
})
.collect();
let matched_count = self.matched_counts.iter().sum();
Ok(AcknowledgementReport {
matched_count,
matched_entries,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn entry(category: AcknowledgeableWarningCategory, identity: &str, target: &str) -> WarningAcknowledgement {
WarningAcknowledgement {
category,
identity: identity.to_string(),
target: target.to_string(),
reason: None,
}
}
const RESERVED_DOMAIN: AcknowledgeableWarningCategory = AcknowledgeableWarningCategory::DocSnippetReservedDomain;
const VIRTUAL_FIELD: AcknowledgeableWarningCategory = AcknowledgeableWarningCategory::VirtualFieldPath;
#[test]
fn a_stale_acknowledgement_that_matched_nothing_fails_finish() {
let ledger = AcknowledgementLedger::new(
&[RESERVED_DOMAIN],
vec![entry(RESERVED_DOMAIN, "extract_uri", "python")],
)
.expect("scope accepts the configured category");
let error = ledger
.finish()
.expect_err("an acknowledgement that matched nothing must fail");
match error {
WarningAckError::Stale { count, entries } => {
assert_eq!(count, 1, "exactly one entry matched nothing");
assert!(
entries.contains("extract_uri") && entries.contains("python"),
"the failure must name the stale entry: {entries}"
);
}
other => panic!("expected Stale, got {other:?}"),
}
}
#[test]
fn a_matched_acknowledgement_suppresses_and_finish_succeeds() {
let mut ledger = AcknowledgementLedger::new(
&[RESERVED_DOMAIN],
vec![entry(RESERVED_DOMAIN, "extract_uri", "python")],
)
.expect("scope accepts the configured category");
let outcome = ledger.check(RESERVED_DOMAIN, "extract_uri", "python");
assert!(
matches!(outcome, AckOutcome::Acknowledged { .. }),
"an exact identity+target match must acknowledge: {outcome:?}"
);
let report = ledger.finish().expect("a fully-matched ledger must not fail");
assert_eq!(
report.matched_count, 1,
"the matched count must be nonzero and accurate"
);
assert_eq!(report.matched_entries.len(), 1);
}
#[test]
fn an_acknowledgement_for_a_different_target_does_not_apply() {
let mut ledger = AcknowledgementLedger::new(
&[RESERVED_DOMAIN],
vec![entry(RESERVED_DOMAIN, "extract_uri", "python")],
)
.expect("scope accepts the configured category");
let outcome = ledger.check(RESERVED_DOMAIN, "extract_uri", "go");
match outcome {
AckOutcome::NotAcknowledged { would_acknowledge } => {
assert!(
would_acknowledge.contains("\"go\""),
"provenance must name the target actually observed, not the configured one: {would_acknowledge}"
);
}
AckOutcome::Acknowledged { .. } => panic!("a target mismatch must never acknowledge"),
}
let error = ledger
.finish()
.expect_err("the configured entry never matched its target");
assert!(matches!(error, WarningAckError::Stale { count: 1, .. }));
}
#[test]
fn an_acknowledgement_for_a_different_identity_does_not_apply() {
let mut ledger = AcknowledgementLedger::new(
&[RESERVED_DOMAIN],
vec![entry(RESERVED_DOMAIN, "extract_uri", "python")],
)
.expect("scope accepts the configured category");
let outcome = ledger.check(RESERVED_DOMAIN, "other_fixture", "python");
assert!(
matches!(outcome, AckOutcome::NotAcknowledged { .. }),
"an identity mismatch must never acknowledge: {outcome:?}"
);
}
#[test]
fn matched_count_is_nonzero_and_accurate_across_repeated_matches() {
let mut ledger = AcknowledgementLedger::new(
&[RESERVED_DOMAIN],
vec![
entry(RESERVED_DOMAIN, "extract_uri", "python"),
entry(RESERVED_DOMAIN, "batch_scrape", "go"),
],
)
.expect("scope accepts the configured categories");
ledger.check(RESERVED_DOMAIN, "extract_uri", "python");
ledger.check(RESERVED_DOMAIN, "extract_uri", "python");
ledger.check(RESERVED_DOMAIN, "batch_scrape", "go");
let report = ledger.finish().expect("every entry matched at least once");
assert_eq!(report.matched_count, 3, "three occurrences matched across two entries");
assert_eq!(report.matched_entries.len(), 2, "one report line per matched entry");
assert!(report.matched_entries.iter().any(|line| line.contains("matched 2x")));
assert!(report.matched_entries.iter().any(|line| line.contains("matched 1x")));
}
#[test]
fn a_category_outside_the_ledgers_scope_is_rejected_even_though_it_is_acknowledgeable_elsewhere() {
let error =
AcknowledgementLedger::new(&[RESERVED_DOMAIN], vec![entry(VIRTUAL_FIELD, "result.0::Ok", "python")])
.expect_err("a category outside this ledger's scope must be rejected");
match error {
WarningAckError::OutOfScope { category, allowed, .. } => {
assert_eq!(category, VIRTUAL_FIELD);
assert_eq!(allowed, "doc_snippet_reserved_domain");
}
other => panic!("expected OutOfScope, got {other:?}"),
}
}
#[test]
fn a_ledger_with_no_configured_entries_reports_zero_matches_and_never_fails() {
let ledger =
AcknowledgementLedger::new(&[RESERVED_DOMAIN], vec![]).expect("an empty entry list is always valid");
let report = ledger
.finish()
.expect("no configured entries means nothing can be stale");
assert_eq!(report.matched_count, 0);
assert!(report.matched_entries.is_empty());
}
}