1use 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 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}