use std::collections::BTreeSet;
use crate::core::contract::core;
use crate::error::Result;
pub type Table<'a> = &'a [(&'a str, &'a str)];
#[derive(Debug, PartialEq, Eq)]
pub struct CoverageReport {
pub unmapped: Vec<String>,
pub stale: Vec<String>,
pub contradictory: Vec<String>,
}
impl CoverageReport {
#[must_use]
pub fn is_clean(&self) -> bool {
self.unmapped.is_empty() && self.stale.is_empty() && self.contradictory.is_empty()
}
}
impl std::fmt::Display for CoverageReport {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if !self.unmapped.is_empty() {
write!(
f,
"operations in the vendored tapes-api contract that this client neither exposes \
nor allow-lists: {:?} — add each to the exposed table (and wire it up) or to the \
unexposed table with the reason it stays unexposed. ",
self.unmapped,
)?;
}
if !self.stale.is_empty() {
write!(
f,
"operations named by a coverage table that the vendored tapes-api contract does \
not have: {:?} — the contract dropped or renamed them, and the mapping must move \
in the same change. ",
self.stale,
)?;
}
if !self.contradictory.is_empty() {
write!(
f,
"operations in both coverage tables: {:?}. ",
self.contradictory,
)?;
}
Ok(())
}
}
pub fn report(exposed: Table<'_>, unexposed: Table<'_>) -> Result<CoverageReport> {
let surface = core()?;
let known: BTreeSet<&str> = surface.operation_ids().collect();
let exposed_ids: BTreeSet<&str> = exposed.iter().map(|(id, _)| *id).collect();
let unexposed_ids: BTreeSet<&str> = unexposed.iter().map(|(id, _)| *id).collect();
let owned =
|ids: BTreeSet<&str>| -> Vec<String> { ids.into_iter().map(ToOwned::to_owned).collect() };
Ok(CoverageReport {
unmapped: owned(
known
.iter()
.filter(|id| !exposed_ids.contains(*id) && !unexposed_ids.contains(*id))
.copied()
.collect(),
),
stale: owned(
exposed_ids
.union(&unexposed_ids)
.filter(|id| !known.contains(*id))
.copied()
.collect(),
),
contradictory: owned(exposed_ids.intersection(&unexposed_ids).copied().collect()),
})
}
pub fn check(exposed: Table<'_>, unexposed: Table<'_>) -> std::result::Result<(), String> {
let report = report(exposed, unexposed).map_err(|e| e.to_string())?;
if report.is_clean() {
return Ok(());
}
Err(report.to_string())
}
pub fn operation_ids() -> Result<Vec<String>> {
let mut ids: Vec<String> = core()?.operation_ids().map(ToOwned::to_owned).collect();
ids.sort();
Ok(ids)
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
use crate::core::contract::ops;
#[test]
fn the_contract_has_operations_to_gate() {
let ids = operation_ids().unwrap();
assert!(ids.contains(&ops::LIST_SESSIONS.to_owned()), "got: {ids:?}");
}
#[test]
fn an_operation_in_neither_table_is_reported_as_unmapped() {
let report = report(&[(ops::LIST_SESSIONS, "sessions list")], &[]).unwrap();
assert!(!report.is_clean());
assert!(
report.unmapped.contains(&ops::GET_SESSION.to_owned()),
"got: {report:?}",
);
assert!(
report.to_string().contains("neither exposes"),
"got: {report}",
);
}
#[test]
fn a_table_entry_the_contract_does_not_have_is_reported_as_stale() {
let report = report(&[("launchMissiles", "nowhere")], &[]).unwrap();
assert_eq!(report.stale, vec!["launchMissiles".to_owned()]);
}
#[test]
fn an_operation_in_both_tables_is_reported_as_contradictory() {
let report = report(
&[(ops::LIST_SESSIONS, "sessions list")],
&[(ops::LIST_SESSIONS, "also here, somehow")],
)
.unwrap();
assert_eq!(report.contradictory, vec![ops::LIST_SESSIONS.to_owned()]);
}
#[test]
fn a_complete_partition_is_clean() {
let ids = operation_ids().unwrap();
let exposed: Vec<(&str, &str)> = ids.iter().map(|id| (id.as_str(), "exposed")).collect();
assert_eq!(check(&exposed, &[]), Ok(()));
}
#[test]
fn the_failure_names_every_offending_id_at_once() {
let err = check(&[], &[]).unwrap_err();
assert!(err.contains(ops::LIST_SESSIONS), "got: {err}");
assert!(err.contains(ops::GET_SESSION), "got: {err}");
}
}