pub mod candidate;
pub mod filter;
pub mod relational;
pub mod spec;
use haz_dag::graph::TaskGraph;
use haz_domain::name::ProjectName;
use haz_domain::task_id::TaskId;
use haz_domain::workspace::Workspace;
use crate::engine::candidate::collect_candidates;
use crate::engine::filter::passes_non_relational;
use crate::engine::relational::{RelationalTargets, passes_relational};
use crate::engine::spec::{QueryError, QuerySpec};
pub fn execute(
workspace: &Workspace,
graph: &TaskGraph,
bearing_project: Option<&ProjectName>,
spec: &QuerySpec,
) -> Result<Vec<TaskId>, QueryError> {
let candidates = collect_candidates(workspace, bearing_project)?;
let bearing_root = bearing_project
.and_then(|name| workspace.projects.get(name))
.map(|p| &p.root);
let targets = RelationalTargets::from_spec(workspace, spec);
let mut selected: Vec<TaskId> = Vec::new();
for candidate in &candidates {
if !passes_non_relational(candidate, spec, bearing_root)? {
continue;
}
let candidate_id = TaskId {
project: candidate.project_name.clone(),
task: candidate.task_name.clone(),
};
if !passes_relational(graph, &candidate_id, &targets) {
continue;
}
selected.push(candidate_id);
}
Ok(selected)
}
pub fn execute_non_relational(
workspace: &Workspace,
bearing_project: Option<&ProjectName>,
spec: &QuerySpec,
) -> Result<Vec<TaskId>, QueryError> {
let graph = TaskGraph::default();
execute(workspace, &graph, bearing_project, spec)
}
#[cfg(test)]
mod tests {
use std::collections::{BTreeMap, BTreeSet};
use std::path::PathBuf;
use std::str::FromStr;
use haz_dag::edge::{Edge, EdgeKind};
use haz_dag::graph::TaskGraph;
use haz_domain::action::TaskAction;
use haz_domain::env::EnvSettings;
use haz_domain::mutex::{Mutex, MutexMode, MutexScope};
use haz_domain::name::{MutexName, ProjectName, TagName, TaskName};
use haz_domain::path::{
CanonicalPath, HazPath, InputSpec, OutputSpec, PathPattern, ProjectRoot, WorkspaceRootPath,
};
use haz_domain::project::Project;
use haz_domain::settings::WorkspaceSettings;
use haz_domain::task::Task;
use haz_domain::task_id::TaskId;
use haz_domain::workspace::Workspace;
use haz_query_lang::expr::{Expr, RawAtom};
use haz_query_lang::span::Span;
use nonempty::NonEmpty;
use super::*;
use crate::expr::relational::{RelationalAtom, parse_relational_atom};
use crate::expr::shortcut::BooleanShortcut;
fn argv(parts: &[&str]) -> NonEmpty<String> {
NonEmpty::from_vec(parts.iter().map(|s| (*s).to_owned()).collect()).unwrap()
}
fn task(name: &str, inputs: &[&str], outputs: &[&str], with_mutex: bool) -> Task {
Task {
name: TaskName::from_str(name).unwrap(),
action: TaskAction::Command(argv(&["true"])),
inputs: inputs
.iter()
.map(|s| InputSpec::parse(s).unwrap())
.collect(),
outputs: outputs
.iter()
.map(|s| OutputSpec::parse(s).unwrap())
.collect(),
deps: vec![],
weak_deps: vec![],
mutex: if with_mutex {
Some(Mutex {
scope: MutexScope::Workspace,
name: MutexName::from_str("db").unwrap(),
mode: MutexMode::Exclusive,
})
} else {
None
},
env: EnvSettings::default(),
}
}
fn project(name: &str, root: &str, tags: &[&str], tasks: Vec<Task>) -> Project {
let mut task_map = BTreeMap::new();
for t in tasks {
task_map.insert(t.name.clone(), t);
}
Project {
name: ProjectName::from_str(name).unwrap(),
root: ProjectRoot::Nested(
CanonicalPath::from_absolute(&HazPath::parse(root).unwrap()).unwrap(),
),
tags: tags
.iter()
.map(|t| TagName::from_str(t).unwrap())
.collect::<BTreeSet<_>>(),
tasks: task_map,
}
}
fn workspace(projects: Vec<Project>) -> Workspace {
let mut map = BTreeMap::new();
for project in projects {
map.insert(project.name.clone(), project);
}
Workspace {
root: WorkspaceRootPath::try_new(PathBuf::from("/abs/workspace")).unwrap(),
projects: map,
overlays: BTreeMap::new(),
settings: WorkspaceSettings::default(),
}
}
fn ids(names: &[(&str, &str)]) -> Vec<haz_domain::task_id::TaskId> {
names
.iter()
.map(|(p, t)| haz_domain::task_id::TaskId {
project: ProjectName::from_str(p).unwrap(),
task: TaskName::from_str(t).unwrap(),
})
.collect()
}
fn tag_atom(s: &str) -> Expr<TagName> {
Expr::Atom(TagName::from_str(s).unwrap())
}
fn project_atom(s: &str) -> Expr<ProjectName> {
Expr::Atom(ProjectName::from_str(s).unwrap())
}
fn task_atom(s: &str) -> Expr<TaskName> {
Expr::Atom(TaskName::from_str(s).unwrap())
}
fn pattern_atom(s: &str) -> Expr<PathPattern> {
Expr::Atom(PathPattern::parse(s).unwrap())
}
fn three_project_workspace() -> Workspace {
workspace(vec![
project(
"lib",
"/lib",
&["backend", "rust"],
vec![
task("build", &["src/**/*.rs"], &["target/lib.so"], false),
task("test", &["src/**/*.rs"], &[], true),
],
),
project(
"web",
"/web",
&["frontend"],
vec![task("bundle", &["src/index.ts"], &["dist/app.js"], false)],
),
project(
"tools",
"/tools",
&["backend"],
vec![task("lint", &[], &[], false)],
),
])
}
#[test]
fn qry_007_empty_spec_returns_full_workspace_candidate_set() {
let ws = three_project_workspace();
let selected = execute_non_relational(&ws, None, &QuerySpec::default()).unwrap();
assert_eq!(
selected,
ids(&[
("lib", "build"),
("lib", "test"),
("tools", "lint"),
("web", "bundle"),
]),
);
}
#[test]
fn qry_007_empty_spec_with_bearing_project_restricts_to_that_project() {
let ws = three_project_workspace();
let bearing = ProjectName::from_str("lib").unwrap();
let selected = execute_non_relational(&ws, Some(&bearing), &QuerySpec::default()).unwrap();
assert_eq!(selected, ids(&[("lib", "build"), ("lib", "test")]));
}
#[test]
fn qry_003_tags_filter_selects_only_tagged_projects() {
let ws = three_project_workspace();
let spec = QuerySpec {
tags: Some(tag_atom("backend")),
..QuerySpec::default()
};
let selected = execute_non_relational(&ws, None, &spec).unwrap();
assert_eq!(
selected,
ids(&[("lib", "build"), ("lib", "test"), ("tools", "lint")]),
);
}
#[test]
fn qry_003_projects_filter_selects_only_matching_project() {
let ws = three_project_workspace();
let spec = QuerySpec {
projects: Some(project_atom("web")),
..QuerySpec::default()
};
let selected = execute_non_relational(&ws, None, &spec).unwrap();
assert_eq!(selected, ids(&[("web", "bundle")]));
}
#[test]
fn qry_003_tasks_filter_selects_only_tasks_with_that_name() {
let ws = three_project_workspace();
let spec = QuerySpec {
tasks: Some(task_atom("build")),
..QuerySpec::default()
};
let selected = execute_non_relational(&ws, None, &spec).unwrap();
assert_eq!(selected, ids(&[("lib", "build")]));
}
#[test]
fn qry_003_combined_per_attribute_filters_intersect() {
let ws = three_project_workspace();
let spec = QuerySpec {
tags: Some(tag_atom("backend")),
tasks: Some(task_atom("test")),
..QuerySpec::default()
};
let selected = execute_non_relational(&ws, None, &spec).unwrap();
assert_eq!(selected, ids(&[("lib", "test")]));
}
#[test]
fn qry_002_tag_expression_with_negation_evaluates() {
let ws = three_project_workspace();
let spec = QuerySpec {
tags: Some(Expr::And(
Box::new(tag_atom("backend")),
Box::new(Expr::Not(Box::new(tag_atom("frontend")))),
)),
..QuerySpec::default()
};
let selected = execute_non_relational(&ws, None, &spec).unwrap();
assert_eq!(
selected,
ids(&[("lib", "build"), ("lib", "test"), ("tools", "lint")]),
);
}
#[test]
fn qry_003_inputs_workspace_absolute_atom_matches_canonicalised_task_pattern() {
let ws = three_project_workspace();
let spec = QuerySpec {
inputs: Some(pattern_atom("/lib/src/main.rs")),
..QuerySpec::default()
};
let selected = execute_non_relational(&ws, None, &spec).unwrap();
assert_eq!(selected, ids(&[("lib", "build"), ("lib", "test")]));
}
#[test]
fn qry_003_inputs_disjoint_workspace_root_excludes_other_projects() {
let ws = three_project_workspace();
let spec = QuerySpec {
inputs: Some(pattern_atom("/web/src/index.ts")),
..QuerySpec::default()
};
let selected = execute_non_relational(&ws, None, &spec).unwrap();
assert_eq!(selected, ids(&[("web", "bundle")]));
}
#[test]
fn qry_003_outputs_glob_atom_matches_literal_task_output() {
let ws = three_project_workspace();
let spec = QuerySpec {
outputs: Some(pattern_atom("/lib/target/*.so")),
..QuerySpec::default()
};
let selected = execute_non_relational(&ws, None, &spec).unwrap();
assert_eq!(selected, ids(&[("lib", "build")]));
}
#[test]
fn qry_003_inputs_project_relative_atom_with_bearing_project() {
let ws = three_project_workspace();
let bearing = ProjectName::from_str("lib").unwrap();
let spec = QuerySpec {
inputs: Some(pattern_atom("src/main.rs")),
..QuerySpec::default()
};
let selected = execute_non_relational(&ws, Some(&bearing), &spec).unwrap();
assert_eq!(selected, ids(&[("lib", "build"), ("lib", "test")]));
}
#[test]
fn qry_003_inputs_project_relative_atom_without_bearing_project_errors() {
let ws = three_project_workspace();
let spec = QuerySpec {
inputs: Some(pattern_atom("src/main.rs")),
..QuerySpec::default()
};
let err = execute_non_relational(&ws, None, &spec).unwrap_err();
match err {
QueryError::CanonicalisePattern { canonical } => assert_eq!(canonical, "src/main.rs"),
other => panic!("expected CanonicalisePattern, got {other:?}"),
}
}
#[test]
fn qry_005_no_inputs_shortcut_selects_input_less_tasks() {
let ws = three_project_workspace();
let spec = QuerySpec {
shortcuts: vec![BooleanShortcut::NoInputs],
..QuerySpec::default()
};
let selected = execute_non_relational(&ws, None, &spec).unwrap();
assert_eq!(selected, ids(&[("tools", "lint")]));
}
#[test]
fn qry_005_mutex_shortcut_selects_only_mutex_tasks() {
let ws = three_project_workspace();
let spec = QuerySpec {
shortcuts: vec![BooleanShortcut::Mutex],
..QuerySpec::default()
};
let selected = execute_non_relational(&ws, None, &spec).unwrap();
assert_eq!(selected, ids(&[("lib", "test")]));
}
#[test]
fn qry_006_combining_multiple_filter_families_intersects() {
let ws = three_project_workspace();
let spec = QuerySpec {
tags: Some(tag_atom("backend")),
shortcuts: vec![BooleanShortcut::Mutex],
..QuerySpec::default()
};
let selected = execute_non_relational(&ws, None, &spec).unwrap();
assert_eq!(selected, ids(&[("lib", "test")]));
}
fn task_id(project: &str, task: &str) -> TaskId {
TaskId {
project: ProjectName::from_str(project).unwrap(),
task: TaskName::from_str(task).unwrap(),
}
}
fn relational_atom(text: &str) -> Expr<RelationalAtom> {
Expr::Atom(
parse_relational_atom(RawAtom {
text: text.to_owned(),
span: Span { start: 0, end: 0 },
})
.unwrap(),
)
}
fn three_project_hard_edge_graph() -> TaskGraph {
let pairs = [
(task_id("lib", "build"), task_id("lib", "test")),
(task_id("lib", "build"), task_id("web", "bundle")),
(task_id("lib", "test"), task_id("tools", "lint")),
];
let mut nodes: BTreeSet<TaskId> = BTreeSet::new();
let mut edges: BTreeSet<Edge> = BTreeSet::new();
for (from, to) in &pairs {
nodes.insert(from.clone());
nodes.insert(to.clone());
edges.insert(Edge {
from: from.clone(),
to: to.clone(),
kind: EdgeKind::Hard,
});
}
TaskGraph { nodes, edges }
}
#[test]
fn qry_004_child_of_selects_direct_successors_of_target_set() {
let ws = three_project_workspace();
let graph = three_project_hard_edge_graph();
let spec = QuerySpec {
child_of: Some(relational_atom("name:build")),
..QuerySpec::default()
};
let selected = execute(&ws, &graph, None, &spec).unwrap();
assert_eq!(selected, ids(&[("lib", "test"), ("web", "bundle")]));
}
#[test]
fn qry_004_parent_of_selects_direct_predecessors_of_target_set() {
let ws = three_project_workspace();
let graph = three_project_hard_edge_graph();
let spec = QuerySpec {
parent_of: Some(relational_atom("name:test")),
..QuerySpec::default()
};
let selected = execute(&ws, &graph, None, &spec).unwrap();
assert_eq!(selected, ids(&[("lib", "build")]));
}
#[test]
fn qry_004_depends_on_traverses_transitively() {
let ws = three_project_workspace();
let graph = three_project_hard_edge_graph();
let spec = QuerySpec {
depends_on: Some(relational_atom("name:build")),
..QuerySpec::default()
};
let selected = execute(&ws, &graph, None, &spec).unwrap();
assert_eq!(
selected,
ids(&[("lib", "test"), ("tools", "lint"), ("web", "bundle")]),
);
}
#[test]
fn qry_004_ancestor_of_traverses_transitively() {
let ws = three_project_workspace();
let graph = three_project_hard_edge_graph();
let spec = QuerySpec {
ancestor_of: Some(relational_atom("name:lint")),
..QuerySpec::default()
};
let selected = execute(&ws, &graph, None, &spec).unwrap();
assert_eq!(selected, ids(&[("lib", "build"), ("lib", "test")]));
}
#[test]
fn qry_004_depends_on_excludes_tasks_in_target_set() {
let ws = three_project_workspace();
let graph = three_project_hard_edge_graph();
let spec = QuerySpec {
depends_on: Some(relational_atom("project:lib")),
..QuerySpec::default()
};
let selected = execute(&ws, &graph, None, &spec).unwrap();
assert_eq!(selected, ids(&[("tools", "lint"), ("web", "bundle")]));
}
#[test]
fn qry_004_relational_filter_intersects_with_per_attribute() {
let ws = three_project_workspace();
let graph = three_project_hard_edge_graph();
let spec = QuerySpec {
child_of: Some(relational_atom("name:build")),
tags: Some(tag_atom("frontend")),
..QuerySpec::default()
};
let selected = execute(&ws, &graph, None, &spec).unwrap();
assert_eq!(selected, ids(&[("web", "bundle")]));
}
#[test]
fn qry_004_empty_target_set_yields_zero_matches() {
let ws = three_project_workspace();
let graph = three_project_hard_edge_graph();
let spec = QuerySpec {
child_of: Some(relational_atom("name:absent")),
..QuerySpec::default()
};
let selected = execute(&ws, &graph, None, &spec).unwrap();
assert!(selected.is_empty());
}
#[test]
fn qry_004_relational_with_boolean_composition_in_atom() {
let ws = three_project_workspace();
let graph = three_project_hard_edge_graph();
let spec = QuerySpec {
child_of: Some(Expr::Or(
Box::new(relational_atom("name:build")),
Box::new(relational_atom("name:test")),
)),
..QuerySpec::default()
};
let selected = execute(&ws, &graph, None, &spec).unwrap();
assert_eq!(
selected,
ids(&[("lib", "test"), ("tools", "lint"), ("web", "bundle")]),
);
}
#[test]
fn qry_004_combined_relational_filters_intersect() {
let ws = three_project_workspace();
let graph = three_project_hard_edge_graph();
let spec = QuerySpec {
depends_on: Some(relational_atom("name:build")),
ancestor_of: Some(relational_atom("name:lint")),
..QuerySpec::default()
};
let selected = execute(&ws, &graph, None, &spec).unwrap();
assert_eq!(selected, ids(&[("lib", "test")]));
}
#[test]
fn execute_with_no_relational_filter_matches_non_relational_entry() {
let ws = three_project_workspace();
let graph = three_project_hard_edge_graph();
let spec = QuerySpec {
tags: Some(tag_atom("backend")),
..QuerySpec::default()
};
let with_graph = execute(&ws, &graph, None, &spec).unwrap();
let without_graph = execute_non_relational(&ws, None, &spec).unwrap();
assert_eq!(with_graph, without_graph);
}
}