1use std::path::{Path, PathBuf};
25
26use haz_cache::{CacheReader, CacheWriter};
27use haz_dag::graph::TaskGraph;
28use haz_discovery::bearing::bearing_project;
29use haz_domain::name::ProjectName;
30use haz_domain::settings::cache::HashAlgo;
31use haz_domain::workspace::Workspace;
32use haz_vfs::StdFilesystem;
33use miette::Result;
34
35use crate::workspace_load::{load_workspace, load_workspace_accumulating};
36
37#[derive(Debug, Clone)]
47pub struct Context {
48 cwd: PathBuf,
49 workspace: Workspace,
50 graph: TaskGraph,
51 fs: StdFilesystem,
52}
53
54impl Context {
55 pub fn open(cwd: &Path) -> Result<Self> {
69 let (workspace, graph) = load_workspace(cwd)?;
70 Ok(Self {
71 cwd: cwd.to_path_buf(),
72 workspace,
73 graph,
74 fs: StdFilesystem::new(),
75 })
76 }
77
78 pub fn open_accumulating(cwd: &Path) -> Result<Self> {
91 let (workspace, graph) = load_workspace_accumulating(cwd)?;
92 Ok(Self {
93 cwd: cwd.to_path_buf(),
94 workspace,
95 graph,
96 fs: StdFilesystem::new(),
97 })
98 }
99
100 #[must_use]
102 pub fn cwd(&self) -> &Path {
103 &self.cwd
104 }
105
106 #[must_use]
109 pub fn workspace(&self) -> &Workspace {
110 &self.workspace
111 }
112
113 #[must_use]
116 pub fn graph(&self) -> &TaskGraph {
117 &self.graph
118 }
119
120 #[must_use]
129 pub fn fs(&self) -> &StdFilesystem {
130 &self.fs
131 }
132
133 #[must_use]
137 pub fn bearing(&self) -> Option<&ProjectName> {
138 bearing_project(&self.cwd, &self.workspace)
139 }
140
141 #[must_use]
149 pub fn hash_algo(&self) -> HashAlgo {
150 self.workspace.settings.cache.hash
151 }
152
153 #[must_use]
161 pub fn cache_reader(&self) -> CacheReader<StdFilesystem> {
162 CacheReader::new(self.fs, self.workspace.root.as_path(), self.hash_algo())
163 }
164
165 #[must_use]
172 pub fn cache_writer(&self) -> CacheWriter<StdFilesystem> {
173 CacheWriter::new(self.fs, self.workspace.root.as_path(), self.hash_algo())
174 }
175}
176
177#[cfg(test)]
178mod tests {
179 use super::Context;
180 use haz_domain::settings::cache::HashAlgo;
181 use std::fs;
182 use std::path::Path;
183
184 fn write_file(path: &Path, contents: &str) {
185 if let Some(parent) = path.parent() {
186 fs::create_dir_all(parent).unwrap();
187 }
188 fs::write(path, contents).unwrap();
189 }
190
191 fn explicit_two_project_workspace(root: &Path) {
192 write_file(
193 &root.join(".haz/workspace.yml"),
194 "projects:\n - /lib_core\n - /apps/web\n",
195 );
196 write_file(
197 &root.join("lib_core/haz.yml"),
198 "name: lib_core\ntasks:\n build:\n command: [\"true\"]\n",
199 );
200 write_file(
201 &root.join("apps/web/haz.yml"),
202 "name: web\ntasks:\n bundle:\n command: [\"true\"]\n deps:\n - lib_core:build\n",
203 );
204 }
205
206 #[test]
207 fn open_loads_workspace_and_graph() {
208 let tempdir = tempfile::TempDir::new().unwrap();
209 let root = tempdir.path();
210 explicit_two_project_workspace(root);
211
212 let ctx = Context::open(root).unwrap();
213 assert_eq!(ctx.workspace().projects.len(), 2);
214 assert_eq!(ctx.graph().nodes.len(), 2);
215 assert_eq!(ctx.graph().edges.len(), 1);
217 }
218
219 #[test]
220 fn open_surfaces_workspace_load_errors() {
221 let tempdir = tempfile::TempDir::new().unwrap();
222 assert!(Context::open(tempdir.path()).is_err());
225 }
226
227 #[test]
228 fn open_accumulating_surfaces_every_parse_error() {
229 let tempdir = tempfile::TempDir::new().unwrap();
232 let root = tempdir.path();
233 write_file(
234 &root.join(".haz/workspace.yml"),
235 "projects:\n - /alpha\n - /beta\n",
236 );
237 write_file(
238 &root.join("alpha/haz.yml"),
239 "name: alpha\ntasks:\n build:\n command: \"AAA\"\n",
240 );
241 write_file(
242 &root.join("beta/haz.yml"),
243 "name: beta\ntasks:\n build:\n command: \"BBB\"\n",
244 );
245
246 let err = Context::open_accumulating(root).unwrap_err();
247 let rendered = format!("{err:?}");
248 assert!(
249 rendered.contains("AAA") && rendered.contains("BBB"),
250 "both broken files must surface through the check context: {rendered}",
251 );
252 }
253
254 #[test]
255 fn bearing_inside_a_project_picks_that_project() {
256 let tempdir = tempfile::TempDir::new().unwrap();
257 let root = tempdir.path();
258 explicit_two_project_workspace(root);
259 fs::create_dir_all(root.join("lib_core/src")).unwrap();
260
261 let ctx = Context::open(&root.join("lib_core/src")).unwrap();
262 let bearing = ctx.bearing().unwrap();
263 assert_eq!(bearing.to_string(), "lib_core");
264 }
265
266 #[test]
267 fn bearing_at_workspace_root_in_explicit_mode_is_none() {
268 let tempdir = tempfile::TempDir::new().unwrap();
269 let root = tempdir.path();
270 explicit_two_project_workspace(root);
271
272 let ctx = Context::open(root).unwrap();
273 assert!(ctx.bearing().is_none());
274 }
275
276 #[test]
277 fn cwd_accessor_returns_the_value_passed_to_open() {
278 let tempdir = tempfile::TempDir::new().unwrap();
279 let root = tempdir.path();
280 explicit_two_project_workspace(root);
281 fs::create_dir_all(root.join("lib_core/src")).unwrap();
282 let starting = root.join("lib_core/src");
283
284 let ctx = Context::open(&starting).unwrap();
285 assert_eq!(ctx.cwd(), starting.as_path());
286 }
287
288 #[test]
289 fn hash_algo_defaults_to_blake3() {
290 let tempdir = tempfile::TempDir::new().unwrap();
294 let root = tempdir.path();
295 explicit_two_project_workspace(root);
296
297 let ctx = Context::open(root).unwrap();
298 assert_eq!(ctx.hash_algo(), HashAlgo::Blake3);
299 }
300
301 #[test]
302 fn hash_algo_honours_workspace_cache_hash_setting() {
303 let tempdir = tempfile::TempDir::new().unwrap();
308 let root = tempdir.path();
309 write_file(
310 &root.join(".haz/workspace.yml"),
311 "projects:\n - /lib\ncache:\n hash: sha256\n",
312 );
313 write_file(
314 &root.join("lib/haz.yml"),
315 "name: lib\ntasks:\n build:\n command: [\"true\"]\n",
316 );
317
318 let ctx = Context::open(root).unwrap();
319 assert_eq!(ctx.hash_algo(), HashAlgo::Sha256);
320 }
321
322 #[test]
323 fn cache_reader_is_bound_to_workspace_root() {
324 let tempdir = tempfile::TempDir::new().unwrap();
325 let root = tempdir.path();
326 explicit_two_project_workspace(root);
327
328 let ctx = Context::open(root).unwrap();
329 let reader = ctx.cache_reader();
330 assert_eq!(reader.cache_root(), root.join(".haz/cache"));
331 assert_eq!(reader.hash_algo(), HashAlgo::Blake3);
332 }
333
334 #[test]
335 fn cache_writer_is_bound_to_workspace_root_and_hash() {
336 let tempdir = tempfile::TempDir::new().unwrap();
340 let root = tempdir.path();
341 write_file(
342 &root.join(".haz/workspace.yml"),
343 "projects:\n - /lib\ncache:\n hash: sha256\n",
344 );
345 write_file(
346 &root.join("lib/haz.yml"),
347 "name: lib\ntasks:\n build:\n command: [\"true\"]\n",
348 );
349
350 let ctx = Context::open(root).unwrap();
351 let writer = ctx.cache_writer();
352 assert_eq!(writer.cache_root(), root.join(".haz/cache"));
353 assert_eq!(writer.hash_algo(), HashAlgo::Sha256);
354 }
355}