aboutsummaryrefslogtreecommitdiff
path: root/crates/ide/src/runnables.rs
diff options
context:
space:
mode:
authorbors[bot] <26634292+bors[bot]@users.noreply.github.com>2020-08-13 16:59:50 +0100
committerGitHub <[email protected]>2020-08-13 16:59:50 +0100
commit018a6cac072767dfd630c22e6d9ce134b7bb09af (patch)
tree4293492e643f9a604c5f30e051289bcea182694c /crates/ide/src/runnables.rs
parent00fb411f3edea72a1a9739f7df6f21cca045730b (diff)
parent6bc2633c90cedad057c5201d1ab7f67b57247004 (diff)
Merge #5750
5750: Rename ra_ide -> ide r=matklad a=matklad bors r+ 🤖 Co-authored-by: Aleksey Kladov <[email protected]>
Diffstat (limited to 'crates/ide/src/runnables.rs')
-rw-r--r--crates/ide/src/runnables.rs883
1 files changed, 883 insertions, 0 deletions
diff --git a/crates/ide/src/runnables.rs b/crates/ide/src/runnables.rs
new file mode 100644
index 000000000..c3e07c8de
--- /dev/null
+++ b/crates/ide/src/runnables.rs
@@ -0,0 +1,883 @@
1use std::fmt;
2
3use cfg::CfgExpr;
4use hir::{AsAssocItem, Attrs, HirFileId, InFile, Semantics};
5use ide_db::RootDatabase;
6use itertools::Itertools;
7use syntax::{
8 ast::{self, AstNode, AttrsOwner, DocCommentsOwner, ModuleItemOwner, NameOwner},
9 match_ast, SyntaxNode,
10};
11
12use crate::{display::ToNav, FileId, NavigationTarget};
13
14#[derive(Debug, Clone)]
15pub struct Runnable {
16 pub nav: NavigationTarget,
17 pub kind: RunnableKind,
18 pub cfg_exprs: Vec<CfgExpr>,
19}
20
21#[derive(Debug, Clone)]
22pub enum TestId {
23 Name(String),
24 Path(String),
25}
26
27impl fmt::Display for TestId {
28 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
29 match self {
30 TestId::Name(name) => write!(f, "{}", name),
31 TestId::Path(path) => write!(f, "{}", path),
32 }
33 }
34}
35
36#[derive(Debug, Clone)]
37pub enum RunnableKind {
38 Test { test_id: TestId, attr: TestAttr },
39 TestMod { path: String },
40 Bench { test_id: TestId },
41 DocTest { test_id: TestId },
42 Bin,
43}
44
45#[derive(Debug, Eq, PartialEq)]
46pub struct RunnableAction {
47 pub run_title: &'static str,
48 pub debugee: bool,
49}
50
51const TEST: RunnableAction = RunnableAction { run_title: "â–¶\u{fe0e} Run Test", debugee: true };
52const DOCTEST: RunnableAction =
53 RunnableAction { run_title: "â–¶\u{fe0e} Run Doctest", debugee: false };
54const BENCH: RunnableAction = RunnableAction { run_title: "â–¶\u{fe0e} Run Bench", debugee: true };
55const BIN: RunnableAction = RunnableAction { run_title: "â–¶\u{fe0e} Run", debugee: true };
56
57impl Runnable {
58 // test package::module::testname
59 pub fn label(&self, target: Option<String>) -> String {
60 match &self.kind {
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))
67 }
68 }
69 }
70
71 pub fn action(&self) -> &'static RunnableAction {
72 match &self.kind {
73 RunnableKind::Test { .. } | RunnableKind::TestMod { .. } => &TEST,
74 RunnableKind::DocTest { .. } => &DOCTEST,
75 RunnableKind::Bench { .. } => &BENCH,
76 RunnableKind::Bin => &BIN,
77 }
78 }
79}
80
81// Feature: Run
82//
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
85// to a shortcut!
86//
87// |===
88// | Editor | Action Name
89//
90// | VS Code | **Rust Analyzer: Run**
91// |===
92pub(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()
96}
97
98pub(crate) fn runnable(
99 sema: &Semantics<RootDatabase>,
100 item: SyntaxNode,
101 file_id: FileId,
102) -> Option<Runnable> {
103 match_ast! {
104 match item {
105 ast::Fn(it) => runnable_fn(sema, it, file_id),
106 ast::Module(it) => runnable_mod(sema, it, file_id),
107 _ => None,
108 }
109 }
110}
111
112fn runnable_fn(
113 sema: &Semantics<RootDatabase>,
114 fn_def: ast::Fn,
115 file_id: FileId,
116) -> Option<Runnable> {
117 let name_string = fn_def.name()?.text().to_string();
118
119 let kind = if name_string == "main" {
120 RunnableKind::Bin
121 } else {
122 let test_id = match sema.to_def(&fn_def).map(|def| def.module(sema.db)) {
123 Some(module) => {
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())
129 }
130 hir::AssocItemContainer::ImplDef(impl_def) => impl_def
131 .target_ty(sema.db)
132 .as_adt()
133 .map(|adt| adt.name(sema.db).to_string()),
134 }
135 });
136
137 let path_iter = module
138 .path_to_root(sema.db)
139 .into_iter()
140 .rev()
141 .filter_map(|it| it.name(sema.db))
142 .map(|name| name.to_string());
143
144 let path = if let Some(impl_trait_name) = impl_trait_name {
145 path_iter
146 .chain(std::iter::once(impl_trait_name))
147 .chain(std::iter::once(name_string))
148 .join("::")
149 } else {
150 path_iter.chain(std::iter::once(name_string)).join("::")
151 };
152
153 TestId::Path(path)
154 }
155 None => TestId::Name(name_string),
156 };
157
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_doc_test(&fn_def) {
164 RunnableKind::DocTest { test_id }
165 } else {
166 return None;
167 }
168 };
169
170 let attrs = Attrs::from_attrs_owner(sema.db, InFile::new(HirFileId::from(file_id), &fn_def));
171 let cfg_exprs = attrs.cfg().collect();
172
173 let nav = if let RunnableKind::DocTest { .. } = kind {
174 NavigationTarget::from_doc_commented(
175 sema.db,
176 InFile::new(file_id.into(), &fn_def),
177 InFile::new(file_id.into(), &fn_def),
178 )
179 } else {
180 NavigationTarget::from_named(sema.db, InFile::new(file_id.into(), &fn_def))
181 };
182 Some(Runnable { nav, kind, cfg_exprs })
183}
184
185#[derive(Debug, Copy, Clone)]
186pub struct TestAttr {
187 pub ignore: bool,
188}
189
190impl TestAttr {
191 fn from_fn(fn_def: &ast::Fn) -> TestAttr {
192 let ignore = fn_def
193 .attrs()
194 .filter_map(|attr| attr.simple_name())
195 .any(|attribute_text| attribute_text == "ignore");
196 TestAttr { ignore }
197 }
198}
199
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.
203///
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.
206fn has_test_related_attribute(fn_def: &ast::Fn) -> bool {
207 fn_def
208 .attrs()
209 .filter_map(|attr| attr.path())
210 .map(|path| path.syntax().to_string().to_lowercase())
211 .any(|attribute_text| attribute_text.contains("test"))
212}
213
214fn has_doc_test(fn_def: &ast::Fn) -> bool {
215 fn_def.doc_comment_text().map_or(false, |comment| comment.contains("```"))
216}
217
218fn runnable_mod(
219 sema: &Semantics<RootDatabase>,
220 module: ast::Module,
221 file_id: FileId,
222) -> Option<Runnable> {
223 if !has_test_function_or_multiple_test_submodules(&module) {
224 return None;
225 }
226 let module_def = sema.to_def(&module)?;
227
228 let path = module_def
229 .path_to_root(sema.db)
230 .into_iter()
231 .rev()
232 .filter_map(|it| it.name(sema.db))
233 .join("::");
234
235 let attrs = Attrs::from_attrs_owner(sema.db, InFile::new(HirFileId::from(file_id), &module));
236 let cfg_exprs = attrs.cfg().collect();
237 let nav = module_def.to_nav(sema.db);
238 Some(Runnable { nav, kind: RunnableKind::TestMod { path }, cfg_exprs })
239}
240
241// We could create runnables for modules with number_of_test_submodules > 0,
242// but that bloats the runnables for no real benefit, since all tests can be run by the submodule already
243fn has_test_function_or_multiple_test_submodules(module: &ast::Module) -> bool {
244 if let Some(item_list) = module.item_list() {
245 let mut number_of_test_submodules = 0;
246
247 for item in item_list.items() {
248 match item {
249 ast::Item::Fn(f) => {
250 if has_test_related_attribute(&f) {
251 return true;
252 }
253 }
254 ast::Item::Module(submodule) => {
255 if has_test_function_or_multiple_test_submodules(&submodule) {
256 number_of_test_submodules += 1;
257 }
258 }
259 _ => (),
260 }
261 }
262
263 number_of_test_submodules > 1
264 } else {
265 false
266 }
267}
268
269#[cfg(test)]
270mod tests {
271 use expect::{expect, Expect};
272
273 use crate::mock_analysis::analysis_and_position;
274
275 use super::{RunnableAction, BENCH, BIN, DOCTEST, TEST};
276
277 fn check(
278 ra_fixture: &str,
279 // FIXME: fold this into `expect` as well
280 actions: &[&RunnableAction],
281 expect: Expect,
282 ) {
283 let (analysis, position) = analysis_and_position(ra_fixture);
284 let runnables = analysis.runnables(position.file_id).unwrap();
285 expect.assert_debug_eq(&runnables);
286 assert_eq!(
287 actions,
288 runnables.into_iter().map(|it| it.action()).collect::<Vec<_>>().as_slice()
289 );
290 }
291
292 #[test]
293 fn test_runnables() {
294 check(
295 r#"
296//- /lib.rs
297<|>
298fn main() {}
299
300#[test]
301fn test_foo() {}
302
303#[test]
304#[ignore]
305fn test_foo() {}
306
307#[bench]
308fn bench() {}
309"#,
310 &[&BIN, &TEST, &TEST, &BENCH],
311 expect![[r#"
312 [
313 Runnable {
314 nav: NavigationTarget {
315 file_id: FileId(
316 1,
317 ),
318 full_range: 1..13,
319 focus_range: Some(
320 4..8,
321 ),
322 name: "main",
323 kind: FN,
324 container_name: None,
325 description: None,
326 docs: None,
327 },
328 kind: Bin,
329 cfg_exprs: [],
330 },
331 Runnable {
332 nav: NavigationTarget {
333 file_id: FileId(
334 1,
335 ),
336 full_range: 15..39,
337 focus_range: Some(
338 26..34,
339 ),
340 name: "test_foo",
341 kind: FN,
342 container_name: None,
343 description: None,
344 docs: None,
345 },
346 kind: Test {
347 test_id: Path(
348 "test_foo",
349 ),
350 attr: TestAttr {
351 ignore: false,
352 },
353 },
354 cfg_exprs: [],
355 },
356 Runnable {
357 nav: NavigationTarget {
358 file_id: FileId(
359 1,
360 ),
361 full_range: 41..75,
362 focus_range: Some(
363 62..70,
364 ),
365 name: "test_foo",
366 kind: FN,
367 container_name: None,
368 description: None,
369 docs: None,
370 },
371 kind: Test {
372 test_id: Path(
373 "test_foo",
374 ),
375 attr: TestAttr {
376 ignore: true,
377 },
378 },
379 cfg_exprs: [],
380 },
381 Runnable {
382 nav: NavigationTarget {
383 file_id: FileId(
384 1,
385 ),
386 full_range: 77..99,
387 focus_range: Some(
388 89..94,
389 ),
390 name: "bench",
391 kind: FN,
392 container_name: None,
393 description: None,
394 docs: None,
395 },
396 kind: Bench {
397 test_id: Path(
398 "bench",
399 ),
400 },
401 cfg_exprs: [],
402 },
403 ]
404 "#]],
405 );
406 }
407
408 #[test]
409 fn test_runnables_doc_test() {
410 check(
411 r#"
412//- /lib.rs
413<|>
414fn main() {}
415
416/// ```
417/// let x = 5;
418/// ```
419fn foo() {}
420"#,
421 &[&BIN, &DOCTEST],
422 expect![[r#"
423 [
424 Runnable {
425 nav: NavigationTarget {
426 file_id: FileId(
427 1,
428 ),
429 full_range: 1..13,
430 focus_range: Some(
431 4..8,
432 ),
433 name: "main",
434 kind: FN,
435 container_name: None,
436 description: None,
437 docs: None,
438 },
439 kind: Bin,
440 cfg_exprs: [],
441 },
442 Runnable {
443 nav: NavigationTarget {
444 file_id: FileId(
445 1,
446 ),
447 full_range: 15..57,
448 focus_range: None,
449 name: "foo",
450 kind: FN,
451 container_name: None,
452 description: None,
453 docs: None,
454 },
455 kind: DocTest {
456 test_id: Path(
457 "foo",
458 ),
459 },
460 cfg_exprs: [],
461 },
462 ]
463 "#]],
464 );
465 }
466
467 #[test]
468 fn test_runnables_doc_test_in_impl() {
469 check(
470 r#"
471//- /lib.rs
472<|>
473fn main() {}
474
475struct Data;
476impl Data {
477 /// ```
478 /// let x = 5;
479 /// ```
480 fn foo() {}
481}
482"#,
483 &[&BIN, &DOCTEST],
484 expect![[r#"
485 [
486 Runnable {
487 nav: NavigationTarget {
488 file_id: FileId(
489 1,
490 ),
491 full_range: 1..13,
492 focus_range: Some(
493 4..8,
494 ),
495 name: "main",
496 kind: FN,
497 container_name: None,
498 description: None,
499 docs: None,
500 },
501 kind: Bin,
502 cfg_exprs: [],
503 },
504 Runnable {
505 nav: NavigationTarget {
506 file_id: FileId(
507 1,
508 ),
509 full_range: 44..98,
510 focus_range: None,
511 name: "foo",
512 kind: FN,
513 container_name: None,
514 description: None,
515 docs: None,
516 },
517 kind: DocTest {
518 test_id: Path(
519 "Data::foo",
520 ),
521 },
522 cfg_exprs: [],
523 },
524 ]
525 "#]],
526 );
527 }
528
529 #[test]
530 fn test_runnables_module() {
531 check(
532 r#"
533//- /lib.rs
534<|>
535mod test_mod {
536 #[test]
537 fn test_foo1() {}
538}
539"#,
540 &[&TEST, &TEST],
541 expect![[r#"
542 [
543 Runnable {
544 nav: NavigationTarget {
545 file_id: FileId(
546 1,
547 ),
548 full_range: 1..51,
549 focus_range: Some(
550 5..13,
551 ),
552 name: "test_mod",
553 kind: MODULE,
554 container_name: None,
555 description: None,
556 docs: None,
557 },
558 kind: TestMod {
559 path: "test_mod",
560 },
561 cfg_exprs: [],
562 },
563 Runnable {
564 nav: NavigationTarget {
565 file_id: FileId(
566 1,
567 ),
568 full_range: 20..49,
569 focus_range: Some(
570 35..44,
571 ),
572 name: "test_foo1",
573 kind: FN,
574 container_name: None,
575 description: None,
576 docs: None,
577 },
578 kind: Test {
579 test_id: Path(
580 "test_mod::test_foo1",
581 ),
582 attr: TestAttr {
583 ignore: false,
584 },
585 },
586 cfg_exprs: [],
587 },
588 ]
589 "#]],
590 );
591 }
592
593 #[test]
594 fn only_modules_with_test_functions_or_more_than_one_test_submodule_have_runners() {
595 check(
596 r#"
597//- /lib.rs
598<|>
599mod root_tests {
600 mod nested_tests_0 {
601 mod nested_tests_1 {
602 #[test]
603 fn nested_test_11() {}
604
605 #[test]
606 fn nested_test_12() {}
607 }
608
609 mod nested_tests_2 {
610 #[test]
611 fn nested_test_2() {}
612 }
613
614 mod nested_tests_3 {}
615 }
616
617 mod nested_tests_4 {}
618}
619"#,
620 &[&TEST, &TEST, &TEST, &TEST, &TEST, &TEST],
621 expect![[r#"
622 [
623 Runnable {
624 nav: NavigationTarget {
625 file_id: FileId(
626 1,
627 ),
628 full_range: 22..323,
629 focus_range: Some(
630 26..40,
631 ),
632 name: "nested_tests_0",
633 kind: MODULE,
634 container_name: None,
635 description: None,
636 docs: None,
637 },
638 kind: TestMod {
639 path: "root_tests::nested_tests_0",
640 },
641 cfg_exprs: [],
642 },
643 Runnable {
644 nav: NavigationTarget {
645 file_id: FileId(
646 1,
647 ),
648 full_range: 51..192,
649 focus_range: Some(
650 55..69,
651 ),
652 name: "nested_tests_1",
653 kind: MODULE,
654 container_name: None,
655 description: None,
656 docs: None,
657 },
658 kind: TestMod {
659 path: "root_tests::nested_tests_0::nested_tests_1",
660 },
661 cfg_exprs: [],
662 },
663 Runnable {
664 nav: NavigationTarget {
665 file_id: FileId(
666 1,
667 ),
668 full_range: 84..126,
669 focus_range: Some(
670 107..121,
671 ),
672 name: "nested_test_11",
673 kind: FN,
674 container_name: None,
675 description: None,
676 docs: None,
677 },
678 kind: Test {
679 test_id: Path(
680 "root_tests::nested_tests_0::nested_tests_1::nested_test_11",
681 ),
682 attr: TestAttr {
683 ignore: false,
684 },
685 },
686 cfg_exprs: [],
687 },
688 Runnable {
689 nav: NavigationTarget {
690 file_id: FileId(
691 1,
692 ),
693 full_range: 140..182,
694 focus_range: Some(
695 163..177,
696 ),
697 name: "nested_test_12",
698 kind: FN,
699 container_name: None,
700 description: None,
701 docs: None,
702 },
703 kind: Test {
704 test_id: Path(
705 "root_tests::nested_tests_0::nested_tests_1::nested_test_12",
706 ),
707 attr: TestAttr {
708 ignore: false,
709 },
710 },
711 cfg_exprs: [],
712 },
713 Runnable {
714 nav: NavigationTarget {
715 file_id: FileId(
716 1,
717 ),
718 full_range: 202..286,
719 focus_range: Some(
720 206..220,
721 ),
722 name: "nested_tests_2",
723 kind: MODULE,
724 container_name: None,
725 description: None,
726 docs: None,
727 },
728 kind: TestMod {
729 path: "root_tests::nested_tests_0::nested_tests_2",
730 },
731 cfg_exprs: [],
732 },
733 Runnable {
734 nav: NavigationTarget {
735 file_id: FileId(
736 1,
737 ),
738 full_range: 235..276,
739 focus_range: Some(
740 258..271,
741 ),
742 name: "nested_test_2",
743 kind: FN,
744 container_name: None,
745 description: None,
746 docs: None,
747 },
748 kind: Test {
749 test_id: Path(
750 "root_tests::nested_tests_0::nested_tests_2::nested_test_2",
751 ),
752 attr: TestAttr {
753 ignore: false,
754 },
755 },
756 cfg_exprs: [],
757 },
758 ]
759 "#]],
760 );
761 }
762
763 #[test]
764 fn test_runnables_with_feature() {
765 check(
766 r#"
767//- /lib.rs crate:foo cfg:feature=foo
768<|>
769#[test]
770#[cfg(feature = "foo")]
771fn test_foo1() {}
772"#,
773 &[&TEST],
774 expect![[r#"
775 [
776 Runnable {
777 nav: NavigationTarget {
778 file_id: FileId(
779 1,
780 ),
781 full_range: 1..50,
782 focus_range: Some(
783 36..45,
784 ),
785 name: "test_foo1",
786 kind: FN,
787 container_name: None,
788 description: None,
789 docs: None,
790 },
791 kind: Test {
792 test_id: Path(
793 "test_foo1",
794 ),
795 attr: TestAttr {
796 ignore: false,
797 },
798 },
799 cfg_exprs: [
800 KeyValue {
801 key: "feature",
802 value: "foo",
803 },
804 ],
805 },
806 ]
807 "#]],
808 );
809 }
810
811 #[test]
812 fn test_runnables_with_features() {
813 check(
814 r#"
815//- /lib.rs crate:foo cfg:feature=foo,feature=bar
816<|>
817#[test]
818#[cfg(all(feature = "foo", feature = "bar"))]
819fn test_foo1() {}
820"#,
821 &[&TEST],
822 expect![[r#"
823 [
824 Runnable {
825 nav: NavigationTarget {
826 file_id: FileId(
827 1,
828 ),
829 full_range: 1..72,
830 focus_range: Some(
831 58..67,
832 ),
833 name: "test_foo1",
834 kind: FN,
835 container_name: None,
836 description: None,
837 docs: None,
838 },
839 kind: Test {
840 test_id: Path(
841 "test_foo1",
842 ),
843 attr: TestAttr {
844 ignore: false,
845 },
846 },
847 cfg_exprs: [
848 All(
849 [
850 KeyValue {
851 key: "feature",
852 value: "foo",
853 },
854 KeyValue {
855 key: "feature",
856 value: "bar",
857 },
858 ],
859 ),
860 ],
861 },
862 ]
863 "#]],
864 );
865 }
866
867 #[test]
868 fn test_runnables_no_test_function_in_module() {
869 check(
870 r#"
871//- /lib.rs
872<|>
873mod test_mod {
874 fn foo1() {}
875}
876"#,
877 &[],
878 expect![[r#"
879 []
880 "#]],
881 );
882 }
883}