use std::io::{self, Write};
use haz_domain::name::ProjectName;
use haz_domain::workspace::Workspace;
use miette::{IntoDiagnostic, Result, WrapErr};
use crate::cli::{CheckArgs, Scope};
use crate::context::Context;
pub fn execute(args: &CheckArgs, ctx: &Context) -> Result<i32> {
let count = count_validated_tasks(args, ctx.bearing(), ctx.workspace());
let mut out = io::stdout().lock();
print_validation_summary(count, &mut out)
.into_diagnostic()
.wrap_err("failed to write validation summary")?;
Ok(0)
}
fn count_validated_tasks(
args: &CheckArgs,
bearing: Option<&ProjectName>,
workspace: &Workspace,
) -> usize {
match args.scope {
Scope::Workspace => workspace_task_count(workspace),
Scope::All | Scope::Default => match bearing {
Some(name) => bearing_task_count(workspace, name),
None => workspace_task_count(workspace),
},
}
}
fn workspace_task_count(workspace: &Workspace) -> usize {
workspace.projects.values().map(|p| p.tasks.len()).sum()
}
fn bearing_task_count(workspace: &Workspace, name: &ProjectName) -> usize {
workspace.projects.get(name).map_or(0, |p| p.tasks.len())
}
fn print_validation_summary(count: usize, out: &mut impl Write) -> io::Result<()> {
writeln!(out, "{count} task(s) validated.")
}
#[cfg(test)]
mod tests {
use super::{count_validated_tasks, execute, print_validation_summary};
use crate::cli::{CheckArgs, Scope};
use crate::context::Context;
use miette::Result;
use std::fs;
use std::path::Path;
use std::str::FromStr;
fn write_file(path: &Path, contents: &str) {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).unwrap();
}
fs::write(path, contents).unwrap();
}
fn run_check(args: &CheckArgs, cwd: &Path) -> Result<i32> {
let ctx = Context::open(cwd)?;
execute(args, &ctx)
}
fn explicit_two_project_workspace(root: &Path) {
write_file(
&root.join(".haz/workspace.yml"),
"projects:\n - /lib_core\n - /apps/web\n",
);
write_file(
&root.join("lib_core/haz.yml"),
"name: lib_core\ntasks:\n build:\n command: [\"true\"]\n",
);
write_file(
&root.join("apps/web/haz.yml"),
"name: web\ntasks:\n bundle:\n command: [\"true\"]\n deps:\n - lib_core:build\n",
);
}
#[test]
fn aux_010_check_runs_every_validation_step_against_clean_workspace() {
let tempdir = tempfile::TempDir::new().unwrap();
let root = tempdir.path();
explicit_two_project_workspace(root);
let args = CheckArgs::default();
let code = run_check(&args, root).unwrap();
assert_eq!(code, 0);
}
#[test]
fn aux_010_check_accumulates_multiple_dag_errors_without_short_circuit() {
let tempdir = tempfile::TempDir::new().unwrap();
let root = tempdir.path();
write_file(
&root.join(".haz/workspace.yml"),
"projects:\n - /lib_a\n - /lib_b\n - /lib_c\n - /lib_d\n - /lib_e\n",
);
write_file(
&root.join("lib_a/haz.yml"),
"name: lib_a\ntasks:\n build:\n command: [\"true\"]\n deps:\n - lib_b:build\n",
);
write_file(
&root.join("lib_b/haz.yml"),
"name: lib_b\ntasks:\n build:\n command: [\"true\"]\n deps:\n - lib_a:build\n",
);
write_file(
&root.join("lib_c/haz.yml"),
"name: lib_c\ntasks:\n build:\n command: [\"true\"]\n deps:\n - lib_x:nonexistent\n",
);
write_file(
&root.join("lib_d/haz.yml"),
"name: lib_d\ntasks:\n build:\n command: [\"true\"]\n outputs:\n - dup.txt\n",
);
write_file(
&root.join("lib_e/haz.yml"),
"name: lib_e\ntasks:\n build:\n command: [\"true\"]\n outputs:\n - /lib_d/dup.txt\n",
);
let args = CheckArgs::default();
let err = run_check(&args, root).unwrap_err();
let rendered = format!("{err:?}");
assert!(
rendered.contains("DAG-014"),
"diagnostic should surface the cycle rule: {rendered}",
);
assert!(
rendered.contains("DAG-011") || rendered.contains("unresolved"),
"diagnostic should surface the unresolved reference: {rendered}",
);
assert!(
rendered.contains("DAG-015"),
"diagnostic should surface the duplicate literal output rule: {rendered}",
);
}
#[test]
fn aux_011_success_output_emits_count_line() {
let mut out = Vec::<u8>::new();
print_validation_summary(3, &mut out).unwrap();
assert_eq!(String::from_utf8(out).unwrap(), "3 task(s) validated.\n");
}
#[test]
fn aux_011_failure_surfaces_every_dag_error_in_one_report() {
let tempdir = tempfile::TempDir::new().unwrap();
let root = tempdir.path();
write_file(
&root.join(".haz/workspace.yml"),
"projects:\n - /lib_a\n - /lib_b\n",
);
write_file(
&root.join("lib_a/haz.yml"),
"name: lib_a\ntasks:\n build:\n command: [\"true\"]\n outputs:\n - dup.txt\n",
);
write_file(
&root.join("lib_b/haz.yml"),
"name: lib_b\ntasks:\n build:\n command: [\"true\"]\n outputs:\n - /lib_a/dup.txt\n deps:\n - lib_a:nonexistent\n",
);
let args = CheckArgs::default();
let err = run_check(&args, root).unwrap_err();
let rendered = format!("{err:?}");
assert!(
rendered.contains("DAG-015"),
"duplicate-output diagnostic missing: {rendered}",
);
assert!(
rendered.contains("DAG-011") || rendered.contains("unresolved"),
"unresolved-reference diagnostic missing: {rendered}",
);
}
#[test]
fn aux_012_check_returns_zero_on_clean_workspace() {
let tempdir = tempfile::TempDir::new().unwrap();
let root = tempdir.path();
fs::create_dir_all(root.join(".git")).unwrap();
write_file(
&root.join("haz.yml"),
"name: only_proj\ntasks:\n build:\n command: [\"true\"]\n",
);
let args = CheckArgs::default();
let code = run_check(&args, root).unwrap();
assert_eq!(code, 0);
}
#[test]
fn aux_012_check_returns_err_on_validation_failure() {
let tempdir = tempfile::TempDir::new().unwrap();
let root = tempdir.path();
write_file(&root.join(".haz/workspace.yml"), "projects:\n - /lib\n");
write_file(
&root.join("lib/haz.yml"),
"name: lib\ntasks:\n build:\n command: [\"true\"]\n deps:\n - lib:nonexistent\n",
);
let args = CheckArgs::default();
assert!(run_check(&args, root).is_err());
}
#[test]
fn aux_009_count_default_with_bearing_picks_bearing_project() {
let tempdir = tempfile::TempDir::new().unwrap();
let root = tempdir.path();
explicit_two_project_workspace(root);
fs::create_dir_all(root.join("lib_core/src")).unwrap();
let (workspace, _graph) = crate::workspace_load::load_workspace(root).unwrap();
let bearing_name = haz_domain::name::ProjectName::from_str("lib_core").unwrap();
let bearing = Some(&bearing_name);
let args = CheckArgs::default();
let n = count_validated_tasks(&args, bearing, &workspace);
assert_eq!(n, 1, "bearing-project count");
}
#[test]
fn aux_009_count_workspace_flag_picks_every_project() {
let tempdir = tempfile::TempDir::new().unwrap();
let root = tempdir.path();
explicit_two_project_workspace(root);
let (workspace, _graph) = crate::workspace_load::load_workspace(root).unwrap();
let bearing_name = haz_domain::name::ProjectName::from_str("lib_core").unwrap();
let bearing = Some(&bearing_name);
let args = CheckArgs {
scope: Scope::Workspace,
};
let n = count_validated_tasks(&args, bearing, &workspace);
assert_eq!(n, 2, "workspace-flag count");
}
#[test]
fn aux_009_count_default_without_bearing_picks_every_project() {
let tempdir = tempfile::TempDir::new().unwrap();
let root = tempdir.path();
explicit_two_project_workspace(root);
let (workspace, _graph) = crate::workspace_load::load_workspace(root).unwrap();
let args = CheckArgs::default();
let n = count_validated_tasks(&args, None, &workspace);
assert_eq!(n, 2, "no-bearing default count");
}
}