//! The most high-level integrated tests for rust-analyzer. //! //! This tests run a full LSP event loop, spawn cargo and process stdlib from //! sysroot. For this reason, the tests here are very slow, and should be //! avoided unless absolutely necessary. //! //! In particular, it's fine *not* to test that client & server agree on //! specific JSON shapes here -- there's little value in such tests, as we can't //! be sure without a real client anyway. mod sourcegen; mod tidy; mod testdir; mod support; use std::{collections::HashMap, path::PathBuf, time::Instant}; use expect_test::expect; use lsp_types::{ notification::DidOpenTextDocument, request::{ CodeActionRequest, Completion, Formatting, GotoTypeDefinition, HoverRequest, WillRenameFiles, }, CodeActionContext, CodeActionParams, CompletionParams, DidOpenTextDocumentParams, DocumentFormattingParams, FileRename, FormattingOptions, GotoDefinitionParams, HoverParams, PartialResultParams, Position, Range, RenameFilesParams, TextDocumentItem, TextDocumentPositionParams, WorkDoneProgressParams, }; use rust_analyzer::lsp_ext::{OnEnter, Runnables, RunnablesParams}; use serde_json::json; use test_utils::skip_slow_tests; use crate::{ support::{project, Project}, testdir::TestDir, }; const PROFILE: &str = ""; // const PROFILE: &'static str = "*@3>100"; #[test] fn completes_items_from_standard_library() { if skip_slow_tests() { return; } let server = Project::with_fixture( r#" //- /Cargo.toml [package] name = "foo" version = "0.0.0" //- /src/lib.rs use std::collections::Spam; "#, ) .with_config(serde_json::json!({ "cargo": { "noSysroot": false } })) .server() .wait_until_workspace_is_loaded(); let res = server.send_request::(CompletionParams { text_document_position: TextDocumentPositionParams::new( server.doc_id("src/lib.rs"), Position::new(0, 23), ), context: None, partial_result_params: PartialResultParams::default(), work_done_progress_params: WorkDoneProgressParams::default(), }); assert!(res.to_string().contains("HashMap")); } #[test] fn test_runnables_project() { if skip_slow_tests() { return; } let server = Project::with_fixture( r#" //- /foo/Cargo.toml [package] name = "foo" version = "0.0.0" //- /foo/src/lib.rs pub fn foo() {} //- /foo/tests/spam.rs #[test] fn test_eggs() {} //- /bar/Cargo.toml [package] name = "bar" version = "0.0.0" //- /bar/src/main.rs fn main() {} "#, ) .root("foo") .root("bar") .server() .wait_until_workspace_is_loaded(); server.request::( RunnablesParams { text_document: server.doc_id("foo/tests/spam.rs"), position: None }, json!([ { "args": { "cargoArgs": ["test", "--package", "foo", "--test", "spam"], "executableArgs": ["test_eggs", "--exact", "--nocapture"], "cargoExtraArgs": [], "overrideCargo": null, "workspaceRoot": server.path().join("foo") }, "kind": "cargo", "label": "test test_eggs", "location": { "targetRange": { "end": { "character": 17, "line": 1 }, "start": { "character": 0, "line": 0 } }, "targetSelectionRange": { "end": { "character": 12, "line": 1 }, "start": { "character": 3, "line": 1 } }, "targetUri": "file:///[..]/tests/spam.rs" } }, { "args": { "overrideCargo": null, "workspaceRoot": server.path().join("foo"), "cargoArgs": [ "test", "--package", "foo", "--test", "spam" ], "cargoExtraArgs": [], "executableArgs": [ "", "--nocapture" ] }, "kind": "cargo", "label": "test-mod ", "location": { "targetUri": "file:///[..]/tests/spam.rs", "targetRange": { "start": { "line": 0, "character": 0 }, "end": { "line": 3, "character": 0 } }, "targetSelectionRange": { "start": { "line": 0, "character": 0 }, "end": { "line": 3, "character": 0 } } }, }, { "args": { "cargoArgs": ["check", "--package", "foo", "--all-targets"], "executableArgs": [], "cargoExtraArgs": [], "overrideCargo": null, "workspaceRoot": server.path().join("foo") }, "kind": "cargo", "label": "cargo check -p foo --all-targets" }, { "args": { "cargoArgs": ["test", "--package", "foo", "--all-targets"], "executableArgs": [], "cargoExtraArgs": [], "overrideCargo": null, "workspaceRoot": server.path().join("foo") }, "kind": "cargo", "label": "cargo test -p foo --all-targets" } ]), ); } // Each package in these workspaces should be run from its own root #[test] fn test_path_dependency_runnables() { if skip_slow_tests() { return; } let server = Project::with_fixture( r#" //- /consumer/Cargo.toml [package] name = "consumer" version = "0.1.0" [dependencies] dependency = { path = "../dependency" } //- /consumer/src/lib.rs #[cfg(test)] mod tests { #[test] fn consumer() {} } //- /dependency/Cargo.toml [package] name = "dependency" version = "0.1.0" [dev-dependencies] devdependency = { path = "../devdependency" } //- /dependency/src/lib.rs #[cfg(test)] mod tests { #[test] fn dependency() {} } //- /devdependency/Cargo.toml [package] name = "devdependency" version = "0.1.0" //- /devdependency/src/lib.rs #[cfg(test)] mod tests { #[test] fn devdependency() {} } "#, ) .root("consumer") .root("dependency") .root("devdependency") .server() .wait_until_workspace_is_loaded(); for runnable in ["consumer", "dependency", "devdependency"] { server.request::( RunnablesParams { text_document: server.doc_id(&format!("{}/src/lib.rs", runnable)), position: None, }, json!([ "{...}", { "label": "cargo test -p [..] --all-targets", "kind": "cargo", "args": { "overrideCargo": null, "workspaceRoot": server.path().join(runnable), "cargoArgs": [ "test", "--package", runnable, "--all-targets" ], "cargoExtraArgs": [], "executableArgs": [] }, }, "{...}", "{...}" ]), ); } } #[test] fn test_format_document() { if skip_slow_tests() { return; } let server = project( r#" //- /Cargo.toml [package] name = "foo" version = "0.0.0" //- /src/lib.rs mod bar; fn main() { } pub use std::collections::HashMap; "#, ) .wait_until_workspace_is_loaded(); server.request::( DocumentFormattingParams { text_document: server.doc_id("src/lib.rs"), options: FormattingOptions { tab_size: 4, insert_spaces: false, insert_final_newline: None, trim_final_newlines: None, trim_trailing_whitespace: None, properties: HashMap::new(), }, work_done_progress_params: WorkDoneProgressParams::default(), }, json!([ { "newText": "", "range": { "end": { "character": 0, "line": 3 }, "start": { "character": 11, "line": 2 } } } ]), ); } #[test] fn test_format_document_2018() { if skip_slow_tests() { return; } let server = project( r#" //- /Cargo.toml [package] name = "foo" version = "0.0.0" edition = "2018" //- /src/lib.rs mod bar; async fn test() { } fn main() { } pub use std::collections::HashMap; "#, ) .wait_until_workspace_is_loaded(); server.request::( DocumentFormattingParams { text_document: server.doc_id("src/lib.rs"), options: FormattingOptions { tab_size: 4, insert_spaces: false, properties: HashMap::new(), insert_final_newline: None, trim_final_newlines: None, trim_trailing_whitespace: None, }, work_done_progress_params: WorkDoneProgressParams::default(), }, json!([ { "newText": "", "range": { "end": { "character": 0, "line": 3 }, "start": { "character": 17, "line": 2 } } }, { "newText": "", "range": { "end": { "character": 0, "line": 6 }, "start": { "character": 11, "line": 5 } } } ]), ); } #[test] fn test_format_document_unchanged() { if skip_slow_tests() { return; } let server = project( r#" //- /Cargo.toml [package] name = "foo" version = "0.0.0" //- /src/lib.rs fn main() {} "#, ) .wait_until_workspace_is_loaded(); server.request::( DocumentFormattingParams { text_document: server.doc_id("src/lib.rs"), options: FormattingOptions { tab_size: 4, insert_spaces: false, insert_final_newline: None, trim_final_newlines: None, trim_trailing_whitespace: None, properties: HashMap::new(), }, work_done_progress_params: WorkDoneProgressParams::default(), }, json!(null), ); } #[test] fn test_missing_module_code_action() { if skip_slow_tests() { return; } let server = project( r#" //- /Cargo.toml [package] name = "foo" version = "0.0.0" //- /src/lib.rs mod bar; fn main() {} "#, ) .wait_until_workspace_is_loaded(); server.request::( CodeActionParams { text_document: server.doc_id("src/lib.rs"), range: Range::new(Position::new(0, 4), Position::new(0, 7)), context: CodeActionContext::default(), partial_result_params: PartialResultParams::default(), work_done_progress_params: WorkDoneProgressParams::default(), }, json!([ { "title": "Create module", "kind": "quickfix", "edit": { "documentChanges": [ { "kind": "create", "uri": "file://[..]/src/bar.rs" } ] } }, { "title": "Create module", "kind": "quickfix", "edit": { "documentChanges": [ { "kind": "create", "uri": "file://[..]src/bar/mod.rs" } ] } } ]), ); server.request::( CodeActionParams { text_document: server.doc_id("src/lib.rs"), range: Range::new(Position::new(2, 4), Position::new(2, 7)), context: CodeActionContext::default(), partial_result_params: PartialResultParams::default(), work_done_progress_params: WorkDoneProgressParams::default(), }, json!([]), ); } #[test] fn test_missing_module_code_action_in_json_project() { if skip_slow_tests() { // return; } let tmp_dir = TestDir::new(); let path = tmp_dir.path(); let project = json!({ "roots": [path], "crates": [ { "root_module": path.join("src/lib.rs"), "deps": [], "edition": "2015", "cfg": [ "cfg_atom_1", "feature=\"cfg_1\""], } ] }); let code = format!( r#" //- /rust-project.json {PROJECT} //- /src/lib.rs mod bar; fn main() {{}} "#, PROJECT = project, ); let server = Project::with_fixture(&code).tmp_dir(tmp_dir).server().wait_until_workspace_is_loaded(); server.request::( CodeActionParams { text_document: server.doc_id("src/lib.rs"), range: Range::new(Position::new(0, 4), Position::new(0, 7)), context: CodeActionContext::default(), partial_result_params: PartialResultParams::default(), work_done_progress_params: WorkDoneProgressParams::default(), }, json!([ { "title": "Create module", "kind": "quickfix", "edit": { "documentChanges": [ { "kind": "create", "uri": "file://[..]/src/bar.rs" } ] } }, { "title": "Create module", "kind": "quickfix", "edit": { "documentChanges": [ { "kind": "create", "uri": "file://[..]src/bar/mod.rs" } ] } } ]), ); server.request::( CodeActionParams { text_document: server.doc_id("src/lib.rs"), range: Range::new(Position::new(2, 4), Position::new(2, 7)), context: CodeActionContext::default(), partial_result_params: PartialResultParams::default(), work_done_progress_params: WorkDoneProgressParams::default(), }, json!([]), ); } #[test] fn diagnostics_dont_block_typing() { if skip_slow_tests() { return; } let librs: String = (0..10).map(|i| format!("mod m{};", i)).collect(); let libs: String = (0..10).map(|i| format!("//- /src/m{}.rs\nfn foo() {{}}\n\n", i)).collect(); let server = Project::with_fixture(&format!( r#" //- /Cargo.toml [package] name = "foo" version = "0.0.0" //- /src/lib.rs {} {} fn main() {{}} "#, librs, libs )) .with_config(serde_json::json!({ "cargo": { "noSysroot": false } })) .server() .wait_until_workspace_is_loaded(); for i in 0..10 { server.notification::(DidOpenTextDocumentParams { text_document: TextDocumentItem { uri: server.doc_id(&format!("src/m{}.rs", i)).uri, language_id: "rust".to_string(), version: 0, text: "/// Docs\nfn foo() {}".to_string(), }, }); } let start = Instant::now(); server.request::( TextDocumentPositionParams { text_document: server.doc_id("src/m0.rs"), position: Position { line: 0, character: 5 }, }, json!([{ "insertTextFormat": 2, "newText": "\n/// $0", "range": { "end": { "character": 5, "line": 0 }, "start": { "character": 5, "line": 0 } } }]), ); let elapsed = start.elapsed(); assert!(elapsed.as_millis() < 2000, "typing enter took {:?}", elapsed); } #[test] fn preserves_dos_line_endings() { if skip_slow_tests() { return; } let server = Project::with_fixture( " //- /Cargo.toml [package] name = \"foo\" version = \"0.0.0\" //- /src/main.rs /// Some Docs\r\nfn main() {} ", ) .server() .wait_until_workspace_is_loaded(); server.request::( TextDocumentPositionParams { text_document: server.doc_id("src/main.rs"), position: Position { line: 0, character: 8 }, }, json!([{ "insertTextFormat": 2, "newText": "\r\n/// $0", "range": { "end": { "line": 0, "character": 8 }, "start": { "line": 0, "character": 8 } } }]), ); } #[test] fn out_dirs_check() { if skip_slow_tests() { return; } let server = Project::with_fixture( r###" //- /Cargo.toml [package] name = "foo" version = "0.0.0" //- /build.rs use std::{env, fs, path::Path}; fn main() { let out_dir = env::var_os("OUT_DIR").unwrap(); let dest_path = Path::new(&out_dir).join("hello.rs"); fs::write( &dest_path, r#"pub fn message() -> &'static str { "Hello, World!" }"#, ) .unwrap(); println!("cargo:rustc-cfg=atom_cfg"); println!("cargo:rustc-cfg=featlike=\"set\""); println!("cargo:rerun-if-changed=build.rs"); } //- /src/main.rs #[rustc_builtin_macro] macro_rules! include {} #[rustc_builtin_macro] macro_rules! include_str {} #[rustc_builtin_macro] macro_rules! concat {} #[rustc_builtin_macro] macro_rules! env {} include!(concat!(env!("OUT_DIR"), "/hello.rs")); #[cfg(atom_cfg)] struct A; #[cfg(bad_atom_cfg)] struct A; #[cfg(featlike = "set")] struct B; #[cfg(featlike = "not_set")] struct B; fn main() { let va = A; let vb = B; let should_be_str = message(); let another_str = include_str!("main.rs"); } "###, ) .with_config(serde_json::json!({ "cargo": { "loadOutDirsFromCheck": true, "noSysroot": true, } })) .server() .wait_until_workspace_is_loaded(); let res = server.send_request::(HoverParams { text_document_position_params: TextDocumentPositionParams::new( server.doc_id("src/main.rs"), Position::new(19, 10), ), work_done_progress_params: Default::default(), }); assert!(res.to_string().contains("&str")); let res = server.send_request::(HoverParams { text_document_position_params: TextDocumentPositionParams::new( server.doc_id("src/main.rs"), Position::new(20, 10), ), work_done_progress_params: Default::default(), }); assert!(res.to_string().contains("&str")); server.request::( GotoDefinitionParams { text_document_position_params: TextDocumentPositionParams::new( server.doc_id("src/main.rs"), Position::new(17, 9), ), work_done_progress_params: Default::default(), partial_result_params: Default::default(), }, json!([{ "originSelectionRange": { "end": { "character": 10, "line": 17 }, "start": { "character": 8, "line": 17 } }, "targetRange": { "end": { "character": 9, "line": 8 }, "start": { "character": 0, "line": 7 } }, "targetSelectionRange": { "end": { "character": 8, "line": 8 }, "start": { "character": 7, "line": 8 } }, "targetUri": "file:///[..]src/main.rs" }]), ); server.request::( GotoDefinitionParams { text_document_position_params: TextDocumentPositionParams::new( server.doc_id("src/main.rs"), Position::new(18, 9), ), work_done_progress_params: Default::default(), partial_result_params: Default::default(), }, json!([{ "originSelectionRange": { "end": { "character": 10, "line": 18 }, "start": { "character": 8, "line": 18 } }, "targetRange": { "end": { "character": 9, "line": 12 }, "start": { "character": 0, "line":11 } }, "targetSelectionRange": { "end": { "character": 8, "line": 12 }, "start": { "character": 7, "line": 12 } }, "targetUri": "file:///[..]src/main.rs" }]), ); } #[test] fn resolve_proc_macro() { if skip_slow_tests() { return; } let server = Project::with_fixture( r###" //- /foo/Cargo.toml [package] name = "foo" version = "0.0.0" edition = "2021" [dependencies] bar = {path = "../bar"} //- /foo/src/main.rs use bar::Bar; #[rustc_builtin_macro] macro derive($item:item) {} trait Bar { fn bar(); } #[derive(Bar)] struct Foo {} fn main() { Foo::bar(); } //- /bar/Cargo.toml [package] name = "bar" version = "0.0.0" edition = "2021" [lib] proc-macro = true //- /bar/src/lib.rs extern crate proc_macro; use proc_macro::{Delimiter, Group, Ident, Span, TokenStream, TokenTree}; macro_rules! t { ($n:literal) => { TokenTree::from(Ident::new($n, Span::call_site())) }; ({}) => { TokenTree::from(Group::new(Delimiter::Brace, TokenStream::new())) }; (()) => { TokenTree::from(Group::new(Delimiter::Parenthesis, TokenStream::new())) }; } #[proc_macro_derive(Bar)] pub fn foo(_input: TokenStream) -> TokenStream { // We hard code the output here for preventing to use any deps let mut res = TokenStream::new(); // ill behaved proc-macro will use the stdout // we should ignore it println!("I am bad guy"); // impl Bar for Foo { fn bar() {} } let mut tokens = vec![t!("impl"), t!("Bar"), t!("for"), t!("Foo")]; let mut fn_stream = TokenStream::new(); fn_stream.extend(vec![t!("fn"), t!("bar"), t!(()), t!({})]); tokens.push(Group::new(Delimiter::Brace, fn_stream).into()); res.extend(tokens); res } "###, ) .with_config(serde_json::json!({ "cargo": { "loadOutDirsFromCheck": true, "noSysroot": true, }, "procMacro": { "enable": true, "server": PathBuf::from(env!("CARGO_BIN_EXE_rust-analyzer")), } })) .root("foo") .root("bar") .server() .wait_until_workspace_is_loaded(); let res = server.send_request::(HoverParams { text_document_position_params: TextDocumentPositionParams::new( server.doc_id("foo/src/main.rs"), Position::new(10, 9), ), work_done_progress_params: Default::default(), }); let value = res.get("contents").unwrap().get("value").unwrap().as_str().unwrap(); expect![[r#" ```rust foo::Bar ``` ```rust fn bar() ```"#]] .assert_eq(value); } #[test] fn test_will_rename_files_same_level() { if skip_slow_tests() { return; } let tmp_dir = TestDir::new(); let tmp_dir_path = tmp_dir.path().to_owned(); let tmp_dir_str = tmp_dir_path.to_str().unwrap(); let base_path = PathBuf::from(format!("file://{}", tmp_dir_str)); let code = r#" //- /Cargo.toml [package] name = "foo" version = "0.0.0" //- /src/lib.rs mod old_file; mod from_mod; mod to_mod; mod old_folder; fn main() {} //- /src/old_file.rs //- /src/old_folder/mod.rs //- /src/from_mod/mod.rs //- /src/to_mod/foo.rs "#; let server = Project::with_fixture(code).tmp_dir(tmp_dir).server().wait_until_workspace_is_loaded(); //rename same level file server.request::( RenameFilesParams { files: vec![FileRename { old_uri: base_path.join("src/old_file.rs").to_str().unwrap().to_string(), new_uri: base_path.join("src/new_file.rs").to_str().unwrap().to_string(), }], }, json!({ "documentChanges": [ { "textDocument": { "uri": format!("file://{}", tmp_dir_path.join("src").join("lib.rs").to_str().unwrap().to_string().replace("C:\\", "/c:/").replace('\\', "/")), "version": null }, "edits": [ { "range": { "start": { "line": 0, "character": 4 }, "end": { "line": 0, "character": 12 } }, "newText": "new_file" } ] } ] }), ); //rename file from mod.rs to foo.rs server.request::( RenameFilesParams { files: vec![FileRename { old_uri: base_path.join("src/from_mod/mod.rs").to_str().unwrap().to_string(), new_uri: base_path.join("src/from_mod/foo.rs").to_str().unwrap().to_string(), }], }, json!(null), ); //rename file from foo.rs to mod.rs server.request::( RenameFilesParams { files: vec![FileRename { old_uri: base_path.join("src/to_mod/foo.rs").to_str().unwrap().to_string(), new_uri: base_path.join("src/to_mod/mod.rs").to_str().unwrap().to_string(), }], }, json!(null), ); //rename same level file server.request::( RenameFilesParams { files: vec![FileRename { old_uri: base_path.join("src/old_folder").to_str().unwrap().to_string(), new_uri: base_path.join("src/new_folder").to_str().unwrap().to_string(), }], }, json!({ "documentChanges": [ { "textDocument": { "uri": format!("file://{}", tmp_dir_path.join("src").join("lib.rs").to_str().unwrap().to_string().replace("C:\\", "/c:/").replace('\\', "/")), "version": null }, "edits": [ { "range": { "start": { "line": 3, "character": 4 }, "end": { "line": 3, "character": 14 } }, "newText": "new_folder" } ] } ] }), ); }