4 use hir::{AsAssocItem, HasAttrs, HasSource, Semantics};
5 use ide_assists::utils::test_related_attribute;
6 use ide_db::{defs::Definition, RootDatabase, SymbolKind};
7 use itertools::Itertools;
9 ast::{self, AstNode, AttrsOwner},
10 match_ast, SyntaxNode,
14 display::{ToNav, TryToNav},
15 FileId, NavigationTarget,
18 #[derive(Debug, Clone)]
20 pub nav: NavigationTarget,
21 pub kind: RunnableKind,
22 pub cfg: Option<CfgExpr>,
25 #[derive(Debug, Clone)]
31 impl fmt::Display for TestId {
32 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
34 TestId::Name(name) => write!(f, "{}", name),
35 TestId::Path(path) => write!(f, "{}", path),
40 #[derive(Debug, Clone)]
41 pub enum RunnableKind {
42 Test { test_id: TestId, attr: TestAttr },
43 TestMod { path: String },
44 Bench { test_id: TestId },
45 DocTest { test_id: TestId },
49 #[derive(Debug, Eq, PartialEq)]
50 pub struct RunnableAction {
51 pub run_title: &'static str,
55 const TEST: RunnableAction = RunnableAction { run_title: "▶\u{fe0e} Run Test", debugee: true };
56 const DOCTEST: RunnableAction =
57 RunnableAction { run_title: "▶\u{fe0e} Run Doctest", debugee: false };
58 const BENCH: RunnableAction = RunnableAction { run_title: "▶\u{fe0e} Run Bench", debugee: true };
59 const BIN: RunnableAction = RunnableAction { run_title: "▶\u{fe0e} Run", debugee: true };
62 // test package::module::testname
63 pub fn label(&self, target: Option<String>) -> String {
65 RunnableKind::Test { test_id, .. } => format!("test {}", test_id),
66 RunnableKind::TestMod { path } => format!("test-mod {}", path),
67 RunnableKind::Bench { test_id } => format!("bench {}", test_id),
68 RunnableKind::DocTest { test_id, .. } => format!("doctest {}", test_id),
69 RunnableKind::Bin => {
70 target.map_or_else(|| "run binary".to_string(), |t| format!("run {}", t))
75 pub fn action(&self) -> &'static RunnableAction {
77 RunnableKind::Test { .. } | RunnableKind::TestMod { .. } => &TEST,
78 RunnableKind::DocTest { .. } => &DOCTEST,
79 RunnableKind::Bench { .. } => &BENCH,
80 RunnableKind::Bin => &BIN,
87 // Shows a popup suggesting to run a test/benchmark/binary **at the current cursor
88 // location**. Super useful for repeatedly running just a single test. Do bind this
92 // | Editor | Action Name
94 // | VS Code | **Rust Analyzer: Run**
96 pub(crate) fn runnables(db: &RootDatabase, file_id: FileId) -> Vec<Runnable> {
97 let sema = Semantics::new(db);
98 let module = match sema.to_module_def(file_id) {
99 None => return Vec::new(),
103 let mut res = Vec::new();
104 runnables_mod(&sema, &mut res, module);
108 fn runnables_mod(sema: &Semantics<RootDatabase>, acc: &mut Vec<Runnable>, module: hir::Module) {
109 acc.extend(module.declarations(sema.db).into_iter().filter_map(|def| {
110 let runnable = match def {
111 hir::ModuleDef::Module(it) => runnable_mod(&sema, it),
112 hir::ModuleDef::Function(it) => runnable_fn(&sema, it),
115 runnable.or_else(|| module_def_doctest(&sema, def))
118 acc.extend(module.impl_defs(sema.db).into_iter().flat_map(|it| it.items(sema.db)).filter_map(
120 hir::AssocItem::Function(it) => {
121 runnable_fn(&sema, it).or_else(|| module_def_doctest(&sema, it.into()))
123 hir::AssocItem::Const(it) => module_def_doctest(&sema, it.into()),
124 hir::AssocItem::TypeAlias(it) => module_def_doctest(&sema, it.into()),
128 for def in module.declarations(sema.db) {
129 if let hir::ModuleDef::Module(submodule) = def {
130 match submodule.definition_source(sema.db).value {
131 hir::ModuleSource::Module(_) => runnables_mod(sema, acc, submodule),
132 hir::ModuleSource::SourceFile(_) => {
133 cov_mark::hit!(dont_recurse_in_outline_submodules)
135 hir::ModuleSource::BlockExpr(_) => {} // inner items aren't runnable
141 pub(crate) fn runnable_fn(sema: &Semantics<RootDatabase>, def: hir::Function) -> Option<Runnable> {
142 let func = def.source(sema.db)?;
143 let name_string = def.name(sema.db).to_string();
145 let kind = if name_string == "main" {
148 let canonical_path = {
149 let def: hir::ModuleDef = def.into();
150 def.canonical_path(sema.db)
152 let test_id = canonical_path.map(TestId::Path).unwrap_or(TestId::Name(name_string));
154 if test_related_attribute(&func.value).is_some() {
155 let attr = TestAttr::from_fn(&func.value);
156 RunnableKind::Test { test_id, attr }
157 } else if func.value.has_atom_attr("bench") {
158 RunnableKind::Bench { test_id }
164 let nav = NavigationTarget::from_named(
166 func.as_ref().map(|it| it as &dyn ast::NameOwner),
167 SymbolKind::Function,
169 let cfg = def.attrs(sema.db).cfg();
170 Some(Runnable { nav, kind, cfg })
173 pub(crate) fn runnable_mod(sema: &Semantics<RootDatabase>, def: hir::Module) -> Option<Runnable> {
174 if !has_test_function_or_multiple_test_submodules(sema, &def) {
178 def.path_to_root(sema.db).into_iter().rev().filter_map(|it| it.name(sema.db)).join("::");
180 let attrs = def.attrs(sema.db);
181 let cfg = attrs.cfg();
182 let nav = def.to_nav(sema.db);
183 Some(Runnable { nav, kind: RunnableKind::TestMod { path }, cfg })
186 // FIXME: figure out a proper API here.
187 pub(crate) fn doc_owner_to_def(
188 sema: &Semantics<RootDatabase>,
190 ) -> Option<Definition> {
191 let res: hir::ModuleDef = match_ast! {
193 ast::SourceFile(_it) => sema.scope(&item).module()?.into(),
194 ast::Fn(it) => sema.to_def(&it)?.into(),
195 ast::Struct(it) => sema.to_def(&it)?.into(),
196 ast::Enum(it) => sema.to_def(&it)?.into(),
197 ast::Union(it) => sema.to_def(&it)?.into(),
198 ast::Trait(it) => sema.to_def(&it)?.into(),
199 ast::Const(it) => sema.to_def(&it)?.into(),
200 ast::Static(it) => sema.to_def(&it)?.into(),
201 ast::TypeAlias(it) => sema.to_def(&it)?.into(),
205 Some(Definition::ModuleDef(res))
208 fn module_def_doctest(sema: &Semantics<RootDatabase>, def: hir::ModuleDef) -> Option<Runnable> {
209 let attrs = match def {
210 hir::ModuleDef::Module(it) => it.attrs(sema.db),
211 hir::ModuleDef::Function(it) => it.attrs(sema.db),
212 hir::ModuleDef::Adt(it) => it.attrs(sema.db),
213 hir::ModuleDef::Variant(it) => it.attrs(sema.db),
214 hir::ModuleDef::Const(it) => it.attrs(sema.db),
215 hir::ModuleDef::Static(it) => it.attrs(sema.db),
216 hir::ModuleDef::Trait(it) => it.attrs(sema.db),
217 hir::ModuleDef::TypeAlias(it) => it.attrs(sema.db),
218 hir::ModuleDef::BuiltinType(_) => return None,
220 if !has_runnable_doc_test(&attrs) {
223 let def_name = def.name(sema.db).map(|it| it.to_string());
225 .canonical_path(sema.db)
226 // This probably belongs to canonical path?
228 let assoc_def = match def {
229 hir::ModuleDef::Function(it) => it.as_assoc_item(sema.db),
230 hir::ModuleDef::Const(it) => it.as_assoc_item(sema.db),
231 hir::ModuleDef::TypeAlias(it) => it.as_assoc_item(sema.db),
234 // FIXME: this also looks very wrong
235 if let Some(assoc_def) = assoc_def {
236 if let hir::AssocItemContainer::Impl(imp) = assoc_def.container(sema.db) {
237 if let Some(adt) = imp.target_ty(sema.db).as_adt() {
238 let name = adt.name(sema.db).to_string();
239 let idx = path.rfind(':').map_or(0, |idx| idx + 1);
240 let (prefix, suffix) = path.split_at(idx);
241 return format!("{}{}::{}", prefix, name, suffix);
248 .or_else(|| def_name.clone().map(TestId::Name))?;
250 let mut nav = def.try_to_nav(sema.db)?;
251 nav.focus_range = None;
252 nav.description = None;
255 let res = Runnable { nav, kind: RunnableKind::DocTest { test_id }, cfg: attrs.cfg() };
259 #[derive(Debug, Copy, Clone)]
260 pub struct TestAttr {
265 fn from_fn(fn_def: &ast::Fn) -> TestAttr {
268 .filter_map(|attr| attr.simple_name())
269 .any(|attribute_text| attribute_text == "ignore");
274 const RUSTDOC_FENCE: &str = "```";
275 const RUSTDOC_CODE_BLOCK_ATTRIBUTES_RUNNABLE: &[&str] =
276 &["", "rust", "should_panic", "edition2015", "edition2018", "edition2021"];
278 fn has_runnable_doc_test(attrs: &hir::Attrs) -> bool {
279 attrs.docs().map_or(false, |doc| {
280 let mut in_code_block = false;
282 for line in String::from(doc).lines() {
283 if let Some(header) = line.strip_prefix(RUSTDOC_FENCE) {
284 in_code_block = !in_code_block;
289 .all(|sub| RUSTDOC_CODE_BLOCK_ATTRIBUTES_RUNNABLE.contains(&sub.trim()))
300 // We could create runnables for modules with number_of_test_submodules > 0,
301 // but that bloats the runnables for no real benefit, since all tests can be run by the submodule already
302 fn has_test_function_or_multiple_test_submodules(
303 sema: &Semantics<RootDatabase>,
304 module: &hir::Module,
306 let mut number_of_test_submodules = 0;
308 for item in module.declarations(sema.db) {
310 hir::ModuleDef::Function(f) => {
311 if let Some(it) = f.source(sema.db) {
312 if test_related_attribute(&it.value).is_some() {
317 hir::ModuleDef::Module(submodule) => {
318 if has_test_function_or_multiple_test_submodules(sema, &submodule) {
319 number_of_test_submodules += 1;
326 number_of_test_submodules > 1
331 use expect_test::{expect, Expect};
339 // FIXME: fold this into `expect` as well
340 actions: &[&RunnableAction],
343 let (analysis, position) = fixture::position(ra_fixture);
344 let runnables = analysis.runnables(position.file_id).unwrap();
345 expect.assert_debug_eq(&runnables);
348 runnables.into_iter().map(|it| it.action()).collect::<Vec<_>>().as_slice()
353 fn test_runnables() {
370 &[&BIN, &TEST, &TEST, &BENCH],
374 nav: NavigationTarget {
387 nav: NavigationTarget {
407 nav: NavigationTarget {
427 nav: NavigationTarget {
449 fn test_runnables_doc_test() {
459 fn should_have_runnable() {}
464 fn should_have_runnable_1() {}
473 fn should_have_runnable_2() {}
478 fn should_have_no_runnable() {}
483 fn should_have_no_runnable_2() {}
488 fn should_have_no_runnable_3() {}
491 /// arbitrary plain text
493 fn should_have_no_runnable_4() {}
496 /// arbitrary plain text
502 fn should_have_no_runnable_5() {}
507 fn should_have_no_runnable_6() {}
512 struct StructWithRunnable(String);
515 &[&BIN, &DOCTEST, &DOCTEST, &DOCTEST, &DOCTEST],
519 nav: NavigationTarget {
532 nav: NavigationTarget {
537 name: "should_have_runnable",
541 "should_have_runnable",
547 nav: NavigationTarget {
552 name: "should_have_runnable_1",
556 "should_have_runnable_1",
562 nav: NavigationTarget {
566 full_range: 150..254,
567 name: "should_have_runnable_2",
571 "should_have_runnable_2",
577 nav: NavigationTarget {
581 full_range: 756..821,
582 name: "StructWithRunnable",
586 "StructWithRunnable",
597 fn test_runnables_doc_test_in_impl() {
616 nav: NavigationTarget {
629 nav: NavigationTarget {
649 fn test_runnables_module() {
663 nav: NavigationTarget {
678 nav: NavigationTarget {
689 "test_mod::test_foo1",
703 fn only_modules_with_test_functions_or_more_than_one_test_submodule_have_runners() {
712 fn nested_test_11() {}
715 fn nested_test_12() {}
720 fn nested_test_2() {}
723 mod nested_tests_3 {}
726 mod nested_tests_4 {}
729 &[&TEST, &TEST, &TEST, &TEST, &TEST, &TEST],
733 nav: NavigationTarget {
739 name: "nested_tests_0",
743 path: "root_tests::nested_tests_0",
748 nav: NavigationTarget {
754 name: "nested_tests_1",
758 path: "root_tests::nested_tests_0::nested_tests_1",
763 nav: NavigationTarget {
767 full_range: 202..286,
768 focus_range: 206..220,
769 name: "nested_tests_2",
773 path: "root_tests::nested_tests_0::nested_tests_2",
778 nav: NavigationTarget {
783 focus_range: 107..121,
784 name: "nested_test_11",
789 "root_tests::nested_tests_0::nested_tests_1::nested_test_11",
798 nav: NavigationTarget {
802 full_range: 140..182,
803 focus_range: 163..177,
804 name: "nested_test_12",
809 "root_tests::nested_tests_0::nested_tests_1::nested_test_12",
818 nav: NavigationTarget {
822 full_range: 235..276,
823 focus_range: 258..271,
824 name: "nested_test_2",
829 "root_tests::nested_tests_0::nested_tests_2::nested_test_2",
843 fn test_runnables_with_feature() {
846 //- /lib.rs crate:foo cfg:feature=foo
849 #[cfg(feature = "foo")]
856 nav: NavigationTarget {
888 fn test_runnables_with_features() {
891 //- /lib.rs crate:foo cfg:feature=foo,feature=bar
894 #[cfg(all(feature = "foo", feature = "bar"))]
901 nav: NavigationTarget {
943 fn test_runnables_no_test_function_in_module() {
960 fn test_doc_runnables_impl_mod() {
978 nav: NavigationTarget {
998 fn test_runnables_in_macro() {
1018 nav: NavigationTarget {
1022 full_range: 90..115,
1023 focus_range: 94..99,
1033 nav: NavigationTarget {
1037 full_range: 106..113,
1038 focus_range: 106..113,
1058 fn dont_recurse_in_outline_submodules() {
1059 cov_mark::check!(dont_recurse_in_outline_submodules);