use std::path::PathBuf;
use crate::{config::Config, exec::exec, Result, TenxError};
pub enum Runnable {
Ok,
Error(String),
}
impl Runnable {
pub fn is_ok(&self) -> bool {
matches!(self, Runnable::Ok)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CheckMode {
Pre,
Post,
Both,
}
impl CheckMode {
pub fn is_pre(&self) -> bool {
matches!(self, CheckMode::Pre | CheckMode::Both)
}
pub fn is_post(&self) -> bool {
matches!(self, CheckMode::Post | CheckMode::Both)
}
}
pub struct Check {
pub name: String,
pub command: String,
pub globs: Vec<String>,
pub default_off: bool,
pub fail_on_stderr: bool,
pub mode: CheckMode,
}
impl Check {
pub fn check(&self, config: &Config) -> Result<()> {
let (status, stdout, stderr) = exec(config.project_root(), &self.command)?;
if !status.success() || (self.fail_on_stderr && !stderr.is_empty()) {
let msg = format!("Check command failed: {}", self.command);
Err(TenxError::Check {
name: self.name.clone(),
user: msg,
model: format!("stdout:\n{}\n\nstderr:\n{}", stdout, stderr),
})
} else {
Ok(())
}
}
fn match_globs(&self, path_str: &str, patterns: &[String]) -> Result<bool> {
for pattern in patterns {
let glob_pattern =
glob::Pattern::new(pattern).map_err(|e| TenxError::Internal(e.to_string()))?;
let clean_path = path_str.trim_start_matches("./");
if glob_pattern.matches(path_str) || glob_pattern.matches(clean_path) {
return Ok(true);
}
}
Ok(false)
}
pub fn is_relevant(&self, paths: &Vec<PathBuf>) -> Result<bool> {
for path in paths {
let path_str = path.to_str().unwrap_or_default();
if self.match_globs(path_str, &self.globs)? {
return Ok(true);
}
}
Ok(false)
}
pub fn runnable(&self) -> Result<Runnable> {
Ok(Runnable::Ok)
}
pub fn default_off(&self) -> bool {
self.default_off
}
}
#[cfg(test)]
mod tests {
use super::*;
fn test_config() -> Config {
let mut config = Config::default();
config.project.root = ".".into();
config
}
#[test]
fn test_match_globs() {
let check = Check {
name: "test".to_string(),
command: "true".to_string(),
globs: vec!["src/*.rs".to_string(), "tests/**/*.rs".to_string()],
default_off: false,
fail_on_stderr: true,
mode: CheckMode::Both,
};
let patterns = check.globs.clone();
assert!(check.match_globs("src/lib.rs", &patterns).unwrap());
assert!(check.match_globs("tests/unit/check.rs", &patterns).unwrap());
assert!(!check.match_globs("README.md", &patterns).unwrap());
}
#[test]
fn test_shell_success() {
let shell = Check {
name: "test".to_string(),
command: "true".to_string(),
globs: vec!["*.rs".to_string()],
default_off: false,
fail_on_stderr: true,
mode: CheckMode::Both,
};
let config = test_config();
let result = shell.check(&config);
assert!(result.is_ok());
}
#[test]
fn test_shell_failure() {
let shell = Check {
name: "test".to_string(),
command: "echo 'error message' >&2 && echo 'output message' && false".to_string(),
globs: vec!["*.rs".to_string()],
default_off: false,
fail_on_stderr: true,
mode: super::CheckMode::Both,
};
let config = test_config();
let result = shell.check(&config);
assert!(result.is_err());
match result {
Err(TenxError::Check { name, user, model }) => {
assert_eq!(name, "test");
assert!(user.contains("Check command failed"));
assert!(model.contains("stdout:\noutput message"));
assert!(model.contains("stderr:\nerror message"));
}
_ => panic!("Expected Check error"),
}
}
}