From 459b0332d3a4905fa9bc93c484538e74d191c80b Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Mon, 17 May 2021 18:43:20 +0300 Subject: internal: use more evocative test folder name --- crates/rust-analyzer/tests/rust-analyzer/main.rs | 892 --------------------- .../rust-analyzer/tests/rust-analyzer/support.rs | 390 --------- .../rust-analyzer/tests/rust-analyzer/testdir.rs | 62 -- crates/rust-analyzer/tests/slow-tests/main.rs | 891 ++++++++++++++++++++ crates/rust-analyzer/tests/slow-tests/support.rs | 390 +++++++++ crates/rust-analyzer/tests/slow-tests/testdir.rs | 62 ++ 6 files changed, 1343 insertions(+), 1344 deletions(-) delete mode 100644 crates/rust-analyzer/tests/rust-analyzer/main.rs delete mode 100644 crates/rust-analyzer/tests/rust-analyzer/support.rs delete mode 100644 crates/rust-analyzer/tests/rust-analyzer/testdir.rs create mode 100644 crates/rust-analyzer/tests/slow-tests/main.rs create mode 100644 crates/rust-analyzer/tests/slow-tests/support.rs create mode 100644 crates/rust-analyzer/tests/slow-tests/testdir.rs diff --git a/crates/rust-analyzer/tests/rust-analyzer/main.rs b/crates/rust-analyzer/tests/rust-analyzer/main.rs deleted file mode 100644 index c940ef214..000000000 --- a/crates/rust-analyzer/tests/rust-analyzer/main.rs +++ /dev/null @@ -1,892 +0,0 @@ -//! 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 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, - SemanticTokensRangeRequest, WillRenameFiles, - }, - CodeActionContext, CodeActionParams, CompletionParams, DidOpenTextDocumentParams, - DocumentFormattingParams, FileRename, FormattingOptions, GotoDefinitionParams, HoverParams, - PartialResultParams, Position, Range, RenameFilesParams, SemanticTokens, - SemanticTokensRangeParams, TextDocumentItem, TextDocumentPositionParams, - WorkDoneProgressParams, -}; -use rust_analyzer::lsp_ext::{OnEnter, Runnables, RunnablesParams}; -use serde_json::{from_value, 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": { - "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" - } - ]), - ); -} - -#[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!([{ - "edit": { - "documentChanges": [ - { - "kind": "create", - "uri": "file:///[..]/src/bar.rs" - } - ] - }, - "kind": "quickfix", - "title": "Create module" - }]), - ); - - 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.to_string(), - ); - - 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!([{ - "edit": { - "documentChanges": [ - { - "kind": "create", - "uri": "file://[..]/src/bar.rs" - } - ] - }, - "kind": "quickfix", - "title": "Create module" - }]), - ); - - 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 = "2018" -[dependencies] -bar = {path = "../bar"} - -//- /foo/src/main.rs -use bar::Bar; -trait Bar { - fn bar(); -} -#[derive(Bar)] -struct Foo {} -fn main() { - Foo::bar(); -} - -//- /bar/Cargo.toml -[package] -name = "bar" -version = "0.0.0" -edition = "2018" - -[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(7, 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" - } - ] - } - ] - }), - ); -} diff --git a/crates/rust-analyzer/tests/rust-analyzer/support.rs b/crates/rust-analyzer/tests/rust-analyzer/support.rs deleted file mode 100644 index 75e677762..000000000 --- a/crates/rust-analyzer/tests/rust-analyzer/support.rs +++ /dev/null @@ -1,390 +0,0 @@ -use std::{ - cell::{Cell, RefCell}, - fs, - path::{Path, PathBuf}, - sync::Once, - time::Duration, -}; - -use crossbeam_channel::{after, select, Receiver}; -use lsp_server::{Connection, Message, Notification, Request}; -use lsp_types::{notification::Exit, request::Shutdown, TextDocumentIdentifier, Url}; -use project_model::ProjectManifest; -use rust_analyzer::{config::Config, lsp_ext, main_loop}; -use serde::Serialize; -use serde_json::{json, to_string_pretty, Value}; -use test_utils::Fixture; -use vfs::AbsPathBuf; - -use crate::testdir::TestDir; - -pub(crate) struct Project<'a> { - fixture: &'a str, - tmp_dir: Option, - roots: Vec, - config: serde_json::Value, -} - -impl<'a> Project<'a> { - pub(crate) fn with_fixture(fixture: &str) -> Project { - Project { - fixture, - tmp_dir: None, - roots: vec![], - config: serde_json::json!({ - "cargo": { - // Loading standard library is costly, let's ignore it by default - "noSysroot": true, - // Can't use test binary as rustc wrapper. - "useRustcWrapperForBuildScripts": false, - } - }), - } - } - - pub(crate) fn tmp_dir(mut self, tmp_dir: TestDir) -> Project<'a> { - self.tmp_dir = Some(tmp_dir); - self - } - - pub(crate) fn root(mut self, path: &str) -> Project<'a> { - self.roots.push(path.into()); - self - } - - pub(crate) fn with_config(mut self, config: serde_json::Value) -> Project<'a> { - fn merge(dst: &mut serde_json::Value, src: serde_json::Value) { - match (dst, src) { - (Value::Object(dst), Value::Object(src)) => { - for (k, v) in src { - merge(dst.entry(k).or_insert(v.clone()), v) - } - } - (dst, src) => *dst = src, - } - } - merge(&mut self.config, config); - self - } - - pub(crate) fn server(self) -> Server { - let tmp_dir = self.tmp_dir.unwrap_or_else(TestDir::new); - static INIT: Once = Once::new(); - INIT.call_once(|| { - env_logger::builder().is_test(true).parse_env("RA_LOG").try_init().unwrap(); - profile::init_from(crate::PROFILE); - }); - - for entry in Fixture::parse(self.fixture) { - let path = tmp_dir.path().join(&entry.path['/'.len_utf8()..]); - fs::create_dir_all(path.parent().unwrap()).unwrap(); - fs::write(path.as_path(), entry.text.as_bytes()).unwrap(); - } - - let tmp_dir_path = AbsPathBuf::assert(tmp_dir.path().to_path_buf()); - let mut roots = - self.roots.into_iter().map(|root| tmp_dir_path.join(root)).collect::>(); - if roots.is_empty() { - roots.push(tmp_dir_path.clone()); - } - let discovered_projects = roots - .into_iter() - .map(|it| ProjectManifest::discover_single(&it).unwrap()) - .collect::>(); - - let mut config = Config::new( - tmp_dir_path, - lsp_types::ClientCapabilities { - text_document: Some(lsp_types::TextDocumentClientCapabilities { - definition: Some(lsp_types::GotoCapability { - link_support: Some(true), - ..Default::default() - }), - code_action: Some(lsp_types::CodeActionClientCapabilities { - code_action_literal_support: Some( - lsp_types::CodeActionLiteralSupport::default(), - ), - ..Default::default() - }), - hover: Some(lsp_types::HoverClientCapabilities { - content_format: Some(vec![lsp_types::MarkupKind::Markdown]), - ..Default::default() - }), - ..Default::default() - }), - window: Some(lsp_types::WindowClientCapabilities { - work_done_progress: Some(false), - ..Default::default() - }), - experimental: Some(json!({ - "serverStatusNotification": true, - })), - ..Default::default() - }, - ); - config.discovered_projects = Some(discovered_projects); - config.update(self.config); - - Server::new(tmp_dir, config) - } -} - -pub(crate) fn project(fixture: &str) -> Server { - Project::with_fixture(fixture).server() -} - -pub(crate) struct Server { - req_id: Cell, - messages: RefCell>, - _thread: jod_thread::JoinHandle<()>, - client: Connection, - /// XXX: remove the tempdir last - dir: TestDir, -} - -impl Server { - fn new(dir: TestDir, config: Config) -> Server { - let (connection, client) = Connection::memory(); - - let _thread = jod_thread::Builder::new() - .name("test server".to_string()) - .spawn(move || main_loop(config, connection).unwrap()) - .expect("failed to spawn a thread"); - - Server { req_id: Cell::new(1), dir, messages: Default::default(), client, _thread } - } - - pub(crate) fn doc_id(&self, rel_path: &str) -> TextDocumentIdentifier { - let path = self.dir.path().join(rel_path); - TextDocumentIdentifier { uri: Url::from_file_path(path).unwrap() } - } - - pub(crate) fn notification(&self, params: N::Params) - where - N: lsp_types::notification::Notification, - N::Params: Serialize, - { - let r = Notification::new(N::METHOD.to_string(), params); - self.send_notification(r) - } - - #[track_caller] - pub(crate) fn request(&self, params: R::Params, expected_resp: Value) - where - R: lsp_types::request::Request, - R::Params: Serialize, - { - let actual = self.send_request::(params); - if let Some((expected_part, actual_part)) = find_mismatch(&expected_resp, &actual) { - panic!( - "JSON mismatch\nExpected:\n{}\nWas:\n{}\nExpected part:\n{}\nActual part:\n{}\n", - to_string_pretty(&expected_resp).unwrap(), - to_string_pretty(&actual).unwrap(), - to_string_pretty(expected_part).unwrap(), - to_string_pretty(actual_part).unwrap(), - ); - } - } - - pub(crate) fn send_request(&self, params: R::Params) -> Value - where - R: lsp_types::request::Request, - R::Params: Serialize, - { - let id = self.req_id.get(); - self.req_id.set(id.wrapping_add(1)); - - let r = Request::new(id.into(), R::METHOD.to_string(), params); - self.send_request_(r) - } - fn send_request_(&self, r: Request) -> Value { - let id = r.id.clone(); - self.client.sender.send(r.clone().into()).unwrap(); - while let Some(msg) = self.recv().unwrap_or_else(|Timeout| panic!("timeout: {:?}", r)) { - match msg { - Message::Request(req) => { - if req.method == "client/registerCapability" { - let params = req.params.to_string(); - if ["workspace/didChangeWatchedFiles", "textDocument/didSave"] - .iter() - .any(|&it| params.contains(it)) - { - continue; - } - } - panic!("unexpected request: {:?}", req) - } - Message::Notification(_) => (), - Message::Response(res) => { - assert_eq!(res.id, id); - if let Some(err) = res.error { - panic!("error response: {:#?}", err); - } - return res.result.unwrap(); - } - } - } - panic!("no response for {:?}", r); - } - pub(crate) fn wait_until_workspace_is_loaded(self) -> Server { - self.wait_for_message_cond(1, &|msg: &Message| match msg { - Message::Notification(n) if n.method == "experimental/serverStatus" => { - let status = n - .clone() - .extract::("experimental/serverStatus") - .unwrap(); - status.quiescent - } - _ => false, - }) - .unwrap_or_else(|Timeout| panic!("timeout while waiting for ws to load")); - self - } - fn wait_for_message_cond( - &self, - n: usize, - cond: &dyn Fn(&Message) -> bool, - ) -> Result<(), Timeout> { - let mut total = 0; - for msg in self.messages.borrow().iter() { - if cond(msg) { - total += 1 - } - } - while total < n { - let msg = self.recv()?.expect("no response"); - if cond(&msg) { - total += 1; - } - } - Ok(()) - } - fn recv(&self) -> Result, Timeout> { - let msg = recv_timeout(&self.client.receiver)?; - let msg = msg.map(|msg| { - self.messages.borrow_mut().push(msg.clone()); - msg - }); - Ok(msg) - } - fn send_notification(&self, not: Notification) { - self.client.sender.send(Message::Notification(not)).unwrap(); - } - - pub(crate) fn path(&self) -> &Path { - self.dir.path() - } -} - -impl Drop for Server { - fn drop(&mut self) { - self.request::((), Value::Null); - self.notification::(()); - } -} - -struct Timeout; - -fn recv_timeout(receiver: &Receiver) -> Result, Timeout> { - let timeout = - if cfg!(target_os = "macos") { Duration::from_secs(300) } else { Duration::from_secs(120) }; - select! { - recv(receiver) -> msg => Ok(msg.ok()), - recv(after(timeout)) -> _ => Err(Timeout), - } -} - -// Comparison functionality borrowed from cargo: - -/// Compares JSON object for approximate equality. -/// You can use `[..]` wildcard in strings (useful for OS dependent things such -/// as paths). You can use a `"{...}"` string literal as a wildcard for -/// arbitrary nested JSON. Arrays are sorted before comparison. -fn find_mismatch<'a>(expected: &'a Value, actual: &'a Value) -> Option<(&'a Value, &'a Value)> { - match (expected, actual) { - (Value::Number(l), Value::Number(r)) if l == r => None, - (Value::Bool(l), Value::Bool(r)) if l == r => None, - (Value::String(l), Value::String(r)) if lines_match(l, r) => None, - (Value::Array(l), Value::Array(r)) => { - if l.len() != r.len() { - return Some((expected, actual)); - } - - let mut l = l.iter().collect::>(); - let mut r = r.iter().collect::>(); - - l.retain(|l| match r.iter().position(|r| find_mismatch(l, r).is_none()) { - Some(i) => { - r.remove(i); - false - } - None => true, - }); - - if !l.is_empty() { - assert!(!r.is_empty()); - Some((&l[0], &r[0])) - } else { - assert_eq!(r.len(), 0); - None - } - } - (Value::Object(l), Value::Object(r)) => { - fn sorted_values(obj: &serde_json::Map) -> Vec<&Value> { - let mut entries = obj.iter().collect::>(); - entries.sort_by_key(|it| it.0); - entries.into_iter().map(|(_k, v)| v).collect::>() - } - - let same_keys = l.len() == r.len() && l.keys().all(|k| r.contains_key(k)); - if !same_keys { - return Some((expected, actual)); - } - - let l = sorted_values(l); - let r = sorted_values(r); - - l.into_iter().zip(r).filter_map(|(l, r)| find_mismatch(l, r)).next() - } - (Value::Null, Value::Null) => None, - // magic string literal "{...}" acts as wildcard for any sub-JSON - (Value::String(l), _) if l == "{...}" => None, - _ => Some((expected, actual)), - } -} - -/// Compare a line with an expected pattern. -/// - Use `[..]` as a wildcard to match 0 or more characters on the same line -/// (similar to `.*` in a regex). -fn lines_match(expected: &str, actual: &str) -> bool { - // Let's not deal with / vs \ (windows...) - // First replace backslash-escaped backslashes with forward slashes - // which can occur in, for example, JSON output - let expected = expected.replace(r"\\", "/").replace(r"\", "/"); - let mut actual: &str = &actual.replace(r"\\", "/").replace(r"\", "/"); - for (i, part) in expected.split("[..]").enumerate() { - match actual.find(part) { - Some(j) => { - if i == 0 && j != 0 { - return false; - } - actual = &actual[j + part.len()..]; - } - None => return false, - } - } - actual.is_empty() || expected.ends_with("[..]") -} - -#[test] -fn lines_match_works() { - assert!(lines_match("a b", "a b")); - assert!(lines_match("a[..]b", "a b")); - assert!(lines_match("a[..]", "a b")); - assert!(lines_match("[..]", "a b")); - assert!(lines_match("[..]b", "a b")); - - assert!(!lines_match("[..]b", "c")); - assert!(!lines_match("b", "c")); - assert!(!lines_match("b", "cb")); -} diff --git a/crates/rust-analyzer/tests/rust-analyzer/testdir.rs b/crates/rust-analyzer/tests/rust-analyzer/testdir.rs deleted file mode 100644 index 36271344b..000000000 --- a/crates/rust-analyzer/tests/rust-analyzer/testdir.rs +++ /dev/null @@ -1,62 +0,0 @@ -use std::{ - fs, io, - path::{Path, PathBuf}, - sync::atomic::{AtomicUsize, Ordering}, -}; - -pub(crate) struct TestDir { - path: PathBuf, - keep: bool, -} - -impl TestDir { - pub(crate) fn new() -> TestDir { - let base = std::env::temp_dir().join("testdir"); - let pid = std::process::id(); - - static CNT: AtomicUsize = AtomicUsize::new(0); - for _ in 0..100 { - let cnt = CNT.fetch_add(1, Ordering::Relaxed); - let path = base.join(format!("{}_{}", pid, cnt)); - if path.is_dir() { - continue; - } - fs::create_dir_all(&path).unwrap(); - return TestDir { path, keep: false }; - } - panic!("Failed to create a temporary directory") - } - #[allow(unused)] - pub(crate) fn keep(mut self) -> TestDir { - self.keep = true; - self - } - pub(crate) fn path(&self) -> &Path { - &self.path - } -} - -impl Drop for TestDir { - fn drop(&mut self) { - if self.keep { - return; - } - remove_dir_all(&self.path).unwrap() - } -} - -#[cfg(not(windows))] -fn remove_dir_all(path: &Path) -> io::Result<()> { - fs::remove_dir_all(path) -} - -#[cfg(windows)] -fn remove_dir_all(path: &Path) -> io::Result<()> { - for _ in 0..99 { - if fs::remove_dir_all(path).is_ok() { - return Ok(()); - } - std::thread::sleep(std::time::Duration::from_millis(10)) - } - fs::remove_dir_all(path) -} diff --git a/crates/rust-analyzer/tests/slow-tests/main.rs b/crates/rust-analyzer/tests/slow-tests/main.rs new file mode 100644 index 000000000..9e89209ea --- /dev/null +++ b/crates/rust-analyzer/tests/slow-tests/main.rs @@ -0,0 +1,891 @@ +//! 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 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": { + "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" + } + ]), + ); +} + +#[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!([{ + "edit": { + "documentChanges": [ + { + "kind": "create", + "uri": "file:///[..]/src/bar.rs" + } + ] + }, + "kind": "quickfix", + "title": "Create module" + }]), + ); + + 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.to_string(), + ); + + 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!([{ + "edit": { + "documentChanges": [ + { + "kind": "create", + "uri": "file://[..]/src/bar.rs" + } + ] + }, + "kind": "quickfix", + "title": "Create module" + }]), + ); + + 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 = "2018" +[dependencies] +bar = {path = "../bar"} + +//- /foo/src/main.rs +use bar::Bar; +trait Bar { + fn bar(); +} +#[derive(Bar)] +struct Foo {} +fn main() { + Foo::bar(); +} + +//- /bar/Cargo.toml +[package] +name = "bar" +version = "0.0.0" +edition = "2018" + +[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(7, 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" + } + ] + } + ] + }), + ); +} diff --git a/crates/rust-analyzer/tests/slow-tests/support.rs b/crates/rust-analyzer/tests/slow-tests/support.rs new file mode 100644 index 000000000..75e677762 --- /dev/null +++ b/crates/rust-analyzer/tests/slow-tests/support.rs @@ -0,0 +1,390 @@ +use std::{ + cell::{Cell, RefCell}, + fs, + path::{Path, PathBuf}, + sync::Once, + time::Duration, +}; + +use crossbeam_channel::{after, select, Receiver}; +use lsp_server::{Connection, Message, Notification, Request}; +use lsp_types::{notification::Exit, request::Shutdown, TextDocumentIdentifier, Url}; +use project_model::ProjectManifest; +use rust_analyzer::{config::Config, lsp_ext, main_loop}; +use serde::Serialize; +use serde_json::{json, to_string_pretty, Value}; +use test_utils::Fixture; +use vfs::AbsPathBuf; + +use crate::testdir::TestDir; + +pub(crate) struct Project<'a> { + fixture: &'a str, + tmp_dir: Option, + roots: Vec, + config: serde_json::Value, +} + +impl<'a> Project<'a> { + pub(crate) fn with_fixture(fixture: &str) -> Project { + Project { + fixture, + tmp_dir: None, + roots: vec![], + config: serde_json::json!({ + "cargo": { + // Loading standard library is costly, let's ignore it by default + "noSysroot": true, + // Can't use test binary as rustc wrapper. + "useRustcWrapperForBuildScripts": false, + } + }), + } + } + + pub(crate) fn tmp_dir(mut self, tmp_dir: TestDir) -> Project<'a> { + self.tmp_dir = Some(tmp_dir); + self + } + + pub(crate) fn root(mut self, path: &str) -> Project<'a> { + self.roots.push(path.into()); + self + } + + pub(crate) fn with_config(mut self, config: serde_json::Value) -> Project<'a> { + fn merge(dst: &mut serde_json::Value, src: serde_json::Value) { + match (dst, src) { + (Value::Object(dst), Value::Object(src)) => { + for (k, v) in src { + merge(dst.entry(k).or_insert(v.clone()), v) + } + } + (dst, src) => *dst = src, + } + } + merge(&mut self.config, config); + self + } + + pub(crate) fn server(self) -> Server { + let tmp_dir = self.tmp_dir.unwrap_or_else(TestDir::new); + static INIT: Once = Once::new(); + INIT.call_once(|| { + env_logger::builder().is_test(true).parse_env("RA_LOG").try_init().unwrap(); + profile::init_from(crate::PROFILE); + }); + + for entry in Fixture::parse(self.fixture) { + let path = tmp_dir.path().join(&entry.path['/'.len_utf8()..]); + fs::create_dir_all(path.parent().unwrap()).unwrap(); + fs::write(path.as_path(), entry.text.as_bytes()).unwrap(); + } + + let tmp_dir_path = AbsPathBuf::assert(tmp_dir.path().to_path_buf()); + let mut roots = + self.roots.into_iter().map(|root| tmp_dir_path.join(root)).collect::>(); + if roots.is_empty() { + roots.push(tmp_dir_path.clone()); + } + let discovered_projects = roots + .into_iter() + .map(|it| ProjectManifest::discover_single(&it).unwrap()) + .collect::>(); + + let mut config = Config::new( + tmp_dir_path, + lsp_types::ClientCapabilities { + text_document: Some(lsp_types::TextDocumentClientCapabilities { + definition: Some(lsp_types::GotoCapability { + link_support: Some(true), + ..Default::default() + }), + code_action: Some(lsp_types::CodeActionClientCapabilities { + code_action_literal_support: Some( + lsp_types::CodeActionLiteralSupport::default(), + ), + ..Default::default() + }), + hover: Some(lsp_types::HoverClientCapabilities { + content_format: Some(vec![lsp_types::MarkupKind::Markdown]), + ..Default::default() + }), + ..Default::default() + }), + window: Some(lsp_types::WindowClientCapabilities { + work_done_progress: Some(false), + ..Default::default() + }), + experimental: Some(json!({ + "serverStatusNotification": true, + })), + ..Default::default() + }, + ); + config.discovered_projects = Some(discovered_projects); + config.update(self.config); + + Server::new(tmp_dir, config) + } +} + +pub(crate) fn project(fixture: &str) -> Server { + Project::with_fixture(fixture).server() +} + +pub(crate) struct Server { + req_id: Cell, + messages: RefCell>, + _thread: jod_thread::JoinHandle<()>, + client: Connection, + /// XXX: remove the tempdir last + dir: TestDir, +} + +impl Server { + fn new(dir: TestDir, config: Config) -> Server { + let (connection, client) = Connection::memory(); + + let _thread = jod_thread::Builder::new() + .name("test server".to_string()) + .spawn(move || main_loop(config, connection).unwrap()) + .expect("failed to spawn a thread"); + + Server { req_id: Cell::new(1), dir, messages: Default::default(), client, _thread } + } + + pub(crate) fn doc_id(&self, rel_path: &str) -> TextDocumentIdentifier { + let path = self.dir.path().join(rel_path); + TextDocumentIdentifier { uri: Url::from_file_path(path).unwrap() } + } + + pub(crate) fn notification(&self, params: N::Params) + where + N: lsp_types::notification::Notification, + N::Params: Serialize, + { + let r = Notification::new(N::METHOD.to_string(), params); + self.send_notification(r) + } + + #[track_caller] + pub(crate) fn request(&self, params: R::Params, expected_resp: Value) + where + R: lsp_types::request::Request, + R::Params: Serialize, + { + let actual = self.send_request::(params); + if let Some((expected_part, actual_part)) = find_mismatch(&expected_resp, &actual) { + panic!( + "JSON mismatch\nExpected:\n{}\nWas:\n{}\nExpected part:\n{}\nActual part:\n{}\n", + to_string_pretty(&expected_resp).unwrap(), + to_string_pretty(&actual).unwrap(), + to_string_pretty(expected_part).unwrap(), + to_string_pretty(actual_part).unwrap(), + ); + } + } + + pub(crate) fn send_request(&self, params: R::Params) -> Value + where + R: lsp_types::request::Request, + R::Params: Serialize, + { + let id = self.req_id.get(); + self.req_id.set(id.wrapping_add(1)); + + let r = Request::new(id.into(), R::METHOD.to_string(), params); + self.send_request_(r) + } + fn send_request_(&self, r: Request) -> Value { + let id = r.id.clone(); + self.client.sender.send(r.clone().into()).unwrap(); + while let Some(msg) = self.recv().unwrap_or_else(|Timeout| panic!("timeout: {:?}", r)) { + match msg { + Message::Request(req) => { + if req.method == "client/registerCapability" { + let params = req.params.to_string(); + if ["workspace/didChangeWatchedFiles", "textDocument/didSave"] + .iter() + .any(|&it| params.contains(it)) + { + continue; + } + } + panic!("unexpected request: {:?}", req) + } + Message::Notification(_) => (), + Message::Response(res) => { + assert_eq!(res.id, id); + if let Some(err) = res.error { + panic!("error response: {:#?}", err); + } + return res.result.unwrap(); + } + } + } + panic!("no response for {:?}", r); + } + pub(crate) fn wait_until_workspace_is_loaded(self) -> Server { + self.wait_for_message_cond(1, &|msg: &Message| match msg { + Message::Notification(n) if n.method == "experimental/serverStatus" => { + let status = n + .clone() + .extract::("experimental/serverStatus") + .unwrap(); + status.quiescent + } + _ => false, + }) + .unwrap_or_else(|Timeout| panic!("timeout while waiting for ws to load")); + self + } + fn wait_for_message_cond( + &self, + n: usize, + cond: &dyn Fn(&Message) -> bool, + ) -> Result<(), Timeout> { + let mut total = 0; + for msg in self.messages.borrow().iter() { + if cond(msg) { + total += 1 + } + } + while total < n { + let msg = self.recv()?.expect("no response"); + if cond(&msg) { + total += 1; + } + } + Ok(()) + } + fn recv(&self) -> Result, Timeout> { + let msg = recv_timeout(&self.client.receiver)?; + let msg = msg.map(|msg| { + self.messages.borrow_mut().push(msg.clone()); + msg + }); + Ok(msg) + } + fn send_notification(&self, not: Notification) { + self.client.sender.send(Message::Notification(not)).unwrap(); + } + + pub(crate) fn path(&self) -> &Path { + self.dir.path() + } +} + +impl Drop for Server { + fn drop(&mut self) { + self.request::((), Value::Null); + self.notification::(()); + } +} + +struct Timeout; + +fn recv_timeout(receiver: &Receiver) -> Result, Timeout> { + let timeout = + if cfg!(target_os = "macos") { Duration::from_secs(300) } else { Duration::from_secs(120) }; + select! { + recv(receiver) -> msg => Ok(msg.ok()), + recv(after(timeout)) -> _ => Err(Timeout), + } +} + +// Comparison functionality borrowed from cargo: + +/// Compares JSON object for approximate equality. +/// You can use `[..]` wildcard in strings (useful for OS dependent things such +/// as paths). You can use a `"{...}"` string literal as a wildcard for +/// arbitrary nested JSON. Arrays are sorted before comparison. +fn find_mismatch<'a>(expected: &'a Value, actual: &'a Value) -> Option<(&'a Value, &'a Value)> { + match (expected, actual) { + (Value::Number(l), Value::Number(r)) if l == r => None, + (Value::Bool(l), Value::Bool(r)) if l == r => None, + (Value::String(l), Value::String(r)) if lines_match(l, r) => None, + (Value::Array(l), Value::Array(r)) => { + if l.len() != r.len() { + return Some((expected, actual)); + } + + let mut l = l.iter().collect::>(); + let mut r = r.iter().collect::>(); + + l.retain(|l| match r.iter().position(|r| find_mismatch(l, r).is_none()) { + Some(i) => { + r.remove(i); + false + } + None => true, + }); + + if !l.is_empty() { + assert!(!r.is_empty()); + Some((&l[0], &r[0])) + } else { + assert_eq!(r.len(), 0); + None + } + } + (Value::Object(l), Value::Object(r)) => { + fn sorted_values(obj: &serde_json::Map) -> Vec<&Value> { + let mut entries = obj.iter().collect::>(); + entries.sort_by_key(|it| it.0); + entries.into_iter().map(|(_k, v)| v).collect::>() + } + + let same_keys = l.len() == r.len() && l.keys().all(|k| r.contains_key(k)); + if !same_keys { + return Some((expected, actual)); + } + + let l = sorted_values(l); + let r = sorted_values(r); + + l.into_iter().zip(r).filter_map(|(l, r)| find_mismatch(l, r)).next() + } + (Value::Null, Value::Null) => None, + // magic string literal "{...}" acts as wildcard for any sub-JSON + (Value::String(l), _) if l == "{...}" => None, + _ => Some((expected, actual)), + } +} + +/// Compare a line with an expected pattern. +/// - Use `[..]` as a wildcard to match 0 or more characters on the same line +/// (similar to `.*` in a regex). +fn lines_match(expected: &str, actual: &str) -> bool { + // Let's not deal with / vs \ (windows...) + // First replace backslash-escaped backslashes with forward slashes + // which can occur in, for example, JSON output + let expected = expected.replace(r"\\", "/").replace(r"\", "/"); + let mut actual: &str = &actual.replace(r"\\", "/").replace(r"\", "/"); + for (i, part) in expected.split("[..]").enumerate() { + match actual.find(part) { + Some(j) => { + if i == 0 && j != 0 { + return false; + } + actual = &actual[j + part.len()..]; + } + None => return false, + } + } + actual.is_empty() || expected.ends_with("[..]") +} + +#[test] +fn lines_match_works() { + assert!(lines_match("a b", "a b")); + assert!(lines_match("a[..]b", "a b")); + assert!(lines_match("a[..]", "a b")); + assert!(lines_match("[..]", "a b")); + assert!(lines_match("[..]b", "a b")); + + assert!(!lines_match("[..]b", "c")); + assert!(!lines_match("b", "c")); + assert!(!lines_match("b", "cb")); +} diff --git a/crates/rust-analyzer/tests/slow-tests/testdir.rs b/crates/rust-analyzer/tests/slow-tests/testdir.rs new file mode 100644 index 000000000..36271344b --- /dev/null +++ b/crates/rust-analyzer/tests/slow-tests/testdir.rs @@ -0,0 +1,62 @@ +use std::{ + fs, io, + path::{Path, PathBuf}, + sync::atomic::{AtomicUsize, Ordering}, +}; + +pub(crate) struct TestDir { + path: PathBuf, + keep: bool, +} + +impl TestDir { + pub(crate) fn new() -> TestDir { + let base = std::env::temp_dir().join("testdir"); + let pid = std::process::id(); + + static CNT: AtomicUsize = AtomicUsize::new(0); + for _ in 0..100 { + let cnt = CNT.fetch_add(1, Ordering::Relaxed); + let path = base.join(format!("{}_{}", pid, cnt)); + if path.is_dir() { + continue; + } + fs::create_dir_all(&path).unwrap(); + return TestDir { path, keep: false }; + } + panic!("Failed to create a temporary directory") + } + #[allow(unused)] + pub(crate) fn keep(mut self) -> TestDir { + self.keep = true; + self + } + pub(crate) fn path(&self) -> &Path { + &self.path + } +} + +impl Drop for TestDir { + fn drop(&mut self) { + if self.keep { + return; + } + remove_dir_all(&self.path).unwrap() + } +} + +#[cfg(not(windows))] +fn remove_dir_all(path: &Path) -> io::Result<()> { + fs::remove_dir_all(path) +} + +#[cfg(windows)] +fn remove_dir_all(path: &Path) -> io::Result<()> { + for _ in 0..99 { + if fs::remove_dir_all(path).is_ok() { + return Ok(()); + } + std::thread::sleep(std::time::Duration::from_millis(10)) + } + fs::remove_dir_all(path) +} -- cgit v1.2.3