4 use hir::{AsAssocItem, Attrs, HirFileId, InFile, Semantics};
5 use ide_db::RootDatabase;
6 use itertools::Itertools;
8 ast::{self, AstNode, AttrsOwner, DocCommentsOwner, ModuleItemOwner, NameOwner},
12 use crate::{display::ToNav, FileId, NavigationTarget};
14 #[derive(Debug, Clone)]
16 pub nav: NavigationTarget,
17 pub kind: RunnableKind,
18 pub cfg_exprs: Vec<CfgExpr>,
21 #[derive(Debug, Clone)]
27 impl fmt::Display for TestId {
28 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
30 TestId::Name(name) => write!(f, "{}", name),
31 TestId::Path(path) => write!(f, "{}", path),
36 #[derive(Debug, Clone)]
37 pub enum RunnableKind {
38 Test { test_id: TestId, attr: TestAttr },
39 TestMod { path: String },
40 Bench { test_id: TestId },
41 DocTest { test_id: TestId },
45 #[derive(Debug, Eq, PartialEq)]
46 pub struct RunnableAction {
47 pub run_title: &'static str,
51 const TEST: RunnableAction = RunnableAction { run_title: "▶\u{fe0e} Run Test", debugee: true };
52 const DOCTEST: RunnableAction =
53 RunnableAction { run_title: "▶\u{fe0e} Run Doctest", debugee: false };
54 const BENCH: RunnableAction = RunnableAction { run_title: "▶\u{fe0e} Run Bench", debugee: true };
55 const BIN: RunnableAction = RunnableAction { run_title: "▶\u{fe0e} Run", debugee: true };
58 // test package::module::testname
59 pub fn label(&self, target: Option<String>) -> String {
61 RunnableKind::Test { test_id, .. } => format!("test {}", test_id),
62 RunnableKind::TestMod { path } => format!("test-mod {}", path),
63 RunnableKind::Bench { test_id } => format!("bench {}", test_id),
64 RunnableKind::DocTest { test_id, .. } => format!("doctest {}", test_id),
65 RunnableKind::Bin => {
66 target.map_or_else(|| "run binary".to_string(), |t| format!("run {}", t))
71 pub fn action(&self) -> &'static RunnableAction {
73 RunnableKind::Test { .. } | RunnableKind::TestMod { .. } => &TEST,
74 RunnableKind::DocTest { .. } => &DOCTEST,
75 RunnableKind::Bench { .. } => &BENCH,
76 RunnableKind::Bin => &BIN,
83 // Shows a popup suggesting to run a test/benchmark/binary **at the current cursor
84 // location**. Super useful for repeatedly running just a single test. Do bind this
88 // | Editor | Action Name
90 // | VS Code | **Rust Analyzer: Run**
92 pub(crate) fn runnables(db: &RootDatabase, file_id: FileId) -> Vec<Runnable> {
93 let sema = Semantics::new(db);
94 let source_file = sema.parse(file_id);
95 source_file.syntax().descendants().filter_map(|i| runnable(&sema, i, file_id)).collect()
98 pub(crate) fn runnable(
99 sema: &Semantics<RootDatabase>,
102 ) -> Option<Runnable> {
105 ast::Fn(it) => runnable_fn(sema, it, file_id),
106 ast::Module(it) => runnable_mod(sema, it, file_id),
113 sema: &Semantics<RootDatabase>,
116 ) -> Option<Runnable> {
117 let name_string = fn_def.name()?.text().to_string();
119 let kind = if name_string == "main" {
122 let test_id = match sema.to_def(&fn_def).map(|def| def.module(sema.db)) {
124 let def = sema.to_def(&fn_def)?;
125 let impl_trait_name = def.as_assoc_item(sema.db).and_then(|assoc_item| {
126 match assoc_item.container(sema.db) {
127 hir::AssocItemContainer::Trait(trait_item) => {
128 Some(trait_item.name(sema.db).to_string())
130 hir::AssocItemContainer::ImplDef(impl_def) => impl_def
133 .map(|adt| adt.name(sema.db).to_string()),
137 let path_iter = module
138 .path_to_root(sema.db)
141 .filter_map(|it| it.name(sema.db))
142 .map(|name| name.to_string());
144 let path = if let Some(impl_trait_name) = impl_trait_name {
146 .chain(std::iter::once(impl_trait_name))
147 .chain(std::iter::once(name_string))
150 path_iter.chain(std::iter::once(name_string)).join("::")
155 None => TestId::Name(name_string),
158 if has_test_related_attribute(&fn_def) {
159 let attr = TestAttr::from_fn(&fn_def);
160 RunnableKind::Test { test_id, attr }
161 } else if fn_def.has_atom_attr("bench") {
162 RunnableKind::Bench { test_id }
163 } else if has_runnable_doc_test(&fn_def) {
164 RunnableKind::DocTest { test_id }
170 let attrs = Attrs::from_attrs_owner(sema.db, InFile::new(HirFileId::from(file_id), &fn_def));
171 let cfg_exprs = attrs.cfg().collect();
173 let nav = if let RunnableKind::DocTest { .. } = kind {
174 NavigationTarget::from_doc_commented(
176 InFile::new(file_id.into(), &fn_def),
177 InFile::new(file_id.into(), &fn_def),
180 NavigationTarget::from_named(sema.db, InFile::new(file_id.into(), &fn_def))
182 Some(Runnable { nav, kind, cfg_exprs })
185 #[derive(Debug, Copy, Clone)]
186 pub struct TestAttr {
191 fn from_fn(fn_def: &ast::Fn) -> TestAttr {
194 .filter_map(|attr| attr.simple_name())
195 .any(|attribute_text| attribute_text == "ignore");
200 /// This is a method with a heuristics to support test methods annotated with custom test annotations, such as
201 /// `#[test_case(...)]`, `#[tokio::test]` and similar.
202 /// Also a regular `#[test]` annotation is supported.
204 /// It may produce false positives, for example, `#[wasm_bindgen_test]` requires a different command to run the test,
205 /// but it's better than not to have the runnables for the tests at all.
206 pub(crate) fn has_test_related_attribute(fn_def: &ast::Fn) -> bool {
209 .filter_map(|attr| attr.path())
210 .map(|path| path.syntax().to_string().to_lowercase())
211 .any(|attribute_text| attribute_text.contains("test"))
214 const RUSTDOC_FENCE: &str = "```";
215 const RUSTDOC_CODE_BLOCK_ATTRIBUTES_RUNNABLE: &[&str] =
216 &["", "rust", "should_panic", "edition2015", "edition2018"];
218 fn has_runnable_doc_test(fn_def: &ast::Fn) -> bool {
219 fn_def.doc_comment_text().map_or(false, |comments_text| {
220 let mut in_code_block = false;
222 for line in comments_text.lines() {
223 if let Some(header) = line.strip_prefix(RUSTDOC_FENCE) {
224 in_code_block = !in_code_block;
229 .all(|sub| RUSTDOC_CODE_BLOCK_ATTRIBUTES_RUNNABLE.contains(&sub.trim()))
241 sema: &Semantics<RootDatabase>,
244 ) -> Option<Runnable> {
245 if !has_test_function_or_multiple_test_submodules(&module) {
248 let module_def = sema.to_def(&module)?;
250 let path = module_def
251 .path_to_root(sema.db)
254 .filter_map(|it| it.name(sema.db))
257 let attrs = Attrs::from_attrs_owner(sema.db, InFile::new(HirFileId::from(file_id), &module));
258 let cfg_exprs = attrs.cfg().collect();
259 let nav = module_def.to_nav(sema.db);
260 Some(Runnable { nav, kind: RunnableKind::TestMod { path }, cfg_exprs })
263 // We could create runnables for modules with number_of_test_submodules > 0,
264 // but that bloats the runnables for no real benefit, since all tests can be run by the submodule already
265 fn has_test_function_or_multiple_test_submodules(module: &ast::Module) -> bool {
266 if let Some(item_list) = module.item_list() {
267 let mut number_of_test_submodules = 0;
269 for item in item_list.items() {
271 ast::Item::Fn(f) => {
272 if has_test_related_attribute(&f) {
276 ast::Item::Module(submodule) => {
277 if has_test_function_or_multiple_test_submodules(&submodule) {
278 number_of_test_submodules += 1;
285 number_of_test_submodules > 1
293 use expect_test::{expect, Expect};
297 use super::{RunnableAction, BENCH, BIN, DOCTEST, TEST};
301 // FIXME: fold this into `expect` as well
302 actions: &[&RunnableAction],
305 let (analysis, position) = fixture::position(ra_fixture);
306 let runnables = analysis.runnables(position.file_id).unwrap();
307 expect.assert_debug_eq(&runnables);
310 runnables.into_iter().map(|it| it.action()).collect::<Vec<_>>().as_slice()
315 fn test_runnables() {
332 &[&BIN, &TEST, &TEST, &BENCH],
336 nav: NavigationTarget {
346 container_name: None,
354 nav: NavigationTarget {
364 container_name: None,
379 nav: NavigationTarget {
389 container_name: None,
404 nav: NavigationTarget {
414 container_name: None,
431 fn test_runnables_doc_test() {
441 fn should_have_runnable() {}
446 fn should_have_runnable_1() {}
455 fn should_have_runnable_2() {}
460 fn should_have_no_runnable() {}
465 fn should_have_no_runnable_2() {}
470 fn should_have_no_runnable_3() {}
473 /// arbitrary plain text
475 fn should_have_no_runnable_4() {}
478 /// arbitrary plain text
484 fn should_have_no_runnable_5() {}
489 fn should_have_no_runnable_6() {}
491 &[&BIN, &DOCTEST, &DOCTEST, &DOCTEST],
495 nav: NavigationTarget {
505 container_name: None,
513 nav: NavigationTarget {
519 name: "should_have_runnable",
521 container_name: None,
527 "should_have_runnable",
533 nav: NavigationTarget {
539 name: "should_have_runnable_1",
541 container_name: None,
547 "should_have_runnable_1",
553 nav: NavigationTarget {
557 full_range: 150..254,
559 name: "should_have_runnable_2",
561 container_name: None,
567 "should_have_runnable_2",
578 fn test_runnables_doc_test_in_impl() {
597 nav: NavigationTarget {
607 container_name: None,
615 nav: NavigationTarget {
623 container_name: None,
640 fn test_runnables_module() {
654 nav: NavigationTarget {
664 container_name: None,
674 nav: NavigationTarget {
684 container_name: None,
690 "test_mod::test_foo1",
704 fn only_modules_with_test_functions_or_more_than_one_test_submodule_have_runners() {
713 fn nested_test_11() {}
716 fn nested_test_12() {}
721 fn nested_test_2() {}
724 mod nested_tests_3 {}
727 mod nested_tests_4 {}
730 &[&TEST, &TEST, &TEST, &TEST, &TEST, &TEST],
734 nav: NavigationTarget {
742 name: "nested_tests_0",
744 container_name: None,
749 path: "root_tests::nested_tests_0",
754 nav: NavigationTarget {
762 name: "nested_tests_1",
764 container_name: None,
769 path: "root_tests::nested_tests_0::nested_tests_1",
774 nav: NavigationTarget {
782 name: "nested_test_11",
784 container_name: None,
790 "root_tests::nested_tests_0::nested_tests_1::nested_test_11",
799 nav: NavigationTarget {
803 full_range: 140..182,
807 name: "nested_test_12",
809 container_name: None,
815 "root_tests::nested_tests_0::nested_tests_1::nested_test_12",
824 nav: NavigationTarget {
828 full_range: 202..286,
832 name: "nested_tests_2",
834 container_name: None,
839 path: "root_tests::nested_tests_0::nested_tests_2",
844 nav: NavigationTarget {
848 full_range: 235..276,
852 name: "nested_test_2",
854 container_name: None,
860 "root_tests::nested_tests_0::nested_tests_2::nested_test_2",
874 fn test_runnables_with_feature() {
877 //- /lib.rs crate:foo cfg:feature=foo
880 #[cfg(feature = "foo")]
887 nav: NavigationTarget {
897 container_name: None,
922 fn test_runnables_with_features() {
925 //- /lib.rs crate:foo cfg:feature=foo,feature=bar
928 #[cfg(all(feature = "foo", feature = "bar"))]
935 nav: NavigationTarget {
945 container_name: None,
978 fn test_runnables_no_test_function_in_module() {