use anyhow::Result;
use serde::{Deserialize, Serialize};
use crate::requirement::{CoverageStatus, ReqStatus};
const MODE_VT: &str = "VT";
const MODE_VA: &str = "VA";
const MODE_VH: &str = "VH";
const MODES: &[&str] = &[MODE_VT, MODE_VA, MODE_VH];
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
pub(crate) struct CoverageKey {
pub(crate) slice: String,
pub(crate) requirement: String,
pub(crate) contributing_change: String,
pub(crate) mode: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
pub(crate) struct CoverageEntry {
#[serde(flatten)]
pub(crate) key: CoverageKey,
pub(crate) status: CoverageStatus,
pub(crate) git_anchor: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) attested_date: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub(crate) touched_paths: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) check: Option<VtCheck>,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default)]
pub(crate) struct CoverageFile {
#[serde(default)]
pub(crate) entry: Vec<CoverageEntry>,
}
pub(crate) fn distinct_keys(keys: impl Iterator<Item = CoverageKey>) -> Vec<CoverageKey> {
let mut seen = std::collections::BTreeSet::new();
let mut out = Vec::new();
for k in keys {
let tag = (
k.slice.clone(),
k.requirement.clone(),
k.contributing_change.clone(),
k.mode.clone(),
);
if seen.insert(tag) {
out.push(k);
}
}
out
}
pub(crate) fn parse(s: &str) -> Result<CoverageFile> {
Ok(toml::from_str(s)?)
}
pub(crate) fn render(f: &CoverageFile) -> Result<String> {
Ok(toml::to_string(f)?)
}
pub(crate) fn mode_is_valid(mode: &str) -> bool {
MODES.contains(&mode)
}
pub(crate) fn upsert(file: &mut CoverageFile, entry: CoverageEntry) {
if let Some(existing) = file.entry.iter_mut().find(|e| e.key == entry.key) {
*existing = entry;
} else {
file.entry.push(entry);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum IsStale {
Fresh,
Stale,
Unknown,
}
impl From<Option<u32>> for IsStale {
fn from(count: Option<u32>) -> Self {
match count {
Some(0) => IsStale::Fresh,
Some(_) => IsStale::Stale,
None => IsStale::Unknown,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Composite {
cells: Vec<(CoverageEntry, IsStale)>,
}
fn key_order(k: &CoverageKey) -> (&str, &str, &str, &str) {
(
k.slice.as_str(),
k.requirement.as_str(),
k.contributing_change.as_str(),
k.mode.as_str(),
)
}
pub(crate) fn composite(entries: &[(CoverageEntry, IsStale)]) -> Composite {
let mut cells = entries.to_vec();
cells.sort_by(|a, b| key_order(&a.0.key).cmp(&key_order(&b.0.key)));
Composite { cells }
}
impl Composite {
pub(crate) fn is_empty(&self) -> bool {
self.cells.is_empty()
}
pub(crate) fn any_fresh_verified(&self) -> bool {
self.cells
.iter()
.any(|(e, s)| e.status == CoverageStatus::Verified && *s == IsStale::Fresh)
}
pub(crate) fn any_failed(&self) -> bool {
self.cells
.iter()
.any(|(e, _)| e.status == CoverageStatus::Failed)
}
pub(crate) fn any_blocked(&self) -> bool {
self.cells
.iter()
.any(|(e, _)| e.status == CoverageStatus::Blocked)
}
pub(crate) fn any_failed_or_blocked(&self) -> bool {
self.any_failed() || self.any_blocked()
}
pub(crate) fn has_fresh_vh(&self) -> bool {
self.cells.iter().any(|(e, s)| {
e.status == CoverageStatus::Verified && *s == IsStale::Fresh && e.key.mode == MODE_VH
})
}
pub(crate) fn only_forward(&self) -> bool {
self.cells.iter().all(|(e, _)| {
matches!(
e.status,
CoverageStatus::Planned | CoverageStatus::InProgress
)
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Verdict {
Coherent,
Divergent(DivergentReason),
Indeterminate,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum DivergentReason {
ObservedFailure,
ObservedBlocked,
EvidenceOutrunsAuthored,
}
impl Verdict {
pub(crate) fn label(self) -> String {
match self {
Verdict::Coherent => "Coherent".to_owned(),
Verdict::Indeterminate => "Indeterminate".to_owned(),
Verdict::Divergent(r) => format!("Divergent: {}", r.label()),
}
}
}
impl DivergentReason {
pub(crate) fn label(self) -> &'static str {
match self {
DivergentReason::EvidenceOutrunsAuthored => "evidence-outruns-authored",
DivergentReason::ObservedFailure => "observed-failure",
DivergentReason::ObservedBlocked => "observed-blocked",
}
}
}
pub(crate) fn drift(authored: ReqStatus, composite: &Composite) -> Verdict {
use ReqStatus::{Active, Deprecated, InProgress, Pending, Retired, Superseded};
if matches!(authored, Retired | Superseded) {
return Verdict::Coherent;
}
if composite.any_failed() {
return Verdict::Divergent(DivergentReason::ObservedFailure);
}
if composite.any_blocked() {
return Verdict::Divergent(DivergentReason::ObservedBlocked);
}
match authored {
Pending | InProgress => {
if composite.any_fresh_verified() {
Verdict::Divergent(DivergentReason::EvidenceOutrunsAuthored)
} else if composite.is_empty() || composite.only_forward() {
Verdict::Coherent
} else {
Verdict::Indeterminate
}
}
Active | Deprecated => {
if composite.is_empty() {
Verdict::Indeterminate
} else if composite.any_fresh_verified() {
Verdict::Coherent
} else {
Verdict::Indeterminate
}
}
Retired | Superseded => Verdict::Coherent,
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
pub(crate) struct VtCheck {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) alias: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) command: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub(crate) extra_args: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) matcher: Option<Matcher>,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
pub(crate) struct Matcher {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) source: Option<MatchSource>,
pub(crate) pattern: String,
#[serde(default)]
pub(crate) regex: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[serde(into = "String", try_from = "String")]
pub(crate) enum MatchSource {
Stdout,
Stderr,
File(String),
}
const MATCH_SOURCE_FILE_PREFIX: &str = "file:";
impl std::fmt::Display for MatchSource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MatchSource::Stdout => f.write_str("stdout"),
MatchSource::Stderr => f.write_str("stderr"),
MatchSource::File(glob) => write!(f, "{MATCH_SOURCE_FILE_PREFIX}{glob}"),
}
}
}
impl From<MatchSource> for String {
fn from(src: MatchSource) -> Self {
src.to_string()
}
}
impl TryFrom<String> for MatchSource {
type Error = String;
fn try_from(s: String) -> Result<Self, Self::Error> {
match s.as_str() {
"stdout" => Ok(MatchSource::Stdout),
"stderr" => Ok(MatchSource::Stderr),
other => match other.strip_prefix(MATCH_SOURCE_FILE_PREFIX) {
Some(glob) => Ok(MatchSource::File(glob.to_owned())),
None => Err(format!("unknown match source: {other}")),
},
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum RunOutcome {
Unobtainable,
Ran {
exit_ok: bool,
matched: Option<bool>,
},
}
pub(crate) fn derive_status(outcome: &RunOutcome) -> CoverageStatus {
match outcome {
RunOutcome::Unobtainable => CoverageStatus::Blocked,
RunOutcome::Ran { exit_ok: false, .. }
| RunOutcome::Ran {
exit_ok: true,
matched: Some(false),
} => CoverageStatus::Failed,
RunOutcome::Ran {
exit_ok: true,
matched: None | Some(true),
} => CoverageStatus::Verified,
}
}
pub(crate) fn evaluate_matcher(pattern: &str, regex: bool, haystack: &str) -> Option<bool> {
if regex {
match regex_lite::Regex::new(pattern) {
Ok(re) => Some(re.is_match(haystack)),
Err(_) => None,
}
} else {
Some(haystack.contains(pattern))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum ValidError {
AliasCommandConflict,
MatcherRequired,
GlobEscapesTree,
BadRegex,
}
pub(crate) fn valid(check: &VtCheck) -> Result<(), ValidError> {
if check.alias.is_some() && check.command.is_some() {
return Err(ValidError::AliasCommandConflict);
}
let matcher_empty = match &check.matcher {
None => true,
Some(m) => m.pattern.is_empty(),
};
if matcher_empty && check.command.is_none() {
return Err(ValidError::MatcherRequired);
}
if let Some(matcher) = &check.matcher {
if let Some(MatchSource::File(glob)) = &matcher.source {
let escapes = glob.starts_with('/') || glob.split('/').any(|segment| segment == "..");
if escapes {
return Err(ValidError::GlobEscapesTree);
}
}
if matcher.regex && regex_lite::Regex::new(&matcher.pattern).is_err() {
return Err(ValidError::BadRegex);
}
}
Ok(())
}
#[cfg(test)]
#[expect(
clippy::unwrap_used,
reason = "tests: fail-fast unwrap on round-trip/parse is idiomatic"
)]
mod tests {
use super::*;
fn key(slice: &str, req: &str, change: &str, mode: &str) -> CoverageKey {
CoverageKey {
slice: slice.to_owned(),
requirement: req.to_owned(),
contributing_change: change.to_owned(),
mode: mode.to_owned(),
}
}
fn entry(k: CoverageKey, status: CoverageStatus, attested: Option<&str>) -> CoverageEntry {
CoverageEntry {
key: k,
status,
git_anchor: "anchor-abc123".to_owned(),
attested_date: attested.map(str::to_owned),
touched_paths: Vec::new(),
check: None,
}
}
fn cell(
slice: &str,
req: &str,
change: &str,
status: CoverageStatus,
stale: IsStale,
) -> (CoverageEntry, IsStale) {
(entry(key(slice, req, change, "VT"), status, None), stale)
}
fn cell_mode(
slice: &str,
req: &str,
change: &str,
mode: &str,
status: CoverageStatus,
stale: IsStale,
) -> (CoverageEntry, IsStale) {
(entry(key(slice, req, change, mode), status, None), stale)
}
#[test]
fn round_trip_preserves_attested_present_and_absent() {
let file = CoverageFile {
entry: vec![
entry(
key("SL-042", "REQ-109", "SL-042", "VH"),
CoverageStatus::Verified,
Some("2026-06-12"),
),
entry(
key("SL-042", "REQ-108", "SL-041", "VT"),
CoverageStatus::Failed,
None,
),
],
};
let back = parse(&render(&file).unwrap()).unwrap();
assert_eq!(
back, file,
"mode + status + git_anchor + attested_date preserved"
);
let first = back.entry.first().unwrap();
assert_eq!(first.key.mode, "VH");
assert_eq!(first.status, CoverageStatus::Verified);
assert_eq!(first.git_anchor, "anchor-abc123");
assert_eq!(first.attested_date.as_deref(), Some("2026-06-12"));
assert!(back.entry.get(1).unwrap().attested_date.is_none());
}
#[test]
fn empty_file_round_trips() {
let empty = CoverageFile::default();
assert_eq!(parse(&render(&empty).unwrap()).unwrap(), empty);
}
#[test]
fn upsert_distinct_keys_appends() {
let mut file = CoverageFile::default();
upsert(
&mut file,
entry(
key("SL-042", "REQ-109", "SL-042", "VT"),
CoverageStatus::Planned,
None,
),
);
upsert(
&mut file,
entry(
key("SL-042", "REQ-108", "SL-042", "VT"),
CoverageStatus::Verified,
None,
),
);
assert_eq!(file.entry.len(), 2, "two distinct keys both surface");
}
#[test]
fn upsert_identical_key_replaces_with_latest_payload() {
let k = key("SL-042", "REQ-109", "SL-042", "VT");
let mut file = CoverageFile::default();
upsert(&mut file, entry(k.clone(), CoverageStatus::Planned, None));
upsert(
&mut file,
entry(k.clone(), CoverageStatus::Verified, Some("2026-06-12")),
);
assert_eq!(file.entry.len(), 1, "same key replaces, never duplicates");
let only = file.entry.first().unwrap();
assert_eq!(only.status, CoverageStatus::Verified, "latest payload wins");
assert_eq!(only.attested_date.as_deref(), Some("2026-06-12"));
}
#[test]
fn entries_differing_only_in_slice_coexist() {
let mut file = CoverageFile::default();
upsert(
&mut file,
entry(
key("SL-042", "REQ-109", "SL-042", "VT"),
CoverageStatus::Verified,
None,
),
);
upsert(
&mut file,
entry(
key("SL-099", "REQ-109", "SL-099", "VT"),
CoverageStatus::Planned,
None,
),
);
assert_eq!(
file.entry.len(),
2,
"same requirement across two slices coexists"
);
}
#[test]
fn mode_membership_is_vt_va_vh_only() {
assert!(mode_is_valid("VT"));
assert!(mode_is_valid("VA"));
assert!(mode_is_valid("VH"));
assert!(!mode_is_valid("VX"));
assert!(!mode_is_valid("vt"));
assert!(!mode_is_valid(""));
}
#[test]
fn coverage_entry_carries_observed_status_not_authored_reqstatus() {
let observed: CoverageStatus = entry(
key("SL-042", "REQ-109", "SL-042", "VT"),
CoverageStatus::Verified,
None,
)
.status;
assert_eq!(observed, CoverageStatus::Verified);
}
#[test]
fn coverage_and_requirement_status_live_in_distinct_stores() {
let coverage_path = ".doctrine/slice/042/coverage.toml";
let requirement_path = ".doctrine/requirement/109/requirement-109.toml";
assert_ne!(coverage_path, requirement_path);
}
#[test]
fn p2_entry_without_touched_paths_parses_and_defaults_empty() {
let body = r#"
[[entry]]
slice = "SL-042"
requirement = "REQ-109"
contributing_change = "SL-042"
mode = "VT"
status = "verified"
git_anchor = "anchor-abc123"
"#;
let file = parse(body).unwrap();
let only = file.entry.first().unwrap();
assert!(only.touched_paths.is_empty(), "absent field defaults empty");
}
#[test]
fn touched_paths_round_trips_when_present() {
let mut e = entry(
key("SL-042", "REQ-110", "SL-042", "VT"),
CoverageStatus::Verified,
None,
);
e.touched_paths = vec!["src/coverage.rs".to_owned(), "src/git.rs".to_owned()];
let file = CoverageFile { entry: vec![e] };
let back = parse(&render(&file).unwrap()).unwrap();
assert_eq!(back, file, "touched_paths survives the round-trip");
}
#[test]
fn is_stale_from_seam_count() {
assert_eq!(IsStale::from(Some(0)), IsStale::Fresh);
assert_eq!(IsStale::from(Some(1)), IsStale::Stale);
assert_eq!(IsStale::from(Some(42)), IsStale::Stale);
assert_eq!(IsStale::from(None), IsStale::Unknown);
}
#[test]
fn composite_is_order_independent() {
let ordered = vec![
cell(
"SL-040",
"REQ-110",
"SL-040",
CoverageStatus::Verified,
IsStale::Fresh,
),
cell(
"SL-042",
"REQ-110",
"SL-041",
CoverageStatus::Planned,
IsStale::Unknown,
),
cell(
"SL-041",
"REQ-110",
"SL-042",
CoverageStatus::Failed,
IsStale::Stale,
),
];
let shuffled = vec![
ordered.get(2).unwrap().clone(),
ordered.first().unwrap().clone(),
ordered.get(1).unwrap().clone(),
];
assert_eq!(
composite(&ordered),
composite(&shuffled),
"the fold is pure over in-memory input — order cannot change the value"
);
}
fn composites() -> Vec<(&'static str, Composite)> {
vec![
("empty", composite(&[])),
(
"fresh-verified",
composite(&[cell(
"SL-042",
"REQ-111",
"SL-042",
CoverageStatus::Verified,
IsStale::Fresh,
)]),
),
(
"stale-verified",
composite(&[cell(
"SL-042",
"REQ-111",
"SL-042",
CoverageStatus::Verified,
IsStale::Stale,
)]),
),
(
"failed-or-blocked",
composite(&[cell(
"SL-042",
"REQ-111",
"SL-042",
CoverageStatus::Failed,
IsStale::Fresh,
)]),
),
(
"forward-only",
composite(&[
cell(
"SL-042",
"REQ-111",
"SL-042",
CoverageStatus::Planned,
IsStale::Unknown,
),
cell(
"SL-043",
"REQ-111",
"SL-043",
CoverageStatus::InProgress,
IsStale::Stale,
),
]),
),
]
}
#[test]
fn verdict_matrix_matches_the_decision_tree() {
use DivergentReason::{EvidenceOutrunsAuthored, ObservedFailure};
use ReqStatus::{Active, Deprecated, InProgress, Pending, Retired, Superseded};
use Verdict::{Coherent, Divergent, Indeterminate};
let expect: Vec<(ReqStatus, [Verdict; 5])> = vec![
(
Pending,
[
Coherent, Divergent(EvidenceOutrunsAuthored), Indeterminate, Divergent(ObservedFailure), Coherent, ],
),
(
InProgress,
[
Coherent,
Divergent(EvidenceOutrunsAuthored),
Indeterminate,
Divergent(ObservedFailure),
Coherent,
],
),
(
Active,
[
Indeterminate, Coherent, Indeterminate, Divergent(ObservedFailure), Indeterminate, ],
),
(
Deprecated,
[
Indeterminate,
Coherent,
Indeterminate,
Divergent(ObservedFailure),
Indeterminate,
],
),
(Retired, [Coherent, Coherent, Coherent, Coherent, Coherent]),
(
Superseded,
[Coherent, Coherent, Coherent, Coherent, Coherent],
),
];
let states = composites();
for (authored, row) in &expect {
for (idx, (label, comp)) in states.iter().enumerate() {
let got = drift(*authored, comp);
let want = *row.get(idx).unwrap();
assert_eq!(
got, want,
"drift({:?}, {label}) expected {want:?}, got {got:?}",
authored
);
}
}
}
#[test]
fn drift_returns_verdict_not_reqstatus() {
let v: Verdict = drift(ReqStatus::Active, &composite(&[]));
assert_eq!(v, Verdict::Indeterminate);
}
#[test]
fn composite_predicates_read_the_cells() {
let c = composite(&[
cell(
"SL-042",
"REQ-111",
"SL-042",
CoverageStatus::Verified,
IsStale::Fresh,
),
cell(
"SL-043",
"REQ-111",
"SL-043",
CoverageStatus::Planned,
IsStale::Unknown,
),
]);
assert!(!c.is_empty());
assert!(c.any_fresh_verified());
assert!(!c.any_failed_or_blocked());
assert!(!c.only_forward(), "a Verified cell is not forward-only");
let stale_verified = composite(&[cell(
"SL-042",
"REQ-111",
"SL-042",
CoverageStatus::Verified,
IsStale::Stale,
)]);
assert!(
!stale_verified.any_fresh_verified(),
"stale Verified is not fresh-verified"
);
}
#[test]
fn any_failed_and_any_blocked_read_distinct_statuses() {
let failed = composite(&[cell(
"SL-042",
"REQ-111",
"SL-042",
CoverageStatus::Failed,
IsStale::Fresh,
)]);
assert!(failed.any_failed(), "a Failed cell reads any_failed");
assert!(!failed.any_blocked(), "a Failed cell is not blocked");
assert!(failed.any_failed_or_blocked(), "OR still true");
let blocked = composite(&[cell(
"SL-042",
"REQ-111",
"SL-042",
CoverageStatus::Blocked,
IsStale::Fresh,
)]);
assert!(blocked.any_blocked(), "a Blocked cell reads any_blocked");
assert!(!blocked.any_failed(), "a Blocked cell is not failed");
assert!(blocked.any_failed_or_blocked(), "OR still true");
}
#[test]
fn drift_failed_outranks_blocked() {
let both = composite(&[
cell(
"SL-042",
"REQ-111",
"SL-042",
CoverageStatus::Failed,
IsStale::Fresh,
),
cell(
"SL-043",
"REQ-111",
"SL-043",
CoverageStatus::Blocked,
IsStale::Fresh,
),
]);
assert_eq!(
drift(ReqStatus::Active, &both),
Verdict::Divergent(DivergentReason::ObservedFailure),
"Failed outranks Blocked"
);
let blocked_only = composite(&[cell(
"SL-042",
"REQ-111",
"SL-042",
CoverageStatus::Blocked,
IsStale::Fresh,
)]);
assert_eq!(
drift(ReqStatus::Active, &blocked_only),
Verdict::Divergent(DivergentReason::ObservedBlocked),
"Blocked-only ⇒ ObservedBlocked"
);
}
#[test]
fn has_fresh_vh_keys_on_mode_status_and_freshness() {
let vh = composite(&[cell_mode(
"SL-042",
"REQ-111",
"SL-042",
"VH",
CoverageStatus::Verified,
IsStale::Fresh,
)]);
assert!(vh.has_fresh_vh(), "fresh Verified VH ⇒ has_fresh_vh");
for mode in ["VT", "VA"] {
let non_vh = composite(&[cell_mode(
"SL-042",
"REQ-111",
"SL-042",
mode,
CoverageStatus::Verified,
IsStale::Fresh,
)]);
assert!(
!non_vh.has_fresh_vh(),
"{mode} Verified is not a human (VH) attestation"
);
}
let stale_vh = composite(&[cell_mode(
"SL-042",
"REQ-111",
"SL-042",
"VH",
CoverageStatus::Verified,
IsStale::Stale,
)]);
assert!(!stale_vh.has_fresh_vh(), "stale VH is not live");
let vh_failed = composite(&[cell_mode(
"SL-042",
"REQ-111",
"SL-042",
"VH",
CoverageStatus::Failed,
IsStale::Fresh,
)]);
assert!(
!vh_failed.has_fresh_vh(),
"a non-Verified VH cell is not confirming"
);
}
fn vtcheck(
alias: Option<&str>,
command: Option<Vec<&str>>,
matcher: Option<Matcher>,
) -> VtCheck {
VtCheck {
alias: alias.map(str::to_owned),
command: command.map(|c| c.into_iter().map(str::to_owned).collect()),
extra_args: Vec::new(),
matcher,
}
}
fn matcher(source: Option<MatchSource>, pattern: &str, regex: bool) -> Matcher {
Matcher {
source,
pattern: pattern.to_owned(),
regex,
}
}
#[test]
fn derive_status_truth_table() {
assert_eq!(
derive_status(&RunOutcome::Unobtainable),
CoverageStatus::Blocked
);
assert_eq!(
derive_status(&RunOutcome::Ran {
exit_ok: true,
matched: None
}),
CoverageStatus::Verified
);
assert_eq!(
derive_status(&RunOutcome::Ran {
exit_ok: true,
matched: Some(true)
}),
CoverageStatus::Verified
);
assert_eq!(
derive_status(&RunOutcome::Ran {
exit_ok: false,
matched: None
}),
CoverageStatus::Failed
);
assert_eq!(
derive_status(&RunOutcome::Ran {
exit_ok: false,
matched: Some(true)
}),
CoverageStatus::Failed
);
assert_eq!(
derive_status(&RunOutcome::Ran {
exit_ok: true,
matched: Some(false)
}),
CoverageStatus::Failed
);
}
#[test]
fn inv3_unobtainable_never_verified() {
assert_ne!(
derive_status(&RunOutcome::Unobtainable),
CoverageStatus::Verified
);
}
#[test]
fn evaluate_matcher_substring_literal_and_metachars() {
assert_eq!(evaluate_matcher("ok", false, "all ok here"), Some(true));
assert_eq!(evaluate_matcher("nope", false, "all ok here"), Some(false));
assert_eq!(evaluate_matcher("a.c", false, "abc"), Some(false));
assert_eq!(evaluate_matcher("a.c", false, "xx a.c yy"), Some(true));
assert_eq!(evaluate_matcher("", false, "anything"), Some(true));
assert_eq!(evaluate_matcher("", false, ""), Some(true));
}
#[test]
fn evaluate_matcher_regex_mode() {
assert_eq!(evaluate_matcher("a.c", true, "abc"), Some(true));
assert_eq!(evaluate_matcher("a.c", true, "axyzc"), Some(false));
assert_eq!(evaluate_matcher("(", true, "anything"), None);
assert_eq!(evaluate_matcher("", true, "anything"), Some(true));
}
#[test]
fn valid_rejects_alias_command_conflict() {
let check = vtcheck(
Some("test"),
Some(vec!["cargo", "test"]),
Some(matcher(None, "ok", false)),
);
assert_eq!(valid(&check), Err(ValidError::AliasCommandConflict));
}
#[test]
fn valid_rejects_empty_matcher_on_alias() {
assert_eq!(
valid(&vtcheck(Some("test"), None, None)),
Err(ValidError::MatcherRequired)
);
assert_eq!(
valid(&vtcheck(Some("test"), None, Some(matcher(None, "", false)))),
Err(ValidError::MatcherRequired)
);
}
#[test]
fn valid_rejects_empty_matcher_on_default_base() {
assert_eq!(
valid(&vtcheck(None, None, None)),
Err(ValidError::MatcherRequired)
);
}
#[test]
fn valid_accepts_empty_matcher_with_literal_command() {
assert_eq!(valid(&vtcheck(None, Some(vec!["true"]), None)), Ok(()));
assert_eq!(
valid(&vtcheck(
None,
Some(vec!["true"]),
Some(matcher(None, "", false))
)),
Ok(())
);
}
#[test]
fn valid_rejects_absolute_file_glob() {
let check = vtcheck(
Some("test"),
None,
Some(matcher(
Some(MatchSource::File("/etc/x".to_owned())),
"ok",
false,
)),
);
assert_eq!(valid(&check), Err(ValidError::GlobEscapesTree));
}
#[test]
fn valid_rejects_ascending_file_glob() {
let check = vtcheck(
Some("test"),
None,
Some(matcher(
Some(MatchSource::File("../x".to_owned())),
"ok",
false,
)),
);
assert_eq!(valid(&check), Err(ValidError::GlobEscapesTree));
let nested = vtcheck(
Some("test"),
None,
Some(matcher(
Some(MatchSource::File(".doctrine/spec/tech/../../x".to_owned())),
"ok",
false,
)),
);
assert_eq!(valid(&nested), Err(ValidError::GlobEscapesTree));
}
#[test]
fn valid_rejects_unparseable_regex() {
let check = vtcheck(Some("test"), None, Some(matcher(None, "(", true)));
assert_eq!(valid(&check), Err(ValidError::BadRegex));
}
#[test]
fn valid_accepts_wellformed_alias_with_matcher() {
let check = vtcheck(
Some("test"),
None,
Some(matcher(Some(MatchSource::Stdout), "ok", false)),
);
assert_eq!(valid(&check), Ok(()));
let file_ok = vtcheck(
Some("test"),
None,
Some(matcher(
Some(MatchSource::File(".doctrine/spec/tech/*/*.md".to_owned())),
"ok",
false,
)),
);
assert_eq!(valid(&file_ok), Ok(()));
}
#[test]
fn match_source_serde_repr_all_three_variants() {
for (src, token) in [
(MatchSource::Stdout, "\"stdout\""),
(MatchSource::Stderr, "\"stderr\""),
(
MatchSource::File(".doctrine/spec/tech/*/*.md".to_owned()),
"\"file:.doctrine/spec/tech/*/*.md\"",
),
] {
let m = matcher(Some(src.clone()), "ok", false);
let rendered = toml::to_string(&m).unwrap();
assert!(
rendered.contains(&format!("source = {token}")),
"expected `source = {token}` in:\n{rendered}"
);
let back: Matcher = toml::from_str(&rendered).unwrap();
assert_eq!(back.source, Some(src), "MatchSource round-trips");
}
}
#[test]
fn vtcheck_full_round_trip_through_entry() {
let mut e = entry(
key("SL-057", "REQ-200", "SL-057", "VT"),
CoverageStatus::Verified,
None,
);
e.check = Some(VtCheck {
alias: Some("test".to_owned()),
command: None,
extra_args: vec!["--quiet".to_owned()],
matcher: Some(matcher(
Some(MatchSource::File(
".doctrine/spec/tech/003/spec-003.md".to_owned(),
)),
"PASS",
true,
)),
});
let file = CoverageFile { entry: vec![e] };
let back = parse(&render(&file).unwrap()).unwrap();
assert_eq!(back, file, "the full VtCheck round-trips byte-clean");
}
#[test]
fn vtcheck_command_variant_round_trips() {
let mut e = entry(
key("SL-057", "REQ-201", "SL-057", "VT"),
CoverageStatus::Verified,
None,
);
e.check = Some(vtcheck(
None,
Some(vec!["cargo", "test"]),
Some(matcher(Some(MatchSource::Stderr), "ok", false)),
));
let file = CoverageFile { entry: vec![e] };
let back = parse(&render(&file).unwrap()).unwrap();
assert_eq!(back, file);
}
#[test]
fn pre_sl057_entry_without_check_parses_to_none() {
let body = r#"
[[entry]]
slice = "SL-042"
requirement = "REQ-109"
contributing_change = "SL-042"
mode = "VT"
status = "verified"
git_anchor = "anchor-abc123"
"#;
let file = parse(body).unwrap();
assert!(
file.entry.first().unwrap().check.is_none(),
"absent check defaults None"
);
}
}