1 //! The most high-level integrated tests for rust-analyzer.
3 //! This tests run a full LSP event loop, spawn cargo and process stdlib from
4 //! sysroot. For this reason, the tests here are very slow, and should be
5 //! avoided unless absolutely necessary.
7 //! In particular, it's fine *not* to test that client & server agree on
8 //! specific JSON shapes here -- there's little value in such tests, as we can't
9 //! be sure without a real client anyway.
11 #![warn(rust_2018_idioms, unused_lifetimes, semicolon_in_expressions_from_macros)]
13 #[cfg(not(feature = "in-rust-tree"))]
19 use std
::{collections::HashMap, path::PathBuf, time::Instant}
;
22 notification
::DidOpenTextDocument
,
24 CodeActionRequest
, Completion
, Formatting
, GotoTypeDefinition
, HoverRequest
,
25 WillRenameFiles
, WorkspaceSymbol
,
27 CodeActionContext
, CodeActionParams
, CompletionParams
, DidOpenTextDocumentParams
,
28 DocumentFormattingParams
, FileRename
, FormattingOptions
, GotoDefinitionParams
, HoverParams
,
29 PartialResultParams
, Position
, Range
, RenameFilesParams
, TextDocumentItem
,
30 TextDocumentPositionParams
, WorkDoneProgressParams
,
32 use rust_analyzer
::lsp_ext
::{OnEnter, Runnables, RunnablesParams}
;
34 use test_utils
::skip_slow_tests
;
37 support
::{project, Project}
,
41 const PROFILE
: &str = "";
42 // const PROFILE: &'static str = "*@3>100";
45 fn completes_items_from_standard_library() {
46 if skip_slow_tests() {
50 let server
= Project
::with_fixture(
58 use std::collections::Spam;
61 .with_config(serde_json
::json
!({
62 "cargo": { "sysroot": "discover" }
65 .wait_until_workspace_is_loaded();
67 let res
= server
.send_request
::<Completion
>(CompletionParams
{
68 text_document_position
: TextDocumentPositionParams
::new(
69 server
.doc_id("src/lib.rs"),
73 partial_result_params
: PartialResultParams
::default(),
74 work_done_progress_params
: WorkDoneProgressParams
::default(),
76 assert
!(res
.to_string().contains("HashMap"));
80 fn test_runnables_project() {
81 if skip_slow_tests() {
85 let server
= Project
::with_fixture(
95 //- /foo/tests/spam.rs
111 .wait_until_workspace_is_loaded();
113 server
.request
::<Runnables
>(
114 RunnablesParams { text_document: server.doc_id("foo/tests/spam.rs"), position: None }
,
118 "cargoArgs": ["test", "--package", "foo", "--test", "spam"],
119 "executableArgs": ["test_eggs", "--exact", "--nocapture"],
120 "cargoExtraArgs": [],
121 "overrideCargo": null
,
122 "workspaceRoot": server
.path().join("foo")
125 "label": "test test_eggs",
128 "end": { "character": 17, "line": 1 }
,
129 "start": { "character": 0, "line": 0 }
131 "targetSelectionRange": {
132 "end": { "character": 12, "line": 1 }
,
133 "start": { "character": 3, "line": 1 }
135 "targetUri": "file:///[..]/tests/spam.rs"
140 "overrideCargo": null
,
141 "workspaceRoot": server
.path().join("foo"),
149 "cargoExtraArgs": [],
156 "label": "test-mod ",
158 "targetUri": "file:///[..]/tests/spam.rs",
169 "targetSelectionRange": {
183 "cargoArgs": ["check", "--package", "foo", "--all-targets"],
184 "executableArgs": [],
185 "cargoExtraArgs": [],
186 "overrideCargo": null
,
187 "workspaceRoot": server
.path().join("foo")
190 "label": "cargo check -p foo --all-targets"
194 "cargoArgs": ["test", "--package", "foo", "--all-targets"],
195 "executableArgs": [],
196 "cargoExtraArgs": [],
197 "overrideCargo": null
,
198 "workspaceRoot": server
.path().join("foo")
201 "label": "cargo test -p foo --all-targets"
207 // Each package in these workspaces should be run from its own root
209 fn test_path_dependency_runnables() {
210 if skip_slow_tests() {
214 let server
= Project
::with_fixture(
216 //- /consumer/Cargo.toml
221 dependency = { path = "../dependency" }
223 //- /consumer/src/lib.rs
230 //- /dependency/Cargo.toml
235 devdependency = { path = "../devdependency" }
237 //- /dependency/src/lib.rs
244 //- /devdependency/Cargo.toml
246 name = "devdependency"
249 //- /devdependency/src/lib.rs
253 fn devdependency() {}
259 .root("devdependency")
261 .wait_until_workspace_is_loaded();
263 for runnable
in ["consumer", "dependency", "devdependency"] {
264 server
.request
::<Runnables
>(
266 text_document
: server
.doc_id(&format
!("{runnable}/src/lib.rs")),
272 "label": "cargo test -p [..] --all-targets",
275 "overrideCargo": null
,
276 "workspaceRoot": server
.path().join(runnable
),
283 "cargoExtraArgs": [],
295 fn test_format_document() {
296 if skip_slow_tests() {
300 let server
= project(
313 pub use std::collections::HashMap;
316 .wait_until_workspace_is_loaded();
318 server
.request
::<Formatting
>(
319 DocumentFormattingParams
{
320 text_document
: server
.doc_id("src/lib.rs"),
321 options
: FormattingOptions
{
323 insert_spaces
: false,
324 insert_final_newline
: None
,
325 trim_final_newlines
: None
,
326 trim_trailing_whitespace
: None
,
327 properties
: HashMap
::new(),
329 work_done_progress_params
: WorkDoneProgressParams
::default(),
335 "end": { "character": 0, "line": 3 }
,
336 "start": { "character": 11, "line": 2 }
344 fn test_format_document_2018() {
345 if skip_slow_tests() {
349 let server
= project(
366 pub use std::collections::HashMap;
369 .wait_until_workspace_is_loaded();
371 server
.request
::<Formatting
>(
372 DocumentFormattingParams
{
373 text_document
: server
.doc_id("src/lib.rs"),
374 options
: FormattingOptions
{
376 insert_spaces
: false,
377 properties
: HashMap
::new(),
378 insert_final_newline
: None
,
379 trim_final_newlines
: None
,
380 trim_trailing_whitespace
: None
,
382 work_done_progress_params
: WorkDoneProgressParams
::default(),
388 "end": { "character": 0, "line": 3 }
,
389 "start": { "character": 17, "line": 2 }
395 "end": { "character": 0, "line": 6 }
,
396 "start": { "character": 11, "line": 5 }
404 fn test_format_document_unchanged() {
405 if skip_slow_tests() {
409 let server
= project(
420 .wait_until_workspace_is_loaded();
422 server
.request
::<Formatting
>(
423 DocumentFormattingParams
{
424 text_document
: server
.doc_id("src/lib.rs"),
425 options
: FormattingOptions
{
427 insert_spaces
: false,
428 insert_final_newline
: None
,
429 trim_final_newlines
: None
,
430 trim_trailing_whitespace
: None
,
431 properties
: HashMap
::new(),
433 work_done_progress_params
: WorkDoneProgressParams
::default(),
440 fn test_missing_module_code_action() {
441 if skip_slow_tests() {
445 let server
= project(
458 .wait_until_workspace_is_loaded();
460 server
.request
::<CodeActionRequest
>(
462 text_document
: server
.doc_id("src/lib.rs"),
463 range
: Range
::new(Position
::new(0, 4), Position
::new(0, 7)),
464 context
: CodeActionContext
::default(),
465 partial_result_params
: PartialResultParams
::default(),
466 work_done_progress_params
: WorkDoneProgressParams
::default(),
470 "title": "Create module at `bar.rs`",
476 "uri": "file://[..]/src/bar.rs"
482 "title": "Create module at `bar/mod.rs`",
488 "uri": "file://[..]src/bar/mod.rs"
496 server
.request
::<CodeActionRequest
>(
498 text_document
: server
.doc_id("src/lib.rs"),
499 range
: Range
::new(Position
::new(2, 8), Position
::new(2, 8)),
500 context
: CodeActionContext
::default(),
501 partial_result_params
: PartialResultParams
::default(),
502 work_done_progress_params
: WorkDoneProgressParams
::default(),
509 fn test_missing_module_code_action_in_json_project() {
510 if skip_slow_tests() {
514 let tmp_dir
= TestDir
::new();
516 let path
= tmp_dir
.path();
518 let project
= json
!({
521 "root_module": path
.join("src/lib.rs"),
524 "cfg": [ "cfg_atom_1", "feature=\"cfg_1\""],
530 //- /rust-project.json
541 Project
::with_fixture(&code
).tmp_dir(tmp_dir
).server().wait_until_workspace_is_loaded();
543 server
.request
::<CodeActionRequest
>(
545 text_document
: server
.doc_id("src/lib.rs"),
546 range
: Range
::new(Position
::new(0, 4), Position
::new(0, 7)),
547 context
: CodeActionContext
::default(),
548 partial_result_params
: PartialResultParams
::default(),
549 work_done_progress_params
: WorkDoneProgressParams
::default(),
553 "title": "Create module at `bar.rs`",
559 "uri": "file://[..]/src/bar.rs"
565 "title": "Create module at `bar/mod.rs`",
571 "uri": "file://[..]src/bar/mod.rs"
579 server
.request
::<CodeActionRequest
>(
581 text_document
: server
.doc_id("src/lib.rs"),
582 range
: Range
::new(Position
::new(2, 8), Position
::new(2, 8)),
583 context
: CodeActionContext
::default(),
584 partial_result_params
: PartialResultParams
::default(),
585 work_done_progress_params
: WorkDoneProgressParams
::default(),
592 fn diagnostics_dont_block_typing() {
593 if skip_slow_tests() {
597 let librs
: String
= (0..10).map(|i
| format
!("mod m{i};")).collect();
598 let libs
: String
= (0..10).map(|i
| format
!("//- /src/m{i}.rs\nfn foo() {{}}\n\n")).collect();
599 let server
= Project
::with_fixture(&format
!(
614 .with_config(serde_json
::json
!({
615 "cargo": { "sysroot": "discover" }
618 .wait_until_workspace_is_loaded();
621 server
.notification
::<DidOpenTextDocument
>(DidOpenTextDocumentParams
{
622 text_document
: TextDocumentItem
{
623 uri
: server
.doc_id(&format
!("src/m{i}.rs")).uri
,
624 language_id
: "rust".to_string(),
626 text
: "/// Docs\nfn foo() {}".to_string(),
630 let start
= Instant
::now();
631 server
.request
::<OnEnter
>(
632 TextDocumentPositionParams
{
633 text_document
: server
.doc_id("src/m0.rs"),
634 position
: Position { line: 0, character: 5 }
,
637 "insertTextFormat": 2,
638 "newText": "\n/// $0",
640 "end": { "character": 5, "line": 0 }
,
641 "start": { "character": 5, "line": 0 }
645 let elapsed
= start
.elapsed();
646 assert
!(elapsed
.as_millis() < 2000, "typing enter took {elapsed:?}");
650 fn preserves_dos_line_endings() {
651 if skip_slow_tests() {
655 let server
= Project
::with_fixture(
663 /// Some Docs\r\nfn main() {}
667 .wait_until_workspace_is_loaded();
669 server
.request
::<OnEnter
>(
670 TextDocumentPositionParams
{
671 text_document
: server
.doc_id("src/main.rs"),
672 position
: Position { line: 0, character: 8 }
,
675 "insertTextFormat": 2,
676 "newText": "\r\n/// $0",
678 "end": { "line": 0, "character": 8 }
,
679 "start": { "line": 0, "character": 8 }
686 fn out_dirs_check() {
687 if skip_slow_tests() {
691 let server
= Project
::with_fixture(
699 use std::{env, fs, path::Path};
702 let out_dir = env::var_os("OUT_DIR").unwrap();
703 let dest_path = Path::new(&out_dir).join("hello.rs");
706 r#"pub fn message() -> &'static str { "Hello, World!" }"#,
709 println!("cargo:rustc-cfg=atom_cfg");
710 println!("cargo:rustc-cfg=featlike=\"set\"");
711 println!("cargo:rerun-if-changed=build.rs");
714 #[rustc_builtin_macro] macro_rules! include {}
715 #[rustc_builtin_macro] macro_rules! include_str {}
716 #[rustc_builtin_macro] macro_rules! concat {}
717 #[rustc_builtin_macro] macro_rules! env {}
719 include!(concat!(env!("OUT_DIR"), "/hello.rs"));
725 #[cfg(featlike = "set")]
727 #[cfg(featlike = "not_set")]
733 let should_be_str = message();
734 let another_str = include_str!("main.rs");
738 .with_config(serde_json
::json
!({
747 .wait_until_workspace_is_loaded();
749 let res
= server
.send_request
::<HoverRequest
>(HoverParams
{
750 text_document_position_params
: TextDocumentPositionParams
::new(
751 server
.doc_id("src/main.rs"),
752 Position
::new(19, 10),
754 work_done_progress_params
: Default
::default(),
756 assert
!(res
.to_string().contains("&str"));
758 let res
= server
.send_request
::<HoverRequest
>(HoverParams
{
759 text_document_position_params
: TextDocumentPositionParams
::new(
760 server
.doc_id("src/main.rs"),
761 Position
::new(20, 10),
763 work_done_progress_params
: Default
::default(),
765 assert
!(res
.to_string().contains("&str"));
767 server
.request
::<GotoTypeDefinition
>(
768 GotoDefinitionParams
{
769 text_document_position_params
: TextDocumentPositionParams
::new(
770 server
.doc_id("src/main.rs"),
771 Position
::new(17, 9),
773 work_done_progress_params
: Default
::default(),
774 partial_result_params
: Default
::default(),
777 "originSelectionRange": {
778 "end": { "character": 10, "line": 17 }
,
779 "start": { "character": 8, "line": 17 }
782 "end": { "character": 9, "line": 8 }
,
783 "start": { "character": 0, "line": 7 }
785 "targetSelectionRange": {
786 "end": { "character": 8, "line": 8 }
,
787 "start": { "character": 7, "line": 8 }
789 "targetUri": "file:///[..]src/main.rs"
793 server
.request
::<GotoTypeDefinition
>(
794 GotoDefinitionParams
{
795 text_document_position_params
: TextDocumentPositionParams
::new(
796 server
.doc_id("src/main.rs"),
797 Position
::new(18, 9),
799 work_done_progress_params
: Default
::default(),
800 partial_result_params
: Default
::default(),
803 "originSelectionRange": {
804 "end": { "character": 10, "line": 18 }
,
805 "start": { "character": 8, "line": 18 }
808 "end": { "character": 9, "line": 12 }
,
809 "start": { "character": 0, "line":11 }
811 "targetSelectionRange": {
812 "end": { "character": 8, "line": 12 }
,
813 "start": { "character": 7, "line": 12 }
815 "targetUri": "file:///[..]src/main.rs"
821 // FIXME: Re-enable once we can run proc-macro tests on rust-lang/rust-analyzer again
823 fn resolve_proc_macro() {
824 use expect_test
::expect
;
825 if skip_slow_tests() {
829 let server
= Project
::with_fixture(
837 bar = {path = "../bar"}
842 #[rustc_builtin_macro]
843 macro derive($item:item) {}
863 extern crate proc_macro;
864 use proc_macro::{Delimiter, Group, Ident, Span, TokenStream, TokenTree};
867 TokenTree::from(Ident::new($n, Span::call_site()))
870 TokenTree::from(Group::new(Delimiter::Brace, TokenStream::new()))
873 TokenTree::from(Group::new(Delimiter::Parenthesis, TokenStream::new()))
876 #[proc_macro_derive(Bar)]
877 pub fn foo(_input: TokenStream) -> TokenStream {
878 // We hard code the output here for preventing to use any deps
879 let mut res = TokenStream::new();
881 // ill behaved proc-macro will use the stdout
882 // we should ignore it
883 println!("I am bad guy");
885 // impl Bar for Foo { fn bar() {} }
886 let mut tokens = vec![t!("impl"), t!("Bar"), t!("for"), t!("Foo")];
887 let mut fn_stream = TokenStream::new();
888 fn_stream.extend(vec![t!("fn"), t!("bar"), t!(()), t!({})]);
889 tokens.push(Group::new(Delimiter::Brace, fn_stream).into());
896 .with_config(serde_json
::json
!({
905 "server": PathBuf
::from(env
!("CARGO_BIN_EXE_rust-analyzer")),
911 .wait_until_workspace_is_loaded();
913 let res
= server
.send_request
::<HoverRequest
>(HoverParams
{
914 text_document_position_params
: TextDocumentPositionParams
::new(
915 server
.doc_id("foo/src/main.rs"),
916 Position
::new(10, 9),
918 work_done_progress_params
: Default
::default(),
920 let value
= res
.get("contents").unwrap().get("value").unwrap().as_str().unwrap();
935 fn test_will_rename_files_same_level() {
936 if skip_slow_tests() {
940 let tmp_dir
= TestDir
::new();
941 let tmp_dir_path
= tmp_dir
.path().to_owned();
942 let tmp_dir_str
= tmp_dir_path
.to_str().unwrap();
943 let base_path
= PathBuf
::from(format
!("file://{tmp_dir_str}"));
960 //- /src/old_folder/mod.rs
962 //- /src/from_mod/mod.rs
964 //- /src/to_mod/foo.rs
968 Project
::with_fixture(code
).tmp_dir(tmp_dir
).server().wait_until_workspace_is_loaded();
970 //rename same level file
971 server
.request
::<WillRenameFiles
>(
973 files
: vec
![FileRename
{
974 old_uri
: base_path
.join("src/old_file.rs").to_str().unwrap().to_string(),
975 new_uri
: base_path
.join("src/new_file.rs").to_str().unwrap().to_string(),
982 "uri": format
!("file://{}", tmp_dir_path
.join("src").join("lib.rs").to_str().unwrap().to_string().replace("C:\\", "/c:/").replace('
\\'
, "/")),
997 "newText": "new_file"
1005 //rename file from mod.rs to foo.rs
1006 server
.request
::<WillRenameFiles
>(
1008 files
: vec
![FileRename
{
1009 old_uri
: base_path
.join("src/from_mod/mod.rs").to_str().unwrap().to_string(),
1010 new_uri
: base_path
.join("src/from_mod/foo.rs").to_str().unwrap().to_string(),
1016 //rename file from foo.rs to mod.rs
1017 server
.request
::<WillRenameFiles
>(
1019 files
: vec
![FileRename
{
1020 old_uri
: base_path
.join("src/to_mod/foo.rs").to_str().unwrap().to_string(),
1021 new_uri
: base_path
.join("src/to_mod/mod.rs").to_str().unwrap().to_string(),
1027 //rename same level file
1028 server
.request
::<WillRenameFiles
>(
1030 files
: vec
![FileRename
{
1031 old_uri
: base_path
.join("src/old_folder").to_str().unwrap().to_string(),
1032 new_uri
: base_path
.join("src/new_folder").to_str().unwrap().to_string(),
1036 "documentChanges": [
1039 "uri": format
!("file://{}", tmp_dir_path
.join("src").join("lib.rs").to_str().unwrap().to_string().replace("C:\\", "/c:/").replace('
\\'
, "/")),
1054 "newText": "new_folder"
1064 fn test_exclude_config_works() {
1065 if skip_slow_tests() {
1069 let server
= Project
::with_fixture(
1090 .with_config(json
!({
1092 "excludeDirs": ["foo", "bar"]
1096 .wait_until_workspace_is_loaded();
1098 server
.request
::<WorkspaceSymbol
>(Default
::default(), json
!([]));