use proptest::prelude::*;
use sloop::flow::{
Flow, Reported, StageEvidence, Step, Verdict, VerdictPolicy, VerdictSource, next_step,
resolve_verdict,
};
use crate::flow_gen::flow_file;
fn verdict() -> impl Strategy<Value = Verdict> {
prop_oneof![Just(Verdict::Pass), Just(Verdict::Fail)]
}
fn flow_with_evidence() -> impl Strategy<Value = (Flow, Vec<StageEvidence>)> {
flow_file()
.prop_flat_map(|(yaml, _)| {
let flow = sloop::flow::parse("generated", &yaml).expect("generated flows parse");
let stage_count = flow.stages.len();
(
Just(flow),
prop::collection::vec((0..stage_count, verdict()), 0..stage_count * 2),
)
})
.prop_map(|(flow, rows)| {
let evidence = rows
.into_iter()
.map(|(index, verdict)| StageEvidence {
stage: flow.stages[index].name.clone(),
verdict,
source: VerdictSource::ExitCode,
reason: None,
})
.collect();
(flow, evidence)
})
}
fn first_row<'a>(evidence: &'a [StageEvidence], stage: &str) -> Option<&'a StageEvidence> {
evidence.iter().find(|row| row.stage == stage)
}
proptest! {
#[test]
fn next_step_is_deterministic((flow, evidence) in flow_with_evidence()) {
prop_assert_eq!(next_step(&flow, &evidence), next_step(&flow, &evidence));
}
#[test]
fn next_step_respects_declaration_order((flow, evidence) in flow_with_evidence()) {
match next_step(&flow, &evidence) {
Step::Run(stage) => {
let position = flow.stages.iter().position(|s| s.name == stage.name)
.expect("returned stage belongs to the flow");
prop_assert!(first_row(&evidence, &stage.name).is_none(),
"ran a stage that already has evidence");
for earlier in &flow.stages[..position] {
let row = first_row(&evidence, &earlier.name)
.expect("every stage before the answer has evidence");
prop_assert_eq!(row.verdict, Verdict::Pass);
}
}
Step::Halted { failed_stage } => {
let position = flow.stages.iter().position(|s| s.name == failed_stage)
.expect("failed stage belongs to the flow");
let row = first_row(&evidence, &failed_stage).expect("halt cites evidence");
prop_assert_eq!(row.verdict, Verdict::Fail);
for earlier in &flow.stages[..position] {
let row = first_row(&evidence, &earlier.name)
.expect("every stage before the halt has evidence");
prop_assert_eq!(row.verdict, Verdict::Pass);
}
}
Step::Complete => {
for stage in &flow.stages {
let row = first_row(&evidence, &stage.name)
.expect("completion requires evidence for every stage");
prop_assert_eq!(row.verdict, Verdict::Pass);
}
}
}
}
#[test]
fn next_step_ignores_duplicate_rows((flow, evidence) in flow_with_evidence()) {
let mut seen = std::collections::HashSet::new();
let deduped: Vec<StageEvidence> = evidence
.iter()
.filter(|row| seen.insert(row.stage.clone()))
.cloned()
.collect();
prop_assert_eq!(next_step(&flow, &evidence), next_step(&flow, &deduped));
}
#[test]
fn reports_only_bind_under_the_reported_policy(
exit in verdict(),
reported in prop::option::of((verdict(), prop::option::of("[ -~]{0,20}"))),
policy_choice in 0..3usize,
) {
let policy = [
VerdictPolicy::Exit,
VerdictPolicy::Commits,
VerdictPolicy::Check { cmd: vec!["true".into()] },
][policy_choice].clone();
let report = reported.map(|(verdict, reason)| Reported { verdict, reason });
prop_assert_eq!(
resolve_verdict(&policy, exit, report),
(exit, VerdictSource::ExitCode, None)
);
}
#[test]
fn a_missing_report_fails_a_reported_stage(
exit in verdict(),
reported in prop::option::of((verdict(), prop::option::of("[ -~]{0,20}"))),
) {
let report = reported.clone().map(|(verdict, reason)| Reported { verdict, reason });
let (verdict, source, reason) = resolve_verdict(&VerdictPolicy::Reported, exit, report);
prop_assert_eq!(source, VerdictSource::Reported);
match reported {
Some((expected, expected_reason)) => {
prop_assert_eq!(verdict, expected);
prop_assert_eq!(reason, expected_reason);
}
None => {
prop_assert_eq!(verdict, Verdict::Fail);
prop_assert!(reason.is_some(), "a defaulted failure explains itself");
}
}
}
}