aboutsummaryrefslogtreecommitdiff
path: root/crates/ide/src
diff options
context:
space:
mode:
Diffstat (limited to 'crates/ide/src')
-rw-r--r--crates/ide/src/lib.rs9
-rw-r--r--crates/ide/src/runnables.rs338
2 files changed, 341 insertions, 6 deletions
diff --git a/crates/ide/src/lib.rs b/crates/ide/src/lib.rs
index b600178ee..baa80cf43 100644
--- a/crates/ide/src/lib.rs
+++ b/crates/ide/src/lib.rs
@@ -445,6 +445,15 @@ impl Analysis {
445 self.with_db(|db| runnables::runnables(db, file_id)) 445 self.with_db(|db| runnables::runnables(db, file_id))
446 } 446 }
447 447
448 /// Returns the set of tests for the given file position.
449 pub fn related_tests(
450 &self,
451 position: FilePosition,
452 search_scope: Option<SearchScope>,
453 ) -> Cancelable<Vec<Runnable>> {
454 self.with_db(|db| runnables::related_tests(db, position, search_scope))
455 }
456
448 /// Computes syntax highlighting for the given file 457 /// Computes syntax highlighting for the given file
449 pub fn highlight(&self, file_id: FileId) -> Cancelable<Vec<HlRange>> { 458 pub fn highlight(&self, file_id: FileId) -> Cancelable<Vec<HlRange>> {
450 self.with_db(|db| syntax_highlighting::highlight(db, file_id, None, false)) 459 self.with_db(|db| syntax_highlighting::highlight(db, file_id, None, false))
diff --git a/crates/ide/src/runnables.rs b/crates/ide/src/runnables.rs
index 1e7baed20..ce3a2e7ba 100644
--- a/crates/ide/src/runnables.rs
+++ b/crates/ide/src/runnables.rs
@@ -1,10 +1,14 @@
1use std::fmt; 1use std::fmt;
2 2
3use ast::NameOwner;
3use cfg::CfgExpr; 4use cfg::CfgExpr;
4use hir::{AsAssocItem, HasAttrs, HasSource, Semantics}; 5use hir::{AsAssocItem, HasAttrs, HasSource, Semantics};
5use ide_assists::utils::test_related_attribute; 6use ide_assists::utils::test_related_attribute;
6use ide_db::{defs::Definition, RootDatabase, SymbolKind}; 7use ide_db::{
8 base_db::FilePosition, defs::Definition, search::SearchScope, RootDatabase, SymbolKind,
9};
7use itertools::Itertools; 10use itertools::Itertools;
11use rustc_hash::FxHashSet;
8use syntax::{ 12use syntax::{
9 ast::{self, AstNode, AttrsOwner}, 13 ast::{self, AstNode, AttrsOwner},
10 match_ast, SyntaxNode, 14 match_ast, SyntaxNode,
@@ -13,17 +17,17 @@ use test_utils::mark;
13 17
14use crate::{ 18use crate::{
15 display::{ToNav, TryToNav}, 19 display::{ToNav, TryToNav},
16 FileId, NavigationTarget, 20 references, FileId, NavigationTarget,
17}; 21};
18 22
19#[derive(Debug, Clone)] 23#[derive(Debug, Clone, Hash, PartialEq, Eq)]
20pub struct Runnable { 24pub struct Runnable {
21 pub nav: NavigationTarget, 25 pub nav: NavigationTarget,
22 pub kind: RunnableKind, 26 pub kind: RunnableKind,
23 pub cfg: Option<CfgExpr>, 27 pub cfg: Option<CfgExpr>,
24} 28}
25 29
26#[derive(Debug, Clone)] 30#[derive(Debug, Clone, Hash, PartialEq, Eq)]
27pub enum TestId { 31pub enum TestId {
28 Name(String), 32 Name(String),
29 Path(String), 33 Path(String),
@@ -38,7 +42,7 @@ impl fmt::Display for TestId {
38 } 42 }
39} 43}
40 44
41#[derive(Debug, Clone)] 45#[derive(Debug, Clone, Hash, PartialEq, Eq)]
42pub enum RunnableKind { 46pub enum RunnableKind {
43 Test { test_id: TestId, attr: TestAttr }, 47 Test { test_id: TestId, attr: TestAttr },
44 TestMod { path: String }, 48 TestMod { path: String },
@@ -106,6 +110,102 @@ pub(crate) fn runnables(db: &RootDatabase, file_id: FileId) -> Vec<Runnable> {
106 res 110 res
107} 111}
108 112
113// Feature: Run Test
114//
115// Shows a popup suggesting to run a test in which the item **at the current cursor
116// location** is used (if any).
117//
118// |===
119// | Editor | Action Name
120//
121// | VS Code | **Rust Analyzer: Run Test**
122// |===
123pub(crate) fn related_tests(
124 db: &RootDatabase,
125 position: FilePosition,
126 search_scope: Option<SearchScope>,
127) -> Vec<Runnable> {
128 let sema = Semantics::new(db);
129 let mut res: FxHashSet<Runnable> = FxHashSet::default();
130
131 find_related_tests(&sema, position, search_scope, &mut res);
132
133 res.into_iter().collect_vec()
134}
135
136fn find_related_tests(
137 sema: &Semantics<RootDatabase>,
138 position: FilePosition,
139 search_scope: Option<SearchScope>,
140 tests: &mut FxHashSet<Runnable>,
141) {
142 if let Some(refs) = references::find_all_refs(&sema, position, search_scope) {
143 for (file_id, refs) in refs.references {
144 let file = sema.parse(file_id);
145 let file = file.syntax();
146 let functions = refs.iter().filter_map(|(range, _)| {
147 let token = file.token_at_offset(range.start()).next()?;
148 let token = sema.descend_into_macros(token);
149 let syntax = token.parent();
150 syntax.ancestors().find_map(ast::Fn::cast)
151 });
152
153 for fn_def in functions {
154 if let Some(runnable) = as_test_runnable(&sema, &fn_def) {
155 // direct test
156 tests.insert(runnable);
157 } else if let Some(module) = parent_test_module(&sema, &fn_def) {
158 // indirect test
159 find_related_tests_in_module(sema, &fn_def, &module, tests);
160 }
161 }
162 }
163 }
164}
165
166fn find_related_tests_in_module(
167 sema: &Semantics<RootDatabase>,
168 fn_def: &ast::Fn,
169 parent_module: &hir::Module,
170 tests: &mut FxHashSet<Runnable>,
171) {
172 if let Some(fn_name) = fn_def.name() {
173 let mod_source = parent_module.definition_source(sema.db);
174 let range = match mod_source.value {
175 hir::ModuleSource::Module(m) => m.syntax().text_range(),
176 hir::ModuleSource::BlockExpr(b) => b.syntax().text_range(),
177 hir::ModuleSource::SourceFile(f) => f.syntax().text_range(),
178 };
179
180 let file_id = mod_source.file_id.original_file(sema.db);
181 let mod_scope = SearchScope::file_part(file_id, range);
182 let fn_pos = FilePosition { file_id, offset: fn_name.syntax().text_range().start() };
183 find_related_tests(sema, fn_pos, Some(mod_scope), tests)
184 }
185}
186
187fn as_test_runnable(sema: &Semantics<RootDatabase>, fn_def: &ast::Fn) -> Option<Runnable> {
188 if test_related_attribute(&fn_def).is_some() {
189 let function = sema.to_def(fn_def)?;
190 runnable_fn(sema, function)
191 } else {
192 None
193 }
194}
195
196fn parent_test_module(sema: &Semantics<RootDatabase>, fn_def: &ast::Fn) -> Option<hir::Module> {
197 fn_def.syntax().ancestors().find_map(|node| {
198 let module = ast::Module::cast(node)?;
199 let module = sema.to_def(&module)?;
200
201 if has_test_function_or_multiple_test_submodules(sema, &module) {
202 Some(module)
203 } else {
204 None
205 }
206 })
207}
208
109fn runnables_mod(sema: &Semantics<RootDatabase>, acc: &mut Vec<Runnable>, module: hir::Module) { 209fn runnables_mod(sema: &Semantics<RootDatabase>, acc: &mut Vec<Runnable>, module: hir::Module) {
110 acc.extend(module.declarations(sema.db).into_iter().filter_map(|def| { 210 acc.extend(module.declarations(sema.db).into_iter().filter_map(|def| {
111 let runnable = match def { 211 let runnable = match def {
@@ -255,7 +355,7 @@ fn module_def_doctest(sema: &Semantics<RootDatabase>, def: hir::ModuleDef) -> Op
255 Some(res) 355 Some(res)
256} 356}
257 357
258#[derive(Debug, Copy, Clone)] 358#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
259pub struct TestAttr { 359pub struct TestAttr {
260 pub ignore: bool, 360 pub ignore: bool,
261} 361}
@@ -349,6 +449,12 @@ mod tests {
349 ); 449 );
350 } 450 }
351 451
452 fn check_tests(ra_fixture: &str, expect: Expect) {
453 let (analysis, position) = fixture::position(ra_fixture);
454 let tests = analysis.related_tests(position, None).unwrap();
455 expect.assert_debug_eq(&tests);
456 }
457
352 #[test] 458 #[test]
353 fn test_runnables() { 459 fn test_runnables() {
354 check( 460 check(
@@ -1074,4 +1180,224 @@ mod tests {
1074 "#]], 1180 "#]],
1075 ); 1181 );
1076 } 1182 }
1183
1184 #[test]
1185 fn find_no_tests() {
1186 check_tests(
1187 r#"
1188//- /lib.rs
1189fn foo$0() { };
1190"#,
1191 expect![[r#"
1192 []
1193 "#]],
1194 );
1195 }
1196
1197 #[test]
1198 fn find_direct_fn_test() {
1199 check_tests(
1200 r#"
1201//- /lib.rs
1202fn foo$0() { };
1203
1204mod tests {
1205 #[test]
1206 fn foo_test() {
1207 super::foo()
1208 }
1209}
1210"#,
1211 expect![[r#"
1212 [
1213 Runnable {
1214 nav: NavigationTarget {
1215 file_id: FileId(
1216 0,
1217 ),
1218 full_range: 31..85,
1219 focus_range: 46..54,
1220 name: "foo_test",
1221 kind: Function,
1222 },
1223 kind: Test {
1224 test_id: Path(
1225 "tests::foo_test",
1226 ),
1227 attr: TestAttr {
1228 ignore: false,
1229 },
1230 },
1231 cfg: None,
1232 },
1233 ]
1234 "#]],
1235 );
1236 }
1237
1238 #[test]
1239 fn find_direct_struct_test() {
1240 check_tests(
1241 r#"
1242//- /lib.rs
1243struct Fo$0o;
1244fn foo(arg: &Foo) { };
1245
1246mod tests {
1247 use super::*;
1248
1249 #[test]
1250 fn foo_test() {
1251 foo(Foo);
1252 }
1253}
1254"#,
1255 expect![[r#"
1256 [
1257 Runnable {
1258 nav: NavigationTarget {
1259 file_id: FileId(
1260 0,
1261 ),
1262 full_range: 71..122,
1263 focus_range: 86..94,
1264 name: "foo_test",
1265 kind: Function,
1266 },
1267 kind: Test {
1268 test_id: Path(
1269 "tests::foo_test",
1270 ),
1271 attr: TestAttr {
1272 ignore: false,
1273 },
1274 },
1275 cfg: None,
1276 },
1277 ]
1278 "#]],
1279 );
1280 }
1281
1282 #[test]
1283 fn find_indirect_fn_test() {
1284 check_tests(
1285 r#"
1286//- /lib.rs
1287fn foo$0() { };
1288
1289mod tests {
1290 use super::foo;
1291
1292 fn check1() {
1293 check2()
1294 }
1295
1296 fn check2() {
1297 foo()
1298 }
1299
1300 #[test]
1301 fn foo_test() {
1302 check1()
1303 }
1304}
1305"#,
1306 expect![[r#"
1307 [
1308 Runnable {
1309 nav: NavigationTarget {
1310 file_id: FileId(
1311 0,
1312 ),
1313 full_range: 133..183,
1314 focus_range: 148..156,
1315 name: "foo_test",
1316 kind: Function,
1317 },
1318 kind: Test {
1319 test_id: Path(
1320 "tests::foo_test",
1321 ),
1322 attr: TestAttr {
1323 ignore: false,
1324 },
1325 },
1326 cfg: None,
1327 },
1328 ]
1329 "#]],
1330 );
1331 }
1332
1333 #[test]
1334 fn tests_are_unique() {
1335 check_tests(
1336 r#"
1337//- /lib.rs
1338fn foo$0() { };
1339
1340mod tests {
1341 use super::foo;
1342
1343 #[test]
1344 fn foo_test() {
1345 foo();
1346 foo();
1347 }
1348
1349 #[test]
1350 fn foo2_test() {
1351 foo();
1352 foo();
1353 }
1354
1355}
1356"#,
1357 expect![[r#"
1358 [
1359 Runnable {
1360 nav: NavigationTarget {
1361 file_id: FileId(
1362 0,
1363 ),
1364 full_range: 52..115,
1365 focus_range: 67..75,
1366 name: "foo_test",
1367 kind: Function,
1368 },
1369 kind: Test {
1370 test_id: Path(
1371 "tests::foo_test",
1372 ),
1373 attr: TestAttr {
1374 ignore: false,
1375 },
1376 },
1377 cfg: None,
1378 },
1379 Runnable {
1380 nav: NavigationTarget {
1381 file_id: FileId(
1382 0,
1383 ),
1384 full_range: 121..185,
1385 focus_range: 136..145,
1386 name: "foo2_test",
1387 kind: Function,
1388 },
1389 kind: Test {
1390 test_id: Path(
1391 "tests::foo2_test",
1392 ),
1393 attr: TestAttr {
1394 ignore: false,
1395 },
1396 },
1397 cfg: None,
1398 },
1399 ]
1400 "#]],
1401 );
1402 }
1077} 1403}