use std::sync::OnceLock;
use freenet_stdlib::prelude::ContractInstanceId;
use parking_lot::RwLock;
use tokio::time::Instant;
use super::property::Severity;
use super::shadow::{Finding, JudgedContract};
const MAX_REMEMBERED_CHECKED: usize = 256;
const STALE_AFTER: std::time::Duration =
std::time::Duration::from_secs(super::shadow::PROBE_INTERVAL.as_secs() * 3);
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MergeFinding {
pub contract: ContractInstanceId,
pub property: &'static str,
pub severity: Severity,
pub would_remove: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CheckedContract {
pub contract: ContractInstanceId,
pub verdicts: usize,
pub inconclusive: usize,
findings: Vec<MergeFinding>,
pub checked_at: Instant,
}
impl CheckedContract {
pub(crate) fn new(
contract: ContractInstanceId,
verdicts: usize,
inconclusive: usize,
checked_at: Instant,
) -> Self {
Self {
contract,
verdicts,
inconclusive,
findings: Vec::new(),
checked_at,
}
}
pub fn findings(&self) -> &[MergeFinding] {
&self.findings
}
pub(crate) fn note_finding(&mut self, finding: MergeFinding) {
if self.findings.iter().any(|f| f.property == finding.property) {
return;
}
self.findings.insert(0, finding);
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MergeCheckStatus {
pub judged_last_tick: usize,
pub without_verdict_last_tick: usize,
pub checked: Vec<CheckedContract>,
pub published_at: Instant,
}
impl Default for MergeCheckStatus {
fn default() -> Self {
Self {
judged_last_tick: 0,
without_verdict_last_tick: 0,
checked: Vec::new(),
published_at: Instant::now(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MergeCheckView {
pub judged_last_tick: usize,
pub without_verdict_last_tick: usize,
pub published_secs_ago: u64,
pub stale: bool,
pub contract: Option<CheckedContract>,
pub checked_secs_ago: Option<u64>,
}
impl MergeCheckStatus {
pub fn was_checked(&self, contract: &ContractInstanceId) -> bool {
self.record_for(contract).is_some()
}
pub fn record_for(&self, contract: &ContractInstanceId) -> Option<&CheckedContract> {
self.checked.iter().find(|c| c.contract == *contract)
}
pub fn findings_for<'a>(
&'a self,
contract: &'a ContractInstanceId,
) -> impl Iterator<Item = &'a MergeFinding> + 'a {
self.record_for(contract)
.into_iter()
.flat_map(|c| c.findings.iter())
}
pub fn view_for(&self, contract: Option<&ContractInstanceId>, now: Instant) -> MergeCheckView {
let age = now.saturating_duration_since(self.published_at);
let record = contract.and_then(|id| self.record_for(id)).cloned();
let checked_secs_ago = record
.as_ref()
.map(|r| now.saturating_duration_since(r.checked_at).as_secs());
MergeCheckView {
judged_last_tick: self.judged_last_tick,
without_verdict_last_tick: self.without_verdict_last_tick,
published_secs_ago: age.as_secs(),
stale: age >= STALE_AFTER,
contract: record,
checked_secs_ago,
}
}
}
static STATUS: RwLock<Option<MergeCheckStatus>> = RwLock::new(None);
static ENABLED: OnceLock<()> = OnceLock::new();
pub fn mark_enabled() {
if ENABLED.set(()).is_err() {
tracing::debug!("merge-check status already marked enabled");
}
}
pub fn is_enabled() -> bool {
ENABLED.get().is_some()
}
impl MergeCheckStatus {
pub fn record(
&mut self,
checked: impl IntoIterator<Item = CheckedContract>,
judged_last_tick: usize,
without_verdict_last_tick: usize,
published_at: Instant,
) {
self.judged_last_tick = judged_last_tick;
self.without_verdict_last_tick = without_verdict_last_tick;
self.published_at = published_at;
for record in checked {
let existing = self
.checked
.iter()
.position(|c| c.contract == record.contract)
.map(|at| self.checked.remove(at));
let mut merged = match existing {
Some(mut prev) => {
prev.verdicts = prev.verdicts.saturating_add(record.verdicts);
prev.inconclusive = prev.inconclusive.saturating_add(record.inconclusive);
for finding in record.findings {
prev.note_finding(finding);
}
prev
}
None => record,
};
merged.checked_at = published_at;
self.checked.insert(0, merged);
}
self.checked.truncate(MAX_REMEMBERED_CHECKED);
}
}
pub(crate) fn checked_contracts(
judged: &[JudgedContract],
findings: &[Finding],
) -> Vec<CheckedContract> {
let checked_at = Instant::now();
let mut out: Vec<CheckedContract> = judged
.iter()
.map(|j| CheckedContract::new(j.contract, j.verdicts, j.inconclusive, checked_at))
.collect();
for finding in findings {
let merge = MergeFinding {
contract: finding.contract,
property: finding.violation.property.as_str(),
severity: finding.violation.property.severity(),
would_remove: finding.would_remove,
};
match out.iter_mut().find(|c| c.contract == finding.contract) {
Some(record) => record.note_finding(merge),
None => {
tracing::warn!(
contract = %finding.contract,
property = merge.property,
"merge finding for a contract the tick did not record as judged"
);
let mut record = CheckedContract::new(finding.contract, 1, 0, checked_at);
record.note_finding(merge);
out.push(record);
}
}
}
out
}
pub(crate) fn publish(
checked: impl IntoIterator<Item = CheckedContract>,
judged_last_tick: usize,
without_verdict_last_tick: usize,
published_at: Instant,
) {
STATUS
.write()
.get_or_insert_with(MergeCheckStatus::default)
.record(
checked,
judged_last_tick,
without_verdict_last_tick,
published_at,
);
}
pub fn view_for(contract: Option<&ContractInstanceId>) -> Option<MergeCheckView> {
STATUS
.read()
.as_ref()
.map(|s| s.view_for(contract, Instant::now()))
}
#[cfg(test)]
mod tests {
use super::*;
fn instance(n: u8) -> ContractInstanceId {
ContractInstanceId::new([n; 32])
}
fn finding(n: u8, property: &'static str) -> MergeFinding {
MergeFinding {
contract: instance(n),
property,
severity: Severity::Violation,
would_remove: true,
}
}
#[test]
fn the_findings_field_stays_private() {
let src = include_str!("status.rs");
let anchor = "pub struct CheckedContract {";
let start = src
.find(anchor)
.expect("CheckedContract is no longer declared here; this pin reads nothing")
+ anchor.len();
let end = start
+ src[start..]
.find("\n}\n")
.expect("CheckedContract's declaration is not brace-balanced");
let decl = &src[start..end];
assert!(
decl.contains("\n findings: Vec<MergeFinding>,"),
"CheckedContract no longer declares `findings` privately. While it was \
`pub` in a `pub mod`, any caller could hand `record` a hand-built record \
carrying duplicate properties, and the insert arm does not deduplicate — \
that is #5403 H1 verbatim. Build records with `CheckedContract::new` and \
read them through `findings()`. got:{decl}"
);
}
fn checked(n: u8, findings: Vec<MergeFinding>) -> CheckedContract {
let mut record = CheckedContract::new(instance(n), 1, 0, Instant::now());
for finding in findings {
record.note_finding(finding);
}
record
}
const NEVER_SETTLES: u8 = 6;
#[tokio::test(flavor = "multi_thread")]
async fn a_first_record_carries_one_finding_per_broken_law_not_per_case()
-> Result<(), Box<dyn std::error::Error>> {
let (id, report, findings) =
crate::conformance::shadow::probe_fixture_contract(NEVER_SETTLES).await?;
let distinct: std::collections::BTreeSet<&str> = findings
.iter()
.map(|f| f.violation.property.as_str())
.collect();
assert!(
distinct.len() > 1,
"the fixture stopped breaking more than one law, so this test can no \
longer tell deduplication from collapsing everything into one row: \
{distinct:?}"
);
assert!(
findings.len() > distinct.len(),
"the probe returned no repeated property ({} findings, {} distinct), so \
this test would pass against an assembly that never deduplicates",
findings.len(),
distinct.len()
);
let records = checked_contracts(&report.judged, &findings);
assert_eq!(
records.len(),
1,
"one probed contract must produce exactly one record: {records:?}"
);
let record = &records[0];
assert_eq!(record.contract, id);
let rendered: Vec<&str> = record.findings.iter().map(|f| f.property).collect();
assert_eq!(
rendered.len(),
distinct.len(),
"the first record for a violating contract carried one finding per \
violating CASE, so its card renders {} rows for {} broken laws — and \
they persist, because later ticks deduplicate against them. got: \
{rendered:?}",
rendered.len(),
distinct.len()
);
for property in &distinct {
assert_eq!(
rendered.iter().filter(|p| *p == property).count(),
1,
"{property} appears {} times on the record",
rendered.iter().filter(|p| *p == property).count()
);
}
let mut status = MergeCheckStatus::default();
status.record(
records,
report.judged.len(),
report.without_verdict,
Instant::now(),
);
let view = status.view_for(Some(&id), Instant::now());
let seen = view.contract.expect("the probed contract is in the window");
assert_eq!(
seen.findings.len(),
distinct.len(),
"the view the contract-detail page reads carried duplicate findings"
);
assert_eq!(
(seen.verdicts, seen.inconclusive),
(report.judged[0].verdicts, report.judged[0].inconclusive),
"the record's case counts are not the probe's own"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn re_recording_a_tick_does_not_change_what_the_page_shows()
-> Result<(), Box<dyn std::error::Error>> {
let (id, report, findings) =
crate::conformance::shadow::probe_fixture_contract(NEVER_SETTLES).await?;
let mut status = MergeCheckStatus::default();
status.record(
checked_contracts(&report.judged, &findings),
report.judged.len(),
report.without_verdict,
Instant::now(),
);
let after_first = status
.record_for(&id)
.expect("the probed contract is in the window")
.findings
.len();
status.record(
checked_contracts(&report.judged, &findings),
report.judged.len(),
report.without_verdict,
Instant::now(),
);
let after_second = status
.record_for(&id)
.expect("the probed contract is still in the window")
.findings
.len();
assert_eq!(
after_first, after_second,
"the insert arm and the merge arm disagree about how many findings one \
contract has, so what an operator sees depends on whether this is the \
tick that first caught the contract"
);
Ok(())
}
#[test]
fn a_finding_for_an_unjudged_contract_does_not_render_as_zero_verdicts() {
use crate::conformance::property::{ConformanceProperty, OutputDigest, Violation};
let orphan = instance(200);
let finding = Finding {
contract: orphan,
violation: Violation {
property: ConformanceProperty::StateCommutativity,
severity: Severity::Violation,
left: OutputDigest::of(b"a"),
right: OutputDigest::of(b"b"),
detail: "synthesised for the unreachable branch".to_string(),
},
would_remove: true,
};
let records = checked_contracts(&[], std::slice::from_ref(&finding));
let record = records.iter().find(|c| c.contract == orphan).expect(
"the finding must not be dropped — a discarded violation renders \
as a contract nobody looked at",
);
assert_eq!(record.findings.len(), 1);
assert!(
record.verdicts >= 1,
"a record carrying a violation reported {} verdicts, so the card would \
render '0 reached a verdict' beside a Violation row — a finding IS a \
verdict",
record.verdicts
);
}
fn record_one(s: &mut MergeCheckStatus, c: CheckedContract) {
s.record([c], 1, 0, Instant::now());
}
#[test]
fn repeated_findings_for_one_contract_and_property_collapse() {
let mut s = MergeCheckStatus::default();
record_one(&mut s, checked(1, vec![finding(1, "state_commutativity")]));
record_one(&mut s, checked(1, vec![finding(1, "state_commutativity")]));
assert_eq!(
s.findings_for(&instance(1)).count(),
1,
"the same law failing twice produced two rows"
);
}
#[test]
fn different_properties_on_one_contract_stay_separate() {
let mut s = MergeCheckStatus::default();
record_one(&mut s, checked(2, vec![finding(2, "state_commutativity")]));
record_one(&mut s, checked(2, vec![finding(2, "state_associativity")]));
assert_eq!(
s.findings_for(&instance(2)).count(),
2,
"two different broken laws collapsed into one row"
);
}
#[test]
fn the_checked_window_is_bounded() {
let mut s = MergeCheckStatus::default();
for i in 0..(MAX_REMEMBERED_CHECKED + 40) {
let n = (i % 251) as u8;
record_one(&mut s, checked(n, vec![finding(n, "state_idempotence")]));
}
assert!(
s.checked.len() <= MAX_REMEMBERED_CHECKED,
"the checked window grew past the cap: {}",
s.checked.len()
);
}
#[test]
fn a_finding_survives_other_contracts_filling_the_window() {
let mut s = MergeCheckStatus::default();
record_one(&mut s, checked(1, vec![finding(1, "state_commutativity")]));
for i in 0..100u8 {
let n = i + 2;
record_one(&mut s, checked(n, vec![finding(n, "state_idempotence")]));
}
assert!(
s.was_checked(&instance(1)),
"the contract fell out of the checked window, so this test no longer \
exercises the case it names"
);
assert_eq!(
s.findings_for(&instance(1)).count(),
1,
"a remembered contract lost its finding, so its page renders a green \
'no violation found' pill for a contract found violating"
);
}
#[test]
fn rechecking_a_contract_moves_it_to_the_front() {
let mut s = MergeCheckStatus::default();
record_one(&mut s, checked(1, vec![]));
record_one(&mut s, checked(2, vec![]));
record_one(&mut s, checked(1, vec![]));
assert_eq!(
s.checked.first().map(|c| c.contract),
Some(instance(1)),
"a re-checked contract was left where it was, so a contract checked every \
tick sinks toward eviction while an idle one is retained"
);
}
#[test]
fn per_contract_case_counts_accumulate_across_ticks() {
let mut s = MergeCheckStatus::default();
s.record(
[CheckedContract::new(instance(1), 2, 5, Instant::now())],
1,
0,
Instant::now(),
);
s.record(
[CheckedContract::new(instance(1), 3, 1, Instant::now())],
1,
0,
Instant::now(),
);
let record = s
.record_for(&instance(1))
.expect("contract is in the window");
assert_eq!((record.verdicts, record.inconclusive), (5, 6));
}
#[test]
fn contracts_without_a_verdict_are_reported_separately() {
let mut s = MergeCheckStatus::default();
s.record([], 10, 3, Instant::now());
assert_eq!(s.without_verdict_last_tick, 3);
assert!(
s.checked.is_empty(),
"no records, which is exactly why the unjudged count has to be its own \
number rather than inferred from an empty list"
);
}
#[test]
fn a_stale_snapshot_says_so() {
let mut s = MergeCheckStatus::default();
let published = Instant::now();
record_one(&mut s, checked(1, vec![]));
s.published_at = published;
let fresh = s.view_for(Some(&instance(1)), published + STALE_AFTER / 2);
assert!(!fresh.stale, "a tick within the window read as stale");
let old = s.view_for(Some(&instance(1)), published + STALE_AFTER * 2);
assert!(
old.stale,
"a snapshot older than {STALE_AFTER:?} did not report itself stale, so a \
peer whose probe has been dead for a week keeps serving its last tick as \
a current clean result"
);
assert!(old.published_secs_ago >= STALE_AFTER.as_secs() * 2);
}
#[test]
fn a_view_clones_one_record_not_the_window() {
let mut s = MergeCheckStatus::default();
record_one(&mut s, checked(1, vec![finding(1, "state_commutativity")]));
record_one(&mut s, checked(2, vec![finding(2, "delta_idempotence")]));
let view = s.view_for(Some(&instance(1)), Instant::now());
let record = view.contract.expect("contract 1 is in the window");
assert_eq!(record.contract, instance(1));
assert_eq!(record.findings.len(), 1);
assert_eq!(record.findings[0].property, "state_commutativity");
assert!(
s.view_for(Some(&instance(9)), Instant::now())
.contract
.is_none(),
"a contract outside the window must read as absent, not as clean"
);
}
}