use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use super::check;
use super::workspace::Workspace;
use crate::core::config::DecisionConfig;
use crate::core::examples::{self, Outcome, Tested};
use crate::core::report::Report;
use crate::core::reqfile::{Check, Reqfile, Requirement};
pub fn run(cwd: &Path, only: Option<&[String]>) -> Result<(String, i32), Vec<String>> {
let workspace = Workspace::load(cwd)?;
let requirements: Vec<(&Reqfile, &Requirement)> = workspace
.reqfiles
.iter()
.flat_map(|r| r.requirements.iter().map(move |req| (r, req)))
.filter(|(_, req)| only.is_none_or(|ids| ids.contains(&req.id)))
.collect();
if let Some(ids) = only {
let unknown: Vec<String> = ids
.iter()
.filter(|id| !requirements.iter().any(|(_, r)| &r.id == *id))
.map(|id| format!("--only: no requirement has the id {id}"))
.collect();
if !unknown.is_empty() {
return Err(unknown);
}
}
let mut tested = Vec::new();
for (reqfile, requirement) in requirements {
let own = workspace
.root
.join(&reqfile.dir)
.join(".reqfile")
.join(&requirement.id);
let cases_dir = own.join("examples");
if !cases_dir.is_dir() {
continue;
}
let mut cases = Vec::new();
for (name, case) in sorted_dirs(&cases_dir)? {
let Some(label) = examples::label(&name) else {
return Err(vec![format!(
"{}: an example folder must be named violation-… or ok-…",
case.display()
)]);
};
let jev = workspace.settings.decision(&reqfile.dir);
let outcome = run_case(
&workspace.root.join(&reqfile.path),
&own,
&case,
requirement,
&jev,
)
.map(|report| examples::outcome(label, &requirement.id, &report))
.unwrap_or_else(Outcome::Error);
cases.push((name, label, outcome));
}
tested.push(Tested {
id: requirement.id.clone(),
measured: requirement
.checks
.iter()
.any(|c| matches!(c, Check::Decision(_))),
cases,
});
}
Ok(examples::render(&tested))
}
fn run_case(
reqfile: &Path,
own: &Path,
case: &Path,
requirement: &Requirement,
jev: &DecisionConfig,
) -> Result<Report, String> {
let repo = TempRepo::new()?;
copy_tree(case, repo.path(), None)?;
fs::copy(reqfile, repo.path().join("Reqfile.yaml")).map_err(|e| e.to_string())?;
let own_copy = repo.path().join(".reqfile").join(&requirement.id);
copy_tree(own, &own_copy, Some("examples"))?;
let quote = |s: &str| serde_json::Value::from(s).to_string();
let config = format!(
"decision:\n model: {}\n api_key_env: {}\n endpoint: {}\n concurrency: {}\n",
quote(&jev.model),
quote(&jev.api_key_env),
quote(&jev.endpoint),
jev.concurrency
);
fs::write(repo.path().join(".reqfile").join("config.yaml"), config)
.map_err(|e| e.to_string())?;
let options = check::Options {
changed: None,
only: Some(vec![requirement.id.clone()]),
fast: false,
log: None,
log_tags: Vec::new(),
};
Ok(check::run(repo.path(), &options))
}
fn sorted_dirs(dir: &Path) -> Result<Vec<(String, PathBuf)>, Vec<String>> {
let fail = |e: std::io::Error| vec![format!("{}: {e}", dir.display())];
let mut dirs = Vec::new();
for entry in fs::read_dir(dir).map_err(fail)? {
let entry = entry.map_err(fail)?;
if entry.file_type().map_err(fail)?.is_dir() {
dirs.push((
entry.file_name().to_string_lossy().into_owned(),
entry.path(),
));
}
}
dirs.sort();
Ok(dirs)
}
fn copy_tree(from: &Path, to: &Path, skip: Option<&str>) -> Result<(), String> {
let fail = |e: std::io::Error| format!("cannot copy {}: {e}", from.display());
fs::create_dir_all(to).map_err(fail)?;
for entry in fs::read_dir(from).map_err(fail)? {
let entry = entry.map_err(fail)?;
if Some(entry.file_name().to_string_lossy().as_ref()) == skip {
continue;
}
let target = to.join(entry.file_name());
let kind = entry.file_type().map_err(fail)?;
if kind.is_symlink() {
return Err(format!(
"{} is a symlink; examples hold regular files only",
entry.path().display()
));
} else if kind.is_dir() {
copy_tree(&entry.path(), &target, None)?;
} else {
fs::copy(entry.path(), &target).map_err(fail)?;
}
}
Ok(())
}
struct TempRepo(tempfile::TempDir);
impl TempRepo {
fn new() -> Result<Self, String> {
let dir = tempfile::Builder::new()
.prefix("reqfile-example-")
.tempdir()
.map_err(|e| format!("cannot create a temporary folder: {e}"))?;
let status = Command::new("git")
.args(["init", "-q"])
.current_dir(dir.path())
.status()
.map_err(|e| format!("could not run git: {e}"))?;
if !status.success() {
return Err("git init failed for an example".into());
}
Ok(Self(dir))
}
fn path(&self) -> &Path {
self.0.path()
}
}