From 5d23d8bc299b718e7f026a7e6c1363dde3342817 Mon Sep 17 00:00:00 2001 From: vsrs Date: Sat, 27 Feb 2021 17:59:52 +0300 Subject: Add runnables::related_tests --- crates/ide/src/lib.rs | 9 ++ crates/ide/src/runnables.rs | 338 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 341 insertions(+), 6 deletions(-) (limited to 'crates/ide/src') 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 { self.with_db(|db| runnables::runnables(db, file_id)) } + /// Returns the set of tests for the given file position. + pub fn related_tests( + &self, + position: FilePosition, + search_scope: Option, + ) -> Cancelable> { + self.with_db(|db| runnables::related_tests(db, position, search_scope)) + } + /// Computes syntax highlighting for the given file pub fn highlight(&self, file_id: FileId) -> Cancelable> { 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 @@ use std::fmt; +use ast::NameOwner; use cfg::CfgExpr; use hir::{AsAssocItem, HasAttrs, HasSource, Semantics}; use ide_assists::utils::test_related_attribute; -use ide_db::{defs::Definition, RootDatabase, SymbolKind}; +use ide_db::{ + base_db::FilePosition, defs::Definition, search::SearchScope, RootDatabase, SymbolKind, +}; use itertools::Itertools; +use rustc_hash::FxHashSet; use syntax::{ ast::{self, AstNode, AttrsOwner}, match_ast, SyntaxNode, @@ -13,17 +17,17 @@ use test_utils::mark; use crate::{ display::{ToNav, TryToNav}, - FileId, NavigationTarget, + references, FileId, NavigationTarget, }; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Hash, PartialEq, Eq)] pub struct Runnable { pub nav: NavigationTarget, pub kind: RunnableKind, pub cfg: Option, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Hash, PartialEq, Eq)] pub enum TestId { Name(String), Path(String), @@ -38,7 +42,7 @@ impl fmt::Display for TestId { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Hash, PartialEq, Eq)] pub enum RunnableKind { Test { test_id: TestId, attr: TestAttr }, TestMod { path: String }, @@ -106,6 +110,102 @@ pub(crate) fn runnables(db: &RootDatabase, file_id: FileId) -> Vec { res } +// Feature: Run Test +// +// Shows a popup suggesting to run a test in which the item **at the current cursor +// location** is used (if any). +// +// |=== +// | Editor | Action Name +// +// | VS Code | **Rust Analyzer: Run Test** +// |=== +pub(crate) fn related_tests( + db: &RootDatabase, + position: FilePosition, + search_scope: Option, +) -> Vec { + let sema = Semantics::new(db); + let mut res: FxHashSet = FxHashSet::default(); + + find_related_tests(&sema, position, search_scope, &mut res); + + res.into_iter().collect_vec() +} + +fn find_related_tests( + sema: &Semantics, + position: FilePosition, + search_scope: Option, + tests: &mut FxHashSet, +) { + if let Some(refs) = references::find_all_refs(&sema, position, search_scope) { + for (file_id, refs) in refs.references { + let file = sema.parse(file_id); + let file = file.syntax(); + let functions = refs.iter().filter_map(|(range, _)| { + let token = file.token_at_offset(range.start()).next()?; + let token = sema.descend_into_macros(token); + let syntax = token.parent(); + syntax.ancestors().find_map(ast::Fn::cast) + }); + + for fn_def in functions { + if let Some(runnable) = as_test_runnable(&sema, &fn_def) { + // direct test + tests.insert(runnable); + } else if let Some(module) = parent_test_module(&sema, &fn_def) { + // indirect test + find_related_tests_in_module(sema, &fn_def, &module, tests); + } + } + } + } +} + +fn find_related_tests_in_module( + sema: &Semantics, + fn_def: &ast::Fn, + parent_module: &hir::Module, + tests: &mut FxHashSet, +) { + if let Some(fn_name) = fn_def.name() { + let mod_source = parent_module.definition_source(sema.db); + let range = match mod_source.value { + hir::ModuleSource::Module(m) => m.syntax().text_range(), + hir::ModuleSource::BlockExpr(b) => b.syntax().text_range(), + hir::ModuleSource::SourceFile(f) => f.syntax().text_range(), + }; + + let file_id = mod_source.file_id.original_file(sema.db); + let mod_scope = SearchScope::file_part(file_id, range); + let fn_pos = FilePosition { file_id, offset: fn_name.syntax().text_range().start() }; + find_related_tests(sema, fn_pos, Some(mod_scope), tests) + } +} + +fn as_test_runnable(sema: &Semantics, fn_def: &ast::Fn) -> Option { + if test_related_attribute(&fn_def).is_some() { + let function = sema.to_def(fn_def)?; + runnable_fn(sema, function) + } else { + None + } +} + +fn parent_test_module(sema: &Semantics, fn_def: &ast::Fn) -> Option { + fn_def.syntax().ancestors().find_map(|node| { + let module = ast::Module::cast(node)?; + let module = sema.to_def(&module)?; + + if has_test_function_or_multiple_test_submodules(sema, &module) { + Some(module) + } else { + None + } + }) +} + fn runnables_mod(sema: &Semantics, acc: &mut Vec, module: hir::Module) { acc.extend(module.declarations(sema.db).into_iter().filter_map(|def| { let runnable = match def { @@ -255,7 +355,7 @@ fn module_def_doctest(sema: &Semantics, def: hir::ModuleDef) -> Op Some(res) } -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] pub struct TestAttr { pub ignore: bool, } @@ -349,6 +449,12 @@ mod tests { ); } + fn check_tests(ra_fixture: &str, expect: Expect) { + let (analysis, position) = fixture::position(ra_fixture); + let tests = analysis.related_tests(position, None).unwrap(); + expect.assert_debug_eq(&tests); + } + #[test] fn test_runnables() { check( @@ -1074,4 +1180,224 @@ mod tests { "#]], ); } + + #[test] + fn find_no_tests() { + check_tests( + r#" +//- /lib.rs +fn foo$0() { }; +"#, + expect![[r#" + [] + "#]], + ); + } + + #[test] + fn find_direct_fn_test() { + check_tests( + r#" +//- /lib.rs +fn foo$0() { }; + +mod tests { + #[test] + fn foo_test() { + super::foo() + } +} +"#, + expect![[r#" + [ + Runnable { + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 31..85, + focus_range: 46..54, + name: "foo_test", + kind: Function, + }, + kind: Test { + test_id: Path( + "tests::foo_test", + ), + attr: TestAttr { + ignore: false, + }, + }, + cfg: None, + }, + ] + "#]], + ); + } + + #[test] + fn find_direct_struct_test() { + check_tests( + r#" +//- /lib.rs +struct Fo$0o; +fn foo(arg: &Foo) { }; + +mod tests { + use super::*; + + #[test] + fn foo_test() { + foo(Foo); + } +} +"#, + expect![[r#" + [ + Runnable { + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 71..122, + focus_range: 86..94, + name: "foo_test", + kind: Function, + }, + kind: Test { + test_id: Path( + "tests::foo_test", + ), + attr: TestAttr { + ignore: false, + }, + }, + cfg: None, + }, + ] + "#]], + ); + } + + #[test] + fn find_indirect_fn_test() { + check_tests( + r#" +//- /lib.rs +fn foo$0() { }; + +mod tests { + use super::foo; + + fn check1() { + check2() + } + + fn check2() { + foo() + } + + #[test] + fn foo_test() { + check1() + } +} +"#, + expect![[r#" + [ + Runnable { + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 133..183, + focus_range: 148..156, + name: "foo_test", + kind: Function, + }, + kind: Test { + test_id: Path( + "tests::foo_test", + ), + attr: TestAttr { + ignore: false, + }, + }, + cfg: None, + }, + ] + "#]], + ); + } + + #[test] + fn tests_are_unique() { + check_tests( + r#" +//- /lib.rs +fn foo$0() { }; + +mod tests { + use super::foo; + + #[test] + fn foo_test() { + foo(); + foo(); + } + + #[test] + fn foo2_test() { + foo(); + foo(); + } + +} +"#, + expect![[r#" + [ + Runnable { + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 52..115, + focus_range: 67..75, + name: "foo_test", + kind: Function, + }, + kind: Test { + test_id: Path( + "tests::foo_test", + ), + attr: TestAttr { + ignore: false, + }, + }, + cfg: None, + }, + Runnable { + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 121..185, + focus_range: 136..145, + name: "foo2_test", + kind: Function, + }, + kind: Test { + test_id: Path( + "tests::foo2_test", + ), + attr: TestAttr { + ignore: false, + }, + }, + cfg: None, + }, + ] + "#]], + ); + } } -- cgit v1.2.3 From f234b80520ed63b168475be38086d053567f4c1e Mon Sep 17 00:00:00 2001 From: vsrs Date: Sat, 27 Feb 2021 21:57:58 +0300 Subject: Remove erroneous comment --- crates/ide/src/runnables.rs | 10 ---------- 1 file changed, 10 deletions(-) (limited to 'crates/ide/src') diff --git a/crates/ide/src/runnables.rs b/crates/ide/src/runnables.rs index ce3a2e7ba..7c92b7625 100644 --- a/crates/ide/src/runnables.rs +++ b/crates/ide/src/runnables.rs @@ -110,16 +110,6 @@ pub(crate) fn runnables(db: &RootDatabase, file_id: FileId) -> Vec { res } -// Feature: Run Test -// -// Shows a popup suggesting to run a test in which the item **at the current cursor -// location** is used (if any). -// -// |=== -// | Editor | Action Name -// -// | VS Code | **Rust Analyzer: Run Test** -// |=== pub(crate) fn related_tests( db: &RootDatabase, position: FilePosition, -- cgit v1.2.3 From daa2637486755f012c738d3516ff7cb3d3dcd234 Mon Sep 17 00:00:00 2001 From: vsrs Date: Thu, 11 Mar 2021 17:39:41 +0300 Subject: Apply review suggestions --- crates/ide/src/runnables.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) (limited to 'crates/ide/src') diff --git a/crates/ide/src/runnables.rs b/crates/ide/src/runnables.rs index 7c92b7625..d8cf66168 100644 --- a/crates/ide/src/runnables.rs +++ b/crates/ide/src/runnables.rs @@ -5,7 +5,10 @@ use cfg::CfgExpr; use hir::{AsAssocItem, HasAttrs, HasSource, Semantics}; use ide_assists::utils::test_related_attribute; use ide_db::{ - base_db::FilePosition, defs::Definition, search::SearchScope, RootDatabase, SymbolKind, + base_db::{FilePosition, FileRange}, + defs::Definition, + search::SearchScope, + RootDatabase, SymbolKind, }; use itertools::Itertools; use rustc_hash::FxHashSet; @@ -168,7 +171,7 @@ fn find_related_tests_in_module( }; let file_id = mod_source.file_id.original_file(sema.db); - let mod_scope = SearchScope::file_part(file_id, range); + let mod_scope = SearchScope::file_range(FileRange { file_id, range }); let fn_pos = FilePosition { file_id, offset: fn_name.syntax().text_range().start() }; find_related_tests(sema, fn_pos, Some(mod_scope), tests) } -- cgit v1.2.3 From 00e52e1f4275cdd597c40d28e508c9f59216894a Mon Sep 17 00:00:00 2001 From: vsrs Date: Thu, 11 Mar 2021 17:58:45 +0300 Subject: Add `Feature: Related Tests` comment --- crates/ide/src/runnables.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) (limited to 'crates/ide/src') diff --git a/crates/ide/src/runnables.rs b/crates/ide/src/runnables.rs index d8cf66168..368bf9aa5 100644 --- a/crates/ide/src/runnables.rs +++ b/crates/ide/src/runnables.rs @@ -113,6 +113,19 @@ pub(crate) fn runnables(db: &RootDatabase, file_id: FileId) -> Vec { res } +// Feature: Related Tests +// +// Provides a sneak peek of all tests where the current item is used. +// +// The simplest way to use this feature is via the context menu: +// - Right-click on the selected item. The context menu opens. +// - Select **Peek related tests** +// +// |=== +// | Editor | Action Name +// +// | VS Code | **Rust Analyzer: Peek related tests** +// |=== pub(crate) fn related_tests( db: &RootDatabase, position: FilePosition, -- cgit v1.2.3