use super::error::ConfigError;
use super::report::{FindingKind, Report};
use super::yaml;
pub const EXAMPLE_FILE: &str = "example.yaml";
pub const FILES_DIR: &str = "files";
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Label {
Violation,
Ok,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Known {
Miss,
FalseAlarm,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Example {
pub expected: Label,
pub findings: Vec<String>,
pub known: Option<Known>,
}
pub fn parse(path: &str, text: &str) -> Result<Example, ConfigError> {
let root = yaml::parse(path, text)?;
let mut fields = root.fields(
path,
EXAMPLE_FILE,
&["expected", "findings", "rationale", "origin", "known"],
)?;
let node = fields.required("expected")?;
let line = node.line;
let expected = match node.text(path, "expected")?.as_str() {
"violation" => Label::Violation,
"ok" => Label::Ok,
other => {
return Err(ConfigError::at(
path,
line,
format!("unknown `expected: {other}`; expected `violation` or `ok`"),
));
}
};
let findings = match fields.optional("findings") {
None => Vec::new(),
Some(node) => {
let line = node.line;
let findings = node
.list(path, "findings")?
.into_iter()
.map(|f| f.text(path, "findings"))
.collect::<Result<Vec<_>, _>>()?;
if expected == Label::Ok && !findings.is_empty() {
return Err(ConfigError::at(
path,
line,
"`findings` name where a violation is flagged; an `ok` example has none",
));
}
findings
}
};
for key in ["rationale", "origin"] {
if let Some(node) = fields.optional(key) {
node.text(path, key)?;
}
}
let known = match fields.optional("known") {
None => None,
Some(node) => {
let line = node.line;
let known = match (node.text(path, "known")?.as_str(), expected) {
("miss", Label::Violation) => Known::Miss,
("false_alarm", Label::Ok) => Known::FalseAlarm,
(other, _) => {
return Err(ConfigError::at(
path,
line,
format!(
"`known: {other}` does not fit `expected: {}`; a violation can be a known `miss`, an ok example a known `false_alarm`",
match expected {
Label::Violation => "violation",
Label::Ok => "ok",
}
),
));
}
};
Some(known)
}
};
Ok(Example {
expected,
findings,
known,
})
}
#[derive(Debug, Clone, PartialEq)]
pub enum Outcome {
Caught,
Missed,
WrongFile(Vec<String>),
NotSelected,
Uncertain,
FalseAlarm,
Passed,
Error(String),
}
impl Outcome {
fn as_labeled(&self) -> bool {
matches!(self, Outcome::Caught | Outcome::Passed)
}
fn is(&self, known: Known) -> bool {
match known {
Known::Miss => matches!(
self,
Outcome::Missed | Outcome::WrongFile(_) | Outcome::NotSelected | Outcome::Uncertain
),
Known::FalseAlarm => matches!(self, Outcome::FalseAlarm | Outcome::Uncertain),
}
}
fn describe(&self) -> String {
match self {
Outcome::Caught => "caught".into(),
Outcome::Missed => "missed".into(),
Outcome::WrongFile(flagged) if flagged.is_empty() => {
"missed: flagged without a file, so its expected findings cannot be confirmed"
.into()
}
Outcome::WrongFile(flagged) => format!(
"missed: flagged {} instead of its expected findings",
flagged.join(", ")
),
Outcome::NotSelected => "not selected".into(),
Outcome::Uncertain => "uncertain".into(),
Outcome::FalseAlarm => "false alarm".into(),
Outcome::Passed => "passed".into(),
Outcome::Error(e) => format!("error: {e}"),
}
}
}
pub fn outcome(example: &Example, id: &str, report: &Report) -> Outcome {
if let Some(error) = report.errors.first() {
return Outcome::Error(error.message.clone());
}
let own: Vec<_> = report
.findings
.iter()
.filter(|f| f.requirement == id)
.collect();
let flags: Vec<_> = own
.iter()
.filter(|f| matches!(f.kind, FindingKind::Violation | FindingKind::Advisory))
.collect();
let uncertain = own.iter().any(|f| f.kind == FindingKind::Uncertain);
let looked = report.checks_run > 0;
match (example.expected, !flags.is_empty(), uncertain) {
(Label::Violation, true, _) => {
let flagged: Vec<String> = flags.iter().filter_map(|f| f.file.clone()).collect();
if example.findings.iter().all(|f| flagged.contains(f)) {
Outcome::Caught
} else {
let mut flagged = flagged;
flagged.dedup();
Outcome::WrongFile(flagged)
}
}
(Label::Violation, false, true) => Outcome::Uncertain,
(Label::Violation, false, false) if !looked => Outcome::NotSelected,
(Label::Violation, false, false) => Outcome::Missed,
(Label::Ok, true, _) => Outcome::FalseAlarm,
(Label::Ok, false, true) => Outcome::Uncertain,
(Label::Ok, false, false) => Outcome::Passed,
}
}
pub struct Case {
pub name: String,
pub label: Label,
pub known: Option<Known>,
pub outcome: Outcome,
}
impl Case {
fn accepted(&self) -> bool {
self.outcome.as_labeled() || self.known.is_some_and(|k| self.outcome.is(k))
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Evidence {
Asserted,
Measured,
None,
}
pub struct Tested {
pub label: String,
pub evidence: Evidence,
pub cases: Vec<Case>,
}
impl Tested {
pub fn failed(&self) -> bool {
self.evidence == Evidence::Asserted && self.cases.iter().any(|c| !c.accepted())
}
pub fn has_errors(&self) -> bool {
self.cases
.iter()
.any(|c| matches!(c.outcome, Outcome::Error(_)))
}
fn render(&self) -> String {
let count = |label: Label, outcome: fn(&Outcome) -> bool| {
self.cases
.iter()
.filter(|c| c.label == label && outcome(&c.outcome))
.count()
};
let total = |label: Label| self.cases.iter().filter(|c| c.label == label).count();
let mut out = match self.evidence {
Evidence::None => format!("{} no evidence: no labeled examples\n", self.label),
Evidence::Measured => format!(
"{} measured, no assertion: {} of {} violations caught ({} missed, {} not selected, {} uncertain); {} of {} correct examples flagged ({} uncertain)\n",
self.label,
count(Label::Violation, |o| *o == Outcome::Caught),
total(Label::Violation),
count(Label::Violation, |o| matches!(
o,
Outcome::Missed | Outcome::WrongFile(_)
)),
count(Label::Violation, |o| *o == Outcome::NotSelected),
count(Label::Violation, |o| *o == Outcome::Uncertain),
count(Label::Ok, |o| *o == Outcome::FalseAlarm),
total(Label::Ok),
count(Label::Ok, |o| *o == Outcome::Uncertain),
),
Evidence::Asserted => {
let as_labeled = self.cases.iter().filter(|c| c.outcome.as_labeled()).count();
format!(
"{} asserted: {} examples, {} as labeled\n",
self.label,
self.cases.len(),
as_labeled
)
}
};
for case in &self.cases {
match (case.outcome.as_labeled(), case.accepted()) {
(false, false) => out += &format!(" {}: {}\n", case.name, case.outcome.describe()),
(false, true) => {
out += &format!(" {}: {} (known)\n", case.name, case.outcome.describe())
}
(true, _) if case.known.is_some() => {
out += &format!(
" {}: {}, as labeled now: remove `known`\n",
case.name,
case.outcome.describe()
)
}
(true, _) => {}
}
}
out
}
}
pub fn render(tested: &[Tested]) -> (String, i32) {
let mut out: String = tested.iter().map(Tested::render).collect();
let examples: usize = tested.iter().map(|t| t.cases.len()).sum();
let failed = tested.iter().filter(|t| t.failed()).count();
let with = |evidence: Evidence| tested.iter().filter(|t| t.evidence == evidence).count();
if !out.is_empty() {
out.push('\n');
}
out += &format!(
"{} requirements: {} asserted, {} measured, {} without evidence; {examples} examples. {failed} failing their labels.\n",
tested.len(),
with(Evidence::Asserted),
with(Evidence::Measured),
with(Evidence::None),
);
let code = if tested.iter().any(Tested::has_errors) {
3
} else if failed > 0 {
1
} else {
0
};
(out, code)
}