use std::path::{Path, PathBuf};
use haz_cache::{CacheReader, CacheWriter};
use haz_dag::graph::TaskGraph;
use haz_discovery::bearing::bearing_project;
use haz_domain::name::ProjectName;
use haz_domain::settings::cache::HashAlgo;
use haz_domain::workspace::Workspace;
use haz_vfs::StdFilesystem;
use miette::Result;
use crate::workspace_load::{load_workspace, load_workspace_accumulating};
#[derive(Debug, Clone)]
pub struct Context {
cwd: PathBuf,
workspace: Workspace,
graph: TaskGraph,
fs: StdFilesystem,
}
impl Context {
pub fn open(cwd: &Path) -> Result<Self> {
let (workspace, graph) = load_workspace(cwd)?;
Ok(Self {
cwd: cwd.to_path_buf(),
workspace,
graph,
fs: StdFilesystem::new(),
})
}
pub fn open_accumulating(cwd: &Path) -> Result<Self> {
let (workspace, graph) = load_workspace_accumulating(cwd)?;
Ok(Self {
cwd: cwd.to_path_buf(),
workspace,
graph,
fs: StdFilesystem::new(),
})
}
#[must_use]
pub fn cwd(&self) -> &Path {
&self.cwd
}
#[must_use]
pub fn workspace(&self) -> &Workspace {
&self.workspace
}
#[must_use]
pub fn graph(&self) -> &TaskGraph {
&self.graph
}
#[must_use]
pub fn fs(&self) -> &StdFilesystem {
&self.fs
}
#[must_use]
pub fn bearing(&self) -> Option<&ProjectName> {
bearing_project(&self.cwd, &self.workspace)
}
#[must_use]
pub fn hash_algo(&self) -> HashAlgo {
self.workspace.settings.cache.hash
}
#[must_use]
pub fn cache_reader(&self) -> CacheReader<StdFilesystem> {
CacheReader::new(self.fs, self.workspace.root.as_path(), self.hash_algo())
}
#[must_use]
pub fn cache_writer(&self) -> CacheWriter<StdFilesystem> {
CacheWriter::new(self.fs, self.workspace.root.as_path(), self.hash_algo())
}
}
#[cfg(test)]
mod tests {
use super::Context;
use haz_domain::settings::cache::HashAlgo;
use std::fs;
use std::path::Path;
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 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 open_loads_workspace_and_graph() {
let tempdir = tempfile::TempDir::new().unwrap();
let root = tempdir.path();
explicit_two_project_workspace(root);
let ctx = Context::open(root).unwrap();
assert_eq!(ctx.workspace().projects.len(), 2);
assert_eq!(ctx.graph().nodes.len(), 2);
assert_eq!(ctx.graph().edges.len(), 1);
}
#[test]
fn open_surfaces_workspace_load_errors() {
let tempdir = tempfile::TempDir::new().unwrap();
assert!(Context::open(tempdir.path()).is_err());
}
#[test]
fn open_accumulating_surfaces_every_parse_error() {
let tempdir = tempfile::TempDir::new().unwrap();
let root = tempdir.path();
write_file(
&root.join(".haz/workspace.yml"),
"projects:\n - /alpha\n - /beta\n",
);
write_file(
&root.join("alpha/haz.yml"),
"name: alpha\ntasks:\n build:\n command: \"AAA\"\n",
);
write_file(
&root.join("beta/haz.yml"),
"name: beta\ntasks:\n build:\n command: \"BBB\"\n",
);
let err = Context::open_accumulating(root).unwrap_err();
let rendered = format!("{err:?}");
assert!(
rendered.contains("AAA") && rendered.contains("BBB"),
"both broken files must surface through the check context: {rendered}",
);
}
#[test]
fn bearing_inside_a_project_picks_that_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 ctx = Context::open(&root.join("lib_core/src")).unwrap();
let bearing = ctx.bearing().unwrap();
assert_eq!(bearing.to_string(), "lib_core");
}
#[test]
fn bearing_at_workspace_root_in_explicit_mode_is_none() {
let tempdir = tempfile::TempDir::new().unwrap();
let root = tempdir.path();
explicit_two_project_workspace(root);
let ctx = Context::open(root).unwrap();
assert!(ctx.bearing().is_none());
}
#[test]
fn cwd_accessor_returns_the_value_passed_to_open() {
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 starting = root.join("lib_core/src");
let ctx = Context::open(&starting).unwrap();
assert_eq!(ctx.cwd(), starting.as_path());
}
#[test]
fn hash_algo_defaults_to_blake3() {
let tempdir = tempfile::TempDir::new().unwrap();
let root = tempdir.path();
explicit_two_project_workspace(root);
let ctx = Context::open(root).unwrap();
assert_eq!(ctx.hash_algo(), HashAlgo::Blake3);
}
#[test]
fn hash_algo_honours_workspace_cache_hash_setting() {
let tempdir = tempfile::TempDir::new().unwrap();
let root = tempdir.path();
write_file(
&root.join(".haz/workspace.yml"),
"projects:\n - /lib\ncache:\n hash: sha256\n",
);
write_file(
&root.join("lib/haz.yml"),
"name: lib\ntasks:\n build:\n command: [\"true\"]\n",
);
let ctx = Context::open(root).unwrap();
assert_eq!(ctx.hash_algo(), HashAlgo::Sha256);
}
#[test]
fn cache_reader_is_bound_to_workspace_root() {
let tempdir = tempfile::TempDir::new().unwrap();
let root = tempdir.path();
explicit_two_project_workspace(root);
let ctx = Context::open(root).unwrap();
let reader = ctx.cache_reader();
assert_eq!(reader.cache_root(), root.join(".haz/cache"));
assert_eq!(reader.hash_algo(), HashAlgo::Blake3);
}
#[test]
fn cache_writer_is_bound_to_workspace_root_and_hash() {
let tempdir = tempfile::TempDir::new().unwrap();
let root = tempdir.path();
write_file(
&root.join(".haz/workspace.yml"),
"projects:\n - /lib\ncache:\n hash: sha256\n",
);
write_file(
&root.join("lib/haz.yml"),
"name: lib\ntasks:\n build:\n command: [\"true\"]\n",
);
let ctx = Context::open(root).unwrap();
let writer = ctx.cache_writer();
assert_eq!(writer.cache_root(), root.join(".haz/cache"));
assert_eq!(writer.hash_algo(), HashAlgo::Sha256);
}
}