use super::context::Ctx;
use super::observe::StepState;
use super::steps::StepSpec;
#[derive(Debug, Clone)]
pub enum Stance {
Applies,
NotApplicable(String),
Excluded(String),
Redundant {
reason: String,
inapplicable: String,
},
}
impl Stance {
#[must_use]
pub const fn word(&self) -> &'static str {
match self {
Self::Applies => "applicable",
Self::NotApplicable(_) => "not-applicable",
Self::Excluded(_) => "excluded",
Self::Redundant { .. } => "redundant",
}
}
#[must_use]
pub const fn acts(&self) -> bool {
matches!(self, Self::Applies)
}
#[must_use]
pub fn detail(&self) -> String {
match self {
Self::Applies => String::new(),
Self::NotApplicable(reason) | Self::Excluded(reason) => reason.clone(),
Self::Redundant {
reason,
inapplicable,
} => format!("{inapplicable}; the stated reason was {reason}"),
}
}
#[must_use]
pub fn framed(&self) -> String {
match self {
Self::Applies => String::new(),
Self::NotApplicable(reason) => format!("not applicable: {reason}"),
Self::Excluded(reason) => {
format!("excluded by {}: {reason}", crate::config::CONFIG_PATH)
}
Self::Redundant { .. } => format!(
"{} excludes a step that does not apply here: {}",
crate::config::CONFIG_PATH,
self.detail()
),
}
}
}
#[must_use]
pub fn stance(ctx: &Ctx, step: &StepSpec) -> Stance {
let inapplicable = (step.applies)(ctx);
match (ctx.excluded(step.name).map(str::to_owned), inapplicable) {
(Some(reason), Some(inapplicable)) => Stance::Redundant {
reason,
inapplicable,
},
(Some(reason), None) => Stance::Excluded(reason),
(None, Some(reason)) => Stance::NotApplicable(reason),
(None, None) => Stance::Applies,
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Observed {
Satisfied {
limitation: Option<String>,
},
Skipped,
Unsatisfied,
Unknown,
}
impl Observed {
#[must_use]
pub fn of(state: &StepState) -> Self {
match state {
StepState::Satisfied { limitation, .. } => Self::Satisfied {
limitation: limitation.clone(),
},
StepState::Inapplicable { .. } => Self::Skipped,
StepState::Unsatisfied { .. } => Self::Unsatisfied,
StepState::Unknown { .. } => Self::Unknown,
}
}
#[must_use]
pub const fn label(&self) -> &'static str {
match self {
Self::Satisfied { .. } => "ok",
Self::Skipped => "skipped",
Self::Unsatisfied => "unsatisfied",
Self::Unknown => "unknown",
}
}
#[must_use]
pub const fn wire(&self) -> &'static str {
match self {
Self::Satisfied {
limitation: Some(_),
} => "satisfied-with-limitation",
Self::Satisfied { limitation: None } => "satisfied",
Self::Skipped => "skipped",
Self::Unsatisfied => "unsatisfied",
Self::Unknown => "unknown",
}
}
}
#[derive(Debug, Clone)]
pub struct Row {
pub name: &'static str,
pub stance: Stance,
pub observed: Option<Observed>,
pub detail: String,
}
impl Row {
#[must_use]
pub const fn stated(step: &StepSpec, stance: Stance) -> Self {
Self {
name: step.name,
stance,
observed: None,
detail: String::new(),
}
}
#[must_use]
pub fn observed(step: &StepSpec, stance: Stance, state: &StepState) -> Self {
Self {
name: step.name,
stance,
observed: Some(Observed::of(state)),
detail: state.detail().to_owned(),
}
}
#[must_use]
pub fn line(&self) -> String {
let Some(observed) = &self.observed else {
return format!(
"{} {} — {}",
self.stance.word(),
self.name,
self.stance.detail()
);
};
let mut line = format!("{} {} — {}", observed.label(), self.name, self.detail);
if let Observed::Satisfied {
limitation: Some(limit),
} = observed
{
use std::fmt::Write as _;
let _ = write!(line, " (limitation: {limit})");
}
line
}
#[must_use]
pub fn event_fields(&self) -> (String, String) {
match &self.observed {
None => (self.stance.word().to_owned(), self.stance.detail()),
Some(Observed::Satisfied { .. }) => ("satisfied".to_owned(), self.detail.clone()),
Some(observed) => (observed.wire().to_owned(), self.detail.clone()),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct Report {
pub rows: Vec<Row>,
}
impl Report {
fn count(&self, wanted: &Observed) -> usize {
self.rows
.iter()
.filter(|row| row.observed.as_ref() == Some(wanted))
.count()
}
#[must_use]
pub fn unsatisfied(&self) -> usize {
self.count(&Observed::Unsatisfied)
}
#[must_use]
pub fn unknown(&self) -> usize {
self.count(&Observed::Unknown)
}
#[must_use]
pub fn judged(&self) -> usize {
self.rows.iter().filter(|row| row.stance.acts()).count()
}
#[must_use]
pub fn checkpointable(&self) -> bool {
self.unsatisfied() == 0 && self.unknown() == 0
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::setup::steps::STEPS;
fn row(observed: Option<Observed>) -> Row {
let stance = if observed.is_some() {
Stance::Applies
} else {
Stance::Excluded("the target runs it elsewhere".into())
};
Row {
name: STEPS[0].name,
stance,
observed,
detail: String::new(),
}
}
#[test]
fn only_a_complete_observation_is_checkpointable() {
let accepted = Report {
rows: vec![
row(Some(Observed::Satisfied { limitation: None })),
row(Some(Observed::Satisfied {
limitation: Some("weaker".into()),
})),
row(Some(Observed::Skipped)),
row(None),
],
};
assert!(accepted.checkpointable());
for refused in [Observed::Unknown, Observed::Unsatisfied] {
let mut report = accepted.clone();
report.rows.push(row(Some(refused.clone())));
assert!(!report.checkpointable(), "{refused:?} was accepted");
}
}
#[test]
fn an_observation_keeps_its_distinctions() {
let limited = StepState::Satisfied {
detail: "found".into(),
limitation: Some("weaker".into()),
};
assert_eq!(Observed::of(&limited).wire(), "satisfied-with-limitation");
let plain = StepState::Satisfied {
detail: "found".into(),
limitation: None,
};
assert_eq!(Observed::of(&plain).wire(), "satisfied");
let skipped = StepState::Inapplicable {
detail: "no line".into(),
};
assert_eq!(Observed::of(&skipped).wire(), "skipped");
let unknown = StepState::Unknown {
detail: "unreadable".into(),
};
assert_eq!(Observed::of(&unknown).wire(), "unknown");
let not = StepState::Unsatisfied {
detail: "absent".into(),
};
assert_eq!(Observed::of(¬).wire(), "unsatisfied");
}
}