1use haz_domain::name::{ProjectName, TaskName};
17use haz_domain::project::Project;
18use haz_domain::task::Task;
19use haz_domain::workspace::Workspace;
20
21use crate::engine::spec::QueryError;
22
23#[derive(Debug, Clone, Copy)]
26pub struct CandidateTask<'w> {
27 pub project_name: &'w ProjectName,
29 pub project: &'w Project,
32 pub task_name: &'w TaskName,
34 pub task: &'w Task,
36}
37
38pub fn collect_candidates<'w>(
51 workspace: &'w Workspace,
52 bearing_project: Option<&ProjectName>,
53) -> Result<Vec<CandidateTask<'w>>, QueryError> {
54 match bearing_project {
55 Some(name) => {
56 let project = workspace.projects.get(name).ok_or_else(|| {
57 QueryError::BearingProjectNotInWorkspace {
58 name: name.to_string(),
59 }
60 })?;
61 Ok(project
62 .tasks
63 .iter()
64 .map(|(task_name, task)| CandidateTask {
65 project_name: &project.name,
66 project,
67 task_name,
68 task,
69 })
70 .collect())
71 }
72 None => Ok(workspace
73 .projects
74 .values()
75 .flat_map(|project| {
76 project
77 .tasks
78 .iter()
79 .map(move |(task_name, task)| CandidateTask {
80 project_name: &project.name,
81 project,
82 task_name,
83 task,
84 })
85 })
86 .collect()),
87 }
88}
89
90#[cfg(test)]
91mod tests {
92 use std::collections::{BTreeMap, BTreeSet};
93 use std::path::PathBuf;
94 use std::str::FromStr;
95
96 use haz_domain::action::TaskAction;
97 use haz_domain::env::EnvSettings;
98 use haz_domain::name::ProjectName;
99 use haz_domain::path::{CanonicalPath, HazPath, ProjectRoot, WorkspaceRootPath};
100 use haz_domain::project::Project;
101 use haz_domain::settings::WorkspaceSettings;
102 use haz_domain::task::Task;
103 use haz_domain::workspace::Workspace;
104 use nonempty::NonEmpty;
105
106 use super::*;
107
108 fn argv(parts: &[&str]) -> NonEmpty<String> {
109 NonEmpty::from_vec(parts.iter().map(|s| (*s).to_owned()).collect()).unwrap()
110 }
111
112 fn bare_task(name: &str) -> Task {
113 Task {
114 name: TaskName::from_str(name).unwrap(),
115 action: TaskAction::Command(argv(&["true"])),
116 inputs: vec![],
117 outputs: vec![],
118 deps: vec![],
119 weak_deps: vec![],
120 mutex: None,
121 env: EnvSettings::default(),
122 }
123 }
124
125 fn project(name: &str, root: &str, task_names: &[&str]) -> Project {
126 let mut tasks = BTreeMap::new();
127 for &task_name in task_names {
128 tasks.insert(TaskName::from_str(task_name).unwrap(), bare_task(task_name));
129 }
130 Project {
131 name: ProjectName::from_str(name).unwrap(),
132 root: ProjectRoot::Nested(
133 CanonicalPath::from_absolute(&HazPath::parse(root).unwrap()).unwrap(),
134 ),
135 tags: BTreeSet::new(),
136 tasks,
137 }
138 }
139
140 fn workspace(projects: Vec<Project>) -> Workspace {
141 let mut map = BTreeMap::new();
142 for project in projects {
143 map.insert(project.name.clone(), project);
144 }
145 Workspace {
146 root: WorkspaceRootPath::try_new(PathBuf::from("/abs/workspace")).unwrap(),
147 projects: map,
148 overlays: BTreeMap::new(),
149 settings: WorkspaceSettings::default(),
150 }
151 }
152
153 #[test]
154 fn qry_vocabulary_workspace_candidate_set_covers_every_task() {
155 let ws = workspace(vec![
156 project("lib", "/lib", &["build", "test"]),
157 project("web", "/web", &["bundle"]),
158 ]);
159 let candidates = collect_candidates(&ws, None).unwrap();
160 let identities: Vec<(String, String)> = candidates
161 .iter()
162 .map(|c| (c.project_name.to_string(), c.task_name.to_string()))
163 .collect();
164 assert_eq!(
165 identities,
166 vec![
167 ("lib".to_owned(), "build".to_owned()),
168 ("lib".to_owned(), "test".to_owned()),
169 ("web".to_owned(), "bundle".to_owned()),
170 ],
171 );
172 }
173
174 #[test]
175 fn qry_vocabulary_bearing_project_candidate_set_restricts_to_that_project() {
176 let ws = workspace(vec![
177 project("lib", "/lib", &["build", "test"]),
178 project("web", "/web", &["bundle"]),
179 ]);
180 let bearing = ProjectName::from_str("lib").unwrap();
181 let candidates = collect_candidates(&ws, Some(&bearing)).unwrap();
182 let identities: Vec<(String, String)> = candidates
183 .iter()
184 .map(|c| (c.project_name.to_string(), c.task_name.to_string()))
185 .collect();
186 assert_eq!(
187 identities,
188 vec![
189 ("lib".to_owned(), "build".to_owned()),
190 ("lib".to_owned(), "test".to_owned()),
191 ],
192 );
193 }
194
195 #[test]
196 fn qry_engine_rejects_unknown_bearing_project() {
197 let ws = workspace(vec![project("lib", "/lib", &["build"])]);
198 let bearing = ProjectName::from_str("absent").unwrap();
199 let err = collect_candidates(&ws, Some(&bearing)).unwrap_err();
200 match err {
201 QueryError::BearingProjectNotInWorkspace { name } => assert_eq!(name, "absent"),
202 other => panic!("expected BearingProjectNotInWorkspace, got {other:?}"),
203 }
204 }
205}