1use std::collections::BTreeSet;
21
22use haz_dag::graph::TaskGraph;
23use haz_dag::traversal::{
24 direct_predecessors, direct_successors, transitive_predecessors, transitive_successors,
25};
26use haz_domain::task_id::TaskId;
27use haz_domain::workspace::Workspace;
28use haz_query_lang::expr::Expr;
29
30use crate::engine::spec::QuerySpec;
31use crate::expr::relational::RelationalAtom;
32
33#[derive(Debug, Default, Clone)]
39pub struct RelationalTargets {
40 pub child_of: Option<BTreeSet<TaskId>>,
42 pub parent_of: Option<BTreeSet<TaskId>>,
44 pub depends_on: Option<BTreeSet<TaskId>>,
46 pub ancestor_of: Option<BTreeSet<TaskId>>,
48}
49
50impl RelationalTargets {
51 #[must_use]
56 pub fn from_spec(workspace: &Workspace, spec: &QuerySpec) -> Self {
57 Self {
58 child_of: spec
59 .child_of
60 .as_ref()
61 .map(|expr| compute_target_set(workspace, expr)),
62 parent_of: spec
63 .parent_of
64 .as_ref()
65 .map(|expr| compute_target_set(workspace, expr)),
66 depends_on: spec
67 .depends_on
68 .as_ref()
69 .map(|expr| compute_target_set(workspace, expr)),
70 ancestor_of: spec
71 .ancestor_of
72 .as_ref()
73 .map(|expr| compute_target_set(workspace, expr)),
74 }
75 }
76}
77
78#[must_use]
85pub fn compute_target_set(workspace: &Workspace, expr: &Expr<RelationalAtom>) -> BTreeSet<TaskId> {
86 let mut target_set: BTreeSet<TaskId> = BTreeSet::new();
87 for project in workspace.projects.values() {
88 for task_name in project.tasks.keys() {
89 let matches = expr.eval(|atom| atom.matches(&project.name, task_name, &project.tags));
90 if matches {
91 target_set.insert(TaskId {
92 project: project.name.clone(),
93 task: task_name.clone(),
94 });
95 }
96 }
97 }
98 target_set
99}
100
101#[must_use]
106pub fn passes_relational(
107 graph: &TaskGraph,
108 candidate: &TaskId,
109 targets: &RelationalTargets,
110) -> bool {
111 if let Some(set) = &targets.child_of
112 && !passes_child_of(graph, candidate, set)
113 {
114 return false;
115 }
116 if let Some(set) = &targets.parent_of
117 && !passes_parent_of(graph, candidate, set)
118 {
119 return false;
120 }
121 if let Some(set) = &targets.depends_on
122 && !passes_depends_on(graph, candidate, set)
123 {
124 return false;
125 }
126 if let Some(set) = &targets.ancestor_of
127 && !passes_ancestor_of(graph, candidate, set)
128 {
129 return false;
130 }
131 true
132}
133
134#[must_use]
138pub fn passes_child_of(graph: &TaskGraph, candidate: &TaskId, targets: &BTreeSet<TaskId>) -> bool {
139 !direct_predecessors(graph, candidate).is_disjoint(targets)
140}
141
142#[must_use]
146pub fn passes_parent_of(graph: &TaskGraph, candidate: &TaskId, targets: &BTreeSet<TaskId>) -> bool {
147 !direct_successors(graph, candidate).is_disjoint(targets)
148}
149
150#[must_use]
155pub fn passes_depends_on(
156 graph: &TaskGraph,
157 candidate: &TaskId,
158 targets: &BTreeSet<TaskId>,
159) -> bool {
160 if targets.contains(candidate) {
161 return false;
162 }
163 !transitive_predecessors(graph, candidate).is_disjoint(targets)
164}
165
166#[must_use]
171pub fn passes_ancestor_of(
172 graph: &TaskGraph,
173 candidate: &TaskId,
174 targets: &BTreeSet<TaskId>,
175) -> bool {
176 if targets.contains(candidate) {
177 return false;
178 }
179 !transitive_successors(graph, candidate).is_disjoint(targets)
180}
181
182#[cfg(test)]
183mod tests {
184 use std::collections::{BTreeMap, BTreeSet};
185 use std::path::PathBuf;
186 use std::str::FromStr;
187
188 use haz_dag::edge::{Edge, EdgeKind};
189 use haz_dag::graph::TaskGraph;
190 use haz_domain::action::TaskAction;
191 use haz_domain::env::EnvSettings;
192 use haz_domain::name::{ProjectName, TagName, TaskName};
193 use haz_domain::path::{CanonicalPath, HazPath, ProjectRoot, WorkspaceRootPath};
194 use haz_domain::project::Project;
195 use haz_domain::settings::WorkspaceSettings;
196 use haz_domain::task::Task;
197 use haz_domain::task_id::TaskId;
198 use haz_domain::workspace::Workspace;
199 use haz_query_lang::expr::Expr;
200 use nonempty::NonEmpty;
201
202 use super::*;
203
204 fn argv(parts: &[&str]) -> NonEmpty<String> {
205 NonEmpty::from_vec(parts.iter().map(|s| (*s).to_owned()).collect()).unwrap()
206 }
207
208 fn bare_task(name: &str) -> Task {
209 Task {
210 name: TaskName::from_str(name).unwrap(),
211 action: TaskAction::Command(argv(&["true"])),
212 inputs: vec![],
213 outputs: vec![],
214 deps: vec![],
215 weak_deps: vec![],
216 mutex: None,
217 env: EnvSettings::default(),
218 }
219 }
220
221 fn project(name: &str, root: &str, tags: &[&str], task_names: &[&str]) -> Project {
222 let mut tasks = BTreeMap::new();
223 for &task_name in task_names {
224 tasks.insert(TaskName::from_str(task_name).unwrap(), bare_task(task_name));
225 }
226 Project {
227 name: ProjectName::from_str(name).unwrap(),
228 root: ProjectRoot::Nested(
229 CanonicalPath::from_absolute(&HazPath::parse(root).unwrap()).unwrap(),
230 ),
231 tags: tags
232 .iter()
233 .map(|t| TagName::from_str(t).unwrap())
234 .collect::<BTreeSet<_>>(),
235 tasks,
236 }
237 }
238
239 fn workspace(projects: Vec<Project>) -> Workspace {
240 let mut map = BTreeMap::new();
241 for p in projects {
242 map.insert(p.name.clone(), p);
243 }
244 Workspace {
245 root: WorkspaceRootPath::try_new(PathBuf::from("/abs/workspace")).unwrap(),
246 projects: map,
247 overlays: BTreeMap::new(),
248 settings: WorkspaceSettings::default(),
249 }
250 }
251
252 fn id(project: &str, task: &str) -> TaskId {
253 TaskId {
254 project: ProjectName::from_str(project).unwrap(),
255 task: TaskName::from_str(task).unwrap(),
256 }
257 }
258
259 fn ids(items: &[(&str, &str)]) -> BTreeSet<TaskId> {
260 items.iter().map(|(p, t)| id(p, t)).collect()
261 }
262
263 fn graph_with_hard_edges(nodes: &[TaskId], edges: &[(TaskId, TaskId)]) -> TaskGraph {
264 TaskGraph {
265 nodes: nodes.iter().cloned().collect(),
266 edges: edges
267 .iter()
268 .map(|(from, to)| Edge {
269 from: from.clone(),
270 to: to.clone(),
271 kind: EdgeKind::Hard,
272 })
273 .collect(),
274 }
275 }
276
277 fn relational_atom(text: &str) -> Expr<RelationalAtom> {
278 use haz_query_lang::expr::RawAtom;
279 use haz_query_lang::span::Span;
280
281 use crate::expr::relational::parse_relational_atom;
282 Expr::Atom(
283 parse_relational_atom(RawAtom {
284 text: text.to_owned(),
285 span: Span { start: 0, end: 0 },
286 })
287 .unwrap(),
288 )
289 }
290
291 #[test]
294 fn target_set_for_name_atom_collects_matching_tasks_across_projects() {
295 let ws = workspace(vec![
296 project("lib", "/lib", &[], &["build", "test"]),
297 project("web", "/web", &[], &["build", "bundle"]),
298 ]);
299 let expr = relational_atom("name:build");
300 let target = compute_target_set(&ws, &expr);
301 assert_eq!(target, ids(&[("lib", "build"), ("web", "build")]));
302 }
303
304 #[test]
305 fn target_set_for_project_atom_collects_every_task_in_that_project() {
306 let ws = workspace(vec![
307 project("lib", "/lib", &[], &["build", "test"]),
308 project("web", "/web", &[], &["bundle"]),
309 ]);
310 let expr = relational_atom("project:lib");
311 let target = compute_target_set(&ws, &expr);
312 assert_eq!(target, ids(&[("lib", "build"), ("lib", "test")]));
313 }
314
315 #[test]
316 fn target_set_for_tag_atom_collects_tasks_under_tagged_projects() {
317 let ws = workspace(vec![
318 project("lib", "/lib", &["backend"], &["build"]),
319 project("tools", "/tools", &["backend"], &["lint"]),
320 project("web", "/web", &["frontend"], &["bundle"]),
321 ]);
322 let expr = relational_atom("tag:backend");
323 let target = compute_target_set(&ws, &expr);
324 assert_eq!(target, ids(&[("lib", "build"), ("tools", "lint")]));
325 }
326
327 #[test]
328 fn target_set_under_boolean_composition_intersects() {
329 let ws = workspace(vec![
330 project("lib", "/lib", &["backend"], &["build", "test"]),
331 project("web", "/web", &["frontend"], &["test"]),
332 ]);
333 let expr = Expr::And(
335 Box::new(relational_atom("tag:backend")),
336 Box::new(relational_atom("name:test")),
337 );
338 let target = compute_target_set(&ws, &expr);
339 assert_eq!(target, ids(&[("lib", "test")]));
340 }
341
342 #[test]
343 fn target_set_is_empty_when_no_task_matches() {
344 let ws = workspace(vec![project("lib", "/lib", &[], &["build"])]);
345 let expr = relational_atom("name:absent");
346 let target = compute_target_set(&ws, &expr);
347 assert!(target.is_empty());
348 }
349
350 #[test]
353 fn child_of_passes_when_direct_predecessor_in_target_set() {
354 let a = id("p", "a");
356 let b = id("p", "b");
357 let graph = graph_with_hard_edges(&[a.clone(), b.clone()], &[(a.clone(), b.clone())]);
358 let targets = ids(&[("p", "a")]);
359 assert!(passes_child_of(&graph, &b, &targets));
360 assert!(!passes_child_of(&graph, &a, &targets));
361 }
362
363 #[test]
364 fn parent_of_passes_when_direct_successor_in_target_set() {
365 let a = id("p", "a");
367 let b = id("p", "b");
368 let graph = graph_with_hard_edges(&[a.clone(), b.clone()], &[(a.clone(), b.clone())]);
369 let targets = ids(&[("p", "b")]);
370 assert!(passes_parent_of(&graph, &a, &targets));
371 assert!(!passes_parent_of(&graph, &b, &targets));
372 }
373
374 #[test]
375 fn child_of_rejects_when_target_is_only_transitive_predecessor() {
376 let a = id("p", "a");
378 let b = id("p", "b");
379 let c = id("p", "c");
380 let graph = graph_with_hard_edges(
381 &[a.clone(), b.clone(), c.clone()],
382 &[(a.clone(), b.clone()), (b, c.clone())],
383 );
384 let targets = ids(&[("p", "a")]);
385 assert!(!passes_child_of(&graph, &c, &targets));
387 }
388
389 #[test]
392 fn depends_on_passes_when_transitive_predecessor_in_target_set() {
393 let a = id("p", "a");
395 let b = id("p", "b");
396 let c = id("p", "c");
397 let graph = graph_with_hard_edges(
398 &[a.clone(), b.clone(), c.clone()],
399 &[(a.clone(), b.clone()), (b.clone(), c.clone())],
400 );
401 let targets = ids(&[("p", "a")]);
402 assert!(passes_depends_on(&graph, &c, &targets));
403 assert!(passes_depends_on(&graph, &b, &targets));
404 assert!(!passes_depends_on(&graph, &a, &targets));
406 }
407
408 #[test]
409 fn depends_on_excludes_self_when_candidate_in_target_set() {
410 let a = id("p", "a");
414 let b = id("p", "b");
415 let c = id("p", "c");
416 let graph = graph_with_hard_edges(
417 &[a.clone(), b.clone(), c.clone()],
418 &[(a.clone(), b.clone()), (b.clone(), c.clone())],
419 );
420 let targets = ids(&[("p", "a"), ("p", "c")]);
421 assert!(!passes_depends_on(&graph, &c, &targets));
422 assert!(passes_depends_on(&graph, &b, &targets));
423 }
424
425 #[test]
428 fn ancestor_of_passes_when_transitive_successor_in_target_set() {
429 let a = id("p", "a");
431 let b = id("p", "b");
432 let c = id("p", "c");
433 let graph = graph_with_hard_edges(
434 &[a.clone(), b.clone(), c.clone()],
435 &[(a.clone(), b.clone()), (b.clone(), c.clone())],
436 );
437 let targets = ids(&[("p", "c")]);
438 assert!(passes_ancestor_of(&graph, &a, &targets));
439 assert!(passes_ancestor_of(&graph, &b, &targets));
440 assert!(!passes_ancestor_of(&graph, &c, &targets));
442 }
443
444 #[test]
445 fn ancestor_of_excludes_self_when_candidate_in_target_set() {
446 let a = id("p", "a");
447 let b = id("p", "b");
448 let c = id("p", "c");
449 let graph = graph_with_hard_edges(
450 &[a.clone(), b.clone(), c.clone()],
451 &[(a.clone(), b.clone()), (b.clone(), c.clone())],
452 );
453 let targets = ids(&[("p", "a"), ("p", "c")]);
454 assert!(!passes_ancestor_of(&graph, &a, &targets));
455 assert!(passes_ancestor_of(&graph, &b, &targets));
456 }
457
458 #[test]
461 fn every_relation_fails_when_target_set_is_empty() {
462 let a = id("p", "a");
463 let b = id("p", "b");
464 let graph = graph_with_hard_edges(&[a.clone(), b.clone()], &[(a.clone(), b.clone())]);
465 let empty: BTreeSet<TaskId> = BTreeSet::new();
466 assert!(!passes_child_of(&graph, &b, &empty));
467 assert!(!passes_parent_of(&graph, &a, &empty));
468 assert!(!passes_depends_on(&graph, &b, &empty));
469 assert!(!passes_ancestor_of(&graph, &a, &empty));
470 }
471
472 #[test]
475 fn passes_relational_returns_true_when_no_filter_set() {
476 let a = id("p", "a");
477 let graph = graph_with_hard_edges(std::slice::from_ref(&a), &[]);
478 let targets = RelationalTargets::default();
479 assert!(passes_relational(&graph, &a, &targets));
480 }
481
482 #[test]
483 fn passes_relational_short_circuits_on_first_failing_filter() {
484 let a = id("p", "a");
488 let b = id("p", "b");
489 let graph = graph_with_hard_edges(&[a.clone(), b.clone()], &[(a.clone(), b.clone())]);
490 let targets = RelationalTargets {
491 child_of: Some(ids(&[("p", "a")])),
492 parent_of: Some(ids(&[("p", "a")])),
493 ..RelationalTargets::default()
494 };
495 assert!(!passes_relational(&graph, &b, &targets));
496 }
497}