use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use haz_cli::cli::{RunArgs, Scope, TaskRef};
use haz_cli::commands::{cache, run};
use haz_cli::context::Context;
use miette::Result;
use tempfile::TempDir;
fn run_run(args: &RunArgs, cwd: &Path) -> Result<i32> {
run::execute(args, &Context::open(cwd)?)
}
fn run_cache_clear(cwd: &Path) -> Result<i32> {
cache::clear(&Context::open(cwd)?)
}
const HAZ_BIN: &str = env!("CARGO_BIN_EXE_haz");
fn fixture_root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/smoke")
}
fn fixture_tempdir() -> TempDir {
let src = fixture_root();
let dst = TempDir::new().expect("tempdir creation failed");
copy_dir_recursive(&src, dst.path());
dst
}
fn copy_dir_recursive(src: &Path, dst: &Path) {
fs::create_dir_all(dst).unwrap();
for entry in fs::read_dir(src).unwrap() {
let entry = entry.unwrap();
let src_path = entry.path();
let dst_path = dst.join(entry.file_name());
if entry.file_type().unwrap().is_dir() {
copy_dir_recursive(&src_path, &dst_path);
} else {
fs::copy(&src_path, &dst_path).unwrap();
}
}
}
fn bare_run_args() -> RunArgs {
RunArgs {
targets: Vec::new(),
scope: Scope::Default,
git_filter: None,
dry_run: false,
output_mode: None,
}
}
fn task_ref(s: &str) -> TaskRef {
TaskRef::parse(s).unwrap()
}
fn true_on_path() -> bool {
let path = std::env::var_os("PATH").unwrap_or_default();
std::env::split_paths(&path).any(|dir| dir.join("true").exists())
}
fn first_manifest(root: &Path) -> Option<PathBuf> {
let cache_root = root.join(".haz/cache");
if !cache_root.is_dir() {
return None;
}
let shards = fs::read_dir(&cache_root).ok()?;
for shard in shards.flatten() {
if !shard.file_type().ok()?.is_dir() {
continue;
}
let entries = fs::read_dir(shard.path()).ok()?;
for entry in entries.flatten() {
if !entry.file_type().ok()?.is_dir() {
continue;
}
let mf = entry.path().join("manifest.json");
if mf.is_file() {
return Some(mf);
}
}
}
None
}
#[test]
fn process_spawn_haz_run_dry_run_inside_lib() {
let tmp = fixture_tempdir();
let lib_dir = tmp.path().join("lib");
let output = Command::new(HAZ_BIN)
.args(["run", "--dry-run"])
.current_dir(&lib_dir)
.output()
.expect("failed to spawn haz binary");
assert!(
output.status.success(),
"haz run --dry-run exited with {:?}\nstdout: {}\nstderr: {}",
output.status,
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("would run") && stdout.contains("lib:default"),
"stdout did not contain the dry-run plan: {stdout}",
);
}
#[test]
fn exec_023_default_target_dry_run_from_lib() {
let tmp = fixture_tempdir();
let args = RunArgs {
dry_run: true,
..bare_run_args()
};
let code = run_run(&args, &tmp.path().join("lib")).unwrap();
assert_eq!(code, 0);
}
#[test]
fn in_process_workspace_scope_dry_run_from_root() {
let tmp = fixture_tempdir();
let args = RunArgs {
scope: Scope::Workspace,
dry_run: true,
..bare_run_args()
};
let code = run_run(&args, tmp.path()).unwrap();
assert_eq!(code, 0);
}
#[test]
fn in_process_positional_pulls_transitive_hard_predecessor() {
let tmp = fixture_tempdir();
let args = RunArgs {
targets: vec![task_ref("app:test")],
dry_run: true,
..bare_run_args()
};
let code = run_run(&args, &tmp.path().join("app")).unwrap();
assert_eq!(code, 0);
}
#[test]
fn in_process_cache_cold_to_warm_to_clear_cycle() {
if !true_on_path() {
eprintln!("skipping: `true` not on $PATH");
return;
}
let tmp = fixture_tempdir();
let lib_dir = tmp.path().join("lib");
let args = RunArgs {
targets: vec![task_ref("~:build")],
..bare_run_args()
};
let code = run_run(&args, &lib_dir).unwrap();
assert_eq!(code, 0);
let manifest_path = first_manifest(tmp.path())
.expect("manifest missing under .haz/cache/ after fresh successful run");
let cold_bytes = fs::read(&manifest_path).unwrap();
let code = run_run(&args, &lib_dir).unwrap();
assert_eq!(code, 0);
let warm_bytes = fs::read(&manifest_path).unwrap();
assert_eq!(
cold_bytes, warm_bytes,
"manifest bytes changed across cold->warm; the second run did not hit the cache",
);
let code = run_cache_clear(tmp.path()).unwrap();
assert_eq!(code, 0);
assert!(
!tmp.path().join(".haz/cache").exists(),
".haz/cache was not removed by cache clear",
);
let code = run_run(&args, &lib_dir).unwrap();
assert_eq!(code, 0);
assert!(
first_manifest(tmp.path()).is_some(),
"manifest missing under .haz/cache/ after post-clear run",
);
}