+++ /dev/null
-//! 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"
- }
- ]
- }
- ]
- }),
- );
-}
+++ /dev/null
-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"));
-}
+++ /dev/null
-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)
-}
--- /dev/null
+//! 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"
+ }
+ ]
+ }
+ ]
+ }),
+ );
+}
--- /dev/null
+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"));
+}
--- /dev/null
+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)
+}