use std::collections::BTreeMap;
use serde::Deserialize;
use crate::coverage::{MatchSource, VtCheck};
const DEFAULT_TIMEOUT_SECS: u64 = 300;
#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "kebab-case", default)]
pub(crate) struct VerificationConfig {
command: Option<Vec<String>>,
default_source: Option<MatchSource>,
timeout_secs: Option<u64>,
aliases: BTreeMap<String, Vec<String>>,
}
impl VerificationConfig {
pub(crate) fn timeout_secs(&self) -> u64 {
self.timeout_secs.unwrap_or(DEFAULT_TIMEOUT_SECS)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Resolved {
pub(crate) argv: Vec<String>,
pub(crate) source: MatchSource,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum ResolveError {
BothAliasAndCommand,
UnknownAlias,
NoRunnable,
}
pub(crate) fn resolve(cfg: &VerificationConfig, check: &VtCheck) -> Result<Resolved, ResolveError> {
if check.alias.is_some() && check.command.is_some() {
return Err(ResolveError::BothAliasAndCommand);
}
let mut argv = match (&check.alias, &check.command) {
(Some(alias), _) => cfg
.aliases
.get(alias)
.cloned()
.ok_or(ResolveError::UnknownAlias)?,
(None, Some(command)) => command.clone(),
(None, None) => cfg.command.clone().ok_or(ResolveError::NoRunnable)?,
};
argv.extend(check.extra_args.iter().cloned());
let source = check
.matcher
.as_ref()
.and_then(|m| m.source.clone())
.or_else(|| cfg.default_source.clone())
.unwrap_or(MatchSource::Stdout);
Ok(Resolved { argv, source })
}
#[cfg(test)]
#[expect(
clippy::unwrap_used,
reason = "tests: fail-fast unwrap on parse/resolve is idiomatic"
)]
mod tests {
use super::*;
use crate::coverage::Matcher;
fn vtcheck(
alias: Option<&str>,
command: Option<Vec<&str>>,
extra_args: 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: extra_args.into_iter().map(str::to_owned).collect(),
matcher,
}
}
fn matcher(source: Option<MatchSource>, pattern: &str) -> Matcher {
Matcher {
source,
pattern: pattern.to_owned(),
regex: false,
}
}
#[test]
fn full_verification_table_parses() {
let cfg = crate::dtoml::parse(
"[verification]\n\
command = [\"just\", \"check\"]\n\
default-source = \"stdout\"\n\
timeout-secs = 120\n\
[verification.aliases]\n\
unit = [\"cargo\", \"test\"]\n",
)
.unwrap()
.verification;
assert_eq!(
cfg.command,
Some(vec!["just".to_owned(), "check".to_owned()])
);
assert_eq!(cfg.default_source, Some(MatchSource::Stdout));
assert_eq!(cfg.timeout_secs(), 120);
assert_eq!(
cfg.aliases.get("unit"),
Some(&vec!["cargo".to_owned(), "test".to_owned()])
);
}
#[test]
fn absent_verification_table_yields_default_and_baked_timeout() {
let cfg = crate::dtoml::parse("title = \"unrelated\"\n")
.unwrap()
.verification;
assert_eq!(cfg, VerificationConfig::default());
assert_eq!(cfg.timeout_secs(), 300);
}
#[test]
fn absent_conduct_still_yields_conduct_defaults_through_dtoml() {
let doc = crate::dtoml::parse("[verification]\ncommand = [\"x\"]\n").unwrap();
assert_eq!(doc.conduct, crate::conduct::ConductConfig::default());
}
#[test]
fn known_alias_resolves_to_its_base_argv() {
let mut aliases = BTreeMap::new();
aliases.insert(
"unit".to_owned(),
vec!["cargo".to_owned(), "test".to_owned()],
);
let cfg = VerificationConfig {
aliases,
..Default::default()
};
let check = vtcheck(Some("unit"), None, vec![], Some(matcher(None, "ok")));
let resolved = resolve(&cfg, &check).unwrap();
assert_eq!(resolved.argv, vec!["cargo".to_owned(), "test".to_owned()]);
assert_eq!(resolved.source, MatchSource::Stdout);
}
#[test]
fn unknown_alias_errors() {
let cfg = VerificationConfig::default();
let check = vtcheck(Some("missing"), None, vec![], Some(matcher(None, "ok")));
assert_eq!(resolve(&cfg, &check), Err(ResolveError::UnknownAlias));
}
#[test]
fn both_alias_and_command_errors() {
let cfg = VerificationConfig::default();
let check = vtcheck(
Some("unit"),
Some(vec!["cargo", "test"]),
vec![],
Some(matcher(None, "ok")),
);
assert_eq!(
resolve(&cfg, &check),
Err(ResolveError::BothAliasAndCommand)
);
}
#[test]
fn default_base_uses_config_command() {
let cfg = VerificationConfig {
command: Some(vec!["just".to_owned(), "check".to_owned()]),
..Default::default()
};
let check = vtcheck(None, None, vec![], Some(matcher(None, "ok")));
let resolved = resolve(&cfg, &check).unwrap();
assert_eq!(resolved.argv, vec!["just".to_owned(), "check".to_owned()]);
}
#[test]
fn default_base_with_no_config_command_errors() {
let cfg = VerificationConfig::default();
let check = vtcheck(None, None, vec![], Some(matcher(None, "ok")));
assert_eq!(resolve(&cfg, &check), Err(ResolveError::NoRunnable));
}
#[test]
fn literal_command_is_taken_verbatim_and_extra_args_append() {
let cfg = VerificationConfig::default();
let check = vtcheck(
None,
Some(vec!["cargo", "test"]),
vec!["--quiet", "--", "mymod"],
None,
);
let resolved = resolve(&cfg, &check).unwrap();
assert_eq!(
resolved.argv,
vec![
"cargo".to_owned(),
"test".to_owned(),
"--quiet".to_owned(),
"--".to_owned(),
"mymod".to_owned(),
],
"argv == base ++ extra_args"
);
}
#[test]
fn extra_args_append_to_alias_base() {
let mut aliases = BTreeMap::new();
aliases.insert(
"unit".to_owned(),
vec!["cargo".to_owned(), "test".to_owned()],
);
let cfg = VerificationConfig {
aliases,
..Default::default()
};
let check = vtcheck(
Some("unit"),
None,
vec!["--release"],
Some(matcher(None, "ok")),
);
let resolved = resolve(&cfg, &check).unwrap();
assert_eq!(
resolved.argv,
vec![
"cargo".to_owned(),
"test".to_owned(),
"--release".to_owned()
]
);
}
#[test]
fn source_precedence_entry_matcher_wins() {
let cfg = VerificationConfig {
command: Some(vec!["x".to_owned()]),
default_source: Some(MatchSource::Stderr),
..Default::default()
};
let check = vtcheck(
None,
None,
vec![],
Some(matcher(Some(MatchSource::Stdout), "ok")),
);
assert_eq!(resolve(&cfg, &check).unwrap().source, MatchSource::Stdout);
}
#[test]
fn source_precedence_falls_to_default_source() {
let cfg = VerificationConfig {
command: Some(vec!["x".to_owned()]),
default_source: Some(MatchSource::Stderr),
..Default::default()
};
let check = vtcheck(None, None, vec![], Some(matcher(None, "ok")));
assert_eq!(resolve(&cfg, &check).unwrap().source, MatchSource::Stderr);
}
#[test]
fn source_precedence_falls_to_stdout() {
let cfg = VerificationConfig {
command: Some(vec!["x".to_owned()]),
..Default::default()
};
let check = vtcheck(None, None, vec![], None);
assert_eq!(resolve(&cfg, &check).unwrap().source, MatchSource::Stdout);
}
}