task_inventory.rs

  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}