use std::path::Path;
use crate::error::{Error, Result};
use crate::manifest::Manifest;
use crate::matcher;
use crate::status::{State, rule_state};
pub struct Verdict {
pub rule: String,
state: State,
}
impl Verdict {
#[must_use]
pub const fn is_fresh(&self) -> bool {
self.state.is_fresh()
}
#[must_use]
pub const fn state(&self) -> &'static str {
self.state.label()
}
#[must_use]
pub const fn reason(&self) -> Option<&'static str> {
self.state.reason()
}
}
pub fn evaluate(cwd: &Path, argv: Option<&[String]>) -> Result<Vec<Verdict>> {
let located = Manifest::locate(cwd)?;
let manifest = &located.manifest;
let base = located.root.as_path();
let cache_dir = base.join(&manifest.cache_dir);
match argv {
Some(argv) => {
let rule =
matcher::first_match(&manifest.commands, argv).ok_or_else(|| Error::NoMatch {
command: argv.join(" "),
})?;
Ok(vec![Verdict {
rule: rule.name.clone(),
state: rule_state(manifest, rule, base, &cache_dir)?,
}])
}
None => manifest
.commands
.iter()
.map(|rule| {
Ok(Verdict {
rule: rule.name.clone(),
state: rule_state(manifest, rule, base, &cache_dir)?,
})
})
.collect(),
}
}
#[cfg(test)]
mod tests {
use std::path::Path;
use super::evaluate;
use crate::Error;
fn write_manifest(dir: &Path, body: &str) {
let cfg = dir.join(".mmz");
std::fs::create_dir_all(&cfg).expect("mkdir .mmz");
std::fs::write(cfg.join("config.yaml"), body).expect("write manifest");
}
fn project(dir: &Path) {
write_manifest(
dir,
"scopes:\n src: [\"*.txt\"]\ncommands:\n - name: sh\n inputs: [src]\n",
);
std::fs::write(dir.join("a.txt"), b"one").expect("write input");
}
fn record_ok(dir: &Path) {
let argv = ["sh".to_owned(), "-c".to_owned(), "exit 0".to_owned()];
crate::run(&argv, dir).expect("recorded run");
}
#[test]
fn targeted_tracks_never_then_fresh_then_stale() {
let dir = tempfile::tempdir().expect("tempdir");
let base = dir.path();
project(base);
let argv = ["sh".to_owned()];
let never = evaluate(base, Some(&argv)).expect("evaluate");
assert_eq!(never.len(), 1);
let verdict = never.first().expect("one verdict");
assert!(!verdict.is_fresh(), "never run is not fresh");
assert_eq!(verdict.state(), "never");
assert_eq!(verdict.reason(), Some("never run"));
record_ok(base);
let fresh = evaluate(base, Some(&argv)).expect("evaluate");
let verdict = fresh.first().expect("one verdict");
assert!(verdict.is_fresh(), "fresh after a recorded run");
assert_eq!(verdict.reason(), None, "fresh carries no reason");
std::fs::write(base.join("a.txt"), b"two").expect("rewrite input");
let stale = evaluate(base, Some(&argv)).expect("evaluate");
let verdict = stale.first().expect("one verdict");
assert!(!verdict.is_fresh(), "changed input is stale");
assert_eq!(verdict.state(), "stale");
}
#[test]
fn untargeted_gates_every_rule() {
let dir = tempfile::tempdir().expect("tempdir");
let base = dir.path();
project(base);
record_ok(base);
let all = evaluate(base, None).expect("evaluate");
assert_eq!(all.len(), 1, "one verdict per rule");
let verdict = all.first().expect("one verdict");
assert_eq!(verdict.rule, "sh");
assert!(verdict.is_fresh());
}
#[test]
fn unmatched_command_is_a_no_match_error() {
let dir = tempfile::tempdir().expect("tempdir");
let base = dir.path();
project(base);
let argv = ["cargo".to_owned(), "test".to_owned()];
assert!(
matches!(evaluate(base, Some(&argv)), Err(Error::NoMatch { .. })),
"a command no rule matches errors, regardless of strict"
);
}
#[test]
fn no_inputs_rule_is_not_fresh() {
let dir = tempfile::tempdir().expect("tempdir");
let base = dir.path();
write_manifest(
base,
"scopes:\n none: [\"*.none\"]\ncommands:\n - name: sh\n inputs: [none]\n",
);
let verdicts = evaluate(base, None).expect("evaluate");
let verdict = verdicts.first().expect("one verdict");
assert!(!verdict.is_fresh(), "a rule with no inputs cannot be fresh");
assert_eq!(verdict.state(), "no-inputs");
}
#[test]
fn missing_manifest_errors() {
let dir = tempfile::tempdir().expect("tempdir");
assert!(evaluate(dir.path(), None).is_err(), "no manifest is fatal");
}
}