ide/
test_explorer.rs

1//! Discovers tests
2
3use hir::{Crate, Module, ModuleDef, Semantics};
4use ide_db::base_db;
5use ide_db::{FileId, RootDatabase, base_db::RootQueryDb};
6use syntax::TextRange;
7
8use crate::{NavigationTarget, Runnable, TryToNav, runnables::runnable_fn};
9
10#[derive(Debug)]
11pub enum TestItemKind {
12    Crate(base_db::Crate),
13    Module,
14    Function,
15}
16
17#[derive(Debug)]
18pub struct TestItem {
19    pub id: String,
20    pub kind: TestItemKind,
21    pub label: String,
22    pub parent: Option<String>,
23    pub file: Option<FileId>,
24    pub text_range: Option<TextRange>,
25    pub runnable: Option<Runnable>,
26}
27
28pub(crate) fn discover_test_roots(db: &RootDatabase) -> Vec<TestItem> {
29    db.all_crates()
30        .iter()
31        .copied()
32        .filter(|&id| id.data(db).origin.is_local())
33        .filter_map(|id| {
34            let test_id = id.extra_data(db).display_name.as_ref()?.to_string();
35            Some(TestItem {
36                kind: TestItemKind::Crate(id),
37                label: test_id.clone(),
38                id: test_id,
39                parent: None,
40                file: None,
41                text_range: None,
42                runnable: None,
43            })
44        })
45        .collect()
46}
47
48fn find_crate_by_id(db: &RootDatabase, crate_id: &str) -> Option<base_db::Crate> {
49    // here, we use display_name as the crate id. This is not super ideal, but it works since we
50    // only show tests for the local crates.
51    db.all_crates().iter().copied().find(|&id| {
52        id.data(db).origin.is_local()
53            && id.extra_data(db).display_name.as_ref().is_some_and(|x| x.to_string() == crate_id)
54    })
55}
56
57fn discover_tests_in_module(
58    db: &RootDatabase,
59    module: Module,
60    prefix_id: String,
61    only_in_this_file: bool,
62) -> Vec<TestItem> {
63    let sema = Semantics::new(db);
64
65    let mut r = vec![];
66    for c in module.children(db) {
67        let module_name = c
68            .name(db)
69            .as_ref()
70            .map(|n| n.as_str().to_owned())
71            .unwrap_or_else(|| "[mod without name]".to_owned());
72        let module_id = format!("{prefix_id}::{module_name}");
73        let module_children = discover_tests_in_module(db, c, module_id.clone(), only_in_this_file);
74        if !module_children.is_empty() {
75            let nav = NavigationTarget::from_module_to_decl(sema.db, c).call_site;
76            r.push(TestItem {
77                id: module_id,
78                kind: TestItemKind::Module,
79                label: module_name,
80                parent: Some(prefix_id.clone()),
81                file: Some(nav.file_id),
82                text_range: Some(nav.focus_or_full_range()),
83                runnable: None,
84            });
85            if !only_in_this_file || c.is_inline(db) {
86                r.extend(module_children);
87            }
88        }
89    }
90    for def in module.declarations(db) {
91        let ModuleDef::Function(f) = def else {
92            continue;
93        };
94        if !f.is_test(db) {
95            continue;
96        }
97        let nav = f.try_to_nav(db).map(|r| r.call_site);
98        let fn_name = f.name(db).as_str().to_owned();
99        r.push(TestItem {
100            id: format!("{prefix_id}::{fn_name}"),
101            kind: TestItemKind::Function,
102            label: fn_name,
103            parent: Some(prefix_id.clone()),
104            file: nav.as_ref().map(|n| n.file_id),
105            text_range: nav.as_ref().map(|n| n.focus_or_full_range()),
106            runnable: runnable_fn(&sema, f),
107        });
108    }
109    r
110}
111
112pub(crate) fn discover_tests_in_crate_by_test_id(
113    db: &RootDatabase,
114    crate_test_id: &str,
115) -> Vec<TestItem> {
116    let Some(crate_id) = find_crate_by_id(db, crate_test_id) else {
117        return vec![];
118    };
119    discover_tests_in_crate(db, crate_id)
120}
121
122pub(crate) fn discover_tests_in_file(db: &RootDatabase, file_id: FileId) -> Vec<TestItem> {
123    let sema = Semantics::new(db);
124
125    let Some(module) = sema.file_to_module_def(file_id) else { return vec![] };
126    let Some((mut tests, id)) = find_module_id_and_test_parents(&sema, module) else {
127        return vec![];
128    };
129    tests.extend(discover_tests_in_module(db, module, id, true));
130    tests
131}
132
133fn find_module_id_and_test_parents(
134    sema: &Semantics<'_, RootDatabase>,
135    module: Module,
136) -> Option<(Vec<TestItem>, String)> {
137    let Some(parent) = module.parent(sema.db) else {
138        let name = module.krate().display_name(sema.db)?.to_string();
139        return Some((
140            vec![TestItem {
141                id: name.clone(),
142                kind: TestItemKind::Crate(module.krate().into()),
143                label: name.clone(),
144                parent: None,
145                file: None,
146                text_range: None,
147                runnable: None,
148            }],
149            name,
150        ));
151    };
152    let (mut r, mut id) = find_module_id_and_test_parents(sema, parent)?;
153    let parent = Some(id.clone());
154    id += "::";
155    let module_name = &module.name(sema.db);
156    let module_name = module_name.as_ref().map(|n| n.as_str()).unwrap_or("[mod without name]");
157    id += module_name;
158    let nav = NavigationTarget::from_module_to_decl(sema.db, module).call_site;
159    r.push(TestItem {
160        id: id.clone(),
161        kind: TestItemKind::Module,
162        label: module_name.to_owned(),
163        parent,
164        file: Some(nav.file_id),
165        text_range: Some(nav.focus_or_full_range()),
166        runnable: None,
167    });
168    Some((r, id))
169}
170
171pub(crate) fn discover_tests_in_crate(
172    db: &RootDatabase,
173    crate_id: base_db::Crate,
174) -> Vec<TestItem> {
175    if !crate_id.data(db).origin.is_local() {
176        return vec![];
177    }
178    let Some(crate_test_id) = &crate_id.extra_data(db).display_name else {
179        return vec![];
180    };
181    let kind = TestItemKind::Crate(crate_id);
182    let crate_test_id = crate_test_id.to_string();
183    let crate_id: Crate = crate_id.into();
184    let module = crate_id.root_module();
185    let mut r = vec![TestItem {
186        id: crate_test_id.clone(),
187        kind,
188        label: crate_test_id.clone(),
189        parent: None,
190        file: None,
191        text_range: None,
192        runnable: None,
193    }];
194    r.extend(discover_tests_in_module(db, module, crate_test_id, false));
195    r
196}