1use gpui::{AppContext, Entity, Task, TestAppContext};
2use itertools::Itertools;
3use paths::tasks_file;
4use pretty_assertions::assert_eq;
5use serde_json::json;
6use settings::SettingsLocation;
7use std::path::Path;
8use std::sync::Arc;
9use util::rel_path::rel_path;
10
11use project::task_store::{TaskSettingsLocation, TaskStore};
12
13use project::{WorktreeId, task_inventory::*};
14use test_inventory::*;
15
16mod test_inventory {
17 use gpui::{AppContext as _, Entity, Task, TestAppContext};
18 use itertools::Itertools;
19 use task::TaskContext;
20 use worktree::WorktreeId;
21
22 use crate::Inventory;
23
24 use super::TaskSourceKind;
25
26 pub(super) fn task_template_names(
27 inventory: &Entity<Inventory>,
28 worktree: Option<WorktreeId>,
29 cx: &mut TestAppContext,
30 ) -> Task<Vec<String>> {
31 let new_tasks = inventory.update(cx, |inventory, cx| {
32 inventory.list_tasks(None, None, worktree, cx)
33 });
34 cx.background_spawn(async move {
35 new_tasks
36 .await
37 .into_iter()
38 .map(|(_, task)| task.label)
39 .sorted()
40 .collect()
41 })
42 }
43
44 pub(super) fn register_task_used(
45 inventory: &Entity<Inventory>,
46 task_name: &str,
47 cx: &mut TestAppContext,
48 ) -> Task<()> {
49 let tasks = inventory.update(cx, |inventory, cx| {
50 inventory.list_tasks(None, None, None, cx)
51 });
52
53 let task_name = task_name.to_owned();
54 let inventory = inventory.clone();
55 cx.spawn(|mut cx| async move {
56 let (task_source_kind, task) = tasks
57 .await
58 .into_iter()
59 .find(|(_, task)| task.label == task_name)
60 .unwrap_or_else(|| panic!("Failed to find task with name {task_name}"));
61
62 let id_base = task_source_kind.to_id_base();
63 inventory.update(&mut cx, |inventory, _| {
64 inventory.task_scheduled(
65 task_source_kind.clone(),
66 task.resolve_task(&id_base, &TaskContext::default())
67 .unwrap_or_else(|| panic!("Failed to resolve task with name {task_name}")),
68 )
69 });
70 })
71 }
72
73 pub(super) fn register_worktree_task_used(
74 inventory: &Entity<Inventory>,
75 worktree_id: WorktreeId,
76 task_name: &str,
77 cx: &mut TestAppContext,
78 ) -> Task<()> {
79 let tasks = inventory.update(cx, |inventory, cx| {
80 inventory.list_tasks(None, None, Some(worktree_id), cx)
81 });
82
83 let inventory = inventory.clone();
84 let task_name = task_name.to_owned();
85 cx.spawn(|mut cx| async move {
86 let (task_source_kind, task) = tasks
87 .await
88 .into_iter()
89 .find(|(_, task)| task.label == task_name)
90 .unwrap_or_else(|| panic!("Failed to find task with name {task_name}"));
91 let id_base = task_source_kind.to_id_base();
92 inventory.update(&mut cx, |inventory, _| {
93 inventory.task_scheduled(
94 task_source_kind.clone(),
95 task.resolve_task(&id_base, &TaskContext::default())
96 .unwrap_or_else(|| panic!("Failed to resolve task with name {task_name}")),
97 );
98 });
99 })
100 }
101
102 pub(super) async fn list_tasks(
103 inventory: &Entity<Inventory>,
104 worktree: Option<WorktreeId>,
105 cx: &mut TestAppContext,
106 ) -> Vec<(TaskSourceKind, String)> {
107 let task_context = &TaskContext::default();
108 inventory
109 .update(cx, |inventory, cx| {
110 inventory.list_tasks(None, None, worktree, cx)
111 })
112 .await
113 .into_iter()
114 .filter_map(|(source_kind, task)| {
115 let id_base = source_kind.to_id_base();
116 Some((source_kind, task.resolve_task(&id_base, task_context)?))
117 })
118 .map(|(source_kind, resolved_task)| (source_kind, resolved_task.resolved_label))
119 .collect()
120 }
121}
122
123#[gpui::test]
124async fn test_task_list_sorting(cx: &mut TestAppContext) {
125 init_test(cx);
126 let inventory = cx.update(|cx| Inventory::new(cx));
127 let initial_tasks = resolved_task_names(&inventory, None, cx).await;
128 assert!(
129 initial_tasks.is_empty(),
130 "No tasks expected for empty inventory, but got {initial_tasks:?}"
131 );
132 let initial_tasks = task_template_names(&inventory, None, cx).await;
133 assert!(
134 initial_tasks.is_empty(),
135 "No tasks expected for empty inventory, but got {initial_tasks:?}"
136 );
137 cx.run_until_parked();
138 let expected_initial_state = [
139 "1_a_task".to_string(),
140 "1_task".to_string(),
141 "2_task".to_string(),
142 "3_task".to_string(),
143 ];
144
145 inventory.update(cx, |inventory, _| {
146 inventory
147 .update_file_based_tasks(
148 TaskSettingsLocation::Global(tasks_file()),
149 Some(&mock_tasks_from_names(
150 expected_initial_state.iter().map(|name| name.as_str()),
151 )),
152 )
153 .unwrap();
154 });
155 assert_eq!(
156 task_template_names(&inventory, None, cx).await,
157 &expected_initial_state,
158 );
159 assert_eq!(
160 resolved_task_names(&inventory, None, cx).await,
161 &expected_initial_state,
162 "Tasks with equal amount of usages should be sorted alphanumerically"
163 );
164
165 register_task_used(&inventory, "2_task", cx).await;
166 assert_eq!(
167 task_template_names(&inventory, None, cx).await,
168 &expected_initial_state,
169 );
170 assert_eq!(
171 resolved_task_names(&inventory, None, cx).await,
172 vec![
173 "2_task".to_string(),
174 "1_a_task".to_string(),
175 "1_task".to_string(),
176 "3_task".to_string()
177 ],
178 );
179
180 register_task_used(&inventory, "1_task", cx).await;
181 register_task_used(&inventory, "1_task", cx).await;
182 register_task_used(&inventory, "1_task", cx).await;
183 register_task_used(&inventory, "3_task", cx).await;
184 assert_eq!(
185 task_template_names(&inventory, None, cx).await,
186 &expected_initial_state,
187 );
188 assert_eq!(
189 resolved_task_names(&inventory, None, cx).await,
190 vec![
191 "3_task".to_string(),
192 "1_task".to_string(),
193 "2_task".to_string(),
194 "1_a_task".to_string(),
195 ],
196 "Most recently used task should be at the top"
197 );
198
199 let worktree_id = WorktreeId::from_usize(0);
200 let local_worktree_location = SettingsLocation {
201 worktree_id,
202 path: rel_path("foo"),
203 };
204 inventory.update(cx, |inventory, _| {
205 inventory
206 .update_file_based_tasks(
207 TaskSettingsLocation::Worktree(local_worktree_location),
208 Some(&mock_tasks_from_names(["worktree_task_1"])),
209 )
210 .unwrap();
211 });
212 assert_eq!(
213 resolved_task_names(&inventory, None, cx).await,
214 vec![
215 "3_task".to_string(),
216 "1_task".to_string(),
217 "2_task".to_string(),
218 "1_a_task".to_string(),
219 ],
220 "Most recently used task should be at the top"
221 );
222 assert_eq!(
223 resolved_task_names(&inventory, Some(worktree_id), cx).await,
224 vec![
225 "3_task".to_string(),
226 "1_task".to_string(),
227 "2_task".to_string(),
228 "worktree_task_1".to_string(),
229 "1_a_task".to_string(),
230 ],
231 );
232 register_worktree_task_used(&inventory, worktree_id, "worktree_task_1", cx).await;
233 assert_eq!(
234 resolved_task_names(&inventory, Some(worktree_id), cx).await,
235 vec![
236 "worktree_task_1".to_string(),
237 "3_task".to_string(),
238 "1_task".to_string(),
239 "2_task".to_string(),
240 "1_a_task".to_string(),
241 ],
242 "Most recently used worktree task should be at the top"
243 );
244
245 inventory.update(cx, |inventory, _| {
246 inventory
247 .update_file_based_tasks(
248 TaskSettingsLocation::Global(tasks_file()),
249 Some(&mock_tasks_from_names(
250 ["10_hello", "11_hello"]
251 .into_iter()
252 .chain(expected_initial_state.iter().map(|name| name.as_str())),
253 )),
254 )
255 .unwrap();
256 });
257 cx.run_until_parked();
258 let expected_updated_state = [
259 "10_hello".to_string(),
260 "11_hello".to_string(),
261 "1_a_task".to_string(),
262 "1_task".to_string(),
263 "2_task".to_string(),
264 "3_task".to_string(),
265 ];
266 assert_eq!(
267 task_template_names(&inventory, None, cx).await,
268 &expected_updated_state,
269 );
270 assert_eq!(
271 resolved_task_names(&inventory, None, cx).await,
272 vec![
273 "worktree_task_1".to_string(),
274 "1_a_task".to_string(),
275 "1_task".to_string(),
276 "2_task".to_string(),
277 "3_task".to_string(),
278 "10_hello".to_string(),
279 "11_hello".to_string(),
280 ],
281 "After global tasks update, worktree task usage is not erased and it's the first still; global task is back to regular order as its file was updated"
282 );
283
284 register_task_used(&inventory, "11_hello", cx).await;
285 assert_eq!(
286 task_template_names(&inventory, None, cx).await,
287 &expected_updated_state,
288 );
289 assert_eq!(
290 resolved_task_names(&inventory, None, cx).await,
291 vec![
292 "11_hello".to_string(),
293 "worktree_task_1".to_string(),
294 "1_a_task".to_string(),
295 "1_task".to_string(),
296 "2_task".to_string(),
297 "3_task".to_string(),
298 "10_hello".to_string(),
299 ],
300 );
301}
302
303#[gpui::test]
304async fn test_reloading_debug_scenarios(cx: &mut TestAppContext) {
305 init_test(cx);
306 let inventory = cx.update(|cx| Inventory::new(cx));
307 inventory.update(cx, |inventory, _| {
308 inventory
309 .update_file_based_scenarios(
310 TaskSettingsLocation::Global(Path::new("")),
311 Some(
312 r#"
313 [{
314 "label": "test scenario",
315 "adapter": "CodeLLDB",
316 "request": "launch",
317 "program": "wowzer",
318 }]
319 "#,
320 ),
321 )
322 .unwrap();
323 });
324
325 let (_, scenario) = inventory
326 .update(cx, |this, cx| {
327 this.list_debug_scenarios(&TaskContexts::default(), vec![], vec![], false, cx)
328 })
329 .await
330 .1
331 .first()
332 .unwrap()
333 .clone();
334
335 inventory.update(cx, |this, _| {
336 this.scenario_scheduled(scenario.clone(), Default::default(), None, None);
337 });
338
339 assert_eq!(
340 inventory
341 .update(cx, |this, cx| {
342 this.list_debug_scenarios(&Default::default(), vec![], vec![], false, cx)
343 })
344 .await
345 .0
346 .first()
347 .unwrap()
348 .clone()
349 .0,
350 scenario
351 );
352
353 inventory.update(cx, |this, _| {
354 this.update_file_based_scenarios(
355 TaskSettingsLocation::Global(Path::new("")),
356 Some(
357 r#"
358 [{
359 "label": "test scenario",
360 "adapter": "Delve",
361 "request": "launch",
362 "program": "wowzer",
363 }]
364 "#,
365 ),
366 )
367 .unwrap();
368 });
369
370 assert_eq!(
371 inventory
372 .update(cx, |this, cx| {
373 this.list_debug_scenarios(&Default::default(), vec![], vec![], false, cx)
374 })
375 .await
376 .0
377 .first()
378 .unwrap()
379 .0
380 .adapter,
381 "Delve",
382 );
383
384 inventory.update(cx, |this, _| {
385 this.update_file_based_scenarios(
386 TaskSettingsLocation::Global(Path::new("")),
387 Some(
388 r#"
389 [{
390 "label": "testing scenario",
391 "adapter": "Delve",
392 "request": "launch",
393 "program": "wowzer",
394 }]
395 "#,
396 ),
397 )
398 .unwrap();
399 });
400
401 assert!(
402 inventory
403 .update(cx, |this, cx| {
404 this.list_debug_scenarios(&TaskContexts::default(), vec![], vec![], false, cx)
405 })
406 .await
407 .0
408 .is_empty(),
409 );
410}
411
412#[gpui::test]
413async fn test_inventory_static_task_filters(cx: &mut TestAppContext) {
414 init_test(cx);
415 let inventory = cx.update(|cx| Inventory::new(cx));
416 let common_name = "common_task_name";
417 let worktree_1 = WorktreeId::from_usize(1);
418 let worktree_2 = WorktreeId::from_usize(2);
419
420 cx.run_until_parked();
421 let worktree_independent_tasks = vec![
422 (
423 TaskSourceKind::AbsPath {
424 id_base: "global tasks.json".into(),
425 abs_path: paths::tasks_file().clone(),
426 },
427 common_name.to_string(),
428 ),
429 (
430 TaskSourceKind::AbsPath {
431 id_base: "global tasks.json".into(),
432 abs_path: paths::tasks_file().clone(),
433 },
434 "static_source_1".to_string(),
435 ),
436 (
437 TaskSourceKind::AbsPath {
438 id_base: "global tasks.json".into(),
439 abs_path: paths::tasks_file().clone(),
440 },
441 "static_source_2".to_string(),
442 ),
443 ];
444 let worktree_1_tasks = [
445 (
446 TaskSourceKind::Worktree {
447 id: worktree_1,
448 directory_in_worktree: rel_path(".zed").into(),
449 id_base: "local worktree tasks from directory \".zed\"".into(),
450 },
451 common_name.to_string(),
452 ),
453 (
454 TaskSourceKind::Worktree {
455 id: worktree_1,
456 directory_in_worktree: rel_path(".zed").into(),
457 id_base: "local worktree tasks from directory \".zed\"".into(),
458 },
459 "worktree_1".to_string(),
460 ),
461 ];
462 let worktree_2_tasks = [
463 (
464 TaskSourceKind::Worktree {
465 id: worktree_2,
466 directory_in_worktree: rel_path(".zed").into(),
467 id_base: "local worktree tasks from directory \".zed\"".into(),
468 },
469 common_name.to_string(),
470 ),
471 (
472 TaskSourceKind::Worktree {
473 id: worktree_2,
474 directory_in_worktree: rel_path(".zed").into(),
475 id_base: "local worktree tasks from directory \".zed\"".into(),
476 },
477 "worktree_2".to_string(),
478 ),
479 ];
480
481 inventory.update(cx, |inventory, _| {
482 inventory
483 .update_file_based_tasks(
484 TaskSettingsLocation::Global(tasks_file()),
485 Some(&mock_tasks_from_names(
486 worktree_independent_tasks
487 .iter()
488 .map(|(_, name)| name.as_str()),
489 )),
490 )
491 .unwrap();
492 inventory
493 .update_file_based_tasks(
494 TaskSettingsLocation::Worktree(SettingsLocation {
495 worktree_id: worktree_1,
496 path: rel_path(".zed"),
497 }),
498 Some(&mock_tasks_from_names(
499 worktree_1_tasks.iter().map(|(_, name)| name.as_str()),
500 )),
501 )
502 .unwrap();
503 inventory
504 .update_file_based_tasks(
505 TaskSettingsLocation::Worktree(SettingsLocation {
506 worktree_id: worktree_2,
507 path: rel_path(".zed"),
508 }),
509 Some(&mock_tasks_from_names(
510 worktree_2_tasks.iter().map(|(_, name)| name.as_str()),
511 )),
512 )
513 .unwrap();
514 });
515
516 assert_eq!(
517 list_tasks_sorted_by_last_used(&inventory, None, cx).await,
518 worktree_independent_tasks,
519 "Without a worktree, only worktree-independent tasks should be listed"
520 );
521 assert_eq!(
522 list_tasks_sorted_by_last_used(&inventory, Some(worktree_1), cx).await,
523 worktree_1_tasks
524 .iter()
525 .chain(worktree_independent_tasks.iter())
526 .cloned()
527 .sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone()))
528 .collect::<Vec<_>>(),
529 );
530 assert_eq!(
531 list_tasks_sorted_by_last_used(&inventory, Some(worktree_2), cx).await,
532 worktree_2_tasks
533 .iter()
534 .chain(worktree_independent_tasks.iter())
535 .cloned()
536 .sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone()))
537 .collect::<Vec<_>>(),
538 );
539
540 assert_eq!(
541 list_tasks(&inventory, None, cx).await,
542 worktree_independent_tasks,
543 "Without a worktree, only worktree-independent tasks should be listed"
544 );
545 assert_eq!(
546 list_tasks(&inventory, Some(worktree_1), cx).await,
547 worktree_1_tasks
548 .iter()
549 .chain(worktree_independent_tasks.iter())
550 .cloned()
551 .collect::<Vec<_>>(),
552 );
553 assert_eq!(
554 list_tasks(&inventory, Some(worktree_2), cx).await,
555 worktree_2_tasks
556 .iter()
557 .chain(worktree_independent_tasks.iter())
558 .cloned()
559 .collect::<Vec<_>>(),
560 );
561}
562
563fn init_test(_cx: &mut TestAppContext) {
564 zlog::init_test();
565 TaskStore::init(None);
566}
567
568fn resolved_task_names(
569 inventory: &Entity<Inventory>,
570 worktree: Option<WorktreeId>,
571 cx: &mut TestAppContext,
572) -> Task<Vec<String>> {
573 let tasks = inventory.update(cx, |inventory, cx| {
574 let mut task_contexts = TaskContexts::default();
575 task_contexts.active_worktree_context =
576 worktree.map(|worktree| (worktree, Default::default()));
577
578 inventory.used_and_current_resolved_tasks(Arc::new(task_contexts), cx)
579 });
580
581 cx.background_spawn(async move {
582 let (used, current) = tasks.await;
583 used.into_iter()
584 .chain(current)
585 .map(|(_, task)| task.original_task().label.clone())
586 .collect()
587 })
588}
589
590fn mock_tasks_from_names<'a>(task_names: impl IntoIterator<Item = &'a str> + 'a) -> String {
591 serde_json::to_string(&serde_json::Value::Array(
592 task_names
593 .into_iter()
594 .map(|task_name| {
595 json!({
596 "label": task_name,
597 "command": "echo",
598 "args": vec![task_name],
599 })
600 })
601 .collect::<Vec<_>>(),
602 ))
603 .unwrap()
604}
605
606async fn list_tasks_sorted_by_last_used(
607 inventory: &Entity<Inventory>,
608 worktree: Option<WorktreeId>,
609 cx: &mut TestAppContext,
610) -> Vec<(TaskSourceKind, String)> {
611 let (used, current) = inventory
612 .update(cx, |inventory, cx| {
613 let mut task_contexts = TaskContexts::default();
614 task_contexts.active_worktree_context =
615 worktree.map(|worktree| (worktree, Default::default()));
616
617 inventory.used_and_current_resolved_tasks(Arc::new(task_contexts), cx)
618 })
619 .await;
620 let mut all = used;
621 all.extend(current);
622 all.into_iter()
623 .map(|(source_kind, task)| (source_kind, task.resolved_label))
624 .sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone()))
625 .collect()
626}