]> git.lizzy.rs Git - rust.git/commitdiff
internal: use more evocative test folder name
authorAleksey Kladov <aleksey.kladov@gmail.com>
Mon, 17 May 2021 15:43:20 +0000 (18:43 +0300)
committerAleksey Kladov <aleksey.kladov@gmail.com>
Mon, 17 May 2021 16:01:54 +0000 (19:01 +0300)
crates/rust-analyzer/tests/rust-analyzer/main.rs [deleted file]
crates/rust-analyzer/tests/rust-analyzer/support.rs [deleted file]
crates/rust-analyzer/tests/rust-analyzer/testdir.rs [deleted file]
crates/rust-analyzer/tests/slow-tests/main.rs [new file with mode: 0644]
crates/rust-analyzer/tests/slow-tests/support.rs [new file with mode: 0644]
crates/rust-analyzer/tests/slow-tests/testdir.rs [new file with mode: 0644]

diff --git a/crates/rust-analyzer/tests/rust-analyzer/main.rs b/crates/rust-analyzer/tests/rust-analyzer/main.rs
deleted file mode 100644 (file)
index c940ef2..0000000
+++ /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::<Completion>(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::<Runnables>(
-        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::<Formatting>(
-        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::<Formatting>(
-        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::<Formatting>(
-        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::<CodeActionRequest>(
-        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::<CodeActionRequest>(
-        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::<CodeActionRequest>(
-        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::<CodeActionRequest>(
-        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::<DidOpenTextDocument>(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::<OnEnter>(
-        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::<OnEnter>(
-        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::<HoverRequest>(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::<HoverRequest>(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::<GotoTypeDefinition>(
-        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::<GotoTypeDefinition>(
-        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::<HoverRequest>(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::<WillRenameFiles>(
-        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::<WillRenameFiles>(
-        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::<WillRenameFiles>(
-        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::<WillRenameFiles>(
-        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 (file)
index 75e6777..0000000
+++ /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<TestDir>,
-    roots: Vec<PathBuf>,
-    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::<Vec<_>>();
-        if roots.is_empty() {
-            roots.push(tmp_dir_path.clone());
-        }
-        let discovered_projects = roots
-            .into_iter()
-            .map(|it| ProjectManifest::discover_single(&it).unwrap())
-            .collect::<Vec<_>>();
-
-        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<i32>,
-    messages: RefCell<Vec<Message>>,
-    _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<N>(&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<R>(&self, params: R::Params, expected_resp: Value)
-    where
-        R: lsp_types::request::Request,
-        R::Params: Serialize,
-    {
-        let actual = self.send_request::<R>(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<R>(&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::<lsp_ext::ServerStatusParams>("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<Option<Message>, 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::<Shutdown>((), Value::Null);
-        self.notification::<Exit>(());
-    }
-}
-
-struct Timeout;
-
-fn recv_timeout(receiver: &Receiver<Message>) -> Result<Option<Message>, 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::<Vec<_>>();
-            let mut r = r.iter().collect::<Vec<_>>();
-
-            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<String, Value>) -> Vec<&Value> {
-                let mut entries = obj.iter().collect::<Vec<_>>();
-                entries.sort_by_key(|it| it.0);
-                entries.into_iter().map(|(_k, v)| v).collect::<Vec<_>>()
-            }
-
-            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 (file)
index 3627134..0000000
+++ /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 (file)
index 0000000..9e89209
--- /dev/null
@@ -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::<Completion>(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::<Runnables>(
+        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::<Formatting>(
+        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::<Formatting>(
+        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::<Formatting>(
+        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::<CodeActionRequest>(
+        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::<CodeActionRequest>(
+        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::<CodeActionRequest>(
+        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::<CodeActionRequest>(
+        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::<DidOpenTextDocument>(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::<OnEnter>(
+        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::<OnEnter>(
+        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::<HoverRequest>(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::<HoverRequest>(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::<GotoTypeDefinition>(
+        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::<GotoTypeDefinition>(
+        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::<HoverRequest>(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::<WillRenameFiles>(
+        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::<WillRenameFiles>(
+        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::<WillRenameFiles>(
+        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::<WillRenameFiles>(
+        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 (file)
index 0000000..75e6777
--- /dev/null
@@ -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<TestDir>,
+    roots: Vec<PathBuf>,
+    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::<Vec<_>>();
+        if roots.is_empty() {
+            roots.push(tmp_dir_path.clone());
+        }
+        let discovered_projects = roots
+            .into_iter()
+            .map(|it| ProjectManifest::discover_single(&it).unwrap())
+            .collect::<Vec<_>>();
+
+        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<i32>,
+    messages: RefCell<Vec<Message>>,
+    _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<N>(&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<R>(&self, params: R::Params, expected_resp: Value)
+    where
+        R: lsp_types::request::Request,
+        R::Params: Serialize,
+    {
+        let actual = self.send_request::<R>(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<R>(&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::<lsp_ext::ServerStatusParams>("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<Option<Message>, 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::<Shutdown>((), Value::Null);
+        self.notification::<Exit>(());
+    }
+}
+
+struct Timeout;
+
+fn recv_timeout(receiver: &Receiver<Message>) -> Result<Option<Message>, 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::<Vec<_>>();
+            let mut r = r.iter().collect::<Vec<_>>();
+
+            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<String, Value>) -> Vec<&Value> {
+                let mut entries = obj.iter().collect::<Vec<_>>();
+                entries.sort_by_key(|it| it.0);
+                entries.into_iter().map(|(_k, v)| v).collect::<Vec<_>>()
+            }
+
+            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 (file)
index 0000000..3627134
--- /dev/null
@@ -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)
+}